synchronized和ReentrantLock分析
参考:
子路老师博客:https://blog.csdn.net/java_lyvee/article/details/110996764
并发编程网:http://ifeve.com/monitors-java-synchronization-mechanism/
代码需求:
有一个猫窝
有猫长老、猫爸、 猫妈妈,还有6只小猫
猫爸爸需要进猫窝拿钱才捕猎,不然就睡觉
猫妈妈需要进猫窝拿扫把才做家务,不然也是睡觉
6只小猫就也想等着进猫窝玩
但是猫窝的进出权限是猫长老控制的
思路:
所以需要有4类线程,分别竞争一个猫窝,用syhchronized来实现。
代码:
⚠️注意:wait条件的时候要用while去判断,不能用if。
@Slf4j(topic = "console")
public class LockTest {
static boolean isMoney = false;
static boolean isClean = false;
public static void main(String[] args) throws InterruptedException {
// 需要while(条件) + wait()
Object home = new Object();
new Thread(() -> {
synchronized (home) {
while (!isMoney) {
log.debug("sleeping");
try {
home.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("hunting");
}
}, "catDad").start();
new Thread(() -> {
synchronized (home) {
while (!isClean) {
log.debug("sleeping");
try {
home.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("cleaning");
}
}, "catMom").start();
//为了能让dad和mon先调度
TimeUnit.MILLISECONDS.sleep(10);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
synchronized (home) {
log.debug("playing!!!");
}
}, "littleKitty"+ i).start();
}
new Thread(() -> {
synchronized (home){
isMoney = true;
isClean = true;
/*
notifyAll只能唤醒所有线程,可能会导致唤醒不应该的线程。
ReentrantLock 的 Condition可以解决这个问题。
*/
home.notifyAll();
}
},"catElder").start();
}
}
缺点:
使用synchronized
的线程唤醒notify
只能叫醒一个线程,而notifyAll
能叫醒该锁的所有线程,假如这时有3个条件的,但我们只要叫醒2个,就会有一个虚假唤醒了。java的AQS提供了ReentrantLock
的Condition
,能很好解决。
ReentrantLock、Condition的await和signal
思路
Q:为啥使用ReentrantLock
的Condition
呢?
A:synchronized的唤醒和唤醒条件的问题,能通过为一个ReentrantLock对象创建多个Condition对象,而各个Condition对象可以分开唤醒各自的waiting线程;
ReentrantLock用法
ReentrantLock可以替换synhcronized关键字,一个是jvm提供的用C++实现的,一个java的实现的。
//创建重入锁对象
ReentrantLock lock = new ReentrantLock();
//创建Condition对象
Condition moneyCondition = lock.newCondition();
new Thread(() -> {
//上锁的方式。通常要和try搭配,然后把解锁写在finally里
lock.lock();
try {
while (!isMoney) {
log.debug("sleeping");
//各自的条件对象调用await方法
moneyCondition.await();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//解锁通常写在finnaly,不然抛异常后锁会不释放。
//synchronized是由jvm管理的,只要抛异常了,锁就自动释放了。
lock.unlock();
}
log.debug("hunting");
}, "catDad").start();
代码:
@Slf4j(topic = "console")
public class ReentrantLockTest {
static boolean isMoney = false;
static boolean isClean = false;
public static void main(String[] args) throws InterruptedException {
//创建一个重入锁
ReentrantLock lock = new ReentrantLock();
//为重入锁创建2个条件对象
Condition moneyCondition = lock.newCondition();
Condition cleanCondition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
while (!isMoney) {
log.debug("sleeping");
//各自的条件对象调用await方法
moneyCondition.await();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
log.debug("hunting");
}, "catDad").start();
new Thread(() -> {
lock.lock();
try {
while (!isClean) {
log.debug("sleeping");
cleanCondition.await();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
log.debug("cleaning");
}, "catMom").start();
TimeUnit.MILLISECONDS.sleep(10);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
lock.lock();
try {
log.debug("playing!!!");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "littleKitty" + i).start();
}
new Thread(() -> {
lock.lock();
try {
//可以为各自的的条件进行唤醒,
//signal对应notify唤醒一个线程
//signal对应notifyAll唤醒所有线程
isMoney = true;
moneyCondition.signalAll();
isClean = true;
cleanCondition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "catElder").start();
}
}
synchronized分析
线程的6个状态
参考这2篇,国内那篇透彻讲解最好别看了
https://blog.csdn.net/Baisitao_/article/details/99766322
https://www.cnblogs.com/aspirant/p/8900276.html
然后我总结了一个图:
总体来说在java并发编程中wait和time-wait有2种,
-
一种是在进行了锁操作(synchroized):
它从waiting/timed-waiting状态不会直接到runnable。会先进行blocked等待获取锁。
-
另外一种是没有进行锁操作的:
它从waiting/timed-waiting状态直接到runnable。不用等待获取锁。
当线程竞争不到锁时阻塞时
通过上面介绍,这是线程是进入blocked阻塞状态。当线程进入blocked状态,会有什么操作呢?
对于synchronized,是jvm来管理的,在jvm中有一个EntryList(双向链表)管理了这些获取不到锁的线程,我们在从jvm的监视器(monitor)相关知识了解到了。[java监视器概念]
进入EntryList的线程的顺序和获得锁启动的顺序是相反的即:先入后出。
可以通过下面代码验证。
代码:
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
ArrayList<Thread> threads = new ArrayList<>();
//用一个list装5个线程,并都进行锁竞争
for (int i = 0; i < 5; i++) {
threads.add(
new Thread(() -> {
synchronized (lock) {
log.debug("{} running", Thread.currentThread().getName());
}
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t" + (i + 1))
);
}
//mian线程上锁时启动依次启动调度5个线程
synchronized (lock) {
log.debug("线程启动顺序: ");
for (Thread thread : threads) {
log.debug("{}", thread.getName());
thread.start();
//为了能按顺序被调度,因为拿不到锁,所以进入EntryList的顺序是t1 - t5
TimeUnit.MILLISECONDS.sleep(100);
}
log.debug("拿到锁的顺序:");
}
}
输出:
14:59:28.372 [main] DEBUG console - 线程启动顺序:
14:59:28.374 [main] DEBUG console - t1
14:59:28.477 [main] DEBUG console - t2
14:59:28.583 [main] DEBUG console - t3
14:59:28.688 [main] DEBUG console - t4
14:59:28.793 [main] DEBUG console - t5
14:59:28.897 [main] DEBUG console - 拿到锁的顺序:
14:59:28.898 [t5] DEBUG console - t5 running
14:59:28.898 [t4] DEBUG console - t4 running
14:59:28.899 [t3] DEBUG console - t3 running
14:59:28.899 [t2] DEBUG console - t2 running
14:59:28.899 [t1] DEBUG console - t1 running
结果入我们说的那样,先阻塞的线程最后才获得锁。
当线程调用wait时
线程调用wait方法进入waiting状态,并释放了锁。再当调用了notify方法来唤醒时,会发生什么?
对于synchronized,线程会把waiting状态的线程放进WaitSet中;
当线程被唤醒时,线程这是肯定获取不到锁,会从WaitSet调度进到EntryList中等待获得锁。
代码:
@Slf4j(topic = "console")
public class WaitAnalyze {
static boolean isMoney = false;
public static void main(String[] args) throws InterruptedException {
Object home = new Object();
new Thread(() -> {
synchronized (home) {
while (!isMoney) {
log.debug("sleeping");
try {
home.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("hunting");
}
}, "CatDad").start();
//为了dad先调度
TimeUnit.MILLISECONDS.sleep(10);
synchronized (home) {
//直接唤醒dad再去启动kitty们
isMoney = true;
home.notifyAll();
for (int i = 0; i < 6; i++) {
log.debug("kitty的启动顺序:{}",i+1);
new Thread(() -> {
synchronized (home) {
log.debug("playing!!!");
}
}, "Kitty" + (i + 1)).start();
//为了kitty的调度顺序和启动顺序一致
TimeUnit.MILLISECONDS.sleep(100);
}
/*
把dad唤醒。
dad的状态:
waiting -> blocking,
从WaitSet转移到EntryList
*/
}
}
输出:
15:01:09.187 [CatDad] DEBUG console - sleeping
15:01:09.199 [main] DEBUG console - kitty的启动顺序:1
15:01:09.305 [main] DEBUG console - kitty的启动顺序:2
15:01:09.410 [main] DEBUG console - kitty的启动顺序:3
15:01:09.516 [main] DEBUG console - kitty的启动顺序:4
15:01:09.621 [main] DEBUG console - kitty的启动顺序:5
15:01:09.726 [main] DEBUG console - kitty的启动顺序:6
15:01:09.830 [Kitty6] DEBUG console - playing!!!
15:01:09.830 [Kitty5] DEBUG console - playing!!!
15:01:09.830 [Kitty4] DEBUG console - playing!!!
15:01:09.830 [Kitty3] DEBUG console - playing!!!
15:01:09.830 [Kitty2] DEBUG console - playing!!!
15:01:09.830 [Kitty1] DEBUG console - playing!!!
15:01:09.831 [CatDad] DEBUG console - hunting
虽然CatDad是在Kitty们在启动调度之前被唤醒,但是CatDad却是最后一个获得锁的。
这就证明了CatDad不是立即唤醒并先获得锁的,而是需要进入EntryList等待获得锁,然后因为EntryList是FILO(先入后出)的,
CatDad就是最后获得锁的。
ReentrantLock分析
ReentrantLock是java中的API,JUC包里的。锁的使用和jvm提供synchronized相似。所以可以通过学习其源码去学习java中锁机制的原理。
这里我们主要阐述的ReentrantLock和synchronized区别是在线程获取不到锁,进入blocked状态,然后到获得锁这个线程的启动顺序的差别。
上面我们知道阻塞状态的线程在synchronized中是把放入过ObjectMonitor的EntryList里,然后顺序是FILO的。
但是ReentrantLock的都是的阻塞队列是由基于AQS实现公平锁(FairSync)和非公平锁(NoFairSync)的所维护的,他的顺序是FIFO的,AQS的release方法可以看到:
把线程节点取出队列:
@ReservedStackAccess
public final boolean release(int arg) {
if (tryRelease(arg)) {
//先取的是头部节点
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
当线程竞争不到锁时阻塞时
代码:
*/
@Slf4j(topic = "console")
@SuppressWarnings({"all"})
public class SyncAnalyze {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
threads = new ArrayList<>();
for (int i = 0; i < 5; i++) {
threads.add(
new Thread(() -> {
reentrantLock.lock();
try {
log.debug("{} running", Thread.currentThread().getName());
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}, "t" + (i + 1))
);
}
TimeUnit.MILLISECONDS.sleep(100);
{
reentrantLock.lock();
log.debug("线程启动顺序: ");
for (Thread thread : threads) {
log.debug("{}", thread.getName());
thread.start();
//为了能按顺序被调度,因为拿不到锁,所以进入EntryList的顺序是t1 - t5
TimeUnit.MILLISECONDS.sleep(100);
}
log.debug("拿到锁的顺序:");
reentrantLock.unlock();
}
}
}
输出:
18:05:16.279 [main] DEBUG console - 线程启动顺序:
18:05:16.279 [main] DEBUG console - t1
18:05:16.382 [main] DEBUG console - t2
18:05:16.484 [main] DEBUG console - t3
18:05:16.589 [main] DEBUG console - t4
18:05:16.690 [main] DEBUG console - t5
18:05:16.795 [main] DEBUG console - 拿到锁的顺序:
18:05:16.796 [t1] DEBUG console - t1 running
18:05:16.808 [t2] DEBUG console - t2 running
18:05:16.821 [t3] DEBUG console - t3 running
18:05:16.834 [t4] DEBUG console - t4 running
18:05:16.846 [t5] DEBUG console - t5 running
可以看到是FIFO的。
当线程调用await时
ReentrantLock当中Condition可以实现和synchronized的wait一样的功能,并且可以支持多条件。
当线程调用await方法,对应synchronized的WaitSet,在这里是ConditionObjet的AQS队列,而ConditionObject是AQS的内部类。
对应源码:
/**
* Implements interruptible condition wait.
* <ol>
* <li> If current thread is interrupted, throw InterruptedException.
* <li> Save lock state returned by {@link #getState}.
* <li> Invoke {@link #release} with saved state as argument,
* throwing IllegalMonitorStateException if it fails.
* <li> Block until signalled or interrupted.
* <li> Reacquire by invoking specialized version of
* {@link #acquire} with saved state as argument.
* <li> If interrupted while blocked in step 4, throw InterruptedException.
* </ol>
*/
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
代码:
@Slf4j(topic = "console")
@SuppressWarnings({"all"})
public class WaitAnalyze {
static boolean isMoney = false;
public static void main(String[] args) throws InterruptedException {
ReentrantLock rHome = new ReentrantLock();
Condition condition = rHome.newCondition();
log.debug("使用ReentrantLock方式");
new Thread(() -> {
rHome.lock();
while (!isMoney) {
log.debug("sleeping");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
rHome.unlock();
log.debug("hunting");
}, "CatDad").start();
//为了dad先调度
TimeUnit.MILLISECONDS.sleep(10);
{
rHome.lock();
isMoney = true;
condition.signalAll();
for (int i = 0; i < 6; i++) {
log.debug("kitty的启动顺序:{}", i + 1);
new Thread(() -> {
rHome.lock();
log.debug("playing!!!");
rHome.unlock();
}, "Kitty" + (i + 1)).start();
//为了kitty的调度顺序和启动顺序一致
TimeUnit.MILLISECONDS.sleep(100);
}
rHome.unlock();
}
}
}
输出:
19:20:03.275 [main] DEBUG console - 使用ReentrantLock方式
19:20:03.277 [CatDad] DEBUG console - sleeping
19:20:03.289 [main] DEBUG console - kitty的启动顺序:1
19:20:03.393 [main] DEBUG console - kitty的启动顺序:2
19:20:03.498 [main] DEBUG console - kitty的启动顺序:3
19:20:03.604 [main] DEBUG console - kitty的启动顺序:4
19:20:03.708 [main] DEBUG console - kitty的启动顺序:5
19:20:03.813 [main] DEBUG console - kitty的启动顺序:6
19:20:03.918 [CatDad] DEBUG console - hunting
19:20:03.918 [Kitty1] DEBUG console - playing!!!
19:20:03.919 [Kitty2] DEBUG console - playing!!!
19:20:03.919 [Kitty3] DEBUG console - playing!!!
19:20:03.919 [Kitty4] DEBUG console - playing!!!
19:20:03.919 [Kitty5] DEBUG console - playing!!!
19:20:03.919 [Kitty6] DEBUG console - playing!!!
和synchronized结果方式类似,只是因为阻塞队列的顺序原因,获得锁的顺序是FIFO的。
也可以证明在锁同步中,线程从wait到runnable状态会会先进入blocked状态等待获取锁。