JUC并发编程笔记(上)多线程基础知识、Synchronized优化、ReentrantLock

创建一个线程,直接设置为守护线程即可

t.setDeamon(true) //讲当前t线程设置为守护线程!

例如:垃圾回收线程就是是一个守护线程!

线程的状态

关于状态有两种说法,一种是说五种状态、一种是六种状态,分别说明!

五种状态:

**六种状态:**根据Thread. State枚举划分(更偏向Java)

  • NEW(初始):线程被创建后尚未启动。(new 出来线程,未start())

  • RUNNABLE(运行):包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系资源,如等待CPU为它分配时间片。(在这种状态分类情况加,获取键盘的读入,被看作为可运行状态)

  • BLOCKED(阻塞):线程阻塞于锁。(等待t1线程结束,但是t1一直不结束)

  • WAITING(等待):线程需要等待其他线程做出一些特定动作(通知或中断)。(线程同步机制,等待另一个线程归还对象锁!)

  • TIME_WAITING(超时等待):该状态不同于WAITING,它可以在指定的时间内自行返回。(sleep、wait等状态,有时间限制的休眠)

  • TERMINATED(终止):该线程已经执行完毕。

三、共享模型之管程


管程就是Monitor锁

线程安全问题产生的原因

举个例子:线程t1、t2 对共享的静态变量i,分别执行i ++ , i -- 操作,由于并发会产生一下两种情况,导致线程静态变量 I不正确

  • I ++ 的原代码被字节码化后,代码分为4个步骤:获取 i、准备常量1、自增、写入自增后的值

  • I – 的原代码被字节码化后,代码分为4个步骤:获取 i、准备常量1、自减、写入自减后的值

因此当我们两个线程并发执行的时候,如果在写入值之前,发生上下文切换,(指令交错)则会导入如下两种情况:

情况一:

t1、t2并发执行,结果出现负数

情况二:

t1、t2并发执行,结果出现正数

结论:多个线程对某个共享资源进行读操作没问题,写操作则会产生线程安全的问题,

临界区

线程中我们把对共享资源修改的区域,称这块代码块为临界区

public class ThreadTest {

static int count = 0 ;

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(() -> {

//临界区

{

count ++ ;

}

});

Thread t2 = new Thread(() -> {

{ //临界区

count – ;

}

});

t1.start(); t2.start();

t1.join(); t2.join();

}

}

竞态条件

多个线程在临界区内执行,由于执行顺序不同,导致结果无法预测,称之为发生了竞态条件!

synchronized解决方案

1、保证静态变量的安全:

public class ThreadTest {

static int count = 0 ;

public static void main(String[] args) throws InterruptedException {

Object obj = new Object();

Thread t1 = new Thread(() -> {

synchronized(obj){

count ++ ;

}

});

Thread t2 = new Thread(() -> {

synchronized (obj){

count – ;

}

});

t1.start(); t2.start();

t1.join(); t2.join();

}

}

2、保证实例变量的安全:

public class ThreadTest {

public static void main(String[] args) throws InterruptedException {

Room room = new Room();

Thread t1 = new Thread(() -> {

room.a();

}) ;

Thread t2 = new Thread(() -> {

room.b();

}) ;

Thread t3 = new Thread(() -> {

room.c();

}) ;

}

}

class Room{

public synchronized void a(){

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“a.begin…”);

}

public synchronized void b(){

System.out.println(“b.begin…”);

}

public void c(){

System.out.println(“c.begin…”);

}

}

//结果:

/*

  • c b 1s a

  • c 1s a b

  • b c 1s a

  • */

具体参考“线程八锁”

变量的线程安全问题

1、成员变量(实例变量、静态变量)

  • 如果成员变量,没有在线程间去共享,那没是线程安全的!

  • 如果成员变量,在线程间被共享:

  • 如果只有读操作,安全

  • 如果有写操作,不安全

2、局部变量

