JAVA锁详解

为什么用锁呢?

这还用问安全呗 !

什么时用锁呢?

多个线程访问同一个共享资源呀!

知道锁相关类么?

Synchronized、ReentrantLock、Volatile、Atomic、Semaphore、BlockingQueue等。

锁哪些分类呢?

按锁的状态、特性、设计分类如下:
  • 独占锁/共享锁
  • 乐观锁/悲观锁
  • 互斥锁/读写锁
  • 公平锁/非公平锁
  • 分段锁
  • 可重入锁
  • 自旋锁
  • 偏向锁/轻量级锁/重量级锁

1. 乐观锁/悲观锁

乐观锁/悲观锁是一种思想。


悲观锁:并发操作采取加锁的形式,适合写操作非常多的场景。
乐观锁:并发操作不加锁(采用CAS算法),适合读操作非常多的场景,性能有所提升。

2. 独占锁/共享锁

独占锁/共享锁是一种广义的说法。
独占锁:一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。
共享锁:共享锁允许多个线程同时获取一个锁,一个锁可同时被多个线程拥有。

锁降级:从写锁变成读锁。
锁升级:从读锁变成写锁。


独占式以ReentrantLock为例,获取锁时每个节点自旋观察前一节点是不是Header节点,是就去尝试获取锁。

独占式锁

共享锁以CountDownLatch为例,它是通过一个计数器来实现的,计数器的初始化值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已完成任务,然后在闭锁上等待的线程就可以恢复执行任务。

CountDownLatch原理
  • 共享式与独占式区别
共享式与独占式区别

3. 互斥锁/读写锁

互斥锁/读写锁就是独享锁/共享锁的具体实现,读锁保证并发读非常高效。


互斥锁:Synchronized、ReentrantLock、ReadWriteLock写锁。
共享锁:ReadWriteLock读锁、CountDownLatch。

ReadWriteLock使用示例:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
boolean readLock = lock.isWriteLocked();
  if (!readLock) {
    System.out.println("get readLock.");
  }

// lock.readLock().unlock();
// 会产生死锁,同一个线程中没有释放读锁就去申请写锁属于锁升级。

// ReentrantReadWriteLock不支持锁升级。
lock.writeLock().lock();
System.out.println("get writeLock.");
// lock.writeLock().unlock();

4. 公平锁/非公平锁

公平锁:多个线程按照申请锁的顺序来获取锁。
非公平锁:有可能后申请的线程比先申请的线程优先获取锁。

