并发编程(七)-多线程设计模式的应用

一、多线程设计模式之两阶段终止模式

1. 描述

两阶段终止模式(Two Phase Termination)

  • 在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

错误思路

  • 使用线程对象的 stop() 方法停止线程
    • stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程
    • 我们目的仅是停止一个线程,但这种做法会让整个程序都停止

2. 实现

2.1 使用interrupt实现

在这里插入图片描述

@Slf4j(topic = "c.TestTwoPhaseTermination")
public class TestTwoPhaseTermination {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination t = new TwoPhaseTermination();
        t.start();
        Thread.sleep(3500);
        log.debug("stop");
        t.stop();
    }
}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    // 监控线程
    private Thread monitorThread;

    // 启动监控线程
    public void start(){
        monitorThread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);// 如果在此处被打断,执行有异常流程
                    log.debug("执行监控记录");// 如果在此处被打断,执行无异常流程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 因为sleep出现异常后,会清除打断标记
                    // 所以需要重新设置打断标记
                    current.interrupt();
                }
                // 执行监控操作
            }
        },"监控线程");
        monitorThread.start();
    }

    // 停止监控线程
    public void stop() {
        monitorThread.interrupt();
    }

}

14:24:26.964 c.TwoPhaseTermination [监控线程] - 执行监控记录
14:24:27.970 c.TwoPhaseTermination [监控线程] - 执行监控记录
14:24:28.979 c.TwoPhaseTermination [监控线程] - 执行监控记录
14:24:29.462 c.TestTwoPhaseTermination [main] - stop
14:24:29.463 c.TwoPhaseTermination [监控线程] - 料理后事
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at cn.itcast.n3.TwoPhaseTermination.lambda$start$0(TestTwoPhaseTermination.java:33)
	at java.lang.Thread.run(Thread.java:748)

2.2 使用volatile实现

  • 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
@Slf4j(topic = "c.Volatile_TwoPhaseTermination")
public class Volatile_TwoPhaseTermination {

    public static void main(String[] args) throws InterruptedException {
        Volatile_TwoPhase thread = new Volatile_TwoPhase();
        log.info("开始监控");
        thread.start();
        // 休眠时打断
        Thread.sleep(1000);
        log.info("停止监控");
        thread.stop();
    }
}

@Slf4j(topic = "c.Volatile_TwoPhase")
class Volatile_TwoPhase {

    Thread monitor;
    // 打断变量用volatile修饰,可以保证多个线程实时可见该变量的值
    public volatile boolean ifStop;

    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                if (ifStop) {
                    log.info("料理后事");
                    break;
                }

                try {
                    Thread.sleep(5000);
                    log.info("执行监控");
                } catch (InterruptedException e) {
                    log.info("休眠时被立即打断");
                }
            }
        },"monitor");
        monitor.start();
    }

    public void stop() {
        ifStop = true;
        // 多执行一次打断动作interrupt(),是为了让休眠时的线程立马被打断,进入下轮循环
        // 而不是等时间休眠完再进入下轮循环
        monitor.interrupt();
    }
}

13:31:35.563 c.Volatile_TwoPhaseTermination [main] - 开始监控
13:31:36.627 c.Volatile_TwoPhaseTermination [main] - 停止监控
13:31:36.627 c.Volatile_TwoPhase [monitor] - 休眠时被立即打断
13:31:36.627 c.Volatile_TwoPhase [monitor] - 料理后事

二、多线程设计模式之保护性暂停(同步模式)

1. 描述

  • 即 Guarded Suspension,用在一个线程同步等待另一个线程的执行结果

要点

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

在这里插入图片描述

2. 实现

@Slf4j(topic = "c.TestGuardedObject")
public class TestGuardedObject {
    public static void main(String[] args) {

        GuardedObject guardedObject = new GuardedObject();
        // t1等待获取结果,超过2s则退出
        new Thread(() -> {
            log.debug("begin");
            Object response = guardedObject.get(2000);
            log.debug("获取的结果:{}", response);
        }, "t1").start();

        // t2 完成后,通知 t1
        new Thread(() -> {
            log.debug("begin");
            try {
            	// t2 延迟 1s 返回结果 
                TimeUnit.SECONDS.sleep(1);
                // TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            guardedObject.complete(new Object());
        }, "t2").start();
    }


}

@Slf4j(topic = "c.GuardedObject")
class GuardedObject {
    // 存放结果
    private Object response;