局部变量并不会产生线程安全问题

每个线程中的栈是私有的!方法入栈后创建的局部变量,其他线程访问不到!

常见的线程安全类

  • String

  • Integer

  • String Buffer

  • Random

  • Vector

  • HashTable

  • java.util.concurrent(JUC) 包下的类

线程安全的类其中的方法是线程安全的!

但是线程安全的方法,组合起来也许会产生线程不安全的情况

例如:Hashtable

Hashtable table = new Hashtable( ) ; //原子性,只能在单一方法中保证

if (table.get(“Key”) == null) {

table.put(“key”) ;

}

我们的get、put方法都是被synchronized修饰的线程安全的方法,但是上述代码依然可能会产生线程不安全的情况

流程:线程 t1 get到key == null ,此时线程t1 执行get方法完毕,此时线程 t2 获得时间片执行 get 同样返回null,然后接着发生上下文切换,t1执行put方法,放入<k1,v1>然后经过上下文切换t2也回到put阶段,线程2中的的此时得到的状态仍然是key == null ,因此重复添加<k1,v2>,发生覆盖!

不可变的线程安全类

例如:String、Integer 他们的值是不能被修改的,实例即使被共享,也不会有线程安全的问题

Java对象头

每一个对象在JVM当中都被分为对象头和内容:(如下是32位的JVM)

例如Integer : 对象头占8个字节,内容为int类型占4个字节 ;

、

Klass部分保存的是对象的类型

Mark Word 保存的是的如下:(由于对象是否加锁,而不同!)

  • Normal : 不加锁 01

  • Biased:偏向锁 01

  • Lightweight Locked : 轻量锁 00

  • Heavyweight : 重量级锁 10

  • Marked for GC :垃圾回收锁

Monitor锁

Monitor被翻译为监视器或管程

  • Monitor的结构图,Monitor是重量级锁!

Monitor的工作原理 *

Monitor和obj的关联流程

详细的流程:

  • 当我们的obj对象被加上了synchronized锁之后,我们obj的对象头就会转化为一个指针指向Monitor(关联起来) ;

  • 当我们的线程去访问临界区代码的时候,回将线程Thread1设置为Monitor的所有者!(获取对象锁)

  • 当新的线程2、线程3 去访问临界区代码时,则会判断obj的对象锁是否有主人,如果有那么就会在EntryList阻塞等待,等待线程1执行完临界区代码块的内容归还锁,接着线程1归还锁后, 线程2、线程3再去争夺对象锁的所有权!

总结 : 我们所说的对象锁,也即是这个Monitor ;

Synchronized优化原理 *

由于Monitor锁每次都需要与操作系统打交道,效率较低,因此我们需要对锁进行优化!如轻量级锁、偏向锁

1、轻量级锁

如果多个线程访问一个一个对象,但是这个访问时间隔开的,不是并发(没有竞争关系),那么就可以使用轻量级锁来优化

轻量级锁对使用者是透明的,依然可以使用synchronized

//测试代码:

static final Object obj = new Object();

public static void method1(){

synchronized(obj){

method2();

}

}

public static void method2(){

synchronized(obj){

}

}

加锁(为对象添加轻量级锁)

  • Thread-0调用method1,方法入栈,执行synchronized代码块,然后创建一个锁的记录对象Lock Record,

  • 让锁记录中的reference指向我们的对象,并尝试用cas替换Object中的Mark word部分与锁地址交换

  • 如果cas(原子性操作)替换成功!对象头中保存了,lock record和状态00,表示该线程为对象加锁!

如果cas失败,有两种情况:

  • 如果其他线程已经持有了该Object的轻量级锁,这表明有竞争,进入锁膨胀过程!

  • 如果是当前线程自己执行了锁重入(重复加锁),那么再加入一条Lock Record锁记录作为重入的计数!

