同步模式之顺序控制(笔试)

简介: 同步模式之顺序控制(笔试)

一、固定运行顺序

题目:有两个线程分别输出1和2,要求输出结果必须先2后1打印

1.1 wait notify 版

import lombok.extern.slf4j.Slf4j;
/**
 * @author xiaowei
 * @date 2022-10-26
 * @description 固定运行顺序  wait notify实现
 * 必须先2后1打印
 **/
@Slf4j(topic = "c.Test01")
public class Test01 {
    // 用来同步的对象
    static final Object lock = new Object();
    //表示 t2 是否运行过
    static boolean t2runned = false;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock){
                // 如果 t2 没有执行过
                while (!t2runned){
                    try {
                        // t1 先等一会
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            }
        },"t1");
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                log.debug("2");
                // 修改运行标记
                t2runned = true;
                // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
                lock.notifyAll();
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

运行结果

20:54:47.396 c.Test01 [t2] - 2
20:54:47.398 c.Test01 [t1] - 1

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

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

1.2 await/signal版本实现

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author xiaowei
 * @date 2022-10-26
 * @description 固定运行顺序 - await/signal版本实现
 **/
@Slf4j(topic = "c.Test06")
public class Test06 {
    public static final ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();
    // t2线程释放执行过
    public static boolean t2Runned = false;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                // 临界区
                while (!t2Runned) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            } finally {
                lock.unlock();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                log.debug("2");
                t2Runned = true;
                condition.signal();
            } finally {
                lock.unlock();
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

运行结果

21:38:38.463 c.Test06 [t2] - 2
21:38:38.464 c.Test06 [t1] - 1

1.3 Park Unpark 版

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

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
 * @author xiaowei
 * @date 2022-10-26
 * @description 固定运行顺序 Park Unpark实现
 **/
@Slf4j(topic = "c.Test02")
public class Test02 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
            LockSupport.park();
            log.debug("1");
        }, "t1");
        t1.start();
        new Thread(() -> {
            log.debug("2");
            // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
            LockSupport.unpark(t1);
        },"t2").start();
    }
}

运行结果

21:33:28.193 c.Test02 [t2] - 2
21:33:28.195 c.Test02 [t1] - 1

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

二、交替输出

题目:线程1 输出 a 5次, 线程2 输出 b 5次, 线程3 输出 c 5次。现在要求输出 abcabcabcabcabcab

2.1 wait/notify版本

通过设置等待标记 flag 来记录当前拥有锁的是哪个线程, 设置下一个标记,来记录下一个唤醒的该是哪个线程。

/**
 * @author xiaowei
 * @date 2022-10-26
 * @description 交替输出 wait notify实现
 **/
public class Test03 {
    public static void main(String[] args) {
        WaitNotify waitNotify = new WaitNotify(1,5);
        new Thread(() -> {
            waitNotify.print("a",1,2);
        }).start();
        new Thread(() -> {
            waitNotify.print("b",2,3);
        }).start();
        new Thread(() -> {
            waitNotify.print("c",3,1);
        }).start();
    }
}
/*
输出内容       等待标记     下一个标记
   a           1             2
   b           2             3
   c           3             1
 */
class WaitNotify {
    // 打印
    public void print(String str,int waitFlag,int nextFlag){
        for (int i = 0; i < loopNumber; i++){
            synchronized (this) {
                while (flag != waitFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                // 修改等待标记 让下一个线程打印
                flag = nextFlag;
                // 唤醒等待线程
                this.notifyAll();
            }
        }
    }
    // 等待标记
    private int flag;
    // 循环次数
    private int loopNumber;
    public WaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }
}

运行结果

abcabcabcabcabc

2.2 await/signal版本

由于 ReentrantLock 具有多个条件变量的特性,即多个 WaitSet 休息室,所以可以通过设置让其进入不同的休息室休息来实现输出当前线程的字符串,并唤醒下一个休息室中的线程。这种方法存在虚假唤醒的情况,因为没有做 while 判断,但此例中每个休息室中只有一个线程,因此不存在虚假唤醒的情况。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.thread.utils.Sleeper.sleep;
/**
 * @author xiaowei
 * @date 2022-10-26 交替输出 await/signal
 * @description
 **/
