Version: Next

各种锁

乐观锁与悲观锁

乐观锁

乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会加锁

但在更新时会判断在此期间别人有没有修改该数据,通常采用在写时先读出当前版本号然后加锁的方法。

具体过程为:

  • 比较当前版本号与上一次的版本号
  • 如果版本号一致则更新
  • 如果版本号不一致,则重复读、比较操作

Java中的乐观锁大多是通过CAS(Compare and Swap)比较和交换操作实现的,CAS是一种原子性更新操作。

悲观锁

悲观锁采用悲观思想处理数据,在每次读取数据时都认为别人会修改数据,所以每次在读写数据时都会加锁,这样别人想读写这个数据时就会阻塞、等待直到获取锁

Java中的悲观锁大部分基于AQS (Abstract Queued Synchronized)抽象的队列同步器架构实现。

AQS定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的synchronized、ReentrantLock、Samaphore、CountDownLatch等

该框架下的锁会先尝试以CAS乐观锁去获取锁,如果获取不到,则会转为悲观锁(如ReentrantLock)

共享锁和独占锁(互斥锁)

Java并发包提供的加锁模式分为独占锁和共享锁

独占锁:

也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的实现

共享锁

允许多个线程同时获取该锁,并发访问共享资源。ReentrantReadWriteLock的加锁和解锁操作最终都调用内部类Sync提供的方法

Sync通过集成AQS实现。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,分别标识AQS队列中等待线程的锁获取模式

独占锁时一种悲观的加锁策略,同一时刻只允许一个读线程读取锁资源,限制了读操作的并发性

因为并发读线程不会影响到数据一致性,因此共享锁采用了乐观的加锁策略,允许多个读线程同时访问共享资源

读写锁

ReadWriteLock接口

  • 唯一实现类:ReentrantReadWriteLock
  • readLock()writeLock()
  • 维护了一对Lock,一个用于读、另一个用于写
  • 读取时,允许多个线程读 (共享锁
  • 写入时,只允许一个线程写 (独占锁)(互斥锁
  • 读写操作互斥

重量级锁与轻量级锁

重量级锁

重量级锁是基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态和内核态之间切换,相对开销较大

synchronized在内部基于监视器锁(Monitor)实现,监视器锁底层基于操作系统的Mutex Lock实现,因此synchronized属于重量级锁

重量级锁需要在用户态和核心态之间做切换,所以synchronized的运行效率不高

JDK 1.6之后,为了减少获取锁和释放锁所带来的的性能消耗,引入了轻量级锁偏向锁

轻量级锁

轻量级锁的设计核心是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能

轻量级锁适用于线程交替之星同步代码块的情况(即互斥操作)

如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁


偏向锁

针对同一个锁被同一个线程多次获取的情况

偏向锁用于在某个线程获取某个锁后,消除这个线程重入的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)

偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取和释放需要多次CAS原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率

在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时

综上,轻量级锁用于提高多个线程交替执行同步块的性能,偏向锁则在某个线程交替执行同步快时,进一步提高性能


分段锁

分段锁并非一种实际的锁,而是一种锁设计思想

用于将数据分段并在每个分段上加单独的锁,把锁进一步细粒度化,以提高并发效率

典型的实现是ConcurrentHashMap


同步锁

互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。

同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。


轮询锁与定时锁

轮询锁

通过ReentrantLock的tryLock()方法获取锁,如果有可用的锁,则获取该锁并返回true,如果无可用锁,则立即返回false

定时锁

通过boolean tryLock(long time, TimeUnit unit) throws InterruptedException获取定时锁。

如果在给定的时间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回true。

如果在给定的时间内获取不到可用锁,将禁用当前线程,并且在发生以下三种情况前,该线程一直处于休眠状态

  • 当前线程获取到了可用锁并返回true
  • 当前线程在进入此方法时设置了该线程的中断状态,或者当前线程在获取锁时被中断,则抛出InterruptedException,并清除当前线程的已中断状态
  • 当前线程获取锁的时间超过了指定的等待时间,则将返回false;如果设定的时间小于等于0,则该方法将完全不等待

公平锁与非公平锁

公平锁

非常公平,不能插队,必须先来后到

公平锁Fair Lock指分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程

非公平锁

非常不公平,可以插队

  • 公平的情况:比如两个任务,一个3秒钟搞定,一个要3小时,如果3秒排到了3小时这个任务后面,那它就得等3小时的任务先跑完才能执行,这不太好
  • 因此synchronized、Lock默认是非公平的

非公平锁Nonfair Lock指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待

因为公平锁需要在多线程的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多

  • ReentrantLock默认构造方法
public ReentrantLock() {
sync = new NonfairSync();
}

可以看到采用的是非公平锁Nonfair

  • ReentrantLock重载构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

传入一个boolean值决定锁的公平性


可重入锁(递归锁)

可重入锁也叫递归锁

指在同一线程中,在外层函数获得到该锁后,内层的递归韩式仍然可以继续获得该锁

ReentrantLocksynchronized都是可重入锁

Synchronized——一把锁

public class Phone {
public synchronized void send() {
System.out.println(Thread.currentThread().getName() + " send message");
call(); // 这里也有一把锁
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName() + " calling");
}
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone1.send();
}, "Thread A ").start();
new Thread(() -> {
phone2.send();
}, "Thread B ").start();
}
}
Thread A send message
Thread A calling
Thread B send message
Thread B calling