解锁(为对象接触,轻量级锁)

  • 当退出我们的synchronized代码块的时候,解锁时,如果有Lock Record锁记录为null,直接移除,表示锁重入记录-1

当退出我们的synchronized的时候,锁记录的值不为null,此时使用cas将Mark word的值恢复给对象头,

  • 成功:解锁成功!

  • 失败:说明轻量级锁进行了锁膨胀,或者已经升级为重量级锁,这时进入重量级解锁流程!

2、锁膨胀

当退出我们的synchronized的时候,此时使用cas将Mark word的值恢复给对象头失败 :说明轻量级锁进行了锁膨胀,或者已经升级为重量级锁,这时进入重量级解锁流程!

static final Object obj = new Object();

public static void method1(){

synchronized(obj){

method2();

}

}

  • 当我们的的Thread-1为obj加轻量级锁发现,obj已经被Thread-01加上轻量级锁了

这是Thread-1加轻量级锁失败,进入锁膨胀流程

  • 即为Object对象申请Monitor锁,让Object指向重量级锁地址 ;

  • 然后自己进入重量级锁中的EntryList中,等待!

此时当Thread-0执行完同步代码块,使用cas将Mark word的值恢复给对象头失败,进入重量级锁的解锁流程,即按照Monitor的地址找到Monitor对象,然后将其Owner设置为null,解锁成功,然后唤醒EntryList中的阻塞线程;执行重量级锁的加锁流程!

3、自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

(多核CPU自旋才有意义)

  • 自旋成功:

  • 自旋失败:

  • 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。

  • Java 7之后不能控制是否开启自旋功能

4、偏向锁

当我们的轻量级锁重入时,效率较低,因为每次重入都需要执行一次cas操作,让Lock Record中的地址与Object对象中的Mark Work比较一次,适合:就一个线程访问

Java 6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的 Mark Word头,之后发现这个线程D是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有

回顾一下64位JVM中的对象

创建对象时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword值为Ox05即最后3位为101,这时它日thread、epoch、age都为0

  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX : BiasedLockingStartupDelay=e来禁用延迟

  • 如果没有开启偏向锁,那么对象创建后,markword值为Ox01即最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode 时才会赋值

  • 前54位表示的偏向锁的ThreadID ;

  • 偏向锁解锁后,线程ID依然会在Mark word中,直到下一个

  • 在上面测试代码运行时在添加VM参数-xx:-UseBiasedLocking 禁用偏向锁

  • 当一个可偏向的对象调用其本身hashcode方法,会禁止偏向锁 【 因为我们加了偏向锁的对象没有空间去保存32位的hashcode】

  • 其他线程访问,也会撤销偏向锁 【对象加偏向锁就是只适用于这个对象只有某一个线程访问】

批量重偏向

当我们的对象上的偏向锁,被撤销20次后,不会再变为轻量级锁,而是以后的对象上的偏向锁,全部偏向一个新的线程 ;

超过40次,我们的对象上的偏向锁,变为不可偏向 ;

总结 :

重量级锁:适合多个线程访问,支持并发 ;缺点:与操作系统交互,效率变低

轻量级锁 : 适合多个线程访问,但是多个线程时错开访问的 ;缺点:一个线程多次为一个对象加锁,锁重入 ;

偏向锁:适合单一线程访问 ,目的是解决锁重入 ; 缺点:只能单一线程访问

5、锁消除

JVM存在一个JTL即时编译 ,由于这个机制会对代码优化,默认锁消除的这个优化机制是开启的,可以手动取消!

//测试代码 :

static int x ;

public static void method1(){

x ++ ;

}

public static void method2(){

Object o = new Object(); //局部对象,不被共享,这个锁相当于每加! JTL会直接将其优化掉!

synchronized (o){

x ++ ;

}

}

结论:我们的两个方法执行效率是差不多一样的

Wait和Notify *

