day15 15、认真学习多线程

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虚拟机栈

  1. Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)

  2. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)

  3. 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
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雨夜※繁华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值