3.4 常用类库-多线程

1.线程与进程

进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行.
一个进程最少有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程

线程调度

  • 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度
    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
  • Java使用的为抢占式调度。
    CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使用率更高。

2.同步与异步

  • 同步:排队执行 , 效率低但是安全.
  • 异步:同时执行 , 效率高但是数据不安全

3.并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。
  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

4.继承Thread类

  • Thread是Java提供的用于实现线程的类

  • 有一个继承的方法run,run方法中的代码就是一条新的执行路径,路径的触发方式不是调用run方法,而是通过Thread对象的start来启动任务

  • 常用方法

在这里插入图片描述

  • 字段

在这里插入图片描述

  • 常用方法

在这里插入图片描述

  • daemon线程(守护线程):即守护用户线程,掌握不了自己的生命,依附于用户线程,用户线程没了守护线程也就没了

  • 用户线程:所有用户进程都死亡了,程序才结束,自己决定自己的死亡

面试题:
如何将一个线程停止?
用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时
我们可以通过变量做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定

4.1 Thread在程序中的使用

  • 常规用法
    新建一个类继承Thread

代码示例:

  • 编写一个线程

class MyThread extends Thread{   //Thread,JAVA提供的用于实现线程的类
    /**
     * 线程线程要执行的任务方法
     * 每个线程都有自己的栈空间,共用一份堆内存
     */
    @Override
    public void run() {
        //这里的代码,就是一条新的执行路径
        //这个执行路径的触发方式,不是调用run方法,而是通过Thread对象的start来启动任务
        for(int i = 0;i < 5;i++){
            System.out.println( i + "这是另一个线程");
        }
    }

  • 再在主函数中编写代码,来实现多线程
public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();        //时间分配是抢占式分配
        for(int i = 0;i < 5;i++){
            System.out.println( i + "这是main中的线程");
        }
    }

  • 由于Java的时间分配是抢占式时间分配,谁抢占到谁就先执行,因此得到如下输出结果
0这是另一个线程
0这是main中的线程
1这是另一个线程
1这是main中的线程
2这是另一个线程
2这是main中的线程
3这是另一个线程
4这是另一个线程
3这是main中的线程
4这是main中的线程

  • 图解

在这里插入图片描述

使用匿名内部类

  • 代码展示
public static void main(String[] args) {
        //new Thread(){}.start();
        
        new Thread(){        //匿名内部类,仅几行代码即可实现一个线程
            @Override
            public void run() {
                for (int i = 0;i < 5;i++){
                    System.out.println(i + "hahaha");
                }
            }
        }.start();
        for (int i = 0;i < 5;i++){
            System.out.println(i + "heiheihei");
        }
    }//end main

  • 运行结果
0hahaha
0heiheihei
1heiheihei
1hahaha
2heiheihei
3heiheihei
2hahaha
4heiheihei
3hahaha
4hahaha

5.Runnable接口

  • 用于给线程执行的任务,但是还是要借助Thread
  • 【创建一个任务对象,里面包含了任务→再创建一个线程,为其分配这个任务→start执行】

与前面继承Thread类相比,优势在于

  • 通过 创建任务→给线程分配的方式 来实现多线程,更适合多个线程同时执行相同任务的情况

  • 可以避免单继承所带来的的局限性(java单继承,但是可以多实现)

  • 任务与线程本身分离,提高了程序的健壮性

  • 线程池技术,接收Runnable类型的任务,不接收Thread类型的线程

  • 代码示例
/**
 * 第二种实现多线程技术
 * 实现Runnable接口
 * 用于给线程执行的任务,但是还是要借助Thread
 */
class MyRunnable implements Runnable{

