方法1:
package com.itheima.bookcurrentment;
分析:
/*
前提:线程123分别输出abc
需求:交替打印abc5次,打印结果示例:abcabcabcabc…
思路:用同步方法,定义多个条件,满足条件时打印,不满足时进入wait等待
设置一个整数,当数为1时打印1,当不是1时(相当于条件不满足)则进入wait等待
输出内容 等待标记 下一个标记
a 1 2
b 2 3
c 3 1
*/
@Slf4j(topic = "c.TestPrint1")
public class TestPrint1 {
public static void main(String[] args) {
//测试:由于要共用同一个WaitNotify对象,因此先创建一个WaitNotify对象,以便后面对它进行加锁
// 创建三个线程,在这个三个线程中调用打印方法,并且在该方法中指定打印内容,等待标记(公共标记与这个等待标记一致则打印内容),以及下一个标记
WaitNotify waitNotify=new WaitNotify(1,5);//初始标记为1,循环次数是5
new Thread(()->{
try {
waitNotify.print("a",1,2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
waitNotify.print("b",2,3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
waitNotify.print("c",3,1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
class WaitNotify{
//打印方法,该方法由不同的线程调用,不同线程输出的内容,等待标记和下一个标记都不相同,因此将这三个值作为参数
public void print(String str,int waitFlag,int nextFlag) throws InterruptedException {
//打印逻辑:加锁,先获取到调用线程的标记,与公共标记相比较,相等则继续打印相应内容,
// 并设置下一个标记(唤醒下一个线程),不相等则进入阻塞
for(int i=0;i<loopNumber;i++){
synchronized (this){
while (flag!=waitFlag){
this.wait();
}
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:使用ReentrantLock实现,充分利用这个锁的特点----它的条件变量(休息室)可以有多个
@Slf4j(topic = "c.TestPrint2")
public class TestPrint2 {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal=new AwaitSignal(5);
//创建了3个休息室
Condition a=awaitSignal.newCondition();
Condition b=awaitSignal.newCondition();
Condition c=awaitSignal.newCondition();
new Thread(()->{
awaitSignal.print("a",a,b);//该线程打印a,打印之前首先进入休息室a等待,当它执行完成后要唤醒b休息室的线程
}).start();
new Thread(()->{
awaitSignal.print("b",b,c);//该线程打印b,打印之前首先进入休息室b等待,当它执行完成后要唤醒的c休息的线程
}).start();
new Thread(()->{
awaitSignal.print("c",c,a);//该线程打印c,打印之前首先进入休息室b等待,当它执行完成后要唤醒的a休息室的线程
}).start();
//由于这三个线程一启动都进入了休息室等待,因此需要一个线程先唤醒a休息室中的线程
TimeUnit.SECONDS.sleep(1);
awaitSignal.lock();//获取锁
try{
log.debug("由主线程开始唤醒a线程后,,,,开始。。。。。。。。");
a.signal();//唤醒a休息室的线程
}finally {
awaitSignal.unlock();//释放锁
}
}
}
class AwaitSignal extends ReentrantLock{
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
//写一个print,由三个线程调用,当不满足条件时进入格各自的休息室等待
//参数1:打印的内容,参数2,进入哪一间休息室,参数3,表示下一件休息室
public void print(String str,Condition current,Condition next){
for(int i=0;i<loopNumber;i++){
lock();//继承了ReentrantLock,可以省略前面的this
try{
current.await();//获得锁之后,先进入休息室等待被唤醒时表示可以继续运行执行它的打印了
System.out.print(str);
next.signal();//打印完成之后去唤醒下一件休息室的线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
打印结果:
19:51:44.940 [main] DEBUG c.TestPrint2 - 由主线程开始唤醒a线程后,,,,开始。。。。。。。。
abcabcabcabcabc
方式3:使用park()和unpark()实现
//这两个方法的特点是它没有对象锁的概念,也没有ReentrantLock锁的概念,其他的休息室之类的都没有了
//它去停止和恢复线程的运行都是以线程自身为单位的,所以它的实现更为简单
@Slf4j(topic = "c.TestPtint3")
public class TestPrint3 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
parkUnpark parkUnpark=new parkUnpark(5);
t1= new Thread(()->{
parkUnpark.print("a",t2);//线程t1,要打印的是a,要唤醒的是t2
});
t2= new Thread(()->{
parkUnpark.print("b",t3);//线程t2,要打印的是b,要唤醒的是t3
});
t3= new Thread(()->{
parkUnpark.print("c",t1);//线程t3,要打印的是c,要唤醒的是t1
});
t1.start();
t2.start();
t3.start();
//这三个线程启动后就park(暂停)住了,需要主线程来唤醒t1,让这些线程继续往下执行
LockSupport.unpark(t1);
}
}
class parkUnpark{
private int loopNumber;
//参数1:要打印的内容,参数2:要唤醒的下一个线程(需要暂停哪个,因为它是基于线程的,执行park就是暂停本线程)
public void print(String str,Thread next){
for(int i=0;i<loopNumber;i++){
//每次进入循环是,先让当前线程park(暂停下来)
LockSupport.park();//暂停当前线程
System.out.print(str);//打印
LockSupport.unpark(next);//唤醒下一个线程
}
}
public parkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
}
打印结果:abcabcabcabcabc