目录
1. 乐观锁 VS 悲观锁
悲观锁:适合写操作多的场景
认为在使用数据的时候一定有别的线程来修改数据,所以会在取数据的时候先加锁,确保数据不会被别的线程修改
乐观锁:适合读操作多的场景
认为在使用数据时不会有别的线程来修改数据,所以不会添加锁,只是在更新数据时去判断是否有其他线程更新了这个数据,如果没有更新,当前线程将自己修改的数据成功写入,如果已经被更新,会根据不同的实现方式执行不同的操作(报错
2. 自旋锁 VS 适应性自旋锁
当某个线程尝试获取同步资源的锁失败,资源被占用时,使用自旋锁会不放弃CPU时间片,通过自旋等待锁的释放,等待一段时间后,通过自旋操作减少CPU切换以及恢复线程导致的消耗。而非自旋锁则会去通过CPU切换状态,使当前线程休眠,CPU切换线程执行其他操作,当占用同步资源的线程释放了锁,恢复线程,再次去尝试获取锁。
缺点:不能代替阻塞。自旋等待时间虽然避免了线程切换的开销,但是会占用处理器时间,如果锁占用时间很短,自旋等待的效果会非常好,反之就会白白浪费处理器资源(所以默认自旋次数为10次)
自适应自旋锁:
自旋的时间不在固定,而是由前一次在同一个锁上的 自旋时间及锁拥有者的状态来决定,如果在同一个锁对象上,自选等待刚刚成功获取过锁,并且持有的锁正在运行中,JVM就会认为该锁自旋获取锁的可能性很大,就会自动增加等待时间。相反,如果某个锁通过自旋很少就成功获取了锁,那么以后获取将可能省略掉自旋过程,以避免浪费处理器资源。
3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
第一部分用于存储对象自身的运行时数据,HashCode
、GC Age
、锁标记位
、是否为偏向锁
等。一般为32位或者64位(视操作系统位数定)。官方称之为Mark Word
,它是实现轻量级锁和偏向锁的关键。 另外一部分存储的是指向方法区对象类型数据的指针(Klass Point
),如果对象是数组的话,还会有一个额外的部分用于存储数据的长度
轻量级锁:可以减少重量级锁对线程的阻塞带来的线程开销
轻量级锁加锁:
在线程执行同步块之前,JVM会先在当前线程的栈帧中创建一个名为锁记录(Lock Record
)的空间,用于存储锁对象目前的Mark Word
的拷贝(JVM会将对象头中的Mark Word
拷贝到锁记录中,
如果当前对象没有被锁定,那么锁标志位为01状态,JVM在执行当前线程时,首先会在当前线程栈帧中创建锁记录Lock Record
的空间用于存储锁对象目前的Mark Word
的拷贝。
然后,虚拟机使用CAS操作将标记字段Mark Word拷贝到锁记录中,并且将Mark Word
更新为指向Lock Record
的指针。如果更新成功了,那么这个线程就有用了该对象的锁,并且对象Mark Word的锁标志位更新为(Mark Word
中最后的2bit)00,即表示此对象处于轻量级锁定状态。
如果更新失败:
JVM会检查当前的Mark Word
中是否存在指向当前线程的栈帧的指针,如果有,说明该锁已经被获取,可以直接调用。如果没有,则说明该锁被其他线程抢占了,如果有两条以上的线程竞争同一个锁,那轻量级锁就不再有效,直接膨胀位重量级锁,没有获得锁的线程会被阻塞。此时,锁的标志位为10.Mark Word
中存储的时指向重量级锁的指针。
轻量级解锁:
使用原子的CAS操作将Displaced Mark Word
替换回到对象头中,如果成功,则表示没有发生竞争关系。如果失败,表示当前锁存在竞争关系。锁就会膨胀成重量级锁。
偏向锁:
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和推出同步块时不需要进行CAS操作来加锁和解锁。
对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了响应速度 | 如线程成始终得不到锁竞争的线程,使用自旋会消耗CPU性能 | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗 | 追求吞吐量,同步块执行速度较长 |
总结: 偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
4. 公平锁 VS 非公平锁
公平锁:
指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
优点:等待锁的线程不会饿死。
缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁:
多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。
优点:可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。
缺点:处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
5. 可重入锁 VS 非可重入锁
可重入锁:
又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁。
当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。
释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。
6. 独享锁(排他锁) VS 共享锁
独享锁也叫排他锁:
指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁:
指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。