原理:

  • Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING 状态

  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片

  • BLOCKED线程会在Owner线程释放锁时唤醒

  • WAITNG线程会在Owner线程调用notify或notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需入EntryList重新竞争

wait : 线程拿到锁的使用权,但是放弃了,进入waiting区 ;

notify : 唤醒正在waiting中的某个线程,到EntryList中准备与其他阻塞中的线程去争抢时间片!(随机挑一个唤醒)

notifyAll : 唤醒waiting中的所有正在等待唤醒的线程

Wait方法重载:

public final void wait() throws InterruptedException { //无限期等待

wait(0);

}

public final native void wait(long timeout) throws InterruptedException; //设置等待时长,一段时间后会自动唤醒

public final void wait(long timeout, int nanos) throws InterruptedException

Wait和Notify的使用

先了解一下Wait和Sleep的区别

Wait和Sleep的区别

  • Sleep是Thread的方法,Wait是Object的方法 ;

  • Sleep可以在任何时候使用,Wait与Synchronized联用 ;

  • Sleep不会释放对象锁,wait会释放对象锁 ;

总结 :

synchronized(lock){

while(条件不成立){

lock.wait() ;

}

//满足条件,干活

}

synchronized(lock){

lock.notifyAll() ; //使用notify可能会产生虚假唤醒!

}

设计模式—保护性暂停模式

一个结果需要从一个线程传到另外一个线程,让他们关联同一个GuardedObject (保护对象);

  • 如果结果不断地从一个线程到另外一个线程 ;那么可以使用消息队列 ;

  • JDK中join得实现、Future的实现都是这种模式 ;

  • 因为要等待另一方的结果,所有归结到同步模式

总结:在两个线程之间通过一个共享对象,线程1通过该共享对象保存当前线程中需要保存的结果 ;线程2则可以通过该对象获取到这个结果 ;

  • 保护性暂停模式:一个线程等待另外一个线程的返回结果 ;

  • join:一个线程等待另外一个线程执行完毕 ;

扩展:可以在获取结果的时候添加超时 , 一旦超时,即使没有获取的结果依然结束等待 ;

设计模式—生产者消费者模式

异步的原因:生产者产生的结果不会被立刻调用 ;

  • 与前面的保护性暂停中的GuardedObject 不同,不需要产生结果和消费结果的线程一一对应

  • 消费队列可以用来平衡生产和消费的线程资源

  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果

  • 数据消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据

  • JDK中各种阻塞队列,采用的就是这种模式

Park和unpark

类似与wait和notify ,但是park休眠的可以使用unpark提前唤醒

public class ParkandUnpark {

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(() -> {

System.out.println(“t1线程开始”);

System.out.println(“t1线程暂停”);

LockSupport.park();

System.out.println(“t1线程结束”);

},“t1”);

t1.start();

Thread.sleep(2000);

System.out.println(“t1线程恢复”);

LockSupport.unpark(t1);

}

}

与Object的 wait & notify 相比

  • wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必

  • park & unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】

  • park & unpark可以先unpark,而wait & notify不能先notify

park和unpark分析图:

总结:我们每个线程都可以比作一个汽车,每个汽车都含有一个park对象,包括counter(油的量),condition(停车、不停车),mutex

当我们的线程执行park的时候,先判断油量是否充足,如果充足,汽车不会停车 ;如果不充足则停车休息 ;

当我们的unaprk执行的时候,就是给汽车加油(有没有油都加一下)也就是counter = 1,加过油后,汽车发现有油了,汽车启动 ;

重新理解线程的状态 *

以偏java的6种线程状态理解 Thread. Sate

假设有线程 Thread t ;

情况一:NEW — > RUNNABLE

当调用t .start方法的时候,由NEW 到RUNNABLE ;

情况二: RUNNABLE <—> WAITING *

1、线程t 获得obj的锁后调用wait ,由RUNNABLE 到 WAITING ;

