1.同步模式之固定顺序输出
t1,t2都运行,谁先运行是不确定的,取决于处理器的调度
想要控制线程顺序:让t2先运行
(1)wait和notify控制
static boolean t2IsRunned = false;//t2是否运行
static final Object obj = new Object();//资源
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj) {
while (!t2IsRunned) {
try {
log.debug("t1,wait");
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("t1运行");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (obj) {
log.debug("t2运行");
t2IsRunned = true;
obj.notify();
}
}, "t2");
t1.start();
t2.start();
}
(2)ReentrantLock的await和signal
static boolean t2IsRunned = false;//t2是否运行
static final ReentrantLock obj = new ReentrantLock();//资源
static Condition condition = obj.newCondition();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
obj.lock();
log.debug("t1上锁");
while (!t2IsRunned) {
try {
log.debug("t1等待");
condition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("t1运行");
}finally {
obj.unlock();
log.debug("t1解锁");
}
}, "t1");
Thread t2 = new Thread(() -> {
try {
obj.lock();
log.debug("t2上锁");
log.debug("t2运行");
t2IsRunned = true;
log.debug("t2唤醒t1");
condition.signal();
}finally {
obj.unlock();
log.debug("t2解锁");
}
}, "t2");
t1.start();
t2.start();
}
(3)park和unpark
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("t1-park停止");
LockSupport.park();
log.debug("t1运行");
}, "t1");
Thread t2 = new Thread(() -> {
log.debug("运行t2");
log.debug("t1补充");
LockSupport.unpark(t1);
}, "t2");
t2.start();
t1.start();
}
2.同步模式之交替输出
需求:
线程t1打印a;t2打印b;t3打印c。需要打印n轮abc
(1)wait和notify
思想:
1.设计一个flag,当flag为1时,t1执行,然后把资源期待得到的flag变为2,依次类推。
2.当不足条件,则wait
3.条件满足,执行,改变期待的flag,然后唤醒其他线程。
@Slf4j(topic = "c.test1")
public class test1 {
public static void main(String[] args) {
Print print = new Print(1, 3);
Thread t1 = new Thread(() -> {
print.print("a", 1, 2);
}, "t1");
Thread t2 = new Thread(() -> {
print.print("b", 2, 3);
}, "t2");
Thread t3 = new Thread(() -> {
print.print("c", 3, 1);
}, "t3");
t1.start();
t2.start();
t3.start();
}
}
/**
* 打印的执行类(资源)
*/
class Print {
private Integer flag;//标记.1-t1;2-t2;3-t3
private Integer loop;//打印的轮数
public Print(Integer flag, Integer loop) {
this.flag = flag;
this.loop = loop;
}
/**
* 线程调用的打印方法
*
* @param content 打印的内容
* @param waitFlag 等待执行的标记
* @param nextFlag 执行后下一个执行线程的标记
*/
public void print(String content, Integer waitFlag, Integer nextFlag) {
for (int i = 0; i < loop; i++) {
synchronized (this) {
while (flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//就是自己执行
System.out.println(content);
flag = nextFlag;
this.notifyAll();
}
}
}
}
(2)ReentrantLock的await和signal
ReentrantLock也可以用上面的方式(用flag控制是不是自己运行,不是则wait)的方式运行,即让Print类实现ReentrantLock即可。
但是根据其多个变量的(多个WaitSet)的特点,可以用3个变量来实现。
思想:先把三个线程分别放在自己的变量中await,然后main唤醒t1,t1执行完,唤醒t2,依次类推即可
这个例子中没有用while去防止虚假唤醒,是因为每个阻塞队列只有一个线程。在正常的情况下,一定要记得使用while防止虚假唤醒
@Slf4j(topic = "c.test1")
public class test1 {
public static void main(String[] args) {
Print print = new Print(3);
Condition condition1 = print.newCondition();
Condition condition2 = print.newCondition();
Condition condition3 = print.newCondition();
Thread t1 = new Thread(() -> {
print.print("a", condition1, condition2);
}, "t1");
Thread t2 = new Thread(() -> {
print.print("b", condition2, condition3);
}, "t2");
Thread t3 = new Thread(() -> {
print.print("c", condition3, condition1);
}, "t3");
t1.start();
t2.start();
t3.start();
//等1s,为了让三个线程都在各自的变量里阻塞
try {
sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//主线程唤醒t1
print.lock();//主线程必须先上锁,再区唤醒
try {
condition1.signal();
}finally {
print.unlock();
}
}
}
/**
* 打印的执行类(资源)
*/
class Print extends ReentrantLock {
private Integer loop;//打印的轮数
public Print(Integer loop) {
this.loop = loop;
}
/**
* 线程调用的打印方法
*
* @param content 打印的内容
* @param myCondition 当前线程await的waitSet
* @param nextConditin 下一个线程await的waitSet
*/
public void print(String content, Condition myCondition, Condition nextConditin) {
for (int i = 0; i < loop; i++) {
this.lock();
try {
try {
//全都先await
myCondition.await();
//被唤醒(达到执行条件)执行
System.out.println(content);
nextConditin.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}finally {
this.unlock();
}
}
}
}
(3)park和unpark
park会阻塞当前线程,unpark会唤醒指定线程
@Slf4j(topic = "c.test1")
public class test1 {
//声明成员变量:1。会初始化;2.t1的时候就要传入t2,提前声明
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
Print print = new Print(3);
t1 = new Thread(() -> {
print.print("a", t2);
}, "t1");
t2 = new Thread(() -> {
print.print("b", t3);
}, "t2");
t3 = new Thread(() -> {
print.print("c", t1);
}, "t3");
t1.start();
t2.start();
t3.start();
//unpark t1
LockSupport.unpark(t1);
}
}
/**
* 打印的执行类(资源)
*/
class Print{
private Integer loop;//打印的轮数
public Print(Integer loop) {
this.loop = loop;
}
/**
* 线程调用的打印方法
* @param content 打印的内容
* @param next 要唤醒的下一个线程
*/
public void print(String content, Thread next) {
for (int i = 0; i < loop; i++) {
LockSupport.park();//如果该他执行了,就会有一个unpark的存储,这个park就不会阻塞住
System.out.println(content);
LockSupport.unpark(next);
}
}
}
3.同步模式之Balking模式
Balking模式是指线程发现前面已经有线程做了一件事,自己就不用做这件事了
举例
有一个监控线程执行监控任务,但是这个监控任务只需要执行一次
1.那么就可以设置一个volatile变量,在第一个线程执行监控任务之后,修改这个变量,让后面的线程不用再做了
2.另外为了保证这个变量修改过程的原子性,还需要对这个过程加锁
4.终止模式之两阶段终止模式
错误的两种终止线程的方式:
- 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
- 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止
(1)通过打断的方式实现
两阶段终止模式:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器
应用:2秒一次进行监控,若需要不监控,可以优雅的停止监控行为
1.当正常情况被打断,有一个料理后事的时间,然后终止
2.进入睡眠模式被打算(异常),设置标记,重新进入循环,有一个料理后事的时间,然后终止
这样的化,无论是正常情况下被打断还是睡眠情况下被打断,都会有一个料理后事的时间
public class Test {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
class TwoPhaseTermination {
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
System.out.println("后置处理");
break;
}
try {
Thread.sleep(1000); // 睡眠
System.out.println("执行监控记录"); //1. 在此被打断不会异常,仅仅是改变了标记为true,会进入下一次循 环,在代码19行进行后置处理
} catch (InterruptedException e) { // 在睡眠期间被打断的化,进入异常处理的逻辑
e.printStackTrace();
// 2.重新设置打断标记,打断 sleep 会清除打断状态为true
//结束后继续进入下一次循环,然后在代码19行进行后置处理
thread.interrupt();
}
}
}
});
monitor.start();
}
// 停止监控线程
public void stop() {
monitor.interrupt();
}
}
(2)通过volatile变量实现
上面实现两阶段终止模式
可以看出,之前想要一个线程A打断线程B,是通过monitor.interrupt();
进行打断
1.在正常运行情况下,修改了打断标记位true,从而在下一次循环进入if语句进行后置处理
2.在睡眠情况下,进入异常catch块修改打断标记,从而在下一次循环进入if语句进行后置处理
这样的问题就是需要通过打断标记来判断进步进入后置处理,就很容易出问题(为啥容易出问题?是因为打断标记容易被修改吗?)
所以考虑设置一个volatile标记变量,在打断的时候修改这个变量,则在下一次循环的时候,就可以不依靠打断标记,而依靠这个变量标记,看是否进入后置处理。
5.同步模式之保护性暂停
(1)基本实现
保护性暂停就是让一个线程等待另一个线程的执行结果,然后在执行
怎么做?
使用一个资源对象GuardedObject(操作等待资源和赋值资源两个操作),等待的线程A判断这个资源对象的资源是否为空,来判断被等待的线程B是否已经完成了。
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 complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
lock.notifyAll();
}
}
}
//使用
public static void main(String[] args) {
//资源对象
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
try {
// 子线程执行下载
List<String> response = download();
log.debug("download complete...");
//子线程完成任务后,给资源对象的资源赋值
guardedObject.complete(response);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
log.debug("waiting...");
// 主线程调用了get,阻塞等待。子线程结束后,获得了结果,阻塞结束
Object response = guardedObject.get();
log.debug("get response: [{}] lines", ((List<String>) response).size());
}
(2)给等待设置超时时间
为了让正在等待的线程有一个时间的限制,可以给设置一个超时时间
要注意的点:
1.wait方法也要传入一个时间,设置等待的时间。
2.但是这个给wait传入的时间不是真正的这个超时时间,因为等待之后,要重新进入while循环,如果一直都是一个时间,就是有问题的。这个传入的时间应该时还需要等待多久的时间。
join底层就是使用了设置超时时间的保护性暂停
(3)多任务处理
思想:
1.GuardedObject类用于进行等待信件和投递信件的造作
2.因为不同的邮递员给不同的邮箱,所以GuardedObject对象是使用id进行区分绑定的
3.需要个邮箱类,进行对GuardedObject对象进行操作
GuardedObject负责等待结果和产生结果
class GuardedObject {
// 标识 Guarded Object
private int id;//id负责表示GuardedObject
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
// 结果
private Object response;
// 获取结果
// timeout 表示要等待多久 2000
public Object get(long timeout) {
synchronized (this) {
// 开始时间 15:00:00
long begin = System.currentTimeMillis();
// 经历的时间
long passedTime = 0;
while (response == null) {
// 这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
// 经历的时间超过了最大等待时间时,退出循环
if (timeout - passedTime <= 0) {
break;
}
try {
this.wait(waitTime); // 虚假唤醒 15:00:01
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s
}
return response;
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
用于解耦的邮箱类(用邮箱类绑定操作类,进行解耦)
用户类和邮递员类
执行类
6异步模式之生产者消费者
思想:
主要就是一个消息队列。当空的时候,取操作wait;满的时候放操作wait。
取和放的操作要加锁
消息类
消息队列类
应用
7.异步模式之工作线程
可以看到上面的代码是一个固定大小为2的线程池,当创建两个线程,两个线程都开始执行点菜的操作,此时就会产生饥饿
解决饥饿:不同任务类型使用不同线程池