    private final Object lock = new Object();

    // 获取结果
    // timeout 表示等待的超时时间
    public Object get(long timeout) {
        synchronized (lock) {
            // 1) 记录最初时间
            long begin = System.currentTimeMillis();
            // 2) 已经经历的时间
            long timePassed = 0;
            while (response == null) {
                // 4) 假设 timeout 是 10s,结果在 4s 时被唤醒了,那么还有 6s 要等
                long waitTime = timeout - timePassed;
                if (waitTime <= 0) {
                    log.debug("超过等待的最大时间,退出...");
                    break;
                }
                try {
                    lock.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 3) 如果提前被唤醒,这时已经经历的时间假设为 4s
                timePassed = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足,通知等待线程
            this.response = response;
            log.debug("notify...");
            lock.notifyAll();
        }
    }

}
  • t1 等待结果超时时间是2s, 当 t2 延迟 1s 返回结果,TimeUnit.SECONDS.sleep(1);
22:15:11.447 c.TestGuardedObject [t2] - begin
22:15:11.449 c.TestGuardedObject [t1] - begin
22:15:12.452 c.GuardedObject [t2] - notify...
22:15:12.452 c.TestGuardedObject [t1] - 获取的结果:java.lang.Object@647f76d5
  • t1 等待结果超时时间是2s, 当 t2 延迟 3s 返回结果,TimeUnit.SECONDS.sleep(3);
22:17:38.536 c.TestGuardedObject [t2] - begin
22:17:38.541 c.TestGuardedObject [t1] - begin
22:17:40.541 c.GuardedObject [t1] - 超过等待的最大时间,退出...
22:17:40.541 c.TestGuardedObject [t1] - 获取的结果:null
22:17:41.541 c.GuardedObject [t2] - notify...

3. join 原理

  • join 体现的是【保护性暂停】模式
  • t1.join(); 调用者轮询检查t1线程 alive 状态
  • 实际调用的是 wait 方法

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) {
        	// 调用者轮询检查线程 alive 状态
            while (isAlive()) {
                wait(0);
            }
        } else {
        	// 调用者轮询检查线程 alive 状态
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                // 防止虚假唤醒,记录已经等待的时间
                now = System.currentTimeMillis() - base;
            }
        }
    }
t1.join();
等价于下面的代码
synchronized (t1) {
	// 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
	while (t1.isAlive()) {
		t1.wait(0);
	}
}

三、多线程设计模式之生产者/消费者(异步模式)

在这里插入图片描述

1. 描述

要点

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

2. 实现

@Slf4j(topic = "c.Test21")
public class Test21 {

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

        for (int i = 0; i < 3; i++) {
            int id = i;
            new Thread(() -> {
                queue.put(new Message(id , "值"+id));
            }, "生产者" + i).start();
        }

        new Thread(() -> {
            while(true) {
                sleep(1);
                Message message = queue.take();
            }
        }, "消费者").start();
    }

}