    @Override
    public void run() {
        //线程任务
        for(int i = 0;i < 5;i ++){
            System.out.println( i + "这是另一个线程");
        }
    }
}
    public static void main(String[] args) {
        /**
         * 第二种实现多线程技术
         * 实现Runnable接口(用的更多)
         */
        MyRunnable r = new MyRunnable();//创建一个任务对象,里面包含了任务
        Thread t = new Thread(r);//创建一个线程,为其分配一个任务
        t.start();//执行这个线程
        for(int i = 0;i < 5;i++){
            System.out.println( i + "这是main中的线程");
        }
    }
输出结果:
0这是另一个线程
1这是另一个线程
2这是另一个线程
3这是另一个线程
0这是main中的线程
4这是另一个线程
1这是main中的线程
2这是main中的线程
3这是main中的线程
4这是main中的线程

6.Callable接口

用的少 我不想写 用到再补

7.线程有关操作

7.1 设置和获取线程名称

在这里插入图片描述

  • 代码示例
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
         	//currentThread():获取当前正在执行的对象
            //getName获取线程名称
            System.out.println(Thread.currentThread().getName());
        }
    }
 		//currentThread():获取当前正在执行的对象
        System.out.println(Thread.currentThread().getName());//main线程
        Thread t = new Thread(new MyRunnable());
        t.setName("第0个线程");//使用setName设置线程名称
        t.start();
        new Thread(new MyRunnable(),"第1个线程").start();
        new Thread(new MyRunnable(),"第2个线程").start();
        new Thread(new MyRunnable(),"第3个线程").start();
        new Thread(new MyRunnable()).start();//没有给线程setName,则系统会自动命名
        new Thread(new MyRunnable()).start();
        new Thread(new MyRunnable()).start();

main
第0个线程
第1个线程
第3个线程
Thread-12个线程
Thread-3
Thread-2

7.2 线程休眠

  • 常用方法

在这里插入图片描述
sleep为Thread的静态方法,因此可以用Thread直接调用:Thread.sleep()

1秒 = 1000毫秒
1毫秒 = 1000微妙 = 1000000纳秒

代码示例:

        for (int i = 0;i < 5;i++){
            System.out.println(i);
            Thread.sleep(1000);			//每次循环暂停1000毫秒后再继续执行
        }
0
1
2
3
4

7.3 线程中断

  • 一个线程是一个独立的执行路径,是否应该结束,应该由其自身决定
    用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时我们可以通过给对象做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定
    (代码里用interrupt打断 但是打断之后只是提示程序员是否终止 因为可以选择不中止 起到一个提示作用 如果要终止 依靠方法的 异常 方法终止 可以添加return)
    代码示例:

  • 首先新建类实现Runnable接口,并继承run方法 新建线程

    static class MyRunnable implements Runnable{

        @Override
        public void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catch
            for (int i =0;i < 10;i ++){
                System.out.println(Thread.currentThread().getName() + ":" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

  • 编写main
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());  //新建线程
        t1.start();

        for (int i =0;i < 5;i ++){//main线程
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {//打标记之后程序进去catch
                e.printStackTrace();
            }
        }
        //main线程打印5次,t1线程打印10次,因此main线程先中断,此时对t1线程打标记
        //给线程t1添加中断标记,但是只是告诉线程它可以死亡,但是未必死亡
        t1.interrupt();


    }

  • 打标记处理:
    对线程对象打标记,触发异常,使程序进入catch,后续的处理依旧由程序员决定

① 修改MyRunnable中的try-catch语句

发现中断标记后进入catch,但是程序可以选择不死亡,继续执行

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
//                    e.printStackTrace();
                    System.out.println("发现了中断标记,但是不死亡");

                }

  • 输出结果
    每个线程都隔1秒打印一个数,由于线程不死亡,因此发现标记之后继续执行
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,但是不死亡
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9



  • ② 修改MyRunnable中的try-catch语句
    发现中断标记后进入catch,程序死亡,中断程序、释放资源
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
//                    e.printStackTrace();
                    System.out.println("发现了中断标记,线程自杀");
                    return;//表示线程结束,资源释放
                }

  • 输出结果:
    每个线程都隔1秒打印一个数,由于发现中断标记后线程自杀死亡,因此发现标记之后结束程序
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,线程自杀