然后再调用notifyALL 、notify 、interrupt ,唤醒线程,放入EntryList中竞争:


  • 争成功:WAITING 到RUNNABLE ;

  • 竞争失败:WAITING 到BLOCK ;

2、当前线程main调用t.join ,则当前线程由RUNNABLE 到 WAITING ;

当t线程结束,或者调用当前线程的interrupt 会由RUNNABLE 到 WAITING ;

情况三:RUNNABLE <—> TIMED_WAITING

1、调用wait(long time),比情况2的方法1多一个超时时间 ;

2、调用t.join(long time) , 比情况2的方法2多一个超时时间 ;

3、调用Thread.sleep(long time)

情况四:RUNNABLE <—> BLOCKED

当线程执行到synchronized(obj)的时候,发现obj的owner已经有线程存在了,那么会由RUNNABLE 到BLOCKED ;

当占有obj锁的线程执行完毕后,当前线程争夺到CPU时间片就会再由BLOCKED 到 RUNNABLE

情况五:RUNNABLE <—>TERMINTED

线程中的代码执行完毕 ;

死锁 *

死锁现象

一个线程需要获得多个对象锁 ,容易发生死锁现象

package com.juc;

public class DeadLockTest {

public static void main(String[] args) {

final Object lock2 = new Object() ;

final Object lock1 = new Object() ;

new Thread(() -> {

synchronized (lock1){

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()+“获得lock1”);

synchronized (lock2){

System.out.println(Thread.currentThread().getName()+“获得lock2”);

}

}

},“t1”).start();

new Thread(() -> {

synchronized (lock2){

System.out.println(Thread.currentThread().getName()+“获得lock2”);

synchronized (lock1){

System.out.println(Thread.currentThread().getName()+“获得lock1”);

}

}

},“t2”).start();

}

}

//测试结果:

//t2获得lock2

//t1获得lock1 发生死锁现象

可以使用 : 顺序加锁的方式解决! 线程1 线程2获取锁的顺序保持一致!例如:都是现加lock1再加lock2

定位死锁
方法一:jps + jstack + 进程号

E:\JavaSE笔记>jps //查看java所有的进程 ;

11760

7028 Launcher

17832 Jps

18168 RemoteMavenServer

14508 DeadLockTest

E:\JavaSE笔记>jstack 14508 查看某个进程中线程的详细信息;

打印出死锁的信息 :

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wkLGc4uf-1633835829460)(JUC并发编程.assets/image-20211008173900282.png)]

方法二:jconsole

win + R : 输入jconsole

、

以上两种方法,及时的定位到死锁的位置 ;

哲学家就餐问题

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。

  • 吃饭时要用两根筷子吃,桌上共有5根筷子,每位哲学家左右手边各有一根筷子。

  • ·如果筷子被身边的人拿着,自己就得等待

活锁

两个线程互相改变对方条件,都无法执行完成 !

package com.juc;

public class LiveLockTest {

static volatile int count = 10 ;

public static void main(String[] args) {

new Thread(() -> {

while (count > 0){

try {

Thread.sleep(200);

} catch (InterruptedException e) {

e.printStackTrace();

}

count-- ;

System.out.println("count= "+ count);

}

},“t1”).start();

new Thread(() -> {

while (count < 20){

try {

Thread.sleep(200);

} catch (InterruptedException e) {

e.printStackTrace();

}

count++ ;

System.out.println("count= "+ count);

}

},“t2”).start();

}

}

//两个线程互相改变对方条件,都无法执行完成 !

//测试结果 :

count= 10

count= 10

count= 11

count= 11

count= 10

饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束

饥饿的情况不易演示,讲读写锁时会涉及饥饿问题

例如:我们的哲学家问题中,我们如果顺序加锁去解决死锁,会发现其中一个哲学家一直拿不到筷子,这个现象就是饥饿现象

如何解决 饥饿、死锁、活锁 这些问题 ?

答 : ReentrantLock