// 消息队列类 , java 线程之间通信
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
    // 消息的队列集合,双向队列
    private LinkedList<Message> list = new LinkedList<>();
    // 队列容量
    private int capcity;

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

    // 获取消息
    public Message take() {
        // 检查队列是否为空
        synchronized (list) {
            while(list.isEmpty()) {
                try {
                    log.debug("队列为空, 消费者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 从队列头部获取消息并返回
            Message message = list.removeFirst();
            log.debug("已消费消息 {}", message);
            list.notifyAll();
            return message;
        }
    }

    // 存入消息
    public void put(Message message) {
        synchronized (list) {
            // 检查对象是否已满
            while(list.size() == capcity) {
                try {
                    log.debug("队列已满, 生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 将消息加入队列尾部
            list.addLast(message);
            log.debug("已生产消息 {}", message);
            list.notifyAll();
        }
    }
}

@Data
@AllArgsConstructor
final class Message {
    private int id;
    private Object value;
}
10:59:44.966 c.MessageQueue [生产者0] - 已生产消息 Message(id=0, value=0)
10:59:44.975 c.MessageQueue [生产者2] - 已生产消息 Message(id=2, value=2)
10:59:44.975 c.MessageQueue [生产者1] - 队列已满, 生产者线程等待
10:59:45.976 c.MessageQueue [消费者] - 已消费消息 Message(id=0, value=0)
10:59:45.976 c.MessageQueue [生产者1] - 已生产消息 Message(id=1, value=1)
10:59:46.990 c.MessageQueue [消费者] - 已消费消息 Message(id=2, value=2)
10:59:48.002 c.MessageQueue [消费者] - 已消费消息 Message(id=1, value=1)
10:59:49.009 c.MessageQueue [消费者] - 队列为空, 消费者线程等待

四、多线程设计模式之控制线程顺序(同步模式)

1. 固定顺序

要求:固定顺序 先打印2后打印1

(1) wait & notify

  • ①需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait;
  • ②如果有些干扰线程错误地 notify 了 wait 线程(虚假唤醒),条件不满足时还要重新等待,使用了 while 循环来解决此问题;
  • ③唤醒『同步对象』上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个;
public class Order_WaitNotify {
    // 『同步对象』锁对象
    static Object lock = new Object();
    // t2 运行标记, 代表 t2 是否执行过
    static boolean t2runed = false;

    public static void main(String[] args) {

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

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

        t1.start();
        t2.start();
    }
}
[t2] - 2
[t1] - 1

(2) park & unpark

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

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
            LockSupport.park();
            System.out.println("1");
        }, "t1");

        Thread t2 = new Thread(() -> {
            System.out.println("2");
            // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
            LockSupport.unpark(t1);
        }, "t2");

        t1.start();
        t2.start();
    }
}
[t2] - 2
[t1] - 1

(3)ReentrantLock使用条件变量Condition

public class Order_ConditionReentrantLock {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition1 = lock.newCondition();
    static Condition condition2 = lock.newCondition();
    static volatile boolean t1Flag;
    static volatile boolean t2Flag;

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            try {
                lock.lock();
                while (!t1Flag) {
                    condition1.await();
                }
                System.out.println(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                lock.lock();
                while (!t2Flag) {
                    condition2.await();
                }
                System.out.println(2);
                t1Flag = true;
                condition1.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t2");

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

        // mian先唤醒t2,t2内部再去唤醒t1
        try {
            lock.lock();
            t2Flag = true;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

[t2] - 2
[t1] - 1

2. 交替顺序

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

(1)wait & notify

public class SwapOrder_WaitNotify {

    public static void main(String[] args) {
        // 从a线程开始打印,交替打印5轮
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify("a", 5);
        new Thread(() -> {
            syncWaitNotify.print("a", "b");
        },"a").start();
        new Thread(() -> {
            syncWaitNotify.print("b", "c");
        },"b").start();
        new Thread(() -> {
            syncWaitNotify.print("c", "a");
        },"c").start();
    }
}

class SyncWaitNotify {

    private String startThread;
    private int loopNumber;

    public SyncWaitNotify(String startThread, int loopNumber) {
        this.startThread = startThread;
        this.loopNumber = loopNumber;
    }

    public void print(String threadName, String nextThread) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (!threadName.equals(this.startThread)) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(threadName);
                startThread = nextThread;
                this.notifyAll();
            }
        }
    }
}
abcabcabcabcabc

(2)park & unpark

public class SwapOrder_ParkUnpark {

    static Thread a;
    static Thread b;
    static Thread c;

    public static void main(String[] args) throws InterruptedException {
        ParkUnpark parkUnpark = new ParkUnpark(5);
        a = new Thread(() -> {
            parkUnpark.print("a", b);
        }, "a");
        b = new Thread(() -> {
            parkUnpark.print("b", c);
        }, "b");
        c = new Thread(() -> {
            parkUnpark.print("c", a);
        }, "c");

        parkUnpark.setThreads(a, b, c);
        parkUnpark.start();
        LockSupport.unpark(a);
    }
}

class ParkUnpark {

    private int loopNum;
    private Thread[] threads;

    public ParkUnpark(int loopNum) {
        this.loopNum = loopNum;
    }

    public void setThreads(Thread... threads) {
        this.threads = threads;
    }

    public void print(String content, Thread nextThread) {
        for (int i = 0; i < loopNum; i++) {
            LockSupport.park();
            System.out.print(content);
            LockSupport.unpark(nextThread);
        }
    }

    public void start() {
        for (Thread thread : threads) {
            thread.start();
        }
    }
}
abcabcabcabcabc

(3)ReentrantLock使用条件变量Condition

public class SwapOrder_ConditionReentrantLock {

    static Thread a;
    static Thread b;
    static Thread c;
    static Condition aCondition;
    static Condition bCondition;
    static Condition cCondition;

    public static void main(String[] args) throws InterruptedException {
        AwaitSignal as = new AwaitSignal(5);
        aCondition = as.newCondition();
        bCondition = as.newCondition();
        cCondition = as.newCondition();
        a = new Thread(() -> {
            as.print("a", aCondition, bCondition);
        },"a");
        b = new Thread(() -> {
            as.print("b", bCondition, cCondition);
        },"b");
        c = new Thread(() -> {
            as.print("c", cCondition, aCondition);
        },"c");

        a.start();
        b.start();
        c.start();
        Thread.sleep(1000);
        as.start(aCondition);
    }
}

class AwaitSignal extends ReentrantLock {

    public void start(Condition first) {
        this.lock();
        try {
            first.signal();
        } finally {
            this.unlock();
        }
    }

    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                current.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();
            }
        }
    }

    // 循环次数
    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
}
abcabcabcabcabc