7.4 设置守护线程

线程分为守护线程和用户线程

用户线程:当一个进程不包含任何存活的用户线程时,进行结束(我们直接创建的线程都是用户线程)

守护线程:用于守护用户线程,当最后一个用户线程结束时,守护线程自动死亡

  • 代码示例:
    新建类实现Runnable接口,并继承run方法 新建线程
    static class MyRunnable implements Runnable{
        @Override
        public void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catch
            for (int i =0;i < 10;i ++){
                System.out.println(Thread.currentThread().getName() + ":" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

  • 编写main
    在线程启动前,用setDaemon()来标记守护线程
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());//t1为子线程
        t1.setDaemon(true);//设置t1为守护线程,在t1启动前设置
        t1.start();//启动守护线程
        
        //main主线程,当主线程结束时,守护线程也会结束
        for (int i =0;i < 5;i ++){
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
main:3
Thread-0:3
main:4
Thread-0:4
Thread-0:5

7.5 停止线程

以下文多线程通信问题中的生产者与消费者问题中的程序为例,我们让线程运行,但是我们只是打开了线程,并没有关闭线程,到最后程序运行完只能手动停止线程

        Thread.currentThread().interrupt();
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());
        System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());

8.线程安全

代码示例:

    /**
     * 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
     */
    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        @Override
        public void run() {//每次被触发就进卖买票操作
            while(count > 0){
                //卖票
                System.out.println("正在准备卖票");
                //try-catch使得卖票的时间更长
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count --;
                System.out.println("出票成功!余票:" + count);
            }
        }//end
    }

    public static void main(String[] args) {
        //线程不安全
        Runnable runnable = new Ticket();
        //启动三个线程
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }

正在准备卖票,请稍等...
正在准备卖票,请稍等...
正在准备卖票,请稍等...
出票成功!余票:8
正在准备卖票,请稍等...
出票成功!余票:9
正在准备卖票,请稍等...
出票成功!余票:7
正在准备卖票,请稍等...
出票成功!余票:5
正在准备卖票,请稍等...
出票成功!余票:6
正在准备卖票,请稍等...
出票成功!余票:4
正在准备卖票,请稍等...
出票成功!余票:3
正在准备卖票,请稍等...
出票成功!余票:2
正在准备卖票,请稍等...
出票成功!余票:1
正在准备卖票,请稍等...
出票成功!余票:0
出票成功!余票:-2
出票成功!余票:-1


通过输出结果观察可知,余票出现了负数,但是代码逻辑上余票count=0时便不再执行了

出现问题原因:假设三段线程为ABC,ABC可能同时进行到while,假设A先进入,此时count = 1,当A进入休眠未进行到count–时,B检测到count = 1,进入while,当B进入休眠未进行到count–时,C检测到count = 1,进入while,此时A运行count–,count = 0,B接着运行count- -,count =-1,C接着运行count- -,count = -2,同时由于线程阻塞以及线程调度,输出的顺序可能不同

这就是多线程完成统一任务时出现的线程不安全问题

8.1显式锁与隐式锁

  • 所谓的显式和隐式,就是在使用的时候使用者是否需要手动写代码去获取锁和释放锁
  • 隐式锁:隐式锁使用synchronized修饰符。在使用sync关键字的时候,当sync代码块执行完成之后程序能够自动获取锁和释放锁
  • 显式锁:显式锁使用Lock关键字。在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象
  • 添加synchronized关键字的同步代码块和同步方法属于隐式锁

8.2隐式锁同步代码块

线程同步,使线程排队执行

实现思路:每个线程在执行时看同一把锁,谁抢到了锁,谁就执行

线程同步实现:synchronized

格式:
synchronized(锁对象){
// 同步代码块
}

