谈谈Java中的锁机制

一、Synchronized关键字原理

什么是Synchronized

Synchronized是java内建的同步机制,提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取锁的线程会被阻塞

Synchronized核心组件

  • Wait Set

    那些调用wait方法被阻塞的线程被放置在这里

  • Contention List

    竞争队列,所有请求锁的线程首先被放在这个竞争队列中

  • Entry List

    Connection List 中那些有资格成为候选资源的线程被移动到Entry List

  • OnDeck

    任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为OnDeck

  • Owner

    当前已经获取到资源的线程被称为 Owner

  • !Owner

    当前释放锁的线程

谈谈Java中的锁机制_第1张图片
图中需要注意的是 :
步骤2中的移动线程是在Owner线程在unlock时,将Contention List中的部分线程移动到Entry List

步骤3中Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,JVM中这种选择行为称为 “竞争切换”

Synchronized是非公平锁,Synchronized在线程进入Contention List时,等待的线程会先尝试自旋获取锁(CAS),如果获取不到就进入Contention List,这对于已经进入队列的线程是不公平的,还有一个不公平的事情是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源

在Java 1.6中,Synchronized进行了优化,有适应自旋,锁消除,锁粗化,轻量级锁及偏向锁,效率上有了本质的提高

Synchronized底层原理

synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。

public class SyncTest {
    public void syncBlock(){
        synchronized (this){
            System.out.println("hello block");
        }
    }
}

谈谈Java中的锁机制_第2张图片

什么是锁的升级与降级

所谓锁的升级、降级就是JVM优化Synchronized运行的机制,当JVM检测到不同的竞争状况时,会自动调整到适合的锁实现,这种切换就是锁的升级、降级

当没有竞争的时候,默认会使用偏斜锁,JVM会利用CAS操作,在对象的Mark Word部分设置线程的ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,这样做的假设是基于在很多应用场景下,大部分的对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销

谈谈Java中的锁机制_第3张图片

如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM就需要撤销偏斜锁,切换到轻量级锁的实现,轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,就是用普通的轻量级锁,否则,进一步升级为重量级锁

锁的降级是当JVM进入**安全点(Stop The World)**的时候,会检查是否有闲置的Monitor,然后试图进行降级

二、ReentrantLock

概念

ReentrantLock继承接口Lock,并实现了接口中定义的方法,它是一种可重入锁,除了能完成Synchronized所能完成的所有工作外,还提供了诸如可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法

可重入就是一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,这是对锁获取粒度的一个概念,也就是锁的持有是以线程为单位,而不是基于调用次数

公平性


ReentrantLock fairLock = new ReentrantLock(true);

当公平性设置为true时,会倾向于将锁赋予等待时间最久的线程,公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法

三、什么是公平锁与非公平锁

公平锁

加锁前检查是否有排队等待的线程,如果前面有线程在等待则加入等待队列,优先让队列头的线程获取锁,先入先出

非公平锁

加锁时不考虑排队等待问题,新加入的线程A直接尝试获取锁,如果当前占用锁的线程B刚好释放锁那么在非公平的情况下A线程直接就获取到了锁而不用加入队列等待,如果A线程直接获取锁失败的话就加入等待队列中

有关图解 : https://www.jianshu.com/p/f584799f1c77

四、Volatile关键字

有时只是为了读写一两个实例字段而使用同步,所带来的开销有些划不来
这时候使用Volatile关键字,它为实例字段的同步访问提供了一种免锁机制,如果声明一个字段为Volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新

Volatile变量不能提供原子性,只能确保如果一个线程对变量做了修改,这个修改对读取这个变量的所有其他线程都可见

五、什么是CAS

CAS机制

无阻塞多线程抢占资源的模型

CAS机制使用了3个基本操作数 :内存地址V,旧的预期值A,要修改的新值B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,如果预期值A与内存地址V中实际值不同时,线程会不断尝试更新预期值A与要修改的值,直到预期值符合内存地址的值为止

CAS的缺点

1、CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力

2、不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性,比如需要保证3个变量共同进行原子性的更新,就只能使用Synchronized

ABA问题

CAS算法实现一个重要前提需要取出内存中A时刻的数据,然后在B时刻进行比较并替换,那么在这个时间差会导致数据变化

例如 :一个线程Thread1从内存位置V中取出P,这时候另一个线程Thread2也从内存中取出data1,并且Thread2进行了一些操作将数据变成Q,然后Thread2又将V位置的数据变成P,这时候线程Thread1进行CAS操作发现内存中仍是P,然后Thread1操作成功,尽管线程Thread1的CAS操作成功,但是不代表这个过程是没有问题的

部分乐观锁的实现是通过版本号(version)来解决ABA问题的,比如MySQL数据库的MVCC就是基于版本号实现的乐观锁,乐观锁在每次执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败,因为每次操作的版本号会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

六、什么是AQS

AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现(通过state的get/set/CAS)。至于能不能重入,能不能阻塞,那就看具体的自定义同步器怎么去设计了,当然,自定义同步器在进行资源访问时要考虑线程安全的影响。这里没有定义成abstract是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口,这样设计可以尽量减少不必要的工作量

七、什么是死锁与活锁

死锁

死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者处于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁

活锁

活锁也是一种死锁,死锁的话,所有线程都处于阻塞状态,活锁是由于某些条件没有满足,导致一直重复尝试,但有可能自行解开,比如设置了重试次数限制,或者超时时间

你可能感兴趣的:(后端技术)