public class Test04 {
    public static void main(String[] args) {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a = awaitSignal.newCondition();
        Condition b = awaitSignal.newCondition();
        Condition c = awaitSignal.newCondition();
        new Thread(() -> {
            awaitSignal.print("a",a,b);
        }).start();
        new Thread(() -> {
            awaitSignal.print("b",b,c);
        }).start();
        new Thread(() -> {
            awaitSignal.print("c",c,a);
        }).start();
        sleep(1);
        awaitSignal.lock();
        try {
            System.out.println("开始...");
            a.signal();
        } finally {
            awaitSignal.unlock();
        }
    }
}
class AwaitSignal extends ReentrantLock {
    private int loopNumber;
    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    /**
     * 打印
     * @param str 打印内容
     * @param current   进入哪一间休息室
     * @param next  下一间休息室
     */
    public void print(String str, Condition current,Condition next) {
        for (int i = 0; i < loopNumber; i++){
            lock();
            try {
                current.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }
        }
    }
}

运行结果

开始...
abcabcabcabcabc

2.3 park/unpark实现

park和unpark没有对象锁的概念了,停止和恢复线程的运行都是以线程自身为单位的,所以实现更为简单。

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
 * @author xiaowei
 * @date 2022-10-26
 * @description 交替输出 Park/Unpark
 **/
@Slf4j(topic = "c.Test05")
public class Test05 {
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) {
        ParkUnpark unpark = new ParkUnpark(5);
        t1 = new Thread(() -> {
            unpark.print("a", t2);
        });
        t2 = new Thread(() -> {
            unpark.print("b", t3);
        });
        t3 = new Thread(() -> {
            unpark.print("c", t1);
        });
        t1.start();
        t2.start();
        t3.start();
        //主线程先唤醒t1
        LockSupport.unpark(t1);
    }
}
class ParkUnpark {
    public void print(String str,Thread next){
        for (int i = 0; i < loopNumber; i++){
            //当前线程先暂停
            LockSupport.park();
            System.out.print(str);
            //唤醒下一个线程
            LockSupport.unpark(next);
        }
    }
    private int loopNumber;
    public ParkUnpark(int loopNumber) {
        this.loopNumber = loopNumber;
    }
}

运行结果

abcabcabcabcabc
目录
相关文章
|
3月前
|
前端开发 JavaScript Java
v-for比v-if优先级更高?面试官:回去等通知吧
v-for比v-if优先级更高?面试官:回去等通知吧
|
10月前
|
前端开发 芯片
【芯片前端】同步FIFO的一个小的延伸——一会加一会减得计数器怎么写
【芯片前端】同步FIFO的一个小的延伸——一会加一会减得计数器怎么写
|
3月前
|
API iOS开发
总是搞不懂的同步异步,阻塞非阻塞
总是搞不懂的同步异步,阻塞非阻塞
56 0
|
11月前
|
Java 调度
谈谈你对Java线程5种状态流转原理的理解?
今天,有位工作5年的小伙伴被问到这样一道面试题,说谈谈你对Java线程5种状态流转原理的理解。当时,平时只关注过线程如何定义和使用,对于线程状态流转脑海一片空白,完全懵了。于是找到我,希望我拍一期视频。 今天,我给大家分享一下我的理解。
93 0
谈谈你对Java线程5种状态流转原理的理解?
|
算法 前端开发 JavaScript
不逼自己一把都不知道自己还能这么优秀(小鹅通学习记录大批量队列同步)
不逼自己一把都不知道自己还能这么优秀(小鹅通学习记录大批量队列同步)
140 0
|
设计模式 算法 安全
并发 并行 同步 异步 你分清了吗
并发 并行 同步 异步 你分清了吗
|
消息中间件 Java
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式(一)
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式(一)
|
算法 Linux
linux多线程同步设计
linux多线程同步设计
152 0
linux多线程同步设计
|
缓存 监控 安全
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式(二)
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式(二)
|
Unix
【408考研】【收藏必会】操作系统底层逻辑
【408考研】【收藏必会】操作系统底层逻辑
70 0
【408考研】【收藏必会】操作系统底层逻辑