锁对象: java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象

  • 代码示例:
    对原有的线程不安全的卖票示例进行修改
    在while循环中加锁
    同步代码块为:当余票大于0时,进行卖票操作
    因此当一个线程正在执行同步代码块时,另外的线程不会执行该代码块,在后面排队等待执行
 /**
     * 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
     *
     *  解决线程不安全问题:排队执行
     */
    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        private Object o = new Object();//创建对象
        @Override
        public void run() {//每次被触发就进卖买票操作
            while(true){
                synchronized (o){//加锁
                    if(count > 0){
                        //卖票
                        System.out.println("正在准备卖票,请稍等...");
                        //try-catch使得卖票的时间更长
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count --;
                        System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
                    }else{
                        break;
                    }
                }
            }//end while
        }//end run
    }

  • 由于只创建了一个任务,因此Object对象只创建了一个,即创建了一把锁
    而后面启动的三个线程由于只有一个任务,因此三个线程在执行的时候看同一把锁,谁抢到锁谁就执行,排队执行
        Runnable runnable = new Ticket();//只有一个任务,因此下面的object对象只创建了一个
        //启动三个线程,o是同一个,只有一个任务,因此在执行的时候只看一个o
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();

		//如果上述写法写成如下,则依旧为不安全线程
		//此时创建了三个任务(new Ticket()),分别创建三个object对象(即锁),此时相当于3个人卖票,每个人卖10张 
		//错误写法!!!!注意
		//new Thread(new Ticket()).start();
		//new Thread(new Ticket()).start();
		//new Thread(new Ticket()).start();
		
		

  • 加了锁之后的输出结果:
正在准备卖票,请稍等...
Thread-0出票成功!余票:9
正在准备卖票,请稍等...
Thread-0出票成功!余票:8
正在准备卖票,请稍等...
Thread-0出票成功!余票:7
正在准备卖票,请稍等...
Thread-0出票成功!余票:6
正在准备卖票,请稍等...
Thread-0出票成功!余票:5
正在准备卖票,请稍等...
Thread-0出票成功!余票:4
正在准备卖票,请稍等...
Thread-0出票成功!余票:3
正在准备卖票,请稍等...
Thread-0出票成功!余票:2
正在准备卖票,请稍等...
Thread-0出票成功!余票:1
正在准备卖票,请稍等...
Thread-0出票成功!余票:0


如果将创建锁的对象写在任务的代码块中,如下所示

此时,每个线程启动时都会创建o对象,因此每个线程都有自己锁o,每个线程在执行时都看自己的锁,这时不能排队,要格外注意!!!!

错误写法:

public void run() {//每次被触发就进卖买票操作
            Object o = new Object();//!!!!!!!三个线程启动时都会创建o对象,即每个线程都有自己的锁o,每个人都看自己的不同的锁,此时不能排队
            while(true){
                synchronized (o){//加锁
                    if(count > 0){
                        //卖票
                        System.out.println("正在准备卖票,请稍等...");
                        //try-catch使得卖票的时间更长
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count --;
                        System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
                    }else{
                        break;
                    }
                }
            }//end while
        }//end run

8.3 同步方法

  • 与同步代码块相似,不同的是,同步方法以方法为单位进行加锁,给方法添加synchronized修饰符

  • 同步方法的锁为this
    同步方法有可能被静态修饰,如果被静态修饰,则同步方法的锁为类.class

代码示例:

	/**
     * 创建一个任务,但是交给三个线程去执行,则会出现线程不安全问题
     * 解决线程不安全问题:排队执行
     */
    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        @Override
        public void run() {//每次被触发就进卖买票操作
            while(true){
                boolean flag = sale();//sale()为加了锁的方法
                if(!flag){
                    break;
                }
            }//end while
        }//end run

        //添加synchronized修饰符,给方法加锁
        public synchronized boolean sale(){
            //this,同步的方法的锁
            //Ticket.class,如果方法为静态方法,则同步方法的锁为类.class
            if(count > 0){
                //卖票
                System.out.println("正在准备卖票,请稍等...");
                //try-catch使得卖票的时间更长
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count --;
                System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
                return true;
            }
            return false;
        }
    }

  • 如果同步代码块锁了一段代码,同步方法锁了另一端代码,锁的对象都是this,那么这当一段代码正在执行时,另一段加锁的代码不能执行
    如下面的代码所示,在循环前加了一把锁,则当一个线程执行这段代码块时,同步方法sale不能执行
  public void run() {
            synchronized (this){//再加一把锁
                
            }
            while(true){
                boolean flag = sale();
                if(!flag){
                    break;
                }
            }//end while
        }//end run

  • 如果有多个同步的方法,且多个方法都是this这把锁,则其中一个方法执行、其他方法无法执行

