Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制

一、Wait和Notify

1、原理

Wait和Notify用于等待。其原理为:

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第1张图片

2、相关API

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待,注意必须是获得对象锁的像锁的线程才能调用。wait方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止
  • obj.wait(long n)有时限的等待, 到 n 毫秒后结束等待,或是被 notify
  • obj.notify()在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();

        sleep(2);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notify();
            // obj.notifyAll();  
        }
    }

notify 的结果:

20:00:53.096 [Thread-0] c.TestWaitNotify - 执行.... 
20:00:53.099 [Thread-1] c.TestWaitNotify - 执行.... 
20:00:55.096 [main] c.TestWaitNotify - 唤醒 obj 上其它线程 
20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代码.... 

notifyAll 的结果:

19:58:15.457 [Thread-0] c.TestWaitNotify - 执行.... 
19:58:15.460 [Thread-1] c.TestWaitNotify - 执行.... 
19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程 
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码.... 
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码.... 

3、wait和sleep的异同

  • 它们 状态都是 TIMED_WAITING
  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁

4、wait/notify使用

    synchronized(lock){
        while (条件不成立) { //方法之虚假唤醒
            lock.wait();	//进入等待状态
        }
    }

    synchronized(lock){
        lock.notifyAll(); //唤醒所有,再通过条件判断唤醒的是否是自己
    }

二、同步设计模式之保护性暂停

1、定义

保护性暂停即 Guarded Suspension,用在一个线程等待另一个线程的执行结果

  • 一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此属于同步模式

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第2张图片

2、实现

该模式的实现主要依靠了wait和notifyAll方法,和上面的使用类似

/**
 * 保护性暂停设计模式实现
 */
class GuardedObject {

    private Object response;
    private final Object lock = new Object();

