learnJUC系列(四)Java的各种锁
日常工作学习中,经常有听到各种锁,如乐观锁/悲观锁、公平锁/非公平锁、可重入锁、自旋锁、死锁等等。此处通过一些具体的demo结合源码,来加深理解。
1、公平锁和非公平锁
1.1 是什么
并发包java.util.concurrent.locks
包下,reentrantLock的创建,可以指定构造函数的boolean类型,来得到公平锁或非公平锁(默认)。如,Lock lock = new ReentrantLock();
公平锁是根据线程的先来后到,而非公平锁允许加塞,优先级不确定。
查看ReentrantLock
源码中的构造函数。
1.2 两者区别
公平锁:Thread acquire a fair lock in the order in which they requested id.
在并发环境下,每个线程在获取锁时,会先查看此锁维护的等待队列,如果为空,或当前线程为第一个,就占有锁,否则加入到等待队列,按照FIFO的规则排队。
非公平锁:a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.
非公平锁会直接尝试占有资源,如果尝试失败,再采用类似公平锁的方式
1.3 其他
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁。默认非公平锁,能够发挥吞吐量大的特点,无需维护顺序,能够充分发挥多线程抢占式的处理优势。
其中,synchronized是一种非公平锁,原因是,其底层使用的是mutex锁实现,而mutex锁不能保证公平性,目的也是为了提高性能。
非公平锁吞吐量大大原因分析:
比如 A 占用锁的时候,B 请求获取锁,发现被 A 占用之后,堵塞等待被唤醒,这个时候 C 同时来获取 A 占用的锁,如果是公平锁 C 后来者发现不可用之后一定排在 B 之后等待被唤醒,而非公平锁则可以让 C 先用,在 B 被唤醒之前 C 已经使用完成,从而节省了 C 等待和唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的原因。
2、可重入锁(即递归锁)
// 两个方法是同一锁,能够进入
public sync void method01()
{
method02();
}
public sync void method02()
{
……
}
2.1 是什么
同一线程外层函数获得锁之后,内层递归函数仍能获取该锁的代码,在同一个线程在外层方法获取锁的时候,再进入内层方法会自动获取锁,即线程可以进入任何一个它已经拥有的锁所同步的代码块。
ReentrantLock、Synchronized就是典型的可重入锁。
2.2 代码示例
package com.panda00hi.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可重入锁示例
*
* @author panda00hi
* @date 2022/4/10
*/
public class ReenterLockDemo {
public static void main(String[] args) {
System.out.println("******Synchronized可重入锁示例******");
Phone phone = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("******ReentrantLock可重入锁示例******");
GetSetTest getSetTest = new GetSetTest();
Thread t3 = new Thread(getSetTest, "t3");
Thread t4 = new Thread(getSetTest, "t4");
t3.start();
t4.start();
}
}
class GetSetTest implements Runnable {
Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
public void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " invoked get()");
set();
} finally {
lock.unlock();
}
}
public void set() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " ####invoked set()");
} finally {
lock.unlock();
}
}
}
/**
* 资源类
*/
class Phone {
public synchronized void sendSMS() {
System.out.println(Thread.currentThread().getName() + " invoked sendSMS()");
sendEmail();
}
public synchronized void sendEmail() {
System.out.println(Thread.currentThread().getName() + " ####invoked sendEmail()");
}
}
运行结果:
******Synchronized可重入锁示例******
t1 invoked sendSMS()
t1 ####invoked sendEmail()
t2 invoked sendSMS()
t2 ####invoked sendEmail()
******ReentrantLock可重入锁示例******
t3 invoked get()
t3 ####invoked set()
t4 invoked get()
t4 ####invoked set()
3、自旋锁
3.1 是什么
尝试获取锁的线程不会立即阻塞,而是采用循环的方式取尝试获得锁,好处是减少线程上下文切换的消耗,没有类似wait的阻塞,缺点是循环会消耗CPU。
如,之前CAS部分有提到的比较交换操作
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;
}
3.2 手写自旋锁
通过CAS完成自旋锁,A线程先进来调用mylock方法自己持有锁5秒钟,B随后进来发现当前线程持有锁,不为null,所以只能等待(自旋),直到A释放锁后,B抢到,结束自旋。
package com.panda00hi.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 手写自旋锁示例
*
* @author panda00hi
* @date 2022/4/10
*/
public class SpinLockDemo {
// 原子引用线程,初始值为null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + " come in");
// 如果为null,则将当前线程放入,返回true,取反,所以while不进入
while (!atomicReference.compareAndSet(null, thread)) {
// AA线程进入myLock方法,BB线程也进入,但是由于AA线程进入后,率先将atomicReference
// 改为了自身线程对象,所以BB等其他线程将进入while循环内部,即自旋,直到AA线程通过
// myUnlock方法,将atomicReference又置为null,BB线程才能跳出while循环,结束自旋
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting...");
}
}
public void myUnlock() {
Thread thread = Thread.currentThread();
// 若为当前线程对象,则置为null
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + " invoked myUnlock()");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnlock();
}, "AA").start();
new Thread(() -> {
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnlock();
}, "BB").start();
new Thread(() -> {
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnlock();
}, "CC").start();
}
}
运行结果:
AA come in
BB come in
CC come in
BB waiting...
CC waiting...
BB waiting...
CC waiting...
AA invoked myUnlock()
CC waiting...
BB waiting...
CC invoked myUnlock()
BB waiting...
BB invoked myUnlock()
4、独占锁(写)、共享锁(读)、互斥锁
4.1 是什么
传统的Synchronized和Lock虽然保证了“读”和“写”的线程安全,但是也导致了“读”的并发性能大大降低,因此需要更细粒度的控制。
独占锁:指该锁一次只能被一个线程锁持有,对ReentrantLock和Synchronized而言都是独占锁。
共享锁:该锁可被多个线程锁持有,ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁。
互斥锁:读锁的共享锁可以保证高并发读是高效的,读写、写读、写写的过程是互斥的。(有写的操作,就是互斥的)
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该允许同时进行。但是,如果有一个线程想要写共享资源,就不应该再有其他线程对资源进行读或写。
总结:读读可共存、读写不可共存、写写不可共存
写操作:原子+独占,整个过程必须是一个完整的过程,不能被加塞、中断
4.2 代码示例
package com.panda00hi.juc;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 读写锁示例
* 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该允许同时进行
* 但是
* 如果有一个线程想要写共享资源,就不应该再有其他线程对资源进行读或写
* 总结
* 读读可共存
* 读写不可共存
* 写写不可共存
* 写操作:原子+独占,整个过程必须是一个完整的过程,不能被加塞、中断
*
* @author panda00hi
* @date 2022/4/10
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 5个线程读,5个线程写
for (int i = 0; i < 5; i++) {
// lambda表达式要求是final类型的,所以提前赋值final型的临时变量
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt);
}, "Thread " + i).start();
}
for (int i = 0; i < 5; i++) {
// lambda表达式要求是final类型的,所以提前赋值final型的临时变量
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, "Thread " + i).start();
}
}
}
/**
* 缓存资源类
* 同一时刻,可供多人读,但是仅供一人写
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// 可以保证一致性安全性,但是失去了多个线程读的并发性能
// private Lock lock = new ReentrantLock();
/**
* 写操作必须保证原子性,独占不可分割。即,同一线程的"正在写入"、"写入完成"是连续的
*/
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + " 正在写入:" + key);
// 模拟网络时延500毫秒
try {
TimeUnit.MICROSECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 写入完成:");
}
/**
* 读是可以共享的,各个线程不一样
*/
public void get(String key) {
System.out.println(Thread.currentThread().getName() + " 正在读取:");
// 模拟网络时延500毫秒
try {
TimeUnit.MICROSECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + " 正在读取:" + result);
}
}
运行结果:
Thread 0 正在写入:0
Thread 2 正在写入:2
Thread 1 正在写入:1
Thread 3 正在写入:3
Thread 4 正在写入:4
Thread 0 正在读取:
Thread 1 正在读取:
Thread 2 正在读取:
Thread 3 正在读取:
Thread 4 正在读取:
Thread 2 正在读取:null
Thread 0 正在读取:null
Thread 4 写入完成:
Thread 0 写入完成:
Thread 1 写入完成:
Thread 3 写入完成:
Thread 1 正在读取:null
Thread 2 写入完成:
Thread 3 正在读取:null
Thread 4 正在读取:null
发现某个线程执行写入的操作会被打断,与预期不符。如果直接使用Synchronized或者Lock的话,会牺牲读的性能,不推荐。
解决方法:使用读写锁,针对读和写的不同操作进行精细控制。JDK中有提供具体的实现。ReadWriteLock接口的其中一个实现类ReentrantReadWriteLock。
4.3 使用读写锁ReentrantReadWriteLock解决问题
package com.panda00hi.juc;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁示例
* 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该允许同时进行
* 但是
* 如果有一个线程想要写共享资源,就不应该再有其他线程对资源进行读或写
* 总结
* 读读可共存
* 读写不可共存
* 写写不可共存
* 写操作:原子+独占,整个过程必须是一个完整的过程,不能被加塞、中断
*
* @author panda00hi
* @date 2022/4/10
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 5个线程读,5个线程写
for (int i = 0; i < 5; i++) {
// lambda表达式要求是final类型的,所以提前赋值final型的临时变量
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt);
}, "Thread " + i).start();
}
for (int i = 0; i < 5; i++) {
// lambda表达式要求是final类型的,所以提前赋值final型的临时变量
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, "Thread " + i).start();
}
}
}
/**
* 缓存资源类
* 同一时刻,可供多人读,但是仅供一人写
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// 可以保证一致性安全性,但是失去了多个线程读的并发性能
// private Lock lock = new ReentrantLock();
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
/**
* 写操作必须保证原子性,独占不可分割。即,同一线程的"正在写入"、"写入完成"是连续的
*/
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写入:" + key);
// 模拟网络时延500毫秒
try {
TimeUnit.MICROSECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入完成:" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
/**
* 读是可以共享的,各个线程不一样
*/
public void get(String key) {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在读取:");
// 模拟网络时延500毫秒
try {
TimeUnit.MICROSECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取完成:key " + key + " result " + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
运行结果:
Thread 0 正在写入:0
Thread 0 写入完成:0
Thread 1 正在写入:1
Thread 1 写入完成:1
Thread 2 正在写入:2
Thread 2 写入完成:2
Thread 3 正在写入:3
Thread 3 写入完成:3
Thread 4 正在写入:4
Thread 4 写入完成:4
Thread 0 正在读取:
Thread 1 正在读取:
Thread 2 正在读取:
Thread 3 正在读取:
Thread 4 正在读取:
Thread 0 读取完成:key 0 result 0
Thread 1 读取完成:key 1 result 1
Thread 4 读取完成:key 4 result 4
Thread 2 读取完成:key 2 result 2
Thread 3 读取完成:key 3 result 3
5、死锁原因分析和问题排查
5.1 是什么
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法继续推进下去。
5.2 复现代码
package com.panda00hi.juc;
import java.util.concurrent.TimeUnit;
/**
* 死锁复现示例
*
* @author panda00hi
* @date 2022/4/10
*/
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA,lockB),"thread AAA").start();
new Thread(new HoldLockThread(lockB,lockA),"thread BBB").start();
}
}
class HoldLockThread implements Runnable {
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + " 自己持有:" + lockA + " 尝试获得:" + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + " 自己持有:" + lockB + " 尝试获得:" + lockA);
}
}
}
}
运行结果:
thread AAA 自己持有:lockA 尝试获得:lockB
thread BBB 自己持有:lockB 尝试获得:lockA
程序进入阻塞状态(死锁)
5.3 死锁定位和解决
jps -l : 查看进程的进程号
➜ learnJUC git:(master) ✗ jps -l
86929 org.jetbrains.jps.cmdline.Launcher
54274 org.jetbrains.jps.cmdline.Launcher
86930 com.panda00hi.juc.DeadLockDemo
87016 sun.tools.jps.Jps
50859
jstack+进程号:查看堆栈信息
➜ learnJUC git:(master) ✗ jstack 86930
2022-04-10 01:22:47
Full thread dump OpenJDK 64-Bit Server VM (25.312-b07 mixed mode):
"Attach Listener" #16 daemon prio=9 os_prio=31 tid=0x0000000130808800 nid=0x4a07 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"DestroyJavaVM" #15 prio=5 os_prio=31 tid=0x000000013081a000 nid=0x1603 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"thread BBB" #14 prio=5 os_prio=31 tid=0x0000000140064000 nid=0xa503 waiting for monitor entry [0x00000001703b2000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.panda00hi.juc.HoldLockThread.run(DeadLockDemo.java:41)
- waiting to lock <0x0000000715719160> (a java.lang.String)
- locked <0x0000000715719198> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"thread AAA" #13 prio=5 os_prio=31 tid=0x0000000130072000 nid=0xa703 waiting for monitor entry [0x00000001701a6000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.panda00hi.juc.HoldLockThread.run(DeadLockDemo.java:41)
- waiting to lock <0x0000000715719198> (a java.lang.String)
- locked <0x0000000715719160> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Service Thread" #12 daemon prio=9 os_prio=31 tid=0x0000000136810800 nid=0xa903 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread3" #11 daemon prio=9 os_prio=31 tid=0x0000000130816000 nid=0x5803 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread2" #10 daemon prio=9 os_prio=31 tid=0x0000000140844000 nid=0x5603 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #9 daemon prio=9 os_prio=31 tid=0x000000013080d800 nid=0x4103 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #8 daemon prio=9 os_prio=31 tid=0x000000014080a000 nid=0x4303 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"JDWP Command Reader" #7 daemon prio=10 os_prio=31 tid=0x000000013080b800 nid=0x4503 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"JDWP Event Helper Thread" #6 daemon prio=10 os_prio=31 tid=0x000000013080a800 nid=0x3f03 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"JDWP Transport Listener: dt_socket" #5 daemon prio=10 os_prio=31 tid=0x000000013080a000 nid=0x4707 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x000000014004e000 nid=0x3e03 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x0000000130010000 nid=0x4d03 in Object.wait() [0x000000016ea0a000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000715588ef0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x0000000715588ef0> (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=0x0000000140038000 nid=0x4f03 in Object.wait() [0x000000016e7fe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000715586c08> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x0000000715586c08> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=31 tid=0x0000000140031000 nid=0x3403 runnable
"ParGC Thread#0" os_prio=31 tid=0x0000000140815800 nid=0x2107 runnable
"ParGC Thread#1" os_prio=31 tid=0x0000000140816000 nid=0x1f03 runnable
"ParGC Thread#2" os_prio=31 tid=0x0000000140817000 nid=0x5403 runnable
"ParGC Thread#3" os_prio=31 tid=0x0000000140817800 nid=0x5203 runnable
"ParGC Thread#4" os_prio=31 tid=0x0000000140818800 nid=0x2c03 runnable
"ParGC Thread#5" os_prio=31 tid=0x0000000140819000 nid=0x2e03 runnable
"ParGC Thread#6" os_prio=31 tid=0x000000014081a000 nid=0x3003 runnable
"ParGC Thread#7" os_prio=31 tid=0x000000014081a800 nid=0x3103 runnable
"ParGC Thread#8" os_prio=31 tid=0x000000014081b800 nid=0x3203 runnable
"VM Periodic Task Thread" os_prio=31 tid=0x0000000136811800 nid=0x5b03 waiting on condition
JNI global references: 1465
Found one Java-level deadlock:
=============================
"thread BBB":
waiting to lock monitor 0x000000013000b0d0 (object 0x0000000715719160, a java.lang.String),
which is held by "thread AAA"
"thread AAA":
waiting to lock monitor 0x000000013000d5f0 (object 0x0000000715719198, a java.lang.String),
which is held by "thread BBB"
Java stack information for the threads listed above:
===================================================
"thread BBB":
at com.panda00hi.juc.HoldLockThread.run(DeadLockDemo.java:41)
- waiting to lock <0x0000000715719160> (a java.lang.String)
- locked <0x0000000715719198> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"thread AAA":
at com.panda00hi.juc.HldLockThread.run(DeadLockDemo.java:41)
- waiting to lock <0x0000000715719198> (a java.lang.String)
- locked <0x0000000715719160> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
可看到关于deadlock
的信息,以及对应的代码位置。