8.4 显式锁

显式锁使用Lock关键字

在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象

显式锁比隐式锁更好,更能体现锁的概念,体现了面向对象的机制

显式锁Lock的子类:ReentrantLock

代码示例:

  • 创建隐式锁
	Lock l = new ReentrantLock();

  • 在进行代码块前锁住
	l.lock();

  • 在代码块结束后开锁
	l.unlock();//代码执行完毕,开锁

  • 完整代码
    public static void main(String[] args) {
        //线程不安全
        //解决方案3:显式锁Lock
        //java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象
        Runnable runnable = new Ticket();//只有一个任务
        //启动三个线程,但使用的都是runnable对象,因此用的都是同一把锁l
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }

   static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        //创建显式锁l
        private Lock l = new ReentrantLock();
        @Override
        public void run() {//每次被触发就进卖买票操作
            while(true){
                l.lock();//进入if之前,锁住
                if(count > 0){
                    //卖票
                    System.out.println("正在准备卖票,请稍等...");
                    //try-catch使得卖票的时间更长
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count --;
                    System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
                }else{
                    break;
                }
                l.unlock();//代码执行完毕,开锁
            }//end while
        }//end run
    }

不论是显式锁还是隐式锁,都可以有效地控制多线程获取资源、解决所出现的线程不安全问题

8.5公平锁与非公平锁

  • 公平锁:排队,先来先到,在Lock构造方法传入Boolean值True,则为公平锁

  • 非公平锁:抢,隐式锁Sync属于非公平锁,Lock默认为非公平锁
    实现公平锁:

  • 显式锁Lock的构造方法中,参数为True则表示公平锁

9. 线程死锁

  • 死锁:多个线程线程互相持有对方所需要的资源,多个线程因竞争资源而造成的一种僵局(互相等待)

死锁举例
拿生活中的场景并结合代码,举一个简单的栗子:

  • 挟持着人质的罪犯与警察两人僵持不下(警察抓着罪犯,而罪犯手上有人质)
    罪犯对警察说:“你放了我,我放人质!”
    然而警察听到后内心想:“我救人质,但是罪犯跑了”
    警察对罪犯说:“你放了人质,我放过你!”
    然而罪犯听到后内心想:“警察放过我,但是人质跑了”