    public Object get(){
        synchronized (lock){
            while (response == null){
                try {
                    lock.wait(); //等待唤醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return response;
    }

    public void put(Object obj){
        synchronized (lock){
            response = obj;
            lock.notifyAll(); //唤醒
        }
    }
}

测试:

    public static void main(String[] args) {
        GuardedObject guarded = new GuardedObject();
        
        //t1线程等待response结果
        new Thread(()->{
            System.out.println("t1等待response");
            Object obj = guarded.get();
            System.out.println("t1获得response");
            System.out.println("response = " + (String)obj);
        }, "t1").start();

        //t2线程设置response结果
        new Thread(()->{
            try {
                Thread.sleep(2000);
                System.out.println("t2设置response");
                guarded.put("123");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();

    }

结果:

t1等待response
t2设置response
t1获得response
response = 123

3、带有超时效果的保护性暂停

设置等待一段时间后仍没有收到response就自动唤醒

/**
 * 保护性暂停设计模式实现
 */
class GuardedObject {

    private Object response;
    private final Object lock = new Object();

    public Object get(long timeout){
        synchronized (lock){
            long begin = System.currentTimeMillis(); //开始时间
            long timePassed = 0;
            while (response == null){
                long lastTime = timeout - timePassed; //剩余时间
                if(lastTime <= 0)
                    break;
                try {
                    lock.wait(lastTime); //等待唤醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                timePassed = System.currentTimeMillis() - begin; //已过去的时间
            }
        }
        return response;
    }

    public void put(Object obj){
        synchronized (lock){
            response = obj;
            lock.notifyAll(); //唤醒
        }
    }
}

4、Join的原理

Join的实现原理其就是我们上面的带有超时效果的保护性暂停,其中只有两点不同:

  • 当超时时间millis为0时,表示一直等待,没有超时时间
  • 环形的条件不是获取某个值,而是线程结束

下面是join方法的源码

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {  //如果millis=0,表示不设置超时时间
            while (isAlive()) { //判断线程是否存活
                wait(0);
            }
        } else {
            while (isAlive()) { //判断线程是否存活
                long delay = millis - now; //剩余时间
                if (delay <= 0) {
                    break;
                }
                wait(delay); //等待delay时间
                now = System.currentTimeMillis() - base; //已经过的时间
            }
        }
    }

5、多任务版 GuardedObject

引入:图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右 侧的 t1,t3,t5 就好比邮递员

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第3张图片

分析:如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类, 这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。

实现

新增 id 用来标识 Guarded Object

class GuardedObject {

    //多个GuardedObject时用于标识
    private int id;

    private Object response;

    public GuardedObject(int id) {
        this.id = id;
    }

    public Object get(long timeout){
        synchronized (this){
            long begin = System.currentTimeMillis(); //开始时间
            long timePassed = 0;

            while (response == null){

                long lastTime = timeout - timePassed; //剩余时间
                if(lastTime <= 0)
                    break;

                try {
                    this.wait(lastTime); //等待唤醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                timePassed = System.currentTimeMillis() - begin; //已过去的时间
            }
        }
        return response;
    }

    public void put(Object obj){
        synchronized (this){
            response = obj;
            this.notifyAll(); //唤醒
        }
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

中间解耦类:使用线程安全的Map来存储GuardedObject,用于解耦

class MailBoxs{

    private static Map<Integer, GuardedObject> map = new Hashtable();

    private static int i = 1;

    //生成唯一id
    private  static synchronized int generateId(){
        return i++;
    }

    //生成GuardedObject
    public static GuardedObject createGuardedObject(){
        GuardedObject go = new GuardedObject(generateId());
        map.put(go.getId(), go);
        return go;
    }

    public static GuardedObject getGuardedObject(int i){
        return map.remove(i);
    }

    public static Set<Integer> getIds(){
        return map.keySet();
    }
}

业务相关类

//收信人
class People extends Thread{
    @Override
    public void run() {
        GuardedObject go = MailBoxs.createGuardedObject();
        System.out.println("开始收信" + go.getId());
        go.get(20000);
        System.out.println("收到信" + go.getId());
    }
}

//送信人
class Postman extends Thread{

    private int id;
    private String mail;

    public Postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        GuardedObject go = MailBoxs.getGuardedObject(id);
        System.out.println("开始送信" + go.getId() + "内容为" + mail);
        go.put(mail);

    }
}

测试

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++){
            new People().start();
        }
        Thread.sleep(3000);
        for (int id : MailBoxs.getIds()){
            new Postman(id, "内容"+id).start();
        }

    }

结果

开始收信1
开始收信2
开始收信3
开始送信2内容为内容2
开始送信3内容为内容3
收到信2
收到信3
开始送信1内容为内容1
收到信1

三、异步设计模式之生产者/消费者

1、定义

要点:用于线程间通信的一种异步模式

  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第4张图片

2、实现

消息类,包含id和消息体

//线程安全的消息类
final class Massage{

    private int id;

    private Object value;

    //没有set方法,只能创建时初始化
    public Massage(int id, Object value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public Object getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Massage{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}

异步消息队列实现

class MassageQueue{

    //消息队列
    private LinkedList<Massage> queue = new LinkedList<>();

    //消息队列的容量
    private int capcity;

    public MassageQueue(int capcity) {
        this.capcity = capcity;
    }

    //消费者消费消息
    public Massage get(){
        synchronized (queue){
            //检查队列是否为空
            while (queue.isEmpty()) {
                try {
                    System.out.println("消费队列已空");
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //从队列的头部获取元素返回
            Massage massage = queue.removeFirst();
            System.out.println("消费者消息"+massage.getId());
            queue.notifyAll();
            return massage;
        }
    }

    //生产者者生产消息
    public void put(Massage massage){
        synchronized (queue) {
            //检查队列是否已满
            while (queue.size() >= capcity) {
                try {
                    System.out.println("消息队列是否已满");
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "生产消息"+massage.getId());
            queue.addLast(massage);
            queue.notifyAll();
        }
    }
}

测试:创建了三个生产者,一个消费者,消息队列的容量为2

    public static void main(String[] args) {
        MassageQueue queue = new MassageQueue(2);

        for (int i = 1; i <= 3; i++) {
            int finalI = i;
            new Thread(()->{
                queue.put(new Massage(finalI, "消息"+finalI));
            }, "生产者"+i).start();
        }

        new Thread(()->{
            while (true){
                try {
                    Thread.sleep(1000);
                    Massage massage = queue.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者").start();
    }

结果:

生产者3生产消息3
生产者2生产消息2
消息队列是否已满
消费者消息3
生产者1生产消息1
消费者消息2
消费者消息1
消费队列已空

四、 Park和 Unpark

它们是 LockSupport 类中的方法,用于暂停和唤醒线程

// 暂停当前线程 
LockSupport.park(); 
 
// 恢复某个线程的运行 
LockSupport.unpark(暂停线程对象);

先pack再unpack

Thread t1 = new Thread(() -> {
    log.debug("start...");
    sleep(1);
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
}, "t1");
t1.start();

sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);

结果:

18:42:52.585 c.TestParkUnpark [t1] - start... 
18:42:53.589 c.TestParkUnpark [t1] - park... 
18:42:54.583 c.TestParkUnpark [main] - unpark... 
18:42:54.583 c.TestParkUnpark [t1] - resume... 

先unpack再pack

Thread t1 = new Thread(() -> {
    log.debug("start...");
    sleep(2);
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
}, "t1");
t1.start();

sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);

结果:同样可以解锁,没有顺序要求

18:43:50.765 c.TestParkUnpark [t1] - start... 
18:43:51.764 c.TestParkUnpark [main] - unpark... 
18:43:52.769 c.TestParkUnpark [t1] - park... 
18:43:52.769 c.TestParkUnpark [t1] - resume... 

特点
与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用(必须使用synchronized加锁),而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

park unpark 原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex

1、当前线程调用 Unsafe.park() 方法时(先于unpark)

  • (2)检查 _counter ,如果为 0,获得 _mutex 互斥锁

  • (3)线程进入 _cond 条件变量阻塞

  • (4)设置 _counter = 0
    Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第5张图片
    2、 调用 Unsafe.unpark(Thread_0) 方法

  • (1)调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

  • (2)唤醒 _cond 条件变量中的 Thread_0

  • (3) Thread_0 恢复运行

  • (4) 设置 _counter 为 0

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第6张图片

3、先调用 调用 Unsafe.unpark(Thread_0) 方法,再调用park方法

  • (1)调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  • (2)当前线程调用 Unsafe.park() 方法
  • (3)检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  • (4)设置 _counter 为 0
    Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第7张图片

五、重新理解线程状态转换 ★

Java线程转换如下图所示:
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第8张图片
1、NEW–>RUNABLE

  • 当调用 t.start()方法时,由 NEW --> RUNNABLE

2、RUNABL<–>WATING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait()方法时,t 线程从RUNNABLE --> WAITING
  • 调用 obj.notify()obj.notifyAll()t.interrupt()
    • 如果竞争失败,t 线程从 WAITING --> RUNNABLE,线程进入Monitor中EntryList
    • 如果竞争成功,t 线程从 WAITING --> BLOCKED,Monitor中Owner指向该线程

3、RUNNABLE <–> WAITING

  • 当前线程调用t.join()方法时,当前线程RUNNABLE --> WAITING
    • 注意是当前线程t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

4、RUNNABLE <–> WAITING

  • 当前线程调用LockSupport.park()方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程)或调用了线程 的interrupt(),会让目标线程从 WAITING --> RUNNABLE

5、RUNNABLE <–> TIMED_WAITING

t 线程用 synchronized(obj)获取了对象锁后

  • 调用 obj.wait(long n)方法时,t 线程从 RUNNABLE --> TIMED_WAITING
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify()obj.notifyAll()t.interrupt()
    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

6、 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用t.join(long n)方法时,当前线程从 RUNNABLE --> TIMED_WAITING
    • 注意是当前线程t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

7、RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING

  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

8、RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis)时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 调用LockSupport.unpark(目标线程)或调用了线程 的 interrupt(),或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE

9、 RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED --> RUNNABLE,其它失败的线程仍然 BLOCKED

10、RUNNABLE <–> TERMINATED

  • 当前线程所有代码运行完毕,进入 TERMINATED

六、多把锁

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

七、活跃性

活跃性关注的是“某件正确的事情最终会发生”。

例如,如果A线程等待B线程释放其持有的资源,而B线程永远都不释放该资源,那么线程A就会永远的等待下去。这样就不具备活跃性。

1、死锁

一个线程需要同时获取多把锁,这时就容易发生死锁,例如:t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 。t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁 如:

        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();

上面的代码永远都不会执行完毕,因为发生了死锁。

2、定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁

首先使用jps查看进程ID
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第9张图片
再使用jstack <进程ID>,查看当前的状态:
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第10张图片
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第11张图片

同样使用jconsole可以查看当前是否存在死锁:连接当前进程,点击线程---->检测死锁。

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第12张图片

就当能看到当前死锁的信息
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第13张图片

3、活锁

活锁出现在两个线程互相改变对方的结束条件,后谁也无法结束,例如

    static volatile int count = 10;
    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            while (count > 0) {
                try {
                    Thread.sleep(200);
                    count--;
                    System.out.println("t1 count: " + count);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        
        new Thread(() -> {
            while (count < 20) {
                try {
                    Thread.sleep(200);
                    count++;
                    System.out.println("t2 count: " + count);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
    }

上面程序虽然没有互相持有资源,但是仍然会一直运行下去,这是因为产生了活锁。

在并发应用程序中,通过等待随机长度的时间或者回退可以有效避免活锁的发生。

4、饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不 易演示,讲读写锁时会涉及饥饿问题

先来看看使用顺序加锁的方式解决之前的死锁问题
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第14张图片
顺序加锁的解决方案

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第15张图片

但是顺序加锁很有可能产生饥饿。

八、ReentrantLock

1、引入:哲学家进餐问题:

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

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第16张图片

哲学家进餐进餐问题如果不加于干预就很容易产生死锁问题,如果使用顺序加锁的方法解决死锁又很容易产生饥饿的现象。这时候就需要使用ReentrantLock来解决

2、介绍

相对于 synchronized 它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与 synchronized 一样,都支持可重入

基本语法:

// 获取锁 
reentrantLock.lock(); 
try {    
	// 临界区 
} finally {    
    // 释放锁    
    reentrantLock.unlock(); 
}

3、可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

示例:

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        m1();
    }

    public static void m1(){
        lock.lock();
        try {
            System.out.println("m1 in");
            m2();
        } finally {
            lock.unlock();
        }
    }

    public static void m2() {
        lock.lock();
        try {
            System.out.println("m2 in");
        } finally {
            lock.unlock();
        }
    }

结果如下图所示,m1和m2中都对同一个ReentrantLock对象加了锁,说明可能重入没有问题。

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第17张图片

4、可打断性

可打断表示加锁时如果失败进入阻塞队列,则可以进行打断,可以使用ReentrantLock::lockInterruptibly()方法设置,示例:

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                System.out.println("尝试加锁");
                lock.lockInterruptibly();//设置可打断锁
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("加锁失败,被打断");
                return;
            }
            try {
                System.out.println("获得锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        t1.start();

        Thread.sleep(1000);
        System.out.println("打断加锁");
        t1.interrupt();
    }

结果:
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第18张图片

5、锁超时

锁超时是指尝试加锁时,如果超过一段时间仍然获得不到锁就会自动放弃加锁,可以使用使用ReentrantLock::trylock()方法设置,示例:

1)立即失败

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("t1尝试获得锁");
            if (!lock.tryLock()) {
                System.out.println("获得锁失败, 返回");
                return;
            }
            try {
                System.out.println("t1获得锁成功");
            } finally {
                lock.unlock();
            }
        }, "t1");

        System.out.println("main获得锁");
        lock.lock();

        t1.start();
    }

结果:

Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第19张图片

2)超时失败

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("t1尝试获得锁");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    System.out.println("获得锁失败, 返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("被打断,返回");
                return;
            }
            try {
                System.out.println("t1获得锁成功");
            } finally {
                lock.unlock();
            }
        }, "t1");

        System.out.println("main获得锁");
        lock.lock();
        t1.start();

        Thread.sleep(2000);
        lock.unlock();
    }

结果:
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第20张图片

6、解决哲学家进餐问题

使用synchronized时会出现死锁
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第21张图片
可以更改为使用ReentrantLock::trylock()方法,这样就不会出现场死锁
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第22张图片

7、公平锁

这里的公平是指阻塞队列(EntryList)中的线程按照先进先出的顺序获得锁,ReentrantLock和Synchronized 默认是不公平的,也就是阻塞队列中的线程通过争抢的方式获得锁。

ReentrantLock可以使用通过构造函数ReentrantLock lock = new ReentrantLock(false);设置为公平锁,公平锁一般没有必要,会降低并发度,后面分析原理时会讲解

8、条件变量

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

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒

使用要点:

  • await 前需要获得锁 await 执行后,会释
  • 放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

语法:

        ReentrantLock lock = new ReentrantLock(); //创建ReentrantLock对象
        Condition condition = lock.newCondition();//创建条件变量对象
        
        new Thread(()->{
            lock.lock();	//加锁
            try {
                condition.await();	//condition条件不满足,进入等待队列。与wait对应
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();
        
        Thread.sleep(1000);
        condition.signal();	 //唤醒等待condition条件变量的线程,与Notify对应

九、同步模式之顺序控制

1、固定运行顺序

比如,必须先 2 后 1 打印

1)wait notify 版

    //用来同步的对象
    static Object obj = new Object();
    // t2 运行标记, 代表 t2 是否执行过
    static boolean t2runed = false;

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                // 如果 t2 没有执行过
                while (!t2runed) {
                    try {
                        // t1 先等一会
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();

                    }
                }
            }
            System.out.println(1);
        });

        Thread t2 = new Thread(() -> {
            System.out.println(2);
            synchronized (obj) {
                //修改运行标记
                t2runed = true;
                // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
                obj.notifyAll();
            }
        });

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

2)park unpark版

可以看到,上面的实现上很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该 wait
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决 此问题
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个

park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』, 不需要『同步对象』和『运行标记』

可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("1");
            LockSupport.park(); //暂停等待
        }, "t1");
        t1.start();


        new Thread(()->{
            System.out.println("2");
            LockSupport.unpark(t1); //唤醒t1
        }, "t2").start();
    }

2、交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现

1) wait notify 版

打印控制类,用flag变量控制本次应该打印的a,loopNums控制打印次数

class SyncWaitNotify{