ReentrantLock使用示例:
Lock lock = new ReentrantLock();
try {
  lock.lock();
  // do something
} finally {
  lock.unlock();
}
两种锁对比:
分类 使用 优点 缺点
公平锁 ReentrantLock(true) 没有饥民 性能差
非公平锁 ReentrantLock() 优先级反转、饥饿现象 吞吐量大
Synchronized使用示例:
public class Singleton {
    private Singleton() { }
    // volatile包含禁止指令重排序的语义,其具有有序性及可见性。
    private volatile static Singleton instance;
    public Singleton getInstance() { 
        // 双重检验锁定方式
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Synchronized是非公平锁,不像ReentrantLock通过AQS实现线程调度,不能使其变成公平锁。

5. 分段锁

分段锁:是一种锁的设计,细化锁的粒度。


ConcurrentHashMap的分段锁称为Segment,Segment继承了ReentrantLock,内部拥有一个Entry数组,数组中每个元素又是一个链表。put元素的时候,不是对整个Map加锁,先通过hashcode找到分段,对这个分段进行加锁,只要不是放在一个分段中,就实现了并行的插入。

ConcurrentHashMap结构图

6. 可重入锁

可重入锁:又名递归锁,同一个线程在外层方法获取锁,在进入内层方法会自动获取锁。


优点:可一定程度避免死锁。

ReentrantLock从名字看出Re entrant Lock是重新进入锁。
Synchronized也是一个可重入锁。

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

上例如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

7. 自旋锁

自旋锁:自旋锁是一种非阻塞锁。尝试获取锁的线程不会立即阻塞,采用循环方式去尝试获取锁。如果两个线程资源竞争不是特别激烈,在“原地”忙等,直到锁的持有者释放了该锁。


优点:减少线程上下文切换的消耗。
缺点:循环会消耗CPU。

自旋锁示例:

通过lock和unlock来控制自旋锁的开启与关闭。是一种非公平锁。

public class SpinLock {
  private AtomicReference<Thread> sign =new AtomicReference<>();

  // 将要更新的值设置为当前线程,并将预期值设置为null。
  public void lock() {
    Thread current = Thread.currentThread();
    while(!sign.compareAndSet(null, current)) {
    }
  }

  // 将要更新的值设置为null,并预期值设置为当前线程。
  public void unlock () {
    Thread current = Thread.currentThread();
    sign.compareAndSet(current, null);
  }
}

Atomic原子类内部的CAS操作,也是通过不断的自循环(while循环)实现。不过这种循环的结束条件是线程成功更新对应的值,但也是自旋锁的一种。

8. 偏向锁/轻量级锁/重量级锁

偏向锁/轻量级锁/重量级锁是Synchronized的三种锁的状态,在Java 5通过引入锁升级的机制提升效率。锁升级却不能降级的策略,提高获得锁和释放锁的效率。


级别从低到高依

无锁状态 > 偏向锁状态 > 轻量级锁状态 > 重量级锁状态

  • 偏向锁:同步代码一直被一个线程访问,该线程会自动获取锁,降低获取锁代价。
  • 轻量级锁:当锁是偏向锁时,被另一个线程访问,偏向锁就会升级为轻量级锁。其他线程会通过自旋的形式尝试获取锁,不会阻塞提高性能。
  • 重量级锁:当锁为轻量级锁时,另一个线程虽然是自旋,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。其他申请的线程进入阻塞,性能降低。
三种锁状态优缺点
CAS算法介绍

CAS(Compare-And-Swap比较并交换)算法为非阻塞算法。sun.misc.Unsafe 类提供了硬件级别的原子操作,以原子方式对内存执行读-改-写操作。


  • CAS算法
    CAS 包含三个操作数:内存值 V、预估值 A、更新值 B。
    当且仅当 V == A 时, V = B; 否则,不会执行任何操作。

  • CAS缺点

缺点 解决方案
长时间自旋不成功循环时间长开销大 VM能支持pause指令,效率会有一定提升
只能保证一个共享变量的原子操作 加锁,共享变量合并成一个共享变量
ABA的问题 使用AtomicStampzedReference解决ABA问题
  • CAS算法ABA问题
    解决方法:增加版本号,每次使用或者更新的时候版本号+1。
ABA问题
  • CAS示例:
// 多线程先计数器
public class Counter {
    private AtomicInteger count = new AtomicInteger();
    public Counter() {}
    public int getCount() {
        return count.get();
    }
    public void increase() {
        count.getAndIncrement();
    }
}

java.util.concurrent 包下的大量类都使用该类操作。以AtomicInteger原子变量类为例,getAndIncrement没有锁的机制,字段value要借助volatile原语,保证线程间的数据可见性。 compareAndSet 利用JNI(Java Native Interface)来完成CPU指令的操作。

synchronized实现原理

  • 锁级别
    无锁状态、偏向锁状态 、轻量级锁状态 、 重量级锁状态,状态会随着竞争情况逐渐升级,只能升级不能降级。

  • 锁标记
    锁标记存放在Java对象头的Mark Word中。

  • 同步实现
    基于Monitor从互斥执行、协作两个方面来支持线程之间的同步。
    1、Java 使用对象锁保证工作在共享的数据集上的线程互斥执行。
    2、使用 notify/notifyAll/wait 方法来协同不同线程之间的工作。
    3、Class和Object都关联了一个Monitor。

Monitor工作机理
  • 线程进入同步方法中。
  • 为了继续执行临界区代码,线程必须获取 Monitor 锁。如果获取锁成功,将成为该监视者对象的拥有者。任一时刻内,监视者对象只属于一个活动线程(The Owner)
  • 拥有监视者对象的线程可以调用 wait() 进入等待集合(Wait Set),同时释放监视锁,进入等待状态。
  • 其他线程调用 notify() / notifyAll() 接口唤醒等待集合中的线程,这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。
  • 同步方法执行完毕了,线程退出临界区,并释放监视锁。
    每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的monitor,获取规则如下:
  • 如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。
  • 如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以synchronized关键字实现的锁是可重入的锁。
  • 如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。
  • 只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。
CAS与Synchronized的使用情景

1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2、对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充: synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

volatile与Synchronized对比

volatile 是线程同步的轻量级实现,volatile 修饰的变量,可以解决变量读时可见性问题,无法保证原子性。对于多线程访问同一个实例变量还是需要加锁同步
在多线程定义中,volatile 关键字主要是在属性上使用的,表示此属性为直接数据操作,而不进行副本的拷贝处理。

volatile synchronized
修饰 只能用于修饰变量 可以用于修饰方法、代码块
线程阻塞 不会发生线程阻塞 会发生阻塞
原子性 不能保证变量的原子性 可以保证变量原子性
可见性 可以保证变量在线程之间访问资源的可见性 可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步
同步性 能保证变量在私有内存和主内存间的同步 synchronize是多线程之间访问资源的同步性
AQS算法介绍

AQS(AbstractQueuedSynchronizer抽象的队列式同步器)维护一个 volatile int state表示同步状态(代表共享资源)和一个FIFO线程等待双向队列(多线程争用资源被阻塞时会进入此队列)完成获取锁线程的排队工作。


  • FIFO双向队列工作步骤

    1. 同步器包含两个节点类型的应用,一个指向头节点,一个指向尾节点,未获取到锁的线程会创建节点线程安全(compareAndSetTail)的加入队列尾部。同步队列遵循FIFO,首节点是获取同步状态成功的节点。


      152240092750145e60c4728.jpg
    2. 未获取到锁的线程将创建一个节点,设置到尾节点。


      1522400927498815436eb72.jpg
    3. 首节点的线程在释放锁时,将会唤醒后继节点。而后继节点将会在获取锁成功时将自己设置为首节点。


      1522400927518234a5bfa31.jpg
  • 资源共享方式
    Exclusive独占:只有一个线程能执行,如ReentrantLock。
    Share共享:多个线程可同时执行,如Semaphore/CountDownLatch。

  • state的访问方式
    getState():获取当前同步状态。
    setState():设置当前同步状态。
    compareAndSetState():使用CAS设置当前状态,该方法能够保证原子性。

  • 同步器:多线程并发的执行,通过某种共享状态来同步,只有当状态满足某条件,才能触发线程执行。

    基于AQS构建的同步器,只在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。不同的自定义同步器争用共享资源的方式也不同,实现时只需要实现共享资源state的获取与释放方式即可。

    一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

    自定义同步器要实现以下几种方法:
    • isHeldExclusively():该线程是否正在独占资源。用到condition才需要去实现它。
    • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
    • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
    • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
    • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

    以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

    以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

concurrent包的实现

Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。


CAS具有volatile读和volatile写的内存语义。

  • Java线程之间通信方式

    1. A线程写volatile变量,随后B线程读这个volatile变量。
    2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
    3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
    4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
  • 通用化的实现模式

    1. 首先,声明共享变量为volatile;
    2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  • 包中各类型关系
    Concurrent包中包含有一系列能够让 Java 的并发编程变得更加简单轻松的类。
    并发工具包详解

    Concurrent包关系

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容