所有的锁(2.0)
所有的锁
乐观锁和悲观锁
悲观锁
synchronized关键字和Lock的实现类都是悲观锁
- 什么是悲观锁?认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
- 适合写操作多的场景,先加锁可以保证写操作时数据正确(写操作包括 增 删 改)、显式的锁定之后再操作同步资源
synchronized 关键字和 Lock 的实现类都是悲观锁
乐观锁
- 概念 : 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
- 乐观锁在 Java 中通过使用无锁编程来实现,最常采用的时CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅度提升。乐观锁一般有两种实现方式(采用版本号机制、CAS算法实现)
1 | package tech.chen.juccode.a08; |
公平锁和非公平锁
什么是公平锁和非公平锁
- 公平锁:是指多个线程按照申请锁的顺序来获取锁类似排队打饭先来后到。
- 非公平锁:是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象。(注意:synchronized 和 ReentrantLock 默认是非公平锁)
- 锁饥饿 : 我们使用5个线程买100张票,使用 ReentrantLock 默认是非公平锁,获取到的结果可能都是 A 线程在出售这100张票,会导致B、C、D、E线程发生锁饥饿(使用公平锁会有什么问题)
为什么会有公平锁、非公平锁的设计
- 恢复挂起的线程 到 真正锁的获取 还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间存在的还是很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大了,所以就减少了线程的开销
什么时候用公平?什么时候用非公平?
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。否则那就用公平锁,大家公平使用
可重入锁
什么是可重入锁
- 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没有释放而阻塞
- 如果是1个有 synchronized 修饰得递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
- 所以 Java 中 ReentrantLock 和 Synchronized 都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁
1 | package tech.chen.juccode.a09; |
运行结果:
main sendSms
main sendEmail
可重入锁的种类
- 隐式锁(即 synchronized 关键字使用的锁)默认是可重入锁,在同步块、同步方法使用。(在一个synchronized修饰的方法或者代码块的内部调用本类的其他 synchronized 修饰的方法或代码块时,是永远可以得到锁的)
- 显示锁 (即 Lock )也有 ReentrantLock 这样的可重入锁。(lock和unlock一定要一 一匹配,如果少了或多了,都会坑到别的线程)
重入的实现机理
- 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
- 当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有 , Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1。
- 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁
- 当执行 monitorexit , Java 虚拟机则需将锁对象的计数器减1。计数器为零代表锁已经释放
独享锁(排他锁) VS 共享锁
独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。
在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁
。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。
死锁及排查
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁
死锁演示
代码示例:
1 | package tech.chen.juccode.a09; |
结果:
两个线程均得不到锁,互相制约,但又不会释放。形成死锁
死锁排查
- 命令
- jps
- jstack 进程号
- jconsole