ReentrantLock *

可重入锁 , 位于java.util.concurrent 包下

相对synchronized 它具备以下特点 :

  • 可以中断 : 线程一拿到对象锁,线程二可以打断

  • 可以设置超时时间 : 线程阻塞一段时间仍未获取对象锁,放弃多锁的获取 ;

  • 可以设置为公平锁 : 线程先到先得,而非按优先级分配

  • 支持多个条件变量 : 可以有多个WaitSet ,不需要全部唤醒

与synchronized一样支持锁重入

1、基本语法

ReentrantLock reentrantLock = new ReentrantLock();

reentrantLock.lock(); //获取锁

try {

//临界区

}finally {

reentrantLock.unlock(); //释放锁

}

2、可重入

当一个线程已经是锁的主人的时候,还可以再次去获取锁的owner,重复获取!

package com.juc;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

private static ReentrantLock reentrantLock = new ReentrantLock();

public static void main(String[] args) {

reentrantLock.lock(); //当前线程获取到锁,称为lock的主人 ;

try{

System.out.println(“进入了main方法”);

m1();

}finally {

reentrantLock.unlock();

}

}

public static void m1(){

reentrantLock.lock(); //锁重入

try{

System.out.println(“进入了m1方法”);

m2();

}finally {

reentrantLock.unlock();

}

}

public static void m2(){

reentrantLock.lock(); //锁重入

try{

System.out.println(“进入了m1方法”);

}finally {

reentrantLock.unlock();

}

}

}

3、可打断

必须是加的lockInterruptibly()可被打断锁,其他线程使用Interrupt可以进行打断操作!

package com.juc;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest02 {

private static ReentrantLock reentrantLock = new ReentrantLock() ;

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(() -> {

try {

reentrantLock.lockInterruptibly(); //线程加入可打断锁!

} catch (InterruptedException e) {

e.printStackTrace();

System.out.println(“t1线程被打断”);

}

try{ //临界区

System.out.println(“t1线程没有打断”);

}finally {

reentrantLock.unlock();

}

},“t1”);

reentrantLock.lock();

Thread.sleep(1000);

t1.interrupt();

}

}

4、锁超时

目的还是不让线程长时间陷入阻塞状态,如果等待一段时间t1线程仍然不释放锁,t2则不再等待 ;

package com.juc;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest02 {

private static ReentrantLock reentrantLock = new ReentrantLock() ;

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(() -> {

if (reentrantLock.tryLock()){ //t1线程尝试获取锁

try{

System.out.println(“获取到锁”);

}finally {

reentrantLock.unlock();

}

}

System.out.println(“获取不到锁”);

},“t1”);

reentrantLock.lock(); //主线程占用lock锁

t1.start();

}

} //获取不到锁

5、公平锁

当我们多个线程竞争的时候,会按照先到先得的原则,而非再去争抢 ;

默认我们的ReentrantLock是不开启公平锁的,因为会降低并发 ;

6、条件变量

synchronized 中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足时进入waitSet等待ReentrantLock的条件变量比synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized是那些不满足条件的线程都在一间休息室等消息

  • 而ReentrantLock支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

public class ReentrantLockTest03 {

private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {

Condition condition01 = lock.newCondition(); //创建两个休息室,类似WaitSet

Condition condition02 = lock.newCondition();

lock.lock();

condition01.await(); //让线程去condition01休息室休息

condition01.signal(); //唤醒condition01休息室中的线程

}

}

ReentrantLock与synchronized的区别:

  • ReentrantLock 是给锁实例加锁!而synchronized则是以关键字的形式给所有对象加锁!

  • 可以使用ReentrantLock,解决死锁问题 ;

  • ReentrantLock 需要在finally当中手动释放锁 ,而synchronized则是代码块结束,释放锁 ;

交替输出的实现 *

  1. 使用wait、notify 实现

  2. 使用park、unpark 实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值