    //用于标记本次需要唤醒线程
    private int flag;
    //打印次数
    private int loopNums;

    public SyncWaitNotify(int flag, int loopNums) {
        this.flag = flag;
        this.loopNums = loopNums;
    }

    public void print(String msg, int flag){
        for (int i = 0; i < loopNums; i++) {
            synchronized (this) {
                while (this.flag != flag){ //如果自己不满足条件,就一直等待
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(msg);
                this.flag = (flag + 1) % 3; //设置下一次唤醒的flag
                this.notifyAll(); //唤醒所有
            }
        }
    }
}

测试:

    public static void main(String[] args) {
        SyncWaitNotify waitNotify = new SyncWaitNotify(0, 5);
        new Thread(()->{
            waitNotify.print("a", 0);
        }).start();
        new Thread(()->{
            waitNotify.print("b", 1);
        }).start();
        new Thread(()->{
            waitNotify.print("c", 2);
        }).start();
    }

结果:

在这里插入图片描述

2)、Await和Signal实现

使用Condition作为条件变量,使用await和Signal用于等待和唤醒

class SyncAwaitSignal extends ReentrantLock {
    private int loopNums;

    public SyncAwaitSignal(int loopNums) {
        this.loopNums = loopNums;
    }

    public void print(String msg, Condition current, Condition next){
        for (int i = 0; i < loopNums; i++) {
            lock();
            try {
                //等待信号
                current.await();
                //唤醒成功执行输出,并唤醒下一个
                System.out.print(msg);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }
        }
    }
}

测试:

    public static void main(String[] args) throws InterruptedException {
        SyncAwaitSignal awaitSignal = new SyncAwaitSignal(5);
        Condition condition_a = awaitSignal.newCondition();
        Condition condition_b = awaitSignal.newCondition();
        Condition condition_c = awaitSignal.newCondition();

        new Thread(()->{
            awaitSignal.print("a", condition_a, condition_b);
        }).start();
        new Thread(()->{
            awaitSignal.print("b", condition_b, condition_c);
        }).start();
        new Thread(()->{
            awaitSignal.print("c", condition_c, condition_a);
        }).start();

        Thread.sleep(1000);
        System.out.println("开始");
        //运行开始时,给唤醒a
        awaitSignal.lock();
        try {
            condition_a.signal();
        } finally {
            awaitSignal.unlock();
        }
    }

结果:
Java并发学习笔记(三):Wait\Notify、保护性暂停、生产者消费者、Park\Unpark、线程状态转换、活跃性、ReentryantLock、顺序控制_第23张图片
3)、 Park Unpark

class SyncPackUnPack {
    private int loopNums;

    public SyncPackUnPack(int loopNums) {
        this.loopNums = loopNums;
    }
    
    public void print(String msg, Thread next){
        for (int i = 0; i < loopNums; i++) {
            //等待唤醒
            LockSupport.park();
            //输出并唤醒下一个线程
            System.out.print(msg);
            LockSupport.unpark(next);
        }
    }
}

测试:

    static Thread t1, t2, t3;

    public static void main(String[] args) {
        SyncPackUnPack packUnPack = new SyncPackUnPack(5);
        t1 = new Thread(()->{
            packUnPack.print("a", t2);
        });
        t2 = new Thread(()->{
            packUnPack.print("b", t3);
        });
        t3 = new Thread(()->{
            packUnPack.print("c", t1);
        });

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

        LockSupport.unpark(t1);
    }

结果:
在这里插入图片描述

你可能感兴趣的:(并发,后端)