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

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

一、固定运行顺序

题目:有两个线程分别输出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
目录
相关文章
|
6月前
|
Java
面试1: 解决线程顺序有几种方式
面试1: 解决线程顺序有几种方式
|
前端开发 芯片
【芯片前端】同步FIFO的一个小的延伸——一会加一会减得计数器怎么写
【芯片前端】同步FIFO的一个小的延伸——一会加一会减得计数器怎么写
|
14天前
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
1月前
多线程通信和同步的方式有哪些?
【10月更文挑战第6天】
104 0
|
程序员
同步模式之犹豫模式Balking
同步模式之犹豫模式Balking是一种多线程编程中的同步模式。在该模式中,线程在执行操作之前会先检查某些条件,如果发现在执行操作之前会导致某些不良后果,则该线程会放弃执行该操作,避免出现问题。
110 0
同步模式之犹豫模式Balking
|
算法 前端开发 JavaScript
不逼自己一把都不知道自己还能这么优秀(小鹅通学习记录大批量队列同步)
不逼自己一把都不知道自己还能这么优秀(小鹅通学习记录大批量队列同步)
157 0
|
消息中间件 Java
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式(一)
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式(一)
|
算法 Linux
linux多线程同步设计
linux多线程同步设计
175 0
linux多线程同步设计
|
缓存 监控 安全
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式(二)
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式
《JUC并发编程 - 模式篇》保护性暂停模式 | 顺序控制模式 | 生产者消费者模式 | 两阶段终止模式 | Balking模式 | 享元模式(二)
|
Java 开发者
同步问题引出|学习笔记
快速学习 同步问题引出
同步问题引出|学习笔记