根据这个场景,来进行代码的实现

  • 罪犯Culprit
	 /**
     * 罪犯
     */
    static class Culprit{
    	//罪犯对警察说
        public synchronized void say(Police p){
            System.out.println("罪犯:你放了我,我放人质!");
            p.fun();
        }
        //听了警察的话,内心回应
        public synchronized void fun(){
            System.out.println("罪犯内心:警察放过我,但是人质跑了");
        }
    }

  • 警察Police
	 /**
     * 警察
     */
    static class Police{
    	//警察对罪犯说
        public synchronized void say(Culprit c){
            System.out.println("警察:你放了人质,我放过你!");
            c.fun();
        }
        //听了罪犯的话,警察回应
        public synchronized void fun(){
            System.out.println("警察内心:我救人质,但是罪犯跑了");
        }

  • 新建线程MyThread,警察对罪犯说
static class MyThread extends Thread{
        private Culprit c;
        private Police p;
        
        //构造方法
        public  MyThread(Culprit c,Police p){
            this.c = c;
            this.p = p;
        }
        @Override
        public void run() {
             /**
             * 警察say方法执行完之后,调用罪犯的fun方法,等待罪犯回应
             */
            p.say(c);//警察说话,让罪犯回应
        }
    }

  • 新建主线程,罪犯对警察说
    public static void main(String[] args) throws InterruptedException {
        Culprit c = new Culprit();//新建一个罪犯对象
        Police p = new Police();//新建一个警察对象
        new MyThread(c,p).start();//新建线程:警察说话,让罪犯回应
        
        /**
         * 罪犯的say方法调用执行完后,调用警察的fun方法,等待警察回应
         */
        c.say(p);//主线程:罪犯说话,让警察回应
    }

此时,有两个线程,而这两个线程中,警察和罪犯都说完了自己的话(执行say),等待对方回应(执行fun),然而等待对方回应前先必须等待对方把话说完(执行say),但是不知道对方有没有先说完(有没有执行完say),因此卡住了,造成了死锁

死锁的结果输出:

罪犯和警察说完之后都在等待对方回应,从而造成了死锁,程序卡在那无法继续进行,只能手动结束程序

罪犯:你放了我,我放人质!
警察:你放了人质,我放过你!


罪犯:你放了我,我放人质!
警察内心:我救人质,但是罪犯跑了
警察:你放了人质,我放过你!
罪犯内心:警察释放我,但是人质跑了

死锁避免

  • 线程按照一定的顺序加锁)
    加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
    根源上解决: 在任何有可能产生锁的方法中,不调用另一个有可能产生锁的方法

10.多线程通信问题

  • 多线程通信问题,也就是生产者与消费者问题
  • 生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全

以下为百度百科对于该问题的解释:

  • 生产者与消费者问题:
    生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

解决办法:

  • 要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
  • 厨师为生产者,服务员为消费者,假设只有一个盘子盛放食品。
    厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…

  • 在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保了数据的安全

  • 定义厨师线程

 /**
     * 厨师,是一个线程
     */
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f = f;
        }

        //运行的线程,生成100道菜
        @Override
        public void run() {
            for (int i = 0 ; i < 100; i ++){
                if(i % 2 == 0){
                    f.setNameAneTaste("小米粥","没味道,不好吃");
                }else{
                    f.setNameAneTaste("老北京鸡肉卷","甜辣味");
                }
            }
        }
    }

  • 定义服务员线程
/**
     * 服务员,是一个线程
     */
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i =0 ; i < 100;i ++){
                //等待
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }//end run

    }//end waiter

  • 新建食物类
 /**
     * 食物,对象
     */
    static class Food{
        private String name;
        private String taste;

        public void setNameAneTaste(String name,String taste){
            this.name = name;

            //加了这段之后,有可能这个地方的时间片更有可能被抢走,从而执行不了this.taste = taste
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
        }//end set

        public void get(){
            System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
        }
    }//end food

  • main方法中去调用两个线程
    public static void main(String[] args) {
        Food f = new Food();
        Cook c = new Cook(f);
        Waiter w = new Waiter(f);
        c.start();//厨师线程
        w.start();//服务生线程     
    }

运行结果:

只截取了一部分,我们可以看到,“小米粥”并没有每次都对应“没味道,不好吃”,“老北京鸡肉卷”也没有每次都对应“甜辣味”,而是一种错乱的对应关系

...
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...

name和taste对应错乱的原因:

当厨师调用set方法时,刚设置完name,程序进行了休眠,此时服务员可能已经将食品端走了,而此时的taste是上一次运行时保留的taste。
两个线程一起运行时,由于使用抢占式调度模式,没有协调,因此出现了该现象

以上运行结果解释如图:
在这里插入图片描述

加入线程安全
针对上面的线程不安全问题,对厨师set和服务员get这两个线程都使用synchronized关键字,实现线程安全,即:当一个线程正在执行时,另外的线程不会执行,在后面排队等待当前的程序执行完后再执行

