前言
我们知道了什么是线程, 知道了什么是锁, 但是怎样编写多线程程序呢? 通过下面的文章将告诉大家编写一个简单的多线程程序需要那几步。
线程间通信
谈到线程间通信就不得不说一个经典的生产者-消费者模型, 我们假定这样一个场景:
- 一个小小的饮品店, 桌子太小了每次只能放一杯饮品
- 桌子上没有饮品的时候顾客就会等待
- 桌子上有饮品的时候店长就不做了(很佛系)
我们尝试用代码实现以下这个场景:
-
把饮品店, 饮品, 做饮品, 取饮品给抽象出来
class IceCream{ //饮品 private static int water = 0; //做饮品 void produce() {} //取饮品 void consume() {} }
-
方法的具体实现, 考虑多线程操作
class IceCream{ //饮品 private static int water = 0; //做饮品 synchronized void produce() throws InterruptedException { //判断桌子上有没有饮品, 有就不做了 if (water != 0) { this.wait(); } //没有咱就做一杯 water++; System.out.println(Thread.currentThread().getName() + " : " + water); //告诉消费者可以取了 this.notifyAll(); } //取饮品 synchronized void consume() throws InterruptedException { //判断桌子上有没有饮品, 没有就等着 if (water != 1) { this.wait(); } //有就取走 water--; System.out.println(Thread.currentThread().getName() + " : " + water); //告诉店家该做了 this.notifyAll(); } }
-
来个main()方法调用一下
public class Test{ public static void main(String[] args) { //创建资源类 IceCream ic = new IceCream(); //创建生产线程 new Thread(()->{ for (int i = 0; i <= 10; i++) { try { ic.produce(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "produce1").start(); //创建消费线程 new Thread(()->{ for (int i = 0; i <= 10; i++) { try { ic.consume(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "consume1").start(); } }
运行一下:
发现非常和睦, produce1做一杯, consume1买一杯。
这时我们需求升级了, 多来个做饮品的, 买饮品的也多排了一队, 但是桌子还是那么大, 简单, 我们再来两个线程就好
public class Test{
public static void main(String[] args) {
//创建资源类
IceCream ic = new IceCream();
//创建生产线程1
new Thread(()->{
for (int i = 0; i <= 10; i++) {
try {
ic.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "produce1").start();
//创建生产线程2
new Thread(()->{
for (int i = 0; i <= 10; i++) {
try {
ic.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "produce2").start();
//创建消费线程1
new Thread(()->{
for (int i = 0; i <= 10; i++) {
try {
ic.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "consume1").start();
//创建消费线程2
new Thread(()->{
for (int i = 0; i <= 10; i++) {
try {
ic.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "consume2").start();
}
}
看一下运行结果:
出大问题, 不是说好的桌子上只能放一杯么, 为什么会出现这种问题呢, 那是因为在程序中发生了虚假唤醒:
之前说过, wait()这个方法是在哪里停下就在那里开始, 回头看一眼代码两个方法中进行判断的地方
//判断桌子上有没有饮品, 有就不做了
if (water != 0) {
this.wait();//在这里醒来就相当于判断已经结束, 要执行下面的代码块了, 但是这是重新进来的, 应该再次进行判断
}
//判断桌子上有没有饮品, 没有就等着
if (water != 1) {
this.wait();//在这里醒来就相当于判断已经结束, 要执行下面的代码块了, 但是这是重新进来的, 应该再次进行判断
}
我们使用while循环替代if, 这样每次醒过来就会重新进行判断, 改完之后的代码如下:
class IceCream{
//饮品
private static int water = 0;
//做饮品
synchronized void produce() throws InterruptedException {
//判断桌子上有没有饮品, 有就不做了
while (water != 0) {//使用while循环解决虚假唤醒问题
this.wait();
}
//没有咱就做一杯
water++;
System.out.println(Thread.currentThread().getName() + " : " + water);
//告诉消费者可以取了
this.notifyAll();
}
//取饮品
synchronized void consume() throws InterruptedException {
//判断桌子上有没有饮品, 没有就等着
while (water != 1) {//使用while循环解决虚假唤醒问题
this.wait();
}
//有就取走
water--;
System.out.println(Thread.currentThread().getName() + " : " + water);
//告诉店家该做了
this.notifyAll();
}
}
再次看一下运行结果:
又和睦起来了!
多线程编程步骤
经过上面的那个案例, 可以总结一下四个步骤:
- 创建资源类, 在资源类中创建共享属性和操作方法
- 在资源类操作方法: 判断、操作、通知
- 创建多个线程, 调用资源类的操作方法
- 防止虚拟唤醒问题
Lock接口版
既然synchronized可以实现上面的案例, 那么Lock接口肯定也可以实现, 使用Condition的两个方法实现线程等待和唤醒
class IceCream{
//饮品
private static int water = 0;
//创建可重入锁
private Lock lock = new ReentrantLock();
//获得condition实例
private Condition condition = lock.newCondition();
//做饮品
void produce() throws InterruptedException {
//上锁
lock.lock();
try {
//判断桌子上有没有饮品, 有就不做了
while (water != 0) {//使用while循环解决虚假唤醒问题
condition.await();
}
//没有咱就做一杯
water++;
System.out.println(Thread.currentThread().getName() + " : " + water);
//告诉消费者可以取了
condition.signalAll();
} finally {
//释放锁
lock.unlock();
}
}
//取饮品
void consume() throws InterruptedException {
//上锁
lock.lock();
try {
//判断桌子上有没有饮品, 没有就等着
while (water != 1) {//使用while循环解决虚假唤醒问题
condition.await();
}
//有就取走
water--;
System.out.println(Thread.currentThread().getName() + " : " + water);
//告诉店家该做了
condition.signalAll();
} finally {
//释放锁
lock.unlock();
}
}
}
看一下运行结果:
完美复刻!
线程定制化通信
上面案例的结果都有一个特点, 两个生产者和两个消费者出现的很乱, 有没有可能让线程间按照我们想要的顺序运行呢?
来看一个简单案例, 要求是
- AA, BB, CC三个线程, 每个线程调用一个方法
- 执行顺序是AA->BB->CC
- 循环执行10轮
看一下思路
根据这个思路, 来编写代码
class ShareResource {
//定义标志位
private static int flag = 1;
//创建可重入锁
private Lock lock = new ReentrantLock();
//创建三个condition
Condition cA = lock.newCondition();
Condition cB = lock.newCondition();
Condition cC = lock.newCondition();
//A方法
public void methodA(int loop) {
lock.lock();
try {
//判断
while (flag != 1) {
cA.await();
}
System.out.println(Thread.currentThread().getName() + "methodA : 第" + loop + "轮");
//设置标志位为2
flag = 2;
//cB唤醒
cB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//B方法
public void methodB(int loop) {
lock.lock();
try {
//判断
while (flag != 2) {
cB.await();
}
System.out.println(Thread.currentThread().getName() + "methodB : 第" + loop + "轮");
//设置标志位为3
flag = 3;
//cC唤醒
cC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//C方法
public void methodC(int loop) {
lock.lock();
try {
//判断
while (flag != 3) {
cC.await();
}
System.out.println(Thread.currentThread().getName() + "methodC : 第" + loop + "轮");
//设置标志位为1
flag = 1;
//cA唤醒
cA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
写个主类测试一下
public class ThreadDemo {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.methodA(i);
}
}, "AA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.methodB(i);
}
}, "BB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.methodC(i);
}
}, "CC").start();
}
}
看下运行结果
按照我们的需求完美运行, 这就是线程定制化通信!
我的个人主页: www.ayu.link
本文连接: ┏ (゜ω゜)=☞