send()方法中的call()方法也会获得锁,体现重入,因此只有send()中的call()方法也执行完毕时,才会释放锁

  • 也可以按照先前八锁现象的分析,其实始终锁的都是调用synchronized方法的对象

ReentrantLock——两把锁

public class Phone {
Lock lock = new ReentrantLock();
public void send() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " send message");
call(); // 这里也有一把锁
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void call() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " calling");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone1.send();
}, "Thread A ").start();
new Thread(() -> {
phone2.send();
}, "Thread B ").start();
}
}
Thread A send message
Thread A calling
Thread B send message
Thread B calling

自旋锁

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}

不断循环监听,自旋等待,直到条件满足,执行操作

自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争的锁就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需要等一等(称为自旋)

在等待持有锁的线程释放锁后可立即获取锁,这样就能避免用户线程在用户态和内核态之间的频繁切换而到时时间消耗

线程在自旋是会占用CPU,在线程长时间自旋获取不到锁时,将会产生CPU的浪费,甚至有些线程永远无法获取锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间。

在线程直线的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁

自己实现一个自旋锁

public class SpinLock {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加锁
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println("当前线程 : " + thread.getName() + "进入了我的锁");
// 只要达不到预期值null,就自旋等待
while (!atomicReference.compareAndSet(null, thread)) {
}
}
//解锁
public void myUnlock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println("当前线程 : " + thread.getName() + "释放了锁");
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
new Thread(() -> {
spinLock.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinLock.myUnlock();
}
}, "Thread 1").start();
new Thread(() -> {
spinLock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinLock.myUnlock();
}
}, "Thread 2").start();
}
}
当前线程 : Thread 1进入了我的锁
当前线程 : Thread 2进入了我的锁
当前线程 : Thread 1释放了锁
当前线程 : Thread 2释放了锁

在Thread 1执行期间,Thread 2尝试拿到锁,但是Thread 1并没有释放锁,于是Thread 2自旋等待


死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源释放才能运行,而导致两个或多个线程都在等待对方释放资源,全部停止执行的情形。某一个同步块同时拥有「两个以上对象的锁」时,就可能发生死锁问题

// 死锁
public class Demo01DeathLock {
public static void main(String[] args) {
Makeup girl1 = new Makeup(0,"小红");
Makeup girl2 = new Makeup(1,"小美");
new Thread(girl1,"小红").start();
new Thread(girl2,"小美").start();
}
}
//口红
class Lipstick {}
//镜子
class Mirror {}
class Makeup extends Thread {
// static表示全局唯一的对象
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
//定义选择
int choice;
//定义使用化妆品的人
String girlName;
public Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
// 化妆
try {
makeUp();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//互相持有对方的锁,就是需要拿到对方的资源
private void makeUp() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) { //获得口红的锁
System.out.println(this.girlName + "获得了口红的锁");
Thread.sleep(1000);
synchronized (mirror) { // 一秒种后,想获得镜子的锁
System.out.println(this.girlName + "获得了镜子的锁");
}
}
} else {
synchronized (mirror) { //获得镜子的锁
System.out.println(this.girlName + "获得了镜子的锁");
Thread.sleep(2000);
synchronized (lipstick) { // 两秒种后,想获得口红的锁
System.out.println(this.girlName + "获得了口红的锁");
}
}
}
}
}
小红获得了口红的锁
小美获得了镜子的锁

互相持有对方锁,程序卡住。

死锁的避免方法:

  • 产生死锁的四个必要条件:
    1. 互斥条件: 一个资源每次只能被一个线程使用
    2. 请求保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
    3. 不可剥夺条件:线程已获得的资源,在未用完之前,不能强行剥夺
    4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

上面列出了死锁的四个必要条件,只需要想办法破除其中任意一个或多个条件,就能避免死锁的发生

如何排查死锁

使用 jps工具

命令:jps -l

➜ ~ jps -l
4561
20275 org.jetbrains.jps.cmdline.Launcher
20276 DeathLock.Demo01DeathLock
20315 sun.tools.jps.Jps

可以获取当前各种Java进程的进程号

使用jstack查看进程信息

命令:jstack 进程号