五、多线程设计模式之Balking犹豫模式(同步模式)

1. 描述

  • Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。

2. 实现

使用Balking犹豫模式防止多次启动相同的监控线程:

@Slf4j(topic = "c.Volatile_TwoPhaseTermination")
public class Volatile_TwoPhaseTermination {

    public static void main(String[] args) throws InterruptedException {
        Volatile_TwoPhase thread = new Volatile_TwoPhase();
        log.info("开始监控");
        // 试图多次启动监控线程
        thread.start();
        thread.start();
        thread.start();
        // 休眠时打断
        Thread.sleep(1000);
        log.info("停止监控");
        thread.stop();
    }
}

@Slf4j(topic = "c.Volatile_TwoPhase")
class Volatile_TwoPhase {

    private Thread monitor;
    // 打断变量用volatile修饰,可以保证多个线程实时可见该变量的值
    private volatile boolean ifStop;
    // 判断是否启动过监控线程,使用Balking犹豫模式防止多次启动相同的监控线程
    private boolean starting = false;

    // 启动监控线程
    public void start() {
        
        // 此处使用的就是 Balking犹豫模式
        synchronized (this) {
            if (starting) {
                return;
            }
            starting = true;
        }

        monitor = new Thread(() -> {
            while (true) {
                if (ifStop) {
                    log.info("料理后事");
                    break;
                }

                try {
                    Thread.sleep(5000);
                    log.info("执行监控");
                } catch (InterruptedException e) {
                    log.info("休眠时被立即打断");
                }
            }
        },"monitor");
        monitor.start();
    }

    public void stop() {
        ifStop = true;
        // 多执行一次打断动作interrupt(),是为了让休眠时的线程立马被打断,进入下轮循环
        // 而不是等时间休眠完再进入下轮循环
        monitor.interrupt();
    }
}
14:03:56.424 c.Volatile_TwoPhaseTermination [main] - 开始监控
14:03:57.479 c.Volatile_TwoPhaseTermination [main] - 停止监控
14:03:57.479 c.Volatile_TwoPhase [monitor] - 休眠时被立即打断
14:03:57.479 c.Volatile_TwoPhase [monitor] - 料理后事

3. Balking犹豫模式的应用

  • 同步方法的懒汉式单例模式中,应用到了 多线程Balking犹豫模式思想
// 同步方法的懒汉式单例模式,多线程下保证只有唯一的实例。
public final class Singleton {
    private Singleton() {
    }
    private static Singleton INSTANCE = null;
    public static synchronized Singleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }

        INSTANCE = new Singleton();
        return INSTANCE;
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值