打怪升级之小白的大数据之旅(二十七)
Java面向对象进阶之多线程安全与唤醒机制
上次回顾
上一期,我们学习了多线程的概念以及多线程的基本使用,本章对多线程的剩余知识点,线程安全与解决,锁机制进行讲解,学完这些知识点,多线程基本上就结束了,下面开始今天的内容
线程安全
线程安全问题引出与原因分析
- M公司为了应对夏季空调销售旺季的需求,于是要求每天至少生产50台空调并且交给手下的销售团队T和R进行销售,给他们的任务是每个团队至少完成25台空调的销售任务
T团队销售情况(使用Thred进行空调售卖)
public class Demo3 {
public static void main(String[] args) {
// T团队的老李来上班了...
Thread t1 = new CompanyT("老李");
// T团队的小张张来上班了...
Thread t2 = new CompanyT("小张张");
// T团队的老李出去售卖空调了.
t1.start();
// T团队的小张张出去售卖空调了
t2.start();
}
}
// T团队
class CompanyT extends Thread{
// 当日的空调库存单
private int airNum = 25;
// 销售团队人员名称
private String name;
// 构造器
public CompanyT(String name) {
this.name = name;
}
@Override
public void run() {
// 当天的销售情况
while (airNum>0)
System.out.println("T团队"+name+"卖出一台空调"+", 当天空调剩余:"+ --airNum + "台");
}
}
R团队销售空调情况(使用Runnable实现类)
package com.test01Thread;
public class Demo4 {
public static void main(String[] args) {
// Runnable实例化
Runnable r = new CompanyR();
// R团队的小白白来上班了...
Thread groupR1 = new Thread(r,"小白白");
// R团队的小倩来上班了...
Thread groupR2 = new Thread(r,"小倩");
// R团队的小白白出去售卖空调了.
groupR1.start();
// R团队的小倩出去售卖空调了
groupR2.start();
}
}
class CompanyR implements Runnable{
// 当日的空调库存单
private int airNum = 25;
@Override
public void run() {
// 当天的销售情况
while (airNum>0)
System.out.println("R团队"+Thread.currentThread().getName()+"卖出一台空调"+", 当天空调剩余:"+ --airNum + "台");
}
}
当天任务结束后,Main老板为庆祝两个销售团队出色地完成任务,于是请大家吃火锅;并请大家分享自己地销售经验
听完两个团队地讲述和看到团队成员地销售单不免惊奇:为什么T团队会卖出50台空调?明明当天的任务是每个团队25台,于是打算为T团队加薪
R团队成员不干了,嚷嚷道:T团队作弊,他们复制了空调的销售单,库存就那么多,怎么可能超额完成,Main老板问道:你怎么知道?R团队中的小倩说,她在今日的销售单时,跟小张张的库存单剩余都是23台,当时以为只是拿错了,没注意。造成这个问题的原因是什么呢?
- 实现Runnable接口的方式,因为都是用同一个Runnable对象创建的线程,因此多线程实际上执行的是同一个任务,这样也就共享了资源
- 通过继承Thread的方式,当新创建一个线程启动时,其绑定的任务同样也新建了一份,这样他们的任务是相互独立的,自然也无法实现资源共享了
总结一下造成线程安全问题出现的原因:
- 多线程执行
- 共享数据
- 多条语句操作共享数据
- 随着两个团队的争吵不断,Main老板终于明白了原因:T团队在售卖空调时,因为团队成员老李和小张张在外出销售时,都各自拿了一份库存单,所以是他们每个人都售出了五台空调
- 这下麻烦了,Main老板赶紧给工厂打电话,要求工人紧急加班生产缺少的那五台空调,虽然T团队超额完成了任务,但工厂工人不乐意了(要求加班工资…)
线程安全问题解决方案
经过上次差点闹出的事故,让工人紧急加班后,Main老板正在思索解决方案,这时有一位自称synchronized的年轻人来应聘,他一下就指出了之前的问题原因:
- 你现在的业务流程不太可靠(线程不安全),让我来统一管理整个空调的库存,每出售一台空调都需经过我的审批,一台卖完后才能来我这再次申请出售,这样每台空调都只能由一个业务员进行出售,问题也就自然解决了
- 上述线程安全问题的必备条件1和2是我们需要的,要解决只能从第三个点上想办法。要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复库存与库存空调不存在的问题,Java中提供了线程同步机制来解决
java中常使用关键字synchronized 来实现同步机制:
-
同步方法
public synchronized void method(){ 可能会产生线程安全问题的代码 }
-
同步代码块
synchronized(同步锁){ 需要同步操作的代码 }
Main老板决定试用synchronized一天,试试他的想法是否可行:
锁对象:
同步锁对象:
- 锁对象可以是任意类型。
- 多个线程对象 要使用同一把锁
- 同步锁有同步代码块和同步方法两种实现方式
同步代码块的锁对象
- 静态代码块中:使用当前类的Class对象
- 非静态代码块中:习惯上先考虑this,但是要注意是否同一个this
// 非静态代码块中使用synchronized package com.test01Thread; /* 因为CompanyT 是Thread的子类,实例化的时候,就相当于重新开辟了一个内存空间,因此synchronized中的同步锁只能使用当前类的Class对象,如果使用This,那么它就相当于每个实例化对象都有一把锁,依旧是多条语句操作共享数据 * */ public class Demo3 { public static void main(String[] args) { // T团队的老李来上班了... Thread t1 = new CompanyT("老李"); // T团队的小张张来上班了... Thread t2 = new CompanyT("小张张"); // T团队的老李出去售卖空调了. t1.start(); // T团队的小张张出去售卖空调了 t2.start(); } } // T团队 class CompanyT extends Thread{ // 当日的空调库存 private static int airNum = 25; // 销售团队人员名称 private String name; // 构造器 public CompanyT(String name) { this.name = name; } @Override public void run() { // 当天的销售情况 while (true) { // 在非静态代码块中添加同步锁,此时因为是Thread的继承子类,所以这里同步锁必须是类的Class synchronized (CompanyT.class) { if (airNum>0) System.out.println("T团队" + name + "卖出一台空调" + ", 当天空调剩余:" + --airNum + "台"); else break; } } } }
同步方法的锁对象
- 静态方法:使用当前类的Class对象作为同步锁
- 非静态方法:默认使用this作为同步锁
// 非静态方法中使用同步锁 package com.test01Thread; /* M公司为了应对夏季空调销售旺季的需求,于是要求每天至少制作10台空调 现在需要交给手下的销售团队T和R进行销售 * */ public class Demo3 { public static void main(String[] args) { // T团队的老李来上班了... Thread t1 = new CompanyT("老李"); // T团队的小张张来上班了... Thread t2 = new CompanyT("小张张"); // T团队的老李出去售卖空调了. t1.start(); // T团队的小张张出去售卖空调了 t2.start(); } } // T团队 class CompanyT extends Thread{ // 当日的空调库存 private static int airNum = 25; // 销售团队人员名称 private String name; // 构造器 public CompanyT(String name) { this.name = name; } @Override public void run() { // 使用 saleAir(); } // 在非静态方法中使用同步锁 public synchronized void saleAir(){ // 当天的销售情况 while (true) { // 在非静态代码块中添加同步锁 if (airNum>0) System.out.println("T团队" + name + "卖出一台空调" + ", 当天空调剩余:" + --airNum + "台"); else break; } } }
经过一天的试用,果然没有再发生重复的库存问题,Main老板决定让synchronized 转正,并涨薪。T和R团队的小伙伴又佩服又羡慕,不禁感叹:知识就是金钱,这句话果真没错,看来以后要多学习
编写多线程程序总结
- 多线程是为了提高程序的运行效率,减少代码的运行时间,多线程是线程操作资源类,并且遵循高内聚低耦合的原则
- 具体实现步骤就是首先编写资源类(共同的资源类),然后考虑线程安全问题(线程操作资源),针对线程安全问题,可以考虑使用同步代码块或同步方法
- 通俗一点的来说,当我们实现某个功能时,想增加它的运行效率,并且它有一个公共的资源可以让我们这个功能进行拆分。就如同M空调公司,它的整体就是空调的生产、销售和安装,它的公共资源就是空调,所以我们可以根据这个条件进行拆分
单例模式的线程安全问题
- 在内部类那一章介绍设计模式时,我为大家介绍了单例模式的两种创建方式:懒汉式和饿汉式,当时说过,饿汉式是由线程安全问题的,今天我就来介绍如何解决饿汉式的线程安全问题
public class Singleton { private static Singleton ourInstance; public static Singleton getInstance() { //一旦创建了对象,之后再次获取对象,都不会再进入同步代码块,提升效率 if (ourInstance == null) { //同步锁,锁住判断语句与创建对象并赋值的语句 synchronized (Singleton.class) { if (ourInstance == null) { ourInstance = new Singleton(); } } } return ourInstance; } private Singleton() { } } //测试类 public class Demo { public static void main(String[] args) { //开启多个线程获取单例 new SingletonThread().start(); new SingletonThread().start(); new SingletonThread().start(); } } //线程类 class SingletonThread extends Thread{ @Override public void run() { Singleton instance = Singleton.getInstance(); System.out.println(instance);//打印对象地址,查看每个线程获取的实例是否同一个 } }
- 饿汉式的单例模式,其线程安全问题同样是内部修改了公共的资源,所以要想解决饿汉式的单例模式,就需要在创建对象添加同步锁,并且再次判断一次是否创建了新的对象
等待唤醒机制
线程间通信
- 依旧以M空调公司举例子,M公司将整个公司业务拆分成了3个线程来做后,效率提高了不少,在前面线程安全问题中,T团队与R团队每天的的销售任务需要先去找synchronized 获取库存单
- 有了库存单,下面的销售和安装团队才可以根据库存单进行空调的销售和安装工作,这个库存单就是共有的资源
- 多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。而多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些通信机制,可以协调它们的工作,以此来帮我们达到多线程共同操作一份数据
等待唤醒机制
- 等待唤醒机制是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制
- 当线程满足某个条件时,就进入等待状态,知道其他线程完成该线程指定的代码后才会将其唤醒,也可以设置等待时间,当时间到了之后自动唤醒
- M公司每天生产空调时,如果买空调的人少了,销售人员也不怎么勤奋了,那么空调库存就会越来越多。同样的,假设销售人员没有卖出去空调,安装人员就无所事事了。所以我们需要等待唤醒机制来告诉线程,确保整个流程的顺利运行
使用等待与唤醒
- 等待
wati()
线程不再活动,不再参与调度,进入到wait set中,也不会再去竞争锁了,它会等待别的线程执行一个特别动作—notify()唤醒它或者等待时间到之后,就会重新进入到调度队列中
- 唤醒
notify()
选取所通知对象的wait set中的一个线程释放notifyAll()
释放所通知对象的wait set中的全部线程
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法
生产者与消费者问题
- 等待唤醒机制可以解决经典的“生产者与消费者”的问题
- 这是一个经典的多线程同步问题的经典案例,该问题描述了两个(多个)共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题
生产者与消费者问题中其实隐含了两个问题:
- 线程安全问题:因为生产者与消费者共享数据缓冲区,不过这个问题可以使用同步解决。
- 线程的协调工作问题:
- 要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻塞状态,等到下次消费者消耗了缓冲区中的数据的时候,通知(notify)正在等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让消费者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产者往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。通过这样的通信机制来解决此类问题
M空调公司一个工人和一个销售员问题
- M空调公司在刚刚对流程进行拆分时,只雇佣了一个工人生产组装空调,然后Main老板自己当销售,因为刚开始的创业,地方有限,只有一个两室的房子,一间用于生产空调,一间用于对外销售,外面房子只能放5台空调就放不下了。于是就有了下面的情况:
package com.test03WriterandCooker;
/*
* 消费者与生产者问题
* */
public class Demo {
public static void main(String[] args) {
// 公共资源
WorkRoom workRoom = new WorkRoom();
// 创建工人,并启动
new Thread(new Worker(workRoom),"工人").start();
// 创建销售,并启动
new Thread(new Sales(workRoom),"Main老板").start();
}
}
// 工人
class Worker implements Runnable{
private WorkRoom workroom;
// 创建构造器
public Worker(WorkRoom workroom) {
this.workroom = workroom;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);//休息一下,放大效果
} catch (InterruptedException e) {
e.printStackTrace();
}
// 不停地生产组装空调
workroom.putAir();
}
}
}
// 销售
class Sales implements Runnable{
private WorkRoom workroom;
// 创建构造器
public Sales(WorkRoom workroom) {
this.workroom = workroom;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(30);//休息一下,放大效果
} catch (InterruptedException e) {
e.printStackTrace();
}
// 不停地销售空调方法
workroom.getAir();
}
}
}
// 公共资源存取空调的工作间
class WorkRoom{
// 最大生产空调数
private int maxAir = 5;
// 当前空调数
private int airNum;
public synchronized void putAir(){
if (airNum>=maxAir) {
try {
this.wait(100);//工作台满后等待
System.out.println("工人休息一下....");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("工人生产了一台空调,并存放到工作间中,当前空调库存总剩余: "+ ++airNum + "台");
this.notify();//随机唤醒在此监视器上等待的一个线程
}
public synchronized void getAir(){
if (airNum<0){
try {
this.wait(10);//工作室空调满后等待
System.out.println("Main老板休息一下....");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Main老板销售了一台空调,并从工作间中获取,当前空调库存总剩余: "+ --airNum + "台");
this.notify();//随机唤醒一个等待的线程
}
}
代码解释:
- 首先创建一个公共资源类,WorkRoom,用于存储空调的房间,并定义房间最大存储空调数量,当前库存以及生产空调、销售空调的方法
- Workon工人类,用于生产空调(调用公共资源类)
- Sales销售人员类,用于销售空调(调用公共资源类)
- Main方法,创建多线程
- 为了解决线程安全问题,在存取空调的方法上使用了同步锁
释放锁操作与死锁
释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
- 当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁
不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()这样的过时来控制线程
死锁
- 同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续
- Main老板在销售空调时,遇到一个头疼的情况,该顾客说先发货再给钱,但是因为缺乏资金,Main老板坚持先给钱在发货,于是就产生了死锁
public class TestDeadLock { public static void main(String[] args) { Object g = new Object(); Object m = new Object(); Owner s = new Owner(g,m); Customer c = new Customer(g,m); new Thread(s).start(); new Thread(c).start(); } } class Owner implements Runnable{ private Object goods; private Object money; public Owner(Object goods, Object money) { super(); this.goods = goods; this.money = money; } @Override public void run() { synchronized (goods) { System.out.println("先给钱"); synchronized (money) { System.out.println("发货"); } } } } class Customer implements Runnable{ private Object goods; private Object money; public Customer(Object goods, Object money) { super(); this.goods = goods; this.money = money; } @Override public void run() { synchronized (money) { System.out.println("先发货"); synchronized (goods) { System.out.println("再给钱"); } } } }
最后,两人争持不下…顾客最后亲自过来取货了…
总结
- 多线程的知识点到今天就结束了,在java中,多线程是一个难点,特别是多线程中的操作公共资源时的线程安全问题以及死锁问题。因为编译可以通过,运行也不报错,所以很容易忽略这些问题而导致数据或程序异常,遇到问题不要慌,一步一步分析原因,如果程序很大,使用@Test进行分步调式,程序不大,使用断点调式即可
- 好了,今天内容就是这些,下一期我会为大家带来网络编程的知识点,并根据网络编程复习一下前面IO还有今天多线程的知识点。