➜ ~ jstack 20276
2020-07-02 01:33:31
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.251-b08 mixed mode):
"Attach Listener" #16 daemon prio=9 os_prio=31 tid=0x00007f9f9c882000 nid=0xa703 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"DestroyJavaVM" #15 prio=5 os_prio=31 tid=0x00007f9f9c809800 nid=0xe03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"小美" #14 prio=5 os_prio=31 tid=0x00007f9f9c881000 nid=0xa803 waiting for monitor entry [0x000070000924c000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeathLock.Makeup.makeUp(Demo01DeathLock.java:62)
- waiting to lock <0x000000076ac30f90> (a DeathLock.Lipstick)
- locked <0x000000076ac338a0> (a DeathLock.Mirror)
at DeathLock.Makeup.run(Demo01DeathLock.java:39)
at java.lang.Thread.run(Thread.java:748)
"小红" #13 prio=5 os_prio=31 tid=0x00007f9f9c852800 nid=0xa903 waiting for monitor entry [0x0000700009149000]
java.lang.Thread.State: BLOCKED (on object monitor)
at DeathLock.Makeup.makeUp(Demo01DeathLock.java:53)
- waiting to lock <0x000000076ac338a0> (a DeathLock.Mirror)
- locked <0x000000076ac30f90> (a DeathLock.Lipstick)
at DeathLock.Makeup.run(Demo01DeathLock.java:39)
at java.lang.Thread.run(Thread.java:748)
"Service Thread" #10 daemon prio=9 os_prio=31 tid=0x00007f9fa0012000 nid=0x4203 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread3" #9 daemon prio=9 os_prio=31 tid=0x00007f9f9e81c000 nid=0x4303 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread2" #8 daemon prio=9 os_prio=31 tid=0x00007f9f9e81b800 nid=0x3e03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #7 daemon prio=9 os_prio=31 tid=0x00007f9f9e81a800 nid=0x3d03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #6 daemon prio=9 os_prio=31 tid=0x00007f9f9e81a000 nid=0x3c03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #5 daemon prio=5 os_prio=31 tid=0x00007f9f9d808800 nid=0x3b03 runnable [0x0000700008a34000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000076ac839b8> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x000000076ac839b8> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:61)
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007f9f9f821800 nid=0x3a03 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007f9f9e80c800 nid=0x3003 in Object.wait() [0x00007000086a5000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076ab08ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x000000076ab08ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007f9f9e011800 nid=0x2e03 in Object.wait() [0x00007000085a2000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076ab06c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x000000076ab06c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=31 tid=0x00007f9f9c80b800 nid=0x2d03 runnable
"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007f9f9c80a800 nid=0x1b07 runnable
"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007f9fa0808800 nid=0x2003 runnable
"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007f9fa0809000 nid=0x1e03 runnable
"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007f9fa0809800 nid=0x2a03 runnable
"GC task thread#4 (ParallelGC)" os_prio=31 tid=0x00007f9fa080a000 nid=0x5403 runnable
"GC task thread#5 (ParallelGC)" os_prio=31 tid=0x00007f9fa080b000 nid=0x5203 runnable
"GC task thread#6 (ParallelGC)" os_prio=31 tid=0x00007f9fa080b800 nid=0x5003 runnable
"GC task thread#7 (ParallelGC)" os_prio=31 tid=0x00007f9fa080c000 nid=0x4e03 runnable
"VM Periodic Task Thread" os_prio=31 tid=0x00007f9fa000c800 nid=0x5503 waiting on condition
JNI global references: 15
Found one Java-level deadlock:
=============================
"小美":
waiting to lock monitor 0x00007f9f9e015168 (object 0x000000076ac30f90, a DeathLock.Lipstick),
which is held by "小红"
"小红":
waiting to lock monitor 0x00007f9f9e017948 (object 0x000000076ac338a0, a DeathLock.Mirror),
which is held by "小美"
Java stack information for the threads listed above:
===================================================
"小美":
at DeathLock.Makeup.makeUp(Demo01DeathLock.java:62)
- waiting to lock <0x000000076ac30f90> (a DeathLock.Lipstick)
- locked <0x000000076ac338a0> (a DeathLock.Mirror)
at DeathLock.Makeup.run(Demo01DeathLock.java:39)
at java.lang.Thread.run(Thread.java:748)
"小红":
at DeathLock.Makeup.makeUp(Demo01DeathLock.java:53)
- waiting to lock <0x000000076ac338a0> (a DeathLock.Mirror)
- locked <0x000000076ac30f90> (a DeathLock.Lipstick)
at DeathLock.Makeup.run(Demo01DeathLock.java:39)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.

可以看到查出了一个死锁信息


锁优化

减少锁的持有时间

减少锁持有的时间指只在有线程安全要求的程序上来加锁来尽量减少同步代码块对锁的持有时间

减小锁粒度

减少锁粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并发度,减伤同一个锁上的竞争

在减少锁的竞争后,偏向锁、轻量级锁的使用效率才会提高

减少锁粒度的典型案例是JDK 1.5 到 JDK 1.8中的分段锁

锁分离

锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想实例就是读写锁(ReadWriteLock),它根据锁的功能将锁分离为读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全又提高了效率

操作分离思想可以进一步延伸为只要操作互相不影响,就可以进一步拆分,比如LinkedBlockingQueue从头部取出数据,并从尾部加入数据

锁粗化

锁粗化为了保证性能,会尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分的太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升

在这种情况下,建议将关联性强的锁集中起来处理,以提高系统整体效率

锁消除

在开发中经常出现在不需要使用锁的情况下误用锁而引起性能下降,这多数是因为程序编码不规范引起的

这是需要检查并消除这些不必要的锁来提高系统的性能