文章目录
15、多线程总结
最近很认真的学习完了多线程,现在总结一波。加强自己的理解!(因为可能有错可以被指出嘛😱)
找到两个很棒的思维导图如下:
15.1 什么是多线程?
15.1.1 第一步:了解进程与线程的区别
我们先来看张图,按照①②③去读一下这个流程,我们就能在脑海里形成一个模糊概念。
下面开始进行文字介绍:
进程是啥呢?
-
进程是指在系统中正在运行的一个应用程序
-
每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
比如同时打开迅雷、Xcode,系统就会分别启动2个进程。
什么是线程呢?
- 就是进程里边一个负责程序执行的控制单元(就是某个功能啊,某个任务啊,也可以理解为执行路径)
- 进程的任务都由线程来控制,执行;
- 一个进程没有线程也就是没有任务、没有执行路径,所以一个进程至少拥有一个线程!
例子:
线程的串行
- 1个线程中任务的执行是串行的。顺序进行
假设一个线程任务是煲水煮饭,那它就必须先煲水,煲完水,才能去煮饭
这就产生了一个效率问题了,我们现实中肯定是煲水和煮饭一起干的啦!上图中,文件也是可以同时开始下载的。这就有了多线程。
15.1.2 多线程概念
概念
- 1、每条线程拥有自己的运行内容——线程任务
- 2、当一个进程开启了多条线程,每条线程可以并行(同时)执行,它们线程执行的任务不同,这就是多线程体现
- 3、开启多个线程就能同时执行多个任务
- 4、多线程技术可以提高程序的执行效率
比如我们实现一个hello的代码
java虚拟机就会创建很多线程,其中有一个是Main线程,来执行我们的代码——打印hello world;
其他线程例如GC垃圾回收线程等。当Main线程执行完,我们的GC垃圾回收机制可能还没执行完。就好像我们360杀毒完毕,清理垃圾还没行!
并行的实质(多线程的实质)
-
同一时刻,CPU只能处理1条线程,只有1条线程在工作(执行)
-
多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)
如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
这也带来了一个很常见的问题:线程开启过多会卡
比如我们打开360,把里面的杀毒,文件清理,垃圾清理,驱动更新全打开
然后又打开酷狗听歌,下载歌,打开百度网盘下载文件等,开启了好多的线程,cpu在这些线程里边随机切换,每个线程平均被切换到的概率降低,我们就会觉得卡了,这个时候cpu切换到同一个线程的任务时间变长,自然也就是卡了,会耗费大量cpu资源
这个图标数字一下飞升!
这个时候,就出现了传说中的多核技术——4核、8核等,每个cpu自由调度切换线程。还有增加cpu频率等。
多线程的好处、弊端
- 好处:解决了多部分任务同时运行
- 弊端:正如上面所示,当线程开启得多了,就会降低cpu的效率。
15.2 线程的创建(两种方式)
15.2.1 方式一:继承Thread类
四步实现多线程
- 创建一个继承Thread类的子类
- 覆写run()方法,实现线程任务的封装
- 创建多个线程对象
- 每个对象都开启线程——start()方法
代码演示
package ThreadTest;
/*
目的:练习使用多线程类创建多线程
@Override
public void run() {//Thread的run方法源码
if (target != null) {
target.run();
}
}
*/
//1、创建继承Thread的子类
class Animal extends Thread {
private String name;
public Animal(String name) {
super(name);
this.name = name;
}
//2、覆写run方法
public void run() {
// System.out.println(4/0);,其他线程发生异常不影响主线程
for (int i = 0; i < 10; i++)
System.out.println("大家好,我叫" + name + "...线程名" + Thread.currentThread().getName());
//show();
}
void show() {
for (int i = -9999; i < 9999; i++) {
}
System.out.print("大家好,我叫" + name + "..." + Thread.currentThread().getName());
}
}
public class Practice1 {
public static void main(String[] args) {
//3、创建线程子类对象
Animal test2 = new Animal("旺财");//创建对象时就已经安排好线程名字了
Animal test1 = new Animal("小强");
//4、每个线程对象开启线程!
test1.start();//线程1
test2.start();//线程2
//System.out.println(3/0);//主线程发生异常不影响其他线程,其他线程发送异常也不影响主线程
for (int i = 0; i < 10; i++)
System.out.println("haha" + i + "...线程名+" + Thread.currentThread().getName());//主线程,如果去掉循环,其实也是随机的
//三个线程随机运行,抢夺资源!
}
}
运行结果
思考:直接使用run()和使用start()方法有区别
run方法无法开启线程,相当于正常的对象调用方法;start()开启线程运行任务。
线程对象.getName()与Thread.currentThread.getName()的区别
第一是返回的是线程对象调用方法时其所属的线程名,第二个返回的是正在运行的线程名,调用的是底层代码
15.2.2 方式二:实现Runnable接口
Runnable接口是用来封装线程任务的一个接口,里面只有一个抽象run方法
,用于封装线程任务。
步骤
- 创建一个类实现Runnable接口
- 必须重写run()方法,否则该类要声明为抽象类而且无法实现多线程
- 创建该类对象,调用Thread的有参构造方法,参数为Runnable类及其子类对象
- 开启线程
代码演示
package ThreadTest;
public class Practice2 implements Runnable {//1、实现Runnbale接口
public void show() {
for (int i = 0; i < 20; i++)
System.out.println("大家好,我是show!" + i);
}
@Override
public void run() {//2、写run方法,封装线程任务
show();
}
public static void main(String[] args) {
//创建Runnable子类对象
Practice2 test = new Practice2();
//调用Thread有参构造方法
Thread thread = new Thread(test);
//开启线程
thread.start();
for (int i = 0; i < 20; i++) {
System.out.println("现在是主线程执行" + i);//线程之间互相不影响,并且随机切换。
}
thread.stop();//进入线程的冻结时期,随机冻结
for (int i = 0; i < 20; i++) {
System.out.println("现在是主线程执行" + i);//线程之间互相不影响,并且随机切换。
}
}
}
运行结果:
15.2.3 两种方法的区别及其优劣性
一、我们先来看一下Thread的源码
1、Thread类的定义也是实现了Runnable接口
2、其run()方法是调用target对象的run方法
3、target是一个私有的Runnable接口
4、线程名
用于自动编号匿名线程。
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
5、返回线程状态
再来看一下Runnable接口(就只有一个抽象方法run封装线程任务):
二、两种方式的区别及优劣性
-
第一点:Runnbale是接口,子类可以多实现,Thread类,子类只能单继承
Thread和Runable本质上没有什么不同,Runnable封装了线程任务,Thread里边实现Runnable,同时定义了很多线程操作的方法
,就是实现接口我们可以继承其他类,而继承Thread的类,无法继承其他类,单继承的局限性。假设一个Animal类,一个dog类,现在dog类有任务需要用到多线程,那么我们一方面需要继承Animal类,又要实现多线程,这个时候我们就只能实现Runnable接口而不能继承Thread类。 -
第二点:因为Runnable是接口的原因,所以它是可以在资源共享方面更加灵活,如果是继承Thread类,我们还需要将资源声明为共享。
-
第三点:将线程的任务从线程的子类中分离封装为对象,很好体现了面向对象的思想
Runable的出现仅仅只是将线程任务进行封装,因为考虑到实现Thread类就无法实现继承其他父类,当一个类又需要多线程,此时就得有run方法,于是将run方法抽取出来,封装成接口——Runnable,这样Thread类也是实现Runnable,进而使得使用多线程的灵活度更高
所以它们是没有本质区别的!实现Runable接口能做到的,继承Thread也能做到!继承Thread类能做到的,实现Runable接口也能做到
基于以上几点,如果只是要简单地执行多线程任务,开发中推荐使用第二种方式——实现Runable接口;如果是很复杂的多线程需求,同时没有其余继承类的要求,我们才使用第一种方式——继承Thread类
总结
如果一个类继承Thread,则不适合资源共享(不代表不可以)。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
15.2.4 多线程是如何执行的(栈中情况分析)
Java虚拟机栈
-
Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)
-
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)
-
Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个 栈帧。 对于我们来说,主要关注的stack栈内存,就是虚拟机栈中 局部变量表部分。
假设我们有三个线程——main,Thread-1,Thread-2
关于一个线程内部(方法开始与结束对应栈帧入栈出栈):
15.3 线程的状态浅析
一个线程相要运行必须得有执行权
。而能够得到执行权(cpu能切换到该线程)就得有具备执行资格
自己画了一张图:
15.4 线程中的一些方法简介
1、Thread类中的方法
关于线程的优先级
2、继承于Object的方法
wait()
notify()、notifyAll()
以上方法都是用来监视线程的状态的,必须在同步代码中使用
。因为wait,notify,notifyAll等方法是监视器(锁)的方法,而同步代码块的锁是任意对象,所以我们抽取这些方法封装在object类中(所有类的直接或者间接父类)
15.5 同步(synchronized)——解决线程安全问题
15.5.1 线程安全问题
看一个买票的例子——
package ThreadTest;
/**
* 演示买票例子
* 使用四个线程模拟四个窗口,进行卖票
* 当继承thread来实现多线程买票时,可以使用static修饰num
* 现在我们使用implements Runnable接口,演示多线程安全问题。
*/
class Tick implements Runnable {
private int num = 100;//票数,也代表票的号码或者座位
public void run() {
while (true)
if (num > 0)
show();
}
void show() {
System.out.println(Thread.currentThread().getName() + "卖出票号为:。。。" + num--);
}
}
public class TicketDemo1 {
public static void main(String[] args) {
Tick tick = new Tick();
Thread t1 = new Thread(tick);
Thread t2 = new Thread(tick);
Thread t3 = new Thread(tick);
Thread t4 = new Thread(tick);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
现在很正常:
现在我们将代码稍微修改一下,加入线程延时10ms看一下
void show() {
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}//注意这里可能抛出异常,要么使用try'catch普抓或者声明!注意run无法声明异常
System.out.println(Thread.currentThread().getName() + "卖出票号为:。。。" + num--);
}
结果:
发现加了延时线程的执行以后,我们就能发现线程安全问题的存在
我们都没有票号为0和-1 的票,竟然能卖出去,这是因为
多线程任务代码里边
- 1:存在多线程访问
- 2:操作共享数据的语句有多条
这就有可能发生线程安全问题
线程安全问题的实质
如图所示,假设我们在if(num>0)那里设置一道关卡,即使线程1进行后被切换掉了,其他线程也进不来。只有拿到关卡的锁的人才进得来,而锁一直被线程1拿着呢,除非它执行完毕,或者自己交出锁,否则别人进不来if()这个关卡!
而这个关卡就是——同步synchronized
解决办法——同步代码块(锁为任意对象)或者同步函数(锁只能是this)
- 涉及关键字 synchronized
- 锁是一个对象
15.5.2 同步机制
如果程序是单线程的,就不必担心此线程在执行时被其他线程“打扰”,就像在现实世界中,在一段时间内如果只能完成一件事情,不用担心做这件事情被其他事情打扰。但是,如果程序中同时使用多线程,好比现实中的“两个人同时通过一扇门”,这时就需要控制,否则容易引起阻塞。
为了处理这种共享资源竞争,可以使用同步机制。所谓同步机制,指的是两个线程同时作用在一个对象上,应该保持对象数据的统一性和整体性。Java 提供 synchronized 关键字,为防止资源冲突提供了内置支持。
共享资源一般是文件、输入/输出端口或打印机。
同步的两种形式
同步函数
在一个类中,用 synchronized 关键字声明的方法为同步方法。格式如下:
class类名
{
public synchronized 类型名称 方法名称()
{
//代码
}
}
Java 有一个专门负责管理线程对象中同步方法访问的工具——同步模型监视器
,它的原理是为每个具有同步代码的对象准备唯一的一把“锁”。当多个线程访问对象时,只有取得锁的线程才能进入同步方法,其他访问共享对象的线程停留在对象中等待。
同步代码块
synchronized 不仅可以用到同步方法,也可以用到同步块。对于同步块,synchronized 格式如下:
synchronized(obj)//锁对象是任意的
{
//代码
}
当线程执行到这里的同步块时,它必须获取 obj 这个对象的锁才能执行同步块,否则线程只能等待获得锁。必须注意的是,Obj 对象的作用范围不同,控制情况也不尽相同。如下代码为简单的一种使用:
public void method()
{
Object obj=new Object();
synchronized(obj)
{
//代码
}
}
上述代码创建局部对象 Obj,由于每一个线程执行到 Object obj=new Object() 时都会产生一个 obj 对象,每一个线程都可以获得新创建的 obj 对象的锁而不会相互影响,因此这段程序不会起到同步作用。
如果同步的是类的属性,情况就不同了。
回到上面的买票例子,我们只需要加一个同步代码块就能解决线程安全问题
同步的前提
- 1、多线程
- 2、使用同一个锁
这种就不属于同步,无法保证同一个锁,实现不了同步。
public void method()
{
Object obj=new Object();
synchronized(obj)
{
//代码
}
}
15.5.3 同步sychronized的锁
1、同步代码块的锁
自己设定的一个参数对象,可以是任意对象
,注意作用域即可,如上面的obj,我们也可以使用this。
开发时优先使用同步代码块,灵活。
2、同步实例函数的锁
同步实例函数的锁使用的是调用对象的隐式指针this
3、静态同步函数的锁
静态同步函数的锁使用的是类加载时由java虚拟机创建的类字节码文件对象
,它在内存里边是唯一的一份!
我们可以使用对象.getClass()
返回对象的类字节码文件对象,也可使用类.class
获取。
验证代码
package ThreadTest;
/**
* 验证同步函数使用的是哪一个锁
*/
class Tick2 implements Runnable {
private static int num = 100;//票数,也代表票的号码或者座位
//使用同步代码块的时候,琐是固定的,但是可以是任意对象
final Object ob = new Object();//常常定义为只允许赋值一次的对象锁!
public boolean flag = true;
public void run() {
if (flag) {
while (true)
//修改如下
// synchronized (this){//同步代码块 ob就是锁!!!!!!!
synchronized (this.getClass()) {//同步代码块
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}//注意这里可能抛出异常,要么使用try'catch普抓或者声明!注意run无法声明异常
System.out.println(Thread.currentThread().getName() + "obj:。。。" + num--);
}
}
} else {
while (true)
show();
}
}
synchronized static void show() {//同步函数
// synchronized void show(){//同步函数
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException ignored) {
}//注意这里可能抛出异常,要么使用try'catch普抓或者声明!注意run无法声明异常
System.out.println(Thread.currentThread().getName() + "show:。。。" + num--);
}
}
}
public class SynFunctionDemo {
public static void main(String[] args) {
Tick2 t1 = new Tick2();
Thread s1 = new Thread(t1);
Thread s2 = new Thread(t1);
s1.start();
//未加延时之前,主线程执行到这里,还具备cpu的执行权,瞬间就执行完这三条语句,修改了flag,所以我们看到的全是show
//现在我们修改一下让主线程延时,使得cpu能够进行切换,就能看到show与obj一起买票,但是线程不安全
try {
Thread.sleep(10);
} catch (InterruptedException ignore) {
}
//这个时候,我们就能发现有show与obj了。
//因为发现obj竟然这个时候能卖出0票,而且show与obj都卖出了第98张票,说明同步代码块与同步函数使用的不是同一把锁。
//现在我们修改同步代码块为this的锁,此时obj与show抢夺资源,一起卖出了100张票,实现同步,多线程安全。
t1.flag = false;
//现在我们修改同步函数为静态同步函数,那它肯定无法使用this,又锁一定要是对象,——只能是类加载进方法区时虚拟机创建的类字节码文件对象
//我们使用this.getClass或者Ticket2.class验证 _此时obj与show抢夺资源,一起卖出了100张票,实现同步,多线程安全。
s2.start();
//s3.start();
}
}
15.5.4 同步的好处与弊端
好处:
解决了线程安全的问题!
弊端:
相对降低了效率——一个线程进去了,cpu还是会随机切换线程,而同步代码中,拿了锁的线程被切掉了,切换到的后面的线程是进不来的,但是还是需要判断,意思就是明知道进不来,还要去试。相对降低效率,但是在承受范围内。
15.5.5 单例设计模式中的线程安全问题(懒汉式)
我们知道单例设计模式分为懒汉式和饿汉式
//饿汉式,类加载时就创建对象
class Single{
private static Single s = new Single();
private Single(){}
public static Single getInstance(){
return s
}
...
}
//懒汉式——用到时才创建对象
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(s==null)
{
s = new Single();
}
return s
}
...
}
注意到懒汉式中,存在两条语句操作s,假设我们使用到了多线程,很明显,如同15.2.2中买票的例子,发生线程安全问题,进而无法保证创建的对象唯一!我们要使用同步解决
形式一
getInstance{
synchronized(Single.class)
if(s==null)
s = new Single();
return s
}
很明显拥有同步的弊端——降低了效率
形式二
使用双重判断消除懒汉式线程安全问题同时解决了效率问题。
getInstance{
if(s==null)
synchronized(Single.class){
if(s==null)
s = new Single();
}
return s
}
15.5.6 死锁情况一(DeadLock,我们要避免的)
发生死锁的情形之一是同步代码块的嵌套——锁中有锁,线程互相抢夺锁
package ThreadTest;
/**
* 目的:编写一个程序演示死锁——锁中有锁,,嵌套同步。我们一定要避免死锁!
*/
class DeanDemo implements Runnable {
public boolean flag;
DeanDemo(boolean flag) {
this.flag = flag;
}
public void run() {
if (flag) {
while (true)
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName() + "。。。在持有lockaA,准备进入下一个锁");
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName() + "成功进入");
}
}
} else {
while (true)
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName() + "。。。持有lockB,准备进入下一个锁");
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName() + "成功进入");
}
}
}
}
}
//定义我的锁
class MyLock {
public static MyLock locka = new MyLock();
public static MyLock lockb = new MyLock();
}
public class DeadLock {
public static void main(String[] args) {
DeanDemo d1 = new DeanDemo(true);
DeanDemo d2 = new DeanDemo(false);
//创建两个线程让他们争锁
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
//两个线程互相争锁,谁也不让,程序卡在哪里
}
}
程序卡在这里 了:
15.5.7 等待唤醒机制——演示线程之间的通信
1、单生产者单消费者
下面使用代码展示等待唤醒机制
我们现在有资源——煤块和铁矿,属性分别为软、hard;现在创建两个线程,拉煤,下一次就拉铁矿并输出资源加属性
代码一:
package ThreadTest;
/**
* //第一次代码,线程不安全的实例,因为多个线程,存在多条操作共享资源的语句,所以有线程安全问题。
* <p>
* 一个例子说明线程之间通信的变换
* 比如拉矿,放矿。线程通信是多个线程对共同的资源进行不同的任务
* 一个线程负责拉矿,一个负责放矿,资源 煤块,属性是软,,铁矿,属性是硬
* 如何保证拉一次,放一次。还要线程安全呢?
*/
class MeiKuai {
String resource;
String shux;
}
class FangMei implements Runnable {
MeiKuai r;
FangMei(MeiKuai m) {
r = m;
}
public void run() {//负责放矿——设置资源属性
int x = 0;//0就去拉铁,1就去拉煤
while (true) {
if (x == 0) {
r.resource = "tie";
r.shux = "hard";
} else {
r.resource = "煤";
r.shux = "软";
}
x = (x + 1) % 2;//实现任务拉铁与煤的切换
}
}
}
class LaMei implements Runnable {
MeiKuai r;
LaMei(MeiKuai m) {
r = m;
}
public void run() {//负责拉矿
while (true) {
System.out.println(r.resource + "....." + r.shux);
}
}
}
public class Practice_waitNotify1 {
public static void main(String[] args) {
//第一次代码,线程不安全的实例
MeiKuai meiKuai = new MeiKuai();
FangMei f = new FangMei(meiKuai);
LaMei l = new LaMei(meiKuai);
//创建线程
Thread t1 = new Thread(f);
Thread t2 = new Thread(l);
t1.start();
t2.start();
}
}
发现跑着跑着,煤块属性就变长hard了,说明出现了线程安全问题
代码二:加入同步代码块
package ThreadTest;
/**
* 第二次实例——修改为线程安全,使用同一个锁,多个线程必须在同一个锁下同步
* //第二次代码,解决了线程安全,但是没有我们预期的功能!拉一次放一次
*/
class FangMei1 implements Runnable {
MeiKuai r;
FangMei1(MeiKuai m) {
r = m;
}
public void run() {//负责放矿——设置资源属性
int x = 0;//0就去拉铁,1就去拉煤
while (true) {
synchronized (r) {
if (x == 0) {
r.resource = "tie";
r.shux = "hard";
} else {
r.resource = "煤";
r.shux = "软";
}
}
x = (x + 1) % 2;//实现任务拉铁与煤的切换
}
}
}
class LaMei1 implements Runnable {
MeiKuai r;
LaMei1(MeiKuai m) {
r = m;
}
public void run() {//负责拉矿
while (true) {
synchronized (r) {
System.out.println(r.resource + "....." + r.shux);
}
}
}
}
public class Practice_waitNotify2 {
public static void main(String[] args) {
//第二次代码,解决了线程安全,但是没有我们预期的功能!拉一次放一次
MeiKuai meiKuai = new MeiKuai();
FangMei1 f = new FangMei1(meiKuai);
LaMei1 l = new LaMei1(meiKuai);
//创建线程
Thread t1 = new Thread(f);
Thread t2 = new Thread(l);
t1.start();
t2.start();
//为什么会一片一片的输出同样的内容?因为使用同一个锁,假设切到了放矿,一直重复赋值,
//当切换到拉矿,则不可能只输出一次,所以一片一片
}
}
结果没有实现一次拉的是煤块,一次拉的是铁矿
代码三:加入等待唤醒机制以后
package ThreadTest;
/**
* 第三次实例——修改为线程安全,使用同一个锁,多个线程必须在同一个锁下同步
* 解决了线程安全,同时做到我们预期的功能!拉一次放一次
*/
class MeiKuai2 {
String resource;
String shux;
boolean flag = false;//刚开始没有资源
}
class FangMei2 implements Runnable {
MeiKuai2 r;
FangMei2(MeiKuai2 m) {
r = m;
}
public void run() {//负责放矿——设置资源属性
int x = 0;//0就去拉铁,1就去拉煤
while (true) {
synchronized (r) {
if (r.flag)
try {
r.wait();
} catch (InterruptedException e) {
}
if (x == 0) {
r.resource = "tie";
r.shux = "hard";
} else {
r.resource = "煤";
r.shux = "软";
}
r.flag = true;//修改已经放资源了
r.notify();//唤醒拉煤的那个家伙,注意这里不加r是无法知道去哪一个线程池唤醒,就会报错
}
x = (x + 1) % 2;//实现任务放铁与煤的切换
}
}
}
class LaMei2 implements Runnable {
MeiKuai2 r;
LaMei2(MeiKuai2 m) {
r = m;
}
public void run() {//负责拉矿
while (true) {
synchronized (r) {
if (!r.flag)
try {
r.wait();
} catch (InterruptedException e) {
}
System.out.println(r.resource + "....." + r.shux);
r.flag = !r.flag;
r.notify();//唤醒放矿的兄弟
}
}
}
}
public class Practice_waitNotify3 {
public static void main(String[] args) {
//第三次代码,解决了线程安全,有我们预期的功能!拉一次放一次
MeiKuai2 meiKuai = new MeiKuai2();
FangMei2 f = new FangMei2(meiKuai);
LaMei2 l = new LaMei2(meiKuai);
//创建线程
Thread t1 = new Thread(f);
Thread t2 = new Thread(l);
t1.start();
t2.start();
}
}
结果实现了我们预期的功能——一次铁一次煤块
但是代码不能体现封装性
代码四:
package ThreadTest;
/**
* 将代码再次优化,加强封装性,我们的资源显然是不允许直接访问的!
* 第四次代码_适合企业开发的模式!
* 等待/唤醒机制。
* <p>
* 涉及的方法:
* <p>
* 1,wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。
* 2,notify():唤醒线程池中一个线程(任意).
* 3,notifyAll():唤醒线程池中的所有线程。
* <p>
* 这些方法都必须定义在同步中。
* 因为这些方法是用于操作线程状态的方法。
* 必须要明确到底操作的是哪个锁上的线程。
* <p>
* 为什么操作线程的方法wait notify notifyAll定义在了Object类中?
* <p>
* 因为这些方法是监视器的方法。监视器其实就是锁。
*/
class MeiKuai3 {
private String resource;
private String shux;
boolean flag = false;//刚开始没有资源
//定义方法使得我们可以访问资源设置资源,
public synchronized void set(String resource, String shux) {
if (this.flag)
try {
this.wait();
} catch (InterruptedException e) {
}
this.shux = shux;
this.resource = resource;
flag = true;//修改为已经放资源了
this.notify();
}
public synchronized void out() {
if (!flag)
try {
this.wait();
} catch (InterruptedException e) {
}
System.out.println(resource + "....." + shux);
flag = false;
this.notify();//唤醒放矿的兄弟
}
}
class FangMei3 implements Runnable {
MeiKuai3 r;
FangMei3(MeiKuai3 m) {
r = m;
}
public void run() {//负责放矿——设置资源属性
int x = 0;
while (true) {
if (x == 0) {
r.set("tie", "hard");
} else {
r.set("煤", "软");
}
x = (x + 1) % 2;
}
}
}
class LaMei3 implements Runnable {
MeiKuai3 r = null;
LaMei3(MeiKuai3 m) {
r = m;
}
public void run() {//负责拉矿
while (true) {
r.out();
}
}
}
public class Practice_waitNotify4 {
public static void main(String[] args) {
//第三次代码,解决了线程安全,有我们预期的功能!拉一次放一次
MeiKuai3 meiKuai = new MeiKuai3();
FangMei3 f = new FangMei3(meiKuai);
LaMei3 l = new LaMei3(meiKuai);
//创建线程
Thread t1 = new Thread(f);
Thread t2 = new Thread(l);
t1.start();
t2.start();
}
}
2、多生产者与多消费者——多线程通信中的线程安全问题
生产者负责生产烤鸭,消费者负责消费烤鸭
创建多个生产者多个消费者引发线程安全问题
代码一
package ThreadTest;
/**
* 演示等待唤醒机制最经典的例子;多生产者与多消费者
* <p>
* 使用到了notifyAll,与wait——涉及到了效率低下的问题——后来在java的1.5版本以后改进,封封装了锁对象为lock类。
* 以及Condition——await(),asignal(),asignalAll()
*/
//出现情况1,生成的烤鸭只有一个,却这么多消费者能吃到
class Resourse {
String name = null;
int count = 0;
public boolean flag = false;
}
class Productor implements Runnable {
private final Resourse r;
Productor(Resourse r) {
this.r = r;
}
public void run() {//生成烤鸭
while (true) {
synchronized (r) {
if (r.flag) {
try {
r.wait();
} catch (InterruptedException ignored) {
}
}
r.name = "烤鸭" + r.count;
System.out.println(Thread.currentThread().getName() + "。。。生产者。。。" + r.name);
++r.count;
r.flag = true;
r.notify();
}
}
}
}
class Consumer implements Runnable {
private final Resourse r;
Consumer(Resourse r) {
this.r = r;
}
public void run() {
while (true) {
synchronized (r) {
if (!r.flag) {
try {
r.wait();
} catch (InterruptedException ignored) {
}
}
System.out.println(Thread.currentThread().getName() + "........消费者。。。" + r.name);
r.flag = false;
r.notify();
}
}
}
}
public class ProductorAndConsumer {
public static void main(String[] args) {
Resourse r = new Resourse();
Productor pro = new Productor(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();//当只有一个消费者与一个生产者的时候,我们发生程序 线程安全,同时实现生成一个消费一个。现在修改为多生产者多消费者
t3.start();
t4.start();
}
}
问题:两个消费者竟然吃到了同一个烤鸭??两个生产者竟然连续生产了两只烤鸭??
我们先看生产者的代码
public void run() {//生成烤鸭
while (true) {
synchronized (r) {
if (r.flag) {
try {
r.wait();
} catch (InterruptedException ignored) {
}
}
r.name = "烤鸭" + r.count;
System.out.println(Thread.currentThread().getName() + "。。。生产者。。。" + r.name);
++r.count;
r.flag = true;
r.notify();
}
原因就在于if(flag)那里的线程进入等待,然后被唤醒的时候不再判断flag,而且notify()可能唤醒的是生产者(我们希望的是唤醒消费者来吃烤鸭),这就导致了连续生产两只烤鸭,同样的道理我们可以剖析出消费者存在类似的问题。
解决方案代码:
package ThreadTest;
/**
* 针对情况1,我们如何改进呢?
* 首先我们明确原因;r.notify我们希望唤醒的是对方,假设唤醒了自己,然后cpu停止了当前线程,切换到了唤醒的这个线程
* 这个时候不会再判断flag,就会继续生成烤鸭,使得上一个线程的生成烤鸭没被消费,同理假设是消费者的这样的过程,就会多次消费同一个烤鸭
* 我们首先解决办法——while判断,这样当醒来的是本方,我们一样要判断有没有烤鸭(flag)
* <p>
* 假设我们还用notify,就会出现第二种情况——死锁,全部线程都睡过去了,没有人去争锁了。
* <p>
* <p>
* 所以我们需要使用唤醒的方法使用notifyAll,为了确保能唤醒对方,即使本方也会醒,但是我们的while解决了这个问题
* 但是也带来了问题:就是假设我们都唤醒,那么本方的flag本来就是true,只会再次睡过去。
* 所以:降低了效率,但是jdk1.5又没有能唤醒指定线程的方法。
*/
class Productor1 implements Runnable {
private final Resourse r;
Productor1(Resourse r) {
this.r = r;
}
public void run() {//生成烤鸭
while (true) {
synchronized (r) {
while (r.flag) {
try {
r.wait();
} catch (InterruptedException e) {
}
}
r.name = "烤鸭" + r.count;
System.out.println(Thread.currentThread().getName() + "。。。生产者。。。" + r.name);
++r.count;
r.flag = true;
// r.notify();,死锁
r.notifyAll();
}
}
}
}
class Consumer1 implements Runnable {
private final Resourse r;
Consumer1(Resourse r) {
this.r = r;
}
public void run() {//无法声明抛出异常,因为这是复写的方法
while (true) {
synchronized (r) {
while (!r.flag) {
try {
r.wait();
} catch (InterruptedException ignored) {
}
}
System.out.println(Thread.currentThread().getName() + "........消费者。。。" + r.name);
r.flag = false;
// r.notify();,死锁
r.notifyAll();//解决问题~!
}
}
}
}
public class ProductorAndConsumer2 {
public static void main(String[] args) {
Resourse r = new Resourse();
Productor1 pro = new Productor1(r);
Consumer1 con = new Consumer1(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
注意使用notifyAll()唤醒当前监视器下所有的线程,降低效率,使用notify会导致死锁情形二——线程全部睡过去了。
java5以后——lock(锁的新特性)
基于上面的例子我们使用lock来改写如下:
package ThreadTest;
import java.util.concurrent.locks.*;
/**
* 使用jdk1.5以后,java5以后的新的更为灵活的锁的使用方法来改写程序,消灭while
* 与notifyAll带来的线程效率问题
* Condition Lock
* jdk1.5以后将同步和锁封装成了对象。
* 并将操作锁的隐式方式定义到了该对象中,
* 将隐式动作变成了显示动作。
* <p>
* Lock接口: 出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成现实锁操作。
* 同时更为灵活。可以一个锁上加上多组监视器。
* lock():获取锁。
* unlock():释放锁,通常需要定义finally代码块中。
* <p>
* <p>
* Condition接口:出现替代了Object中的wait notify notifyAll方法。
* 将这些监视器方法单独进行了封装,变成Condition监视器对象。
* 可以任意锁进行组合。
* await();
* signal();
* signalAll();
*/
class Resourse1 {
private String name = null;
private int count = 0;
public boolean flag = false;
Lock mylock = new ReentrantLock();
Condition con_condition = mylock.newCondition();
Condition pro_condition = mylock.newCondition();
void set(String name) {
mylock.lock();//获取锁
try {
while (flag) {
try {
pro_condition.await();
} catch (InterruptedException ignored) {
}
}
this.name = name + count;
System.out.println(Thread.currentThread().getName() + "。。。生产者。。。" + this.name);
++count;
flag = true;
con_condition.signal();
} finally {
mylock.unlock();//释放锁
}
}
void out() {
mylock.lock();//获取锁
try {
while (!flag) {
try {
con_condition.await();
} catch (InterruptedException ignored) {
}
}
System.out.println(Thread.currentThread().getName() + "........消费者。。。" + name);
flag = false;
pro_condition.signal();
} finally {
mylock.unlock();//释放锁
}
}
}
class Productor2 implements Runnable {
private final Resourse1 r;
Productor2(Resourse1 r) {
this.r = r;
}
public void run() {//生成烤鸭
while (true) {
r.set("烤鸭");
}
}
}
class Consumer2 implements Runnable {
private final Resourse1 r;
Consumer2(Resourse1 r) {
this.r = r;
}
public void run() {//无法声明抛出异常,因为这是复写的方法
while (true) {
r.out();
}
}
}
public class ProductorAndConsumer3 {
public static void main(String[] args) {
Resourse1 r = new Resourse1();
Productor2 pro = new Productor2(r);
Consumer2 con = new Consumer2(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
看到一个讲到我不知道的知识点的多线程总结:Java基础——多线程篇
a good picture