代码如下所示,分别给两个方法添加synchronized修饰符,以方法为单位进行加锁,实现线程安全

	/**
     * 食物,对象
     */
    static class Food{
        private String name;
        private String taste;

        public synchronized void setNameAneTaste(String name,String taste){
            this.name = name;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
        }//end set

        public synchronized void get(){
            System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
        }
        
    }//end food

输出结果:

由输出可见,又出现了新的问题:
虽然加入了线程安全,set和get方法不再像前面一样同时执行并且菜名和味道一一对应,但是set和get方法并没有交替执行(通俗地讲,不是厨师一做完服务员就端走),而是无序地执行(厨师有可能做完之后继续做,做好几道,服务员端好几次…无规律地做和端)

...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...

实现生产者与消费者问题
由上面可知,加入线程安全依旧无法实现该问题。因此,要解决该问题,回到前面的引入部分,严格按照生产者与消费者问题中所说地去编写程序

生产者与消费者问题:
生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全

  • 厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…

  • 在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保数据的安全

在这里插入图片描述

  • 首先在Food类中加一个标记flag:
    True表示厨师生产,服务员休眠
    False表示服务员端菜,厨师休眠
	private boolean flag = true;

  • 对set方法进行修改
    当且仅当flag为True(True表示厨师生产,服务员休眠)时,才能进行做菜操作
    做菜结束时,将flag置为False(False表示服务员端菜,厨师休眠),这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
    然后唤醒在当前this下休眠的所有进程,而厨师线程进行休眠
		public synchronized void setNameAneTaste(String name,String taste){
            if(flag){//当标记为true时,表示厨师可以生产,该方法才执行
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;//生产完之后,标记置为false,这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
                this.notifyAll();//唤醒在当前this下休眠的所有进程
                try {
                    this.wait();//此时厨师线程进行休眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }//end set

  • 对get方法进行修改
    当且仅当flag为False(False表示服务员端菜,厨师休眠)时,才能进行端菜操作
    端菜结束时,将flag置为True(True表示厨师生产,服务员休眠),这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师生产一份的情况
    然后唤醒在当前this下休眠的所有进程,而服务员线程进行休眠
        public synchronized void get(){
            if(!flag){//厨师休眠的时候,服务员开始端菜
                System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
                flag = true;//端完之后,标记置为true,这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师只生产一份的情况
                this.notifyAll();//唤醒在当前this下休眠的所有进程
                try {
                    this.wait();//此时服务员线程进行休眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }// end if
        }//end get

作了以上调整之后的程序输出:

我们可以看到,没有出现数据错乱,并且菜的顺序是交替依次进行的

...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...

11.线程池Executors

  • 池:容器的意思

使用一个线程通常要经过创建线程、创建任务、执行任务、关闭线程,在这个过程中,创建任务和执行任务的时间很少

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,浪费的时间多,因此o频繁创建线程o会大大降低系统的效率(频繁创建线程和销毁线程需要时间)

线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源

  • 作用
    降低资源消耗
    提高响应速度
    提高线程的可管理性

分类
不论是哪一类,获取线程池的对象都是ExecutorService

1 缓存线程池
长度没有限制

  • 创建缓存线程池:
    .newCachedThreadPool()
//创建缓存线程池
 ExecutorService service = Executors.newCachedThreadPool();

  • 向线程池中加入新的任务,指挥线程池执行新的任务(run):
//向线程池中加入新的任务,指挥线程池执行新的任务(run)
        service.execute(new Runnable() {//execute中传入任务对象即可
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        //向线程池中加入新的任务,指挥线程池执行新的任务(run)
        service.execute(new Runnable() {//execute中传入任务对象即可
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        //向线程池中加入新的任务,执行新的任务(run)
        service.execute(new Runnable() {//execute中传入任务对象即可
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

  • 输出结果:
    由输出可知,三个线程名称为1,2,3
pool-1-thread-3任务执行
pool-1-thread-1任务执行
pool-1-thread-2任务执行

添加休眠时间,使程序休眠一段时间

	Thread.sleep(1000);//停一秒之后,再去执行线程,此时缓存线程池中已有内容,执行缓存池中的内容

  • 指挥线程池执行任务
    此时缓存池中已有内容,再去执行任务时,执行缓存池中空闲的任务
        //向线程池中加入任务,指挥线程池执行新的任务(run)
        service.execute(new Runnable() {//execute中传入任务对象即可
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

  • 输出结果:

由输出结果可知,线程实现了重复使用,休眠后执行的任务是缓存池中已有的空闲任务3

pool-1-thread-1任务执行
pool-1-thread-3任务执行
pool-1-thread-2任务执行
pool-1-thread-3任务执行

2 定长线程池
相对于缓存线程池,长度有限制,线程池中的当前线程数目不会超过给定的长度

当该值为0的时候,意味着没有任何线程,线程池会终止

代码示例:

创建定长线程池,这里指定线程池大小为2
.newFixedThreadPool(参数),参数为线程池的长度

//创建定长线程池,指定了线程池的大小为2
		ExecutorService service = Executors.newFixedThreadPool(2);

向线程池中加入新的任务,指挥线程池执行任务
如下面代码所示,添加3个任务

		//向线程池中加入任务,指挥线程池执行新的任务(run)
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

输出结果:

由于线程池长度为2,因此最多两个任务,线程池中的当前线程数目不会超过2

pool-1-thread-2任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行

3 单线程线程池
与定长线程池中传入参数为1的作用相同,即线程池中只有一个线程

  • 创建单线程线程池
    .newSingleThreadExecutor()
        ExecutorService service = Executors.newSingleThreadExecutor();

  • 向线程池中加入新的任务,指挥线程池执行任务
	service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

  • 输出结果:
    由输出可知,线程池中只有一个线程
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行

4 周期性任务定长线程池
为定长线程池

把一个任务定时在某个时期执行,或者是周期性执行

  • 任务在某个时期执行
    创建单线程线程池
    .newScheduledThreadPool(参数),参数为线程池的长度
        //创建 周期性任务定长线程池
        //任务创建出来的结果不一样
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);

  • 向线程池中加入新的任务,指挥线程池执行任务
    .schedule(参数1,参数2,参数3)
    参数1:定时执行的任务
    参数2:表示时长的数字x(每隔x运行一次任务)
    参数3:时长数字的时间单位,由TimeUnit的常量制定
        /**
         * 定时执行一次
         * 参数1:定时执行的任务
         * 参数2:表示时长的数字x(每隔x运行一次任务)
         * 参数3:时长数字的时间单位,由TimeUnit的常量制定
         */
        service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        },5, TimeUnit.SECONDS);//任务在5秒钟后执行

  • 输出结果:
    5秒钟后输出
pool-1-thread-1任务执行

周期性执行

  • 创建单线程线程池
    .newScheduledThreadPool(参数),参数为线程池的长度
        //创建 周期性任务定长线程池
        //任务创建出来的结果不一样
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);

  • 向线程池中加入新的任务,指挥线程池执行任务
    .schedule(参数1,参数2,参数3,参数4)
    参数1:定时执行的任务
    参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)
    参数3:表示时长的数字x(每隔x运行一次任务)
    参数4:时长数字的时间单位,由TimeUnit的常量制定
       /**
         * 周期性执行
         * 参数1:任务
         * 参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)
		 * 参数3:表示时长的数字x(每隔x运行一次任务)
         * 参数4:时长数字的时间单位,由TimeUnit的常量制定
         */
        service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        },5,1,TimeUnit.SECONDS);//5秒后执行,每隔1秒执行一次

  • 输出结果:
    5秒钟后开始输出,之后每隔1秒输出一次,直到停止程序
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行

  • 无论是哪种线程池,使用完毕后必须手动关闭线程池,否则会一直在内存中存在
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值