并发常见问题
Q1:Synchroized原理?
Synchroized是由jvm实现的一种实现互斥同步的方式。查看编译后的字节码文件,发现其实是使用了monitorenter和monitoorexit指令。指令内部实现:
- 当指令运行到monitorenter时,获取锁,把锁的计数器+1。
- 当指令运行到monitorexit时,锁计数器-1。
- 当锁计数器为0时,锁被释放。
- 如果获取锁失败,则当前线程阻塞等待,直到获取锁。
Q2:Synchronized锁的使用?
Synchronized的用法有两种,分别是Synchronized方法和Synchronized块。如:
方法锁
方法所又分为锁实例方法和锁静态方法。
使用示例方法锁时,当一个类有多个实例时,调用该方法时不一定能拿到锁,只有当调用的是同一个实例的该方法才能使用同一个锁,达到加锁的效果。
public synchronized static void test1() {
//todo
}
使用静态方法锁时,锁住的是类对象,是唯一的,所以在调用静态方法的锁时,使用的是同一把锁,可以满足加锁效果。
public synchronized void test2() {
//todo
}
代码块
代码块中可以传入实例对象或类对象或实例对象的object。
锁实例对象时,所得是该类的实例对象,要使锁起效,需要确保加锁的是同一个实例。
public void test3() {
synchronized (this) {
//todo
}
}
锁类的实例对象时,由于类是唯一的,所以在调用时满足加锁效果。
public void test3() {
synchronized (Foo.class) {
//todo
}
}
锁实例对象object时,需要根据传入的对象来加锁,常见是传入String对象作为锁。
public void test3() {
String lock = “test”;
synchronized (lock) {
//todo
}
}
锁的选择
方法锁的锁颗粒度较大,在并发编程中会造成效率低下。因此在场景允许的情况下,尽量的使用颗粒度更小的锁。对需要操作的部分代码进行代码块级别的加锁,从而达到提升效率。
Q3:什么是可重入性,为什么Synchronized是可重入锁?
当前线程拿到锁之后,同一个类中一个同步方法调用另一个同步方法也仍可以获取锁,而不会导致死锁,就实现了可重入。
Synchronized在内部维护了锁拥有者和计数器。重入时计数器加1,同步方法结束时计数器减1,当计数器为0时释放锁。其他线程发现锁拥有者不是自己时,就会进行等待,直到可获取锁。
Q4:Synchronized优化?(todo)
使用CAS操作使线程不需进行阻塞的操作,减少线程状态切换。
锁实现效率从低到高 偏向锁 -> 轻量级锁 -> 重量级锁。jvm会根据锁竞争状态进行升级。
todo
据说升级后不能降级?
Q5:为什么说Synchronized是非公平锁?
非公平性主要体现在获取锁的行为上,并不是按照申请锁的先后顺序分配锁。而是在锁释放之后,每一个等待锁的线程都有可能获取到锁。
- 优点:提高了执行效率。
- 缺点:产生线程饥饿现象。
Q6:什么是锁消除?什么是锁粗化?
锁消除
根据逃逸分析,判断对象的使用是否会被外部的方法作为一个全局对象使用。如果会则可能发生线程安全问题,此时就发生了锁逃逸。如果不会发生锁逃逸,虚拟机进行锁消除操作,忽略同步而直接运行。
锁粗化
锁的作用粒度是越小越好,但是如果一系列的操作要反复的进行加锁和释放锁操作,会导致不必要的性能损耗,此时可以适当的增加锁的作用域,即锁粗化。
Q7:为什么说Synchronized是悲观锁?
Synchronized的加锁策略是:不管会不会产生锁竞争,都先进行加锁,先尝试获取锁,如果没有竞争则执行同步代码块,如果发生了竞争则等锁释放再获取锁进行加锁。 乐观锁的核心算法是CAS,策略是比较执行,先执行,如果没有竞争则修改为新值,如果发生竞争则舍弃本次操作。
Q8:乐观锁一定好吗?
cas避免了独占锁的现象,能提高并发性能。 缺点如下:
- 乐观锁只能保证一个共享变量的原子操作。
- 长时间的自旋会导致cpu的开销大。
- ABA问题,第一次对比时值是A,之后被另一线程改为B,又被另一线程改为A,第二次读取时发现是A。cas认为值没有被改变。可以引入版本号来解决aba问题。
Q9:可重入锁ReentrantLock与Synchronized实现原理不同点?
Synchronized是JVM原生的锁的实现方式。ReentrantLock是基于Lock接口的一个实现类,本质是一个AQS框架(AbstractQueuedSynchronizer)来实现。
Q10:Synchronized和ReentrantLock的异同?
Synchronized是一个java的关键字,ReentrantLock是java的Lock接口下的一个实现类。以下简称Synchronized为内置锁,ReentrantLock为重入锁。
相同
- 加锁方式同步
- 同步都是阻塞的
- 都是可重入的锁
区别
- 构成方式不同,内置锁是java提供的关键字,重入锁是JDK1.5之后的API层面的互斥锁。
- 使用方式不同,内置锁无须显式的获取和释放锁,重入锁需要显式的获取和释放锁。
- 灵活性不同,内置锁不可中断,除非抛出异常,重入锁可以。
- 内置锁中断方式:
- 代码执行完毕,正常释放锁。
- 抛出异常,jvm退出等待。
- 重入锁中断方式:
- tryLock((long timeout, TimeUnit unit),超时退出。
- lockInterruptibly(),调用interrupt()方法可中断。
- 内置锁中断方式:
- 是否允许公平锁,内置锁是非公平锁,重入锁默认(无参构造器)为不公平锁,可以选择非公平锁(传入true为公平锁,false为不公平锁)。
- 是否提供非阻塞获取锁方式,内置锁不支持,重入锁可用tryLock(),返回的是一个boolean类型。根据boolean执行不同的方法。也可以tryLock(long timeout, TimeUnit unit),等待一段时间获取锁,在时间内获取锁则返回true,否则false。
- 锁是否可以绑定条件,内置锁不可以,重入锁可以同时绑定多个Condition实例。
- 性能,java6之前内置锁性能明显低于重入锁,之后再低竞争情况下,内置锁优于重入锁,高竞争内置锁性能下降,而重入锁基本维持常态。
Q11:ReentrantLock 是如何实现可重入性的?
ReentrantLock内部的同步器Sync实现了CAS算法,把线程对象放在了一个双向链表结构中,每次获取锁都会进行比较维护的线程ID和当前线程ID是否一致,一致即可重入。
Q12:除了ReetrantLock,你还用过JUC中哪些并发工具(todo)?
- ConcurrentHashMap等线程安全的容器
- ArrayBlockingQueue、PriorityBlockingQueue等并发队列
- Executor框架的线程池
- AtomicInteger等原子操作类
- TimeUnit
Q13:ReadWriteLock和StampedLock?
ReadWriteLock是java1.5提供的读写锁。并发情况下,读读不锁,读写,写写会锁,用于进行读写分离。在读操作远远大于写操作的情况下,性能提高明显。 但是在实际应用中,ReadWriteLock的性能不好,因此提供了新的StampedLock,基于CLH,获取锁的方式是一个long类型的票据(stamp),是保证不会产生饥饿且FIFO。StampedLock是一个不可重入锁。SteampedLock的乐观读机制在大量读操作,少量写操作时,能提供极高的性能。
Q14:jUC同步器?(todo)https://blog.csdn.net/FAw67J7/article/details/79885944
Q15:java线程池是如何实现的?
java中的threadpool中有两个重要的成员属性。workqueue和works,workqueue中存放所有需要执行的任务,works中进行处理。
Q16:线程池的核心构造参数?
corePoolSize
:线程池的核心线程数maximumPoolSize
:线程池允许的最大线程数keepAliveTime
:超过核心线程数时闲置线程的存活时间workQueue
:线程等待队列
Q17:线程池中的线程是怎么被创建的?是一开始就随着线程池的创建而创建的吗?
线程池默认初始化后不启动Worker,等待有请求才会启动。每当调用execute()方法时,线程池会做如下判断:
- 如果正在运行的线程数量小于
corePoolSize
,那么立马创建线程运行这个任务。 - 如果正在运行的线程数量大于或等于
corePoolSize
,那么这个任务放入队列。 - 如果队列满了,且正在运行线程数量小于
maximumPoolSize
,那么创建非核心线程立即运行任务。 - 如果队列满了,且正在运行的线程数量大于或等于
maximumPoolSize
,则线程池会抛出异常RejectExecutionException
。 - 当一个任务完成时,将会从队列中取下一个任务来执行。
- 当线程空闲时间超过
keepAliveTime
时,如果当前运行线程数大于corePoolSize
,则线程就会被销毁。直到线程数为corePoolSize
大小。
Q18:java中默认实现的线程池?并比较异同。
SingleThreadExecutor 单线程线程池
线程池中只有一个核心线程,最大线程数也是1。如果这个线程因为异常结束,则会有新的线程来替代。该线程池可以保证任务按提交顺序执行。
corePoolSize
:1maximumPoolSize
:1keepAliveTime
:0LworkQueue
:new LinkedBlockQueue<Runnable>()
FixedThreadpool 固定大小线程池
线程池中的线程大小固定不变,每次excute()就产生一个线程,知道上线。如果线程异常结束,则生成一个线程补充。
corePoolSize
:nmaximumPoolSize
:nkeepAliveTime
:0LworkQueue
:new LinkedBlockQueue<Runnable>()
CachedThreadPool 缓存线程池
最大线程数不做限制,只要有新的任务就创建一个线程。线程空闲60后被销毁。SynchronousQueue
是一个队列大小为1的阻塞队列。
corePoolSize
:0maximumPoolSize
:Integer.MAX_VALUE
keepAliveTime
:60LworkQueue
:new SynchronousQueue<Runnable>()
ScheduledThreadPool 调度线程池
核心线程数固定,最大线程数无界。支持定时已经周期性的执行任务的线程池。
corePoolSize
:nmaximumPoolSize
:Integer.MAX_VALUE
keepAliveTime
:0workQueue
:new DelayedWorkQueue<Runnable>()
Q19:java线程池中怎么提交任务
execute()
ExecutorService.execute()
接收一个Runnable
实例。
submit()
ExecutorService.submit()
返回一个Future
实例。
Q20:volatile是怎么保证变量对所有线程的可见性的?
当共享变量的值被修改后,会立即刷新到主内存中。当其他线程需要读取共享变量时,会去主内存中获取新值。
被volatile关键字修饰的变量,具有两层语义:
- 保证变量的可见性
- 禁止进行指令重排
Q21:是否基于volatile变量的运算就能保证并发安全?
不是,volatile不能保证原子性。所以在并发操作下也不安全。
Q22:对比下 volatile 对比 Synchronized 的异同?
volatile只保证了可见性,不保证原子性;而Synchronized既保证了可见性,也保证了原子性。
Q23:现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
使用join,运行的时候按照
//other....
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
//other....
Q24:java中wait和sleep方法的不同?
wait是Object类中的方法,在等待时会释放锁。sleep是Thread类中的方法,会一直持有锁。wait常用于线程之间的交互,而sleep用于线程的暂停。