Java锁的研究,包括相关概念,锁的分类,锁优化,死锁等.
概念
- 原子性(atomicity): 指一个操作不可中断,要么全部成功要么全部失败
- 可见性(visibility): 指当一个线程修改了共享变量后,其他线程能立即得知这个修改(它要对付内存缓存和编译器优化的各种反常行为)
- 同步/异步: 同步指一个任务需要依赖另一个任务时,必须等待依赖的任务完成;异步指不需要等待依赖的任务完成.
- 阻塞/非阻塞: 从CPU消耗上说,阻塞就是CPU等待慢操作完成;非阻塞就是慢操作执行时CPU去干其他事.
锁分类
公平锁/非公平锁
公平锁是指线程按照申请锁的顺序来获取锁.
非公平锁的吞吐量更大.
ReentrantLock
可以通过构造函数指定是否是公平锁,默认是非公平锁.
synchronized
是非公平锁,并且没任何办法变成公平锁.
可重入锁
又名递归锁
,指在外层获取锁后,在内层会自动获取锁.
可一定程度避免死锁.
ReentrantLock
与synchronized
都是可重入锁.
以下代码描述了可重入锁:
synchronized void setA() {
setB();
}
synchronized void setB() {
}
独享锁/共享锁
独享锁指该锁只能被一个线程持有,共享锁指该锁可以同时被多个线程持有.
ReentrantLock
与synchronized
是独享锁,ReadWriteLock
的读锁是共享锁,写锁是独享锁
互斥锁/读写锁
ReentrantLock
是互斥锁,ReadWriteLock
是读写锁
读锁是共享锁,写锁是排他锁.
乐观锁/悲观锁
指看待并发同步的角度,悲观锁假定会发生冲突,因此会加锁(在mysql角度,依靠数据库的加锁机制).
乐观锁假定不会发生冲突,只在提交时检测是否冲突,CAS(compare and swap)是最常见的乐观锁(在mysql角度,一般采用版本号
方式实现,但乐观锁不能解决脏读
问题)
乐观锁适合大量读操作的场景,会带来大量性能提升.
分段锁
分段锁是一种锁的设计.
ConcurrentHashMap
使用的就是分段锁.
偏向锁/轻量级锁/重量级锁
这三种指的是synchronized
锁的状态(jdk1.6后引入的).
偏向锁指一段代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价.
轻量级锁指锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其它线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能.
重量级锁指锁为轻量级锁的时候,另一个线程在自旋一定次数后,如果还没有获取到锁,就会进入阻塞,该锁升级为重量级锁.重量级锁会让其它申请的线程进入阻塞,性能降低.
自旋锁
自旋锁指尝试获取锁的进程不会立即阻塞,而是采用循环的方式去尝试获取锁,好处是减少线程上下文切换的消耗,坏处是会消耗CPU.
锁的优化
- 减少锁持有时间
- 减小锁粒度
- 读写分离锁代替独占锁: 适合读多写少的情况
- 锁分离: 像LinkedBlockingQueue, 把take()与put()分离
- 锁粗化: 主要避免循环内反复申请锁
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
预防
- 以确定顺序获得锁
- 超时放弃
其他死锁类型
- 线程池死锁,比如任务A获得线程后需要等待任务B的执行结果,任务B需要获得线程才能执行,但线程池没有多余线程了(比如线程池是单线程的)
- 连接池死锁,比如数据库连接池大小都为1的N1与N2,线程A以顺序N1,N2来获得数据库连接;线程B以顺序N2,N1来获得数据库连接,就可能发生死锁
synchronized
根据修饰对象分类
- 修饰代码块
synchronized(this|object) {}
synchronized(类.class) {}
- 修饰方法
- 修饰非静态方法
- 修饰静态方法
根据获取的锁分类
- 获取对象锁
synchronized(this|object) {}
- 修饰非静态方法
- 获取类锁
synchronized(类.class) {}
- 修饰静态方法
注意事项
- synchronized关键字不能继承: 父类方法有synchronized关键字,之类覆盖时不会继承,必须手动指定
- 定义接口方法时不能使用synchronized
- 构造方法不能使用synchronized,但可以使用synchronized代码块
与ReentrantLock比较
共同点
- 都是协调多线程对变量的访问
- 都是可重入锁
- 都保证了可见性与互斥性
不同点
- ReentrantLock显示获得、释放锁,synchronized隐式获得释放锁
- ReentrantLock可响应中断、可轮回,synchronized不可响应中断
- ReentrantLock是API级别的,synchronized是JVM级别的
- ReentrantLock可以实现公平锁
- ReentrantLock通过Condition可以绑定多个条件
- 底层实现不一样, synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略
- synchronized不支持分段加锁
性能
从jdk1.5后,jvm对synchronized进行了优化(偏向锁/轻量级锁…),性能就差不多了.
推荐只在synchronized无法满足的情况下使用ReentrantLock.
wait & notify & notifyAll
先说两个概念:
- 锁池: 假设线程A拥有某个对象的锁,其他线程要获得锁,就会进入此对象的锁池
- 等待池: 假设线程A调用某个对象的wait()方法,就会释放锁,进入此对象的等待池
然后就很好理解:
- wait: 释放锁,并进入对象的锁池
- notify: (不释放锁)随机唤醒一个等待池中线程进入锁池
- notifyAll: (不释放锁)唤醒全部等待池中线程进入锁池
sleep
线程sleep方法不会释放锁
与wait的区别
- wait释放锁,sleep不释放
- wait用来进行线程间通信,sleep用来在执行时暂停
并发工具类
Semaphore 信号量
控制同时执行某个操作/访问某个资源的数量
Semaphore管理着一组许可(permit),操作要先获得许可才能进行
比如厕所只有5个位置,有人离开了其他人才能进入.
CountDownLatch 闭锁
可以延迟线程的进度直到达到终止状态,它强调的是一个/多个线程需要等待另外的n个线程干完某事后才继续执行.
比如裁判等待运动员都跑到终点后再宣布结果.
CyclicBarrier 栅栏
它强调的是多个线程全部都到达栅栏位置后再继续执行.
比如运动员等待所有人都跑到终点后再一起去喝酒.