韩顺平Java-第十七章:多线程基础

一 线程介绍

1 线程相关概念
(1)程序

​ 是为完成特定任务、用某种语言编写的一组指令的集合(简单说就是写的代码)。

(2)进程

① 进程是指运行中的程序,比如使用的QQ,就启动了一个进程,操作系统会对该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将会为迅雷分配新的内存空间。

② 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程。

(3)什么是线程

① 线程是由进程创建的,是进程的一个实体;

② 一个进程可以拥有多个线程,就比如迅雷可以同时(包括并发和并行)下载多个文件。

(4)其他相关概念

① 单线程:同一个时刻,只允许执行一个线程;

② 多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件;

③ 并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的感觉,简单的说,单核cpu实现的多任务就是并发。

在这里插入图片描述

④ 并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。并发和并行可能同时存在。

在这里插入图片描述

(5)查看电脑有多少个cpu核

public class CpuNums {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        //获取当前电脑cpu的数量(核心数)
        int nums = runtime.availableProcessors();
        System.out.println(nums );
    }
}

在这里插入图片描述

二 线程的使用

1 线程的创建

​ 在Java线程创建有两种方式。

(1)继承Thread类,重写run方法

在这里插入图片描述

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        //创建Cat对象,可以当做线程使用
        Cat cat = new Cat();
        cat.start();//启动线程-> 最终会执行cat的run方法
    }

class Cat extends Thread {
    int times = 0;
    @Override
    public void run() {//重写run方法,写上自己的业务逻辑
        while (true) {
            //该线程每隔1秒。在控制台输出 “喵喵, 我是小猫咪”
            System.out.println("喵喵, 我是小猫咪" + (++times) + " 线程名=" + Thread.currentThread().getName());
            //让该线程休眠1秒 ctrl+alt+t
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(times == 80) {
                break;//当times 到80, 退出while, 这时线程也就退出..
            }
        }
    }
}

在main里面建立一个线程,此时main内的线程会和main建立的Thread0线程交替执行(并发),进程可以创建线程,线程也可以创建线程。
在这里插入图片描述

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        //创建Cat对象,可以当做线程使用
        Cat cat = new Cat();

        cat.start();//启动线程-> 最终会执行cat的run方法

        System.out.println("主线程继续执行" + Thread.currentThread().getName());//名字main
        for(int i = 0; i < 60; i++) {
            System.out.println("主线程 i=" + i);
            //让主线程休眠
            Thread.sleep(1000);
        } 
    }
}

class Cat extends Thread {

    int times = 0;
    @Override
    public void run() {//重写run方法,写上自己的业务逻辑
     
        while (true) {
            //该线程每隔1秒。在控制台输出 “喵喵, 我是小猫咪”
            System.out.println("喵喵, 我是小猫咪" + (++times) + " 线程名=" + Thread.currentThread().getName());
            //让该线程休眠1秒 ctrl+alt+t
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(times == 80) {
                break;//当times 到80, 退出while, 这时线程也就退出..
            }
        }
    }

}

问题 为什么调用Start方法,而不是run方法去启动线程?

run方法就是一个普通的方法, 没有真正的启动一个线程,如果调用cat.run();就会把run方法执行完毕,才向下执行

而调用cat.start();会进行如下的步骤:

在这里插入图片描述

/*
            (1)
            public synchronized void start() {
                start0();
            }
            (2)
            //start0() 是本地方法,是JVM调用, 底层是c/c++实现
            //真正实现多线程的效果, 是start0(), 而不是 run🚩
            private native void start0();
         */
(2)实现Runnable接口,重写run方法

Java是单继承的,在某些情况下,一个类可能已经继承了某一个父类,这时候如果想要创建线程只能通过实现接口来创建了。
在这里插入图片描述
因为Runable接口当中只有run() 方法,因此不能直接调用start()方法,所以需要一个代理(即新建一个Thread类,将dog当作参数输入)完成该方法的调用。

public class Thread02 {
    public static void main(String[] args) {Dog dog = new Dog();//dog.start(); 这里不能调用start//创建了Thread对象,把 dog对象(实现Runnable),放入ThreadThread thread = new Thread(dog);
​        thread.start();
    }
}


class Dog implements Runnable { //通过实现Runnable接口,开发线程

    int count = 0;
    
    @Override
    public void run() { //普通方法
        while (true) {
            System.out.println("小狗汪汪叫..hi" + (++count) + Thread.currentThread().getName());
     
            //休眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
     
            if (count == 10) {
                break;
            }
        }
    }
    
}

​模拟代理模式。

//线程代理类 , 模拟了一个极简的Thread类
class ThreadProxy implements Runnable {//你可以把Proxy类当做 ThreadProxy

    private Runnable target = null;//属性,类型是 Runnable
     
    @Override
    public void run() {
        if (target != null) {
            target.run();//动态绑定(运行类型Tiger)
        }
    }
     
    public ThreadProxy(Runnable target) {
        this.target = target;
    }
     
    public void start() {
        start0();//这个方法时真正实现多线程方法
    }
     
    public void start0() {
        run();
    }

}
class Animal {
}

class Tiger extends Animal implements Runnable {
    @Override
    public void run() {
        System.out.println("老虎嗷嗷叫....");
    }
}
public class Thread02 {
    public static void main(String[] args) {
        Tiger tiger = new Tiger();//实现了 Runnable
        ThreadProxy threadProxy = new ThreadProxy(tiger);
        threadProxy.start();
    }
}
2 多线程执行
public class Thread03 {
    public static void main(String[] args) {
        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);
        thread1.start();//启动第1个线程
        thread2.start();//启动第2个线程
        //...
    }
}

class T1 implements Runnable {

    int count = 0;
     
    @Override
    public void run() {
        while (true) {
            //每隔1秒输出 “hello,world”,输出10次
            System.out.println("hello,world " + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 60) {
                break;
            }
        }
    }

}

class T2 implements Runnable {

    int count = 0;
     
    @Override
    public void run() {
        //每隔1秒输出 “hi”,输出5次
        while (true) {
            System.out.println("hi " + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 50) {
                break;
            }
        }
    }

}


在这里插入图片描述
​ 注意:线程全部结束,该进程才会结束。

3 继承Thread vs 实现Runnable 的区别

(1)从Java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,因为Thread类本身就实现了Runnable接口;

(2)实现Runnable接口方式更适合多个线程共享一个资源的情况,并且避免了单继承限制。

​ 下图程序当中,线程thread01和thread02就共享了t3。
在这里插入图片描述

4 多线程售票问题

在这里插入图片描述

public class SellTicket {
    public static void main(String[] args) {
        //测试
//        SellTicket01 sellTicket01 = new SellTicket01();
//        SellTicket01 sellTicket02 = new SellTicket01();
//        SellTicket01 sellTicket03 = new SellTicket01();
//
//        //这里我们会出现超卖..
//        sellTicket01.start();//启动售票线程
//        sellTicket02.start();//启动售票线程
//        sellTicket03.start();//启动售票线程

        System.out.println("===使用实现接口方式来售票=====");
        SellTicket02 sellTicket02 = new SellTicket02();
     
        new Thread(sellTicket02).start();//第1个线程-窗口
        new Thread(sellTicket02).start();//第2个线程-窗口
        new Thread(sellTicket02).start();//第3个线程-窗口
    }

}

//使用Thread方式
class SellTicket01 extends Thread {

    private static int ticketNum = 100;//让多个线程共享 ticketNum
     
    @Override
    public void run() {
        while (true) {
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                break;
            }
     
            //休眠50毫秒, 模拟
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
     
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                    + " 剩余票数=" + (--ticketNum));
        }
    }

}

//实现接口方式
class SellTicket02 implements Runnable {
    private int ticketNum = 100;//让多个线程共享 ticketNum

    @Override
    public void run() {
        while (true) {
     
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                break;
            }
     
            //休眠50毫秒, 模拟
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
     
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                    + " 剩余票数=" + (--ticketNum));//1 - 0 - -1  - -2
     
        }
    }

}

可能会出现多售票的情况,因为进程不是同步的。

5 线程终止
(1)基本说明

① 当线程完成任务后,会自动退出;

② 还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式。

(2)应用实例
public class ThreadExit_ {
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();

        //如果希望main线程去控制t1 线程的终止, 必须可以修改 loop
        //让t1 退出run方法,从而终止 t1线程 -> 通知方式
     
        //让主线程休眠 10 秒,再通知 t1线程退出
        System.out.println("main线程休眠10s...");
        Thread.sleep(10 * 1000);
        t1.setLoop(false);
    }
}

class T extends Thread {
    private int count = 0;
    //设置一个控制变量
    private boolean loop = true;
    @Override
    public void run() {
        while (loop) {

            try {
                Thread.sleep(50);// 让当前线程休眠50ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T 运行中...." + (++count));
        }
     
    }
     
    public void setLoop(boolean loop) {
        this.loop = loop;
    }

}

三 线程方法

1 常用方法第一组

(1)setName //设置线程名称,使其与参数name相同;

(2)getName //返回该线程的名称;

(3)start //使该线程开始执行;Java虚拟机底层调用该线程的start0方法;

(4)run //调用线程对象run方法;

(5)setPriority //更改线程的优先级;

(6)getPriority //获取线程的优先级;

(7)sleep //在指定的毫秒数内让当前正在执行的线程休眠(暂停执行);

(8)interrupt //中断线程,注意不是终止,如果线程正在休眠,则停止休眠,继续执行。

public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        //测试相关的方法
        T t = new T();
        t.setName("老韩");
        t.setPriority(Thread.MIN_PRIORITY);//1
        t.start();//启动子线程

        //主线程打印5 hi ,然后我就中断 子线程的休眠
        for(int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi " + i);
        }
     
        System.out.println(t.getName() + " 线程的优先级 =" + t.getPriority());//1
     
        t.interrupt();//当执行到这里,就会中断 t线程的休眠.
    }
}

class T extends Thread { //自定义的线程类
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 100; i++) {
                //Thread.currentThread().getName() 获取当前线程的名称
                System.out.println(Thread.currentThread().getName() + "  吃包子~~~~" + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
                Thread.sleep(20000);//20秒
            } catch (InterruptedException e) {
                //当该线程执行到一个interrupt 方法时,就会catch 一个 异常, 可以加入自己的业务代码
                //InterruptedException 是捕获到一个中断异常.
                System.out.println(Thread.currentThread().getName() + "被 interrupt了");
            }
        }
    }
}
2 注意事项和细节

(1)start底层会创建新的线程,调用run,run就是一个简单的方法调用,不会启动新线程;

(2)线程优先级的范围;
在这里插入图片描述
(3)interrupt,中断线程,但没有真正的结束线程,所以一般用于中断正在休眠的线程;

(4)sleep:线程的静态方法,使当前线程休眠。

3 常用方法第二组
(1)yield:线程的礼让

​ 让出CPU,让其他线程执行,但是礼让的时间不确定,所以不一定礼让成功。

(2)join:线程的插队

​ 插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务。

(3)案例

​ main线程创建了一个子线程,每隔1s输出hello,输出20次,主线程每隔1s,输出hi,输出20次。要求两个线程同时执行,当主线程输出5次后,就让子线程运行完毕,主线程再继续执行。

public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {

        T2 t2 = new T2();
        t2.start();
     
        for(int i = 1; i <= 20; i++) {
            Thread.sleep(1000);
            System.out.println("主线程(小弟) 吃了 " + i  + " 包子");
            if(i == 5) {
                System.out.println("主线程(小弟) 让 子线程(老大) 先吃");
                //join, 线程插队
                //t2.join();// 这里相当于让t2 线程先执行完毕
                Thread.yield();//礼让,不一定成功..
                System.out.println("线程(老大) 吃完了 主线程(小弟) 接着吃..");
            }
     
        }
    }

}

class T2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                Thread.sleep(1000);//休眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程(老大) 吃了 " + i +  " 包子");
        }
    }
}
4 课堂练习题

在这里插入图片描述
在这里插入图片描述

public class ThreadMethodExercise {
    public static void main(String[] args) throws InterruptedException {
        TT tt = new TT();
        Thread thread = new Thread(tt);
        for (int i = 1; i <= 10; i++) {
            Thread.sleep(1000);
            System.out.println("Hi" + i);
            if (i == 5){
                thread.start();
                thread.join();
            }
        }
        System.out.println("主线程结束..");
    }
}
class TT implements Runnable{
    private int count;
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Hello" + (++count));

            if (count == 10){
                System.out.println("子线程结束..");
                break;
            }
        }
    }
}
5 用户线程和守护线程
(1)用户线程

也叫工作线程,当线程的任务执行完或通知方式通知它结束。

(2)守护线程

一般是为了工作线程服务的,当所有的用户线程结束,守护线程自动结束。常见的守护线程就是垃圾回收机制。

注意守护线程的设置要放在创建线程的前面,如下。

​ myDaemonThread.setDaemon(true);
​ myDaemonThread.start();

public class ThreadMethod03 {
    public static void main(String[] args) throws InterruptedException {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        //如果我们希望当main线程结束后,子线程自动结束
        //,只需将子线程设为守护线程即可
        myDaemonThread.setDaemon(true);
        myDaemonThread.start();

        for( int i = 1; i <= 10; i++) {//main线程
            System.out.println("宝强在辛苦的工作...");
            Thread.sleep(1000);
        }
    }

}

class MyDaemonThread extends Thread {
    public void run() {
        for (; ; ) {//无限循环
            try {
                Thread.sleep(1000);//休眠1000毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("马蓉和宋喆快乐聊天,哈哈哈~~~");
        }
    }
}

四 线程的生命周期

线程有七种状态:NEW、Runable(Ready、Running)、TimeWaiting、Waiting、Blocked、Teminated。

在这里插入图片描述

public class ThreadState_ {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        System.out.println(t.getName() + " 状态 " + t.getState());
        t.start();

        while (Thread.State.TERMINATED != t.getState()) {
            System.out.println(t.getName() + " 状态 " + t.getState());
            Thread.sleep(500);
        }
     
        System.out.println(t.getName() + " 状态 " + t.getState());
     
    }
}

class T extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 10; i++) {
                System.out.println("hi " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            break;
        }
    }
}

五 Synchronized🚩

1 线程同步机制

(1)在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性;

(2)线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直至该线程完成操作,其他线程才能对该内存地址进行操作。

2 同步具体方法 - Synchronized
(1)同步代码块
synchronized(对象){  //得到对象的锁,才能操作同步代码

	//需要被同步代码;

}
(2)同步方法

​ synchronized还可以放在方法声明中,表示整个方法。

public synchronized void m(String name){

	//需要被同步的代码

}
3 使用同步解决售票问题

​ 要注意不能直接在run()方法前加synchronized,因为这样会使得只有一个窗口可以进入卖票,使得其他俩窗口一直处于锁死状态,直至票卖完退出。还有就是在继承类的方式当中,要给sell()方法加上static,这样才能保证三个线程调用的是同一个方法。

public class Practice02{
    public static void main(String[] args) {

        System.out.println("===使用继承类方式来售票=====");
        SellTicket03 sellTicket1 = new SellTicket03();
        SellTicket03 sellTicket2 = new SellTicket03();
        SellTicket03 sellTicket3 = new SellTicket03();
     
        sellTicket1.start();//启动售票线程
        sellTicket2.start();//启动售票线程
        sellTicket3.start();//启动售票线程


//        System.out.println("===使用实现接口方式来售票=====");
//        SellTicket04 sellTicket02 = new SellTicket04();
//
//        new Thread(sellTicket02).start();//第1个线程-窗口
//        new Thread(sellTicket02).start();//第2个线程-窗口
//        new Thread(sellTicket02).start();//第3个线程-窗口

    }
}


//使用Thread方式

class SellTicket03 extends Thread {

    private static int ticketNum = 100;//让多个线程共享 ticketNum
    private static boolean bool = true;//控制run方法的变量
    public static synchronized void sell(){//同步方法,同一时刻,只能有一个线程执行该方法
        if (ticketNum <= 0) {
            System.out.println("售票结束...");
            bool = false;
            return;
        }
     
        //休眠50毫秒, 模拟
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     
        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));
    }
    @Override
    public void  run() {
        while (bool) {
            sell();
        }
    }

}

//实现接口方式

class SellTicket04 implements Runnable {
    private int ticketNum = 100;//让多个线程共享 ticketNum
    private boolean bool = true;
    public synchronized void sell(){
        if (ticketNum <= 0) {
            System.out.println("售票结束...");
            bool = false;
            return;
        }

        //休眠50毫秒, 模拟
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     
        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));//1 - 0 - -1
    }
    @Override
    public void run() {
        while (bool) {
            sell();
        }
    }

}

六 互斥锁🚩

1 基本介绍

(1)Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性;

(2)每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任意时刻,只能有一个线程访问该对象;

(3)关键字synchronized来于对象的互斥锁练习,当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问;

(4)同步的局限性:导致程序的执行效率要降低;

(5)同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象);【该方法适用于实现接口的同步,因为继承类的话不同线程操作的是不同的对象】

(6)同步方法(静态的)的锁为当前类本身。【该方法适用于继承类的同步,也可以用于实现接口的同步】

代码示例 - 同步方法(非静态)

//实现接口的同步
//第一种,在方法上添加synchronized修饰,默认锁在this对象
class SellTicket04 implements Runnable {
    private  int ticketNum = 100;//让多个线程共享 ticketNum
    private  boolean bool = true;
    public synchronized void sell(){
        if (ticketNum <= 0) {
            System.out.println("售票结束...");
            bool = false;
            return;
        }

        //休眠50毫秒, 模拟
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     
        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));//1 - 0 - -1
    }
    @Override
    public void run() {
        while (bool) {
            sell();
        }
    }
}
//实现接口的同步
//第二种,在方法上内部设置同步代码块添加修饰,锁可以在this对象,也可以是其他对象,但是必须是同一个对象,若不是同一个对象,那么不同线程操作的对象就不同了,就不存在同步问题了。
class SellTicket04 implements Runnable {
    private  int ticketNum = 100;//让多个线程共享 ticketNum
    private  boolean bool = true;
    public void sell(){
        synchronized(this){//此时锁在this对象
            if (ticketNum <= 0) {
            System.out.println("售票结束...");
            bool = false;
            return;
        }

        //休眠50毫秒, 模拟
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     
        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));//1 - 0 - -1
        }
        
    }
    @Override
    public void run() {
        while (bool) {
            sell();
        }
    }

}

代码示例 - 同步代码块(非静态)

class SellTicket04 implements Runnable {
    private  int ticketNum = 100;//让多个线程共享 ticketNum
    private  boolean bool = true;
    Object object = new Object();
    public void sell(){
        synchronized(object){//此时锁在object对象
            if (ticketNum <= 0) {
            System.out.println("售票结束...");
            bool = false;
            return;
        }

        //休眠50毫秒, 模拟
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     
        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));//1 - 0 - -1
        }
        
    }
    @Override
    public void run() {
        while (bool) {
            sell();
        }
    }
    
}

代码示例 - 同步方法(静态)

//继承类的同步
//第一种,静态方法直接写synchronized修饰,默认所在该类
class SellTicket03 extends Thread {

    private static int ticketNum = 100;//让多个线程共享 ticketNum
    private static boolean bool = true;
    public static synchronized void sell(){
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                bool = false;
                return;
            }
     
            //休眠50毫秒, 模拟
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
     
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                    + " 剩余票数=" + (--ticketNum));
     
    }
    @Override
    public void  run() {
        while (bool) {
     
            sell();
     
        }
    }

}
//实现接口的同步
//第一种,在静态方法上添加synchronized修饰,默认锁在该类
class SellTicket04 implements Runnable {
    private  int ticketNum = 100;//让多个线程共享 ticketNum
    private  boolean bool = true;
    public static synchronized void sell(){
        if (ticketNum <= 0) {
            System.out.println("售票结束...");
            bool = false;
            return;
        }

        //休眠50毫秒, 模拟
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     
        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));//1 - 0 - -1
    }
    @Override
    public void run() {
        while (bool) {
            sell();
        }
    }

}

代码示例 - 同步代码块(静态)

//继承类的同步
//第二种,静态方法内部写同步代码块,默认的锁在该类
class SellTicket03 extends Thread {

    private static int ticketNum = 100;//让多个线程共享 ticketNum
    private static boolean bool = true;
    public static  void sell(){
        synchronized(SellTicket03.class){
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                bool = false;
                return;
            }
     
            //休眠50毫秒, 模拟
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
     
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                    + " 剩余票数=" + (--ticketNum));
        }
     
    }
    @Override
    public void  run() {
        while (bool) {
            sell();
        }
    }

}
//实现接口的同步
//第二种,在静态方法上内设置添加synchronized修饰的同步代码块,默认锁在该类
class SellTicket04 implements Runnable {
    private static int ticketNum = 100;//让多个线程共享 ticketNum
    private static boolean bool = true;
    public static void sell(){
        synchronized(SellTicket04.class){
            if (ticketNum <= 0) {
            System.out.println("售票结束...");
            bool = false;
            return;
        }

        //休眠50毫秒, 模拟
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     
        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));//1 - 0 - -1
        }
    }
    @Override
    public void run() {
        while (bool) {
            sell();
        }
    }

}
2 注意事项和细节

(1)同步方法如果没有使用static修饰:默认锁对象为this;

(2)如果方法使用static修饰,默认锁对象:当前类.class

(3)实现的落地步骤:

①  需要先分析上锁的代码;

②  选择同步代码块【范围小,效率高,优选】或者同步方法;

③  要求多个线程的锁对象为同一个即可。

七 死锁

1 基本介绍

​ 多个线程都占用了对方的资源,都不肯相让,导致了死锁,在编程中一定要避免死锁的发生。

2 应用实例
public class DeadLock_ {
    public static void main(String[] args) {
        //模拟死锁现象
        DeadLockDemo A = new DeadLockDemo(true);
        A.setName("A线程");
        DeadLockDemo B = new DeadLockDemo(false);
        B.setName("B线程");
        A.start();
        B.start();
    }
}


//线程
class DeadLockDemo extends Thread {
    static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用static
    static Object o2 = new Object();
    boolean flag;

    public DeadLockDemo(boolean flag) {//构造器
        this.flag = flag;
    }
     
    @Override
    public void run() {
     
        //下面业务逻辑的分析
        //1. 如果flag 为 T, 线程A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
        //2. 如果线程A 得不到 o2 对象锁,就会Blocked
        //3. 如果flag 为 F, 线程B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
        //4. 如果线程B 得不到 o1 对象锁,就会Blocked
        if (flag) {
            synchronized (o1) {//对象互斥锁, 下面就是同步代码
                System.out.println(Thread.currentThread().getName() + " 进入1");
                synchronized (o2) { // 这里获得li对象的监视权
                    System.out.println(Thread.currentThread().getName() + " 进入2");
                }
                
            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + " 进入3");
                synchronized (o1) { // 这里获得li对象的监视权
                    System.out.println(Thread.currentThread().getName() + " 进入4");
                }
            }
        }
    }

}

八 释放锁

1 下面操作会释放锁

(1)当前线程的同步方法、同步代码块执行结束;

(2)当前线程在同步代码块、同步方法中遇到break、return;

(3)当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束;

(4)当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程停止,并释放锁。

2 下面操作不会释放锁

(1)线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yield()方法暂停当前线程的执行,不会释放锁;

(2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。

提示:不推荐使用suspend()和resume()来控制线程。

九 wait 和 notify

1 为什么需要wait()方法和notify()方法?

举一个栗子~ 假如小万和小丁约好一起去吃自助餐,小丁进去之后发现自己喜欢吃的菜都被拿光了!!!而此时工作人员还没有进行补货~ 发生的一系列故事
在这里插入图片描述
【解释说明】
这里想要的菜看作是锁,而5个人看作是竞争锁的5个线程,小丁在这个菜面前进进出出,其实并没有实质性地释放锁,但由于这盘菜始终没有工作人员补货,小丁也拿不到自己想要吃的菜,小丁就会陷入忙等,而其它人又竞争不到这盘菜,可看作是线程竞争不到CPU资源,其他人就处于一直在阻塞的状态,也什么事情都干不了

所有线程都可以竞争这个锁,这里就会出现一个极端的情况:线程刚释放锁又是该线程获得锁,发现里面没有货,又释放又进去,一直循环着,而其它线程拿不到锁,处于阻塞状态啥也干不了,导致一个问题 —— 线程饿死

这里引入了一个新的概念—— 线程饿死,通过上述案例,总结为:
【线程饿死】 指一个或多个线程由于某种原因无法获取所需的执行机会,导致它们无法继续正常执行,从而被阻塞在某个状态,不能完成其任务,这种情况通常是优先级设置不当导致的

上述问题如何解决呢?
有些同学可能会觉得线程有记账信息,可以避免此问题,但实际上,并不能!!!
线程的记账信息其实是一个比较宏观的东西,它需要多个线程多运行一段时间才能生成,对于上面的案例,线程饿死,同一个线程进进出出的情况是一瞬间的事,故线程的记账信息无法解决~

正确:使用wait()和notify()可以有效解决上述问题!!! 上述情况如下:
在这里插入图片描述【解释说明】
小丁站在菜面前发现没菜时,就先wait()释放锁,并进行阻塞等待,即暂时不参与CPU调度,不参与锁竞争,当服务员进行补菜,通知notify小丁有菜了,可以去拿菜了,小丁再去重新竞争资源拿到菜,小丁在阻塞等待时,小万和其他3个人,也是要拿这盘菜,但是条件不满足,也是wait()等待就行
【wait()】 发现条件不满足或是时机不成熟时,线程就先阻塞等待
【notify()】 其它线程构造一个成熟的条件,就可以唤醒该线程,唤醒后就可以参与锁竞争了

协调好多个线程的执行顺序是很重要的,其实在日常生活中,还有很多这样的案例,为了更深刻理解,再比如小丁还喜欢打篮球,球场上的每个人都是独立的"执行流" ,即每个人可当作是一个线程,完成一个具体的进攻得分好几个动作, 需要多个人一起相互配合,必须按照一定的顺序执行,例如1号球员先传球,2号球员拿到球才能扣篮,没拿到球时只能wait()阻塞等待,对应线程中,线程1 先 “传球” 完成自己的任务,通知notify()2号球员已经传球了,线程2 拿到球才能 "扣篮"才能完成自己的任务,是讲究顺序的,wait()和notify()就是解决上述问题的

【wait()和notify()的作用】即合理的协调多个线程之间的执行先后顺序

2 wait()方法
(1)wait()方法的作用

作用:让某个线程先暂停下来,等一等

wait()方法的初心就是阻塞等待,让线程进入 WAITING 状态!不过我们要区分开来,不要把概念弄混淆了,wait()方法导致阻塞,竞争锁也可以导致阻塞,这是两种不同线程进入阻塞的方式!

【注意】 wait()和notify()是Object的方法,只要是个类对象,不是内置类型(也叫基本数据类型),都可以使用wait()和notify()方法

(2)wait()做的事情

1)释放当前的锁
2)使当前执行代码的线程进行阻塞等待(即把线程放到等待队列中)
3)满足一定条件时收到通知,被唤醒,同时重新尝试获取这个锁

(3)wait()结束等待的条件

1)其他线程调用该对象的 notify() 方法(这里强调同一对象)
2)wait()方法等待时间超时 (wait() 方法会提供一个带有 timeout 参数的版本,来指定等待时间,超过这个时间,wait()就会结束)
3)其他线程调用该等待线程的 interrupted() 方法,导致 wait() 抛出 InterruptedException 异常

(4)带参数的wait方法 —— wait(timeout)

wait() 方法也提供了一个带参数的版本,timeout参数即为指定的最大等待时间
不带参数的wait()即为死等,只有notify()方法能唤醒它
带参数的wait(timeout),则为等到最大时间还没有通知,就自己唤醒自己

(5)wait()必须写在 synchronized 代码块里

这里需要重要注意: wait()必须写在 synchronized 代码块里!!! 两者必须搭配使用,否则会抛出异常

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        System.out.println("wait前:");
        obj.wait();
        System.out.println("wait后:");
    }
}

上述代码中,直接调用obj.wait(),并没有进行加锁,运行后,抛出 IllegalMonitorStateException,即非法的锁状态异常,运行结果如下:
在这里插入图片描述
【为什么会出现这种情况?】
回顾wait()需要做的事情,先进行解锁!!! 如果这把锁都没获取到,就尝试解锁,就会产生异常!(在现实生活中,如果你开门,都没有这把锁,就尝试开锁,东西都没有,咋进行开锁操作捏!所以就会抛出异常)

所以在使用wait()方法前,必须要先进行加锁,就是把wait()写在 synchronized 代码块里面!!!

【注意事项】同时需要注意,加锁的锁对象必须要和wait()的锁对象是同一个,如果加锁对象和调用wait()对象不是同一个,也会抛出IllegalMonitorStateException 异常!!!

正确使用wait()方法如下:

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("等待中");
            object.wait();
            System.out.println("等待结束");
        }
    }
}

打印结果如下:
在这里插入图片描述打印结果显示一直在等待中,分析可以得到,在执行到 object.wait() 方法后就一直等待下去,但是程序肯定不能一直这么等待下去呀,这个时候就需要使用到另外一个方法唤醒的方法notify()!!! 下面notify()方法闪亮登场~

3 notify()方法
(1)notify()方法的作用

作用:把该线程唤醒,使其能够继续执行

(2)notify()方法的用法

1)notify()方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,同时使它们重新获取该对象的对象锁
2)如果有多个线程等待,则由线程调度器随机挑选出一个呈 wait 状态的线程,并不遵循"先来后到"的原则,仍然是随机的
3)在notify()方法后,当前线程不会立刻释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized 代码块之后才会释放对象锁

在接下来的代码里,我们会更深刻理解以上3点

(3)notify()方法必须写在 synchronized 代码块里

这里需要重要注意: notify()也必须写在 synchronized 代码块里!!! 两者必须搭配使用,否则会抛出异常

**【注意事项】**同时需要注意,必须先执行wait()方法,然后再执行notify()方法,此时才会有效果,试想一下,如果现在还没有执行wait(),就执行notify()方法,这不就相当于一炮打空嘛~没啥效果!没有起到实际作用,虽然没有额外的副作用,也不会抛出异常,但是代码的功能就不能正确执行了

使用wait()和notify()方法,更好理解wait()和notify()方法的用法,代码如下:

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();

        Thread t1 = new Thread(() -> {
            try {
                System.out.println("wait开始");
                synchronized (obj) {
                    obj.wait();
                }
                System.out.println("wait结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        t1.start();

        Thread.sleep(1000); //保证t1先启动,wait()先执行

        Thread t2 = new Thread(() -> {
            synchronized (obj) {
                System.out.println("notify开始");
                obj.notify();
                System.out.println("notify结束");
            }
        });

        t2.start();
    }
}

分别创建 t1 和 t2 两个线程,且它们对同一个对象加锁,在此代码中让 t1 线程中执行wait(),t2 线程中执行notify(),先后启动t1和t2线程,观察代码运行结果:
在这里插入图片描述
【解释说明】
1)t1 线程先执行,执行到wait()方法,t1 线程的锁就被wait()方法释放,且 t1 线程自身就阻塞等待了
2)1s之后 t2 线程开始执行,执行到notify()方法,就会通知 t1 线程,t1 线程被唤醒,继续执行
需要注意的是,notify()方法在 synchronized 代码块内部,因此,只有等 t2 线程释放锁之后,t1 线程才能再竞争到锁,t1 才能继续往下执行,所以先打印的是"notify()结束",再是打印"wait结束"

在上述代码中,虽然是 t1 线程先执行的,但是可以通过wait()方法、notify() 方法的控制,让 t2 线程先执行一部分逻辑,执行完后,t2 线程通过 notify()方法唤醒 t1 线程,使 t1 线程继续往下执行!这正是它们的意义,wait()方法、notify() 方法可以合理的协调多个线程之间的执行先后顺序,使线程执行顺序变得可控起来!

(4)notifyAll() 方法

notify()方法只是唤醒某一个等待线程,使用notifyAll()方法可以一次唤醒等待同一对象的所有线程

存在这样一个情况:可以有多个线程,等待同一个对象,比如在 t1、t2、t3 线程中,都调用 object.wait()
1)此时在 main 线程中调用 object.notify(),就会随机唤醒上述三个线程中的一个,而另外两个线程仍然是处于 WAITING 状态,
2)但是如果调用 object.notifyAll(),此时就会把上述三个线程全部唤醒,此时这三个线程就会重新竞争锁,再依次执行
【注意】 此时需要三个线程都wait()等待,再通知,不然又要空打一炮啦

【案例】:用多线程模拟蜜蜂和熊的关系。
蜜蜂是生产者,熊是消费者。密蜂生产蜂密是累加的过程,熊吃蜂蜜是批量(满100吃掉)的过程
生产者和消费者之间使用通知方式告知对方,注意不能出现死锁的现象。
100只密峰,每次生产的蜂密是1
熊吃蜂密是20(批量的情况)。

Box(蜜罐)类

public class Box {
    private int  MAX=20;
    private int count;
    //添加蜂蜜加1
    public synchronized int add(){
        while(count>=MAX){
            try{
                this.notifyAll();
                this.wait();
            }
            catch(Exception e){
                e.printStackTrace();
            }
        }
        return ++count;

    }
    //移出蜂蜜-MAX
    public synchronized void remove(){
        while(count<MAX){
            try{
                this.wait();    //当蜜罐里的蜂蜜不足20时,熊处在等待队列
            }
            catch(Exception e){
                e.printStackTrace();
            }
        }
        count=0;
        this.notify();      //熊取完蜂蜜后,通知蜜蜂继续生产蜂蜜

    }
}

Bee(蜜蜂)类

public class Bee extends Thread{

    private String name;
    private Box box;
    public Bee(String name,Box box){
        this.name=name;
        this.box=box;
    }
    public void run(){
        while(true){
            int n=box.add();
            System.out.println(name+"生产了蜂蜜:1,蜂蜜还有:"+n);
        }
    }
}

Bear(熊)类

public class Bear extends Thread {
    private String name;
    private Box box;
    public Bear(String name,Box box){
        this.name=name;
        this.box=box;
    }
    public void run(){
        while(true){
            box.remove();
            System.out.println(name+"吃掉了蜂蜜:20 ");
        }
    }
}

Main(程序入口)类

public class Main {
    public static void main(String[] args) {
        Box box = new Box();
        for(int i=1;i<=100;i++){
            new Bee("Bee-"+i,box).start();
        }
        new Bear("黑熊1",box).start();
        new Bear("黑熊2",box).start();
    }
}
4 面试题 —— join()、sleep()方法和wait()方法的对比
(1) join()和wait()方法的区别
  1. 从java包来看
    【join()方法】
    join()方法在java.lang.Thread 声明
    【wait()方法】
    wait()方法在java.lang.Object 声明

  2. 从作用效果来看
    【join()方法】
    当在 t1 线程中调用了t2.join(),这是让 t1 线程等待 t2 线程全部执行完毕才能再执行,这使得线程之间的执行从"并行"变成"串行"
    【wait()方法】
    wait()和notify()方法搭配使用,可以让 t2 线程执行一部分,再让 t1 线程执行一部分,t1 线程执行一部分再让 t2 线程执行一部分…

(2) sleep()方法和wait()方法的对比
  1. 相同点
    wait()有一个带参数版本,用来体现超时时间,超过这个时间会被自动唤醒,此时和sleep()方法类似
    同时,wait()和sleep()方法都能提前被唤醒

  2. 不同点
    1)最大的区别:在于两者的初心不同,即设计这个东西到底要解决啥问题的不同
    【sleep()】sleep()方法单纯是让当前线程休眠一会
    【wait()】wait()解决的是线程之间的顺序控制
    2)进一步的,实现或使用上,也是有明显区别的,wait()需要搭配锁使用,而sleep不需要,sleep()方法是让程序按照指定时间短暂休眠让出CPU给其它线程,到时间自动恢复,而不带参数的wait()只有被唤醒后,线程才能重新尝试获得锁,得到锁后才能继续执行

十 本章作业

1 编程题

在这里插入图片描述

在这里插入图片描述

public class Homework01 {
    public static void main(String[] args) {
        A a = new A();
        B b = new B(a);//一定要注意.
        a.start();
        b.start();
    }
}

//创建A线程类
class A extends Thread {
    private boolean loop = true;

    @Override
    public void run() {
        //输出1-100数字
        while (loop) {
            System.out.println((int)(Math.random() * 100 + 1));
            //休眠
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("a线程退出...");
     
    }
     
    public void setLoop(boolean loop) {//可以修改loop变量
        this.loop = loop;
    }

}

//直到第2个线程从键盘读取了“Q”命令
class B extends Thread {
    private A a;
    private Scanner scanner = new Scanner(System.in);

    public B(A a) {//构造器中,直接传入A类对象
        this.a = a;
    }
     
    @Override
    public void run() {
        while (true) {
            //接收到用户的输入
            System.out.println("请输入你指令(Q)表示退出:");
            char key = scanner.next().toUpperCase().charAt(0);
            if(key == 'Q') {
                //以通知的方式结束a线程
                a.setLoop(false);
                System.out.println("b线程退出.");
                break;
            }
        }
    }

}
2 编程题

在这里插入图片描述

在这里插入图片描述

public class Homework02 {
    public static void main(String[] args) {
        T t = new T();
        Thread thread1 = new Thread(t);
        thread1.setName("t1");
        Thread thread2 = new Thread(t);
        thread2.setName("t2");
        thread1.start();
        thread2.start();
    }
}

//编程取款的线程
//1.因为这里涉及到多个线程共享资源,所以我们使用实现Runnable方式
//2. 每次取出 1000
class T implements  Runnable {
    private int money = 10000;

    @Override
    public void run() {
        while (true) {
     
            //解读
            //1. 这里使用 synchronized 实现了线程同步
            //2. 当多个线程执行到这里时,就会去争夺 this对象锁
            //3. 哪个线程争夺到(获取)this对象锁,就执行 synchronized 代码块, 执行完后,会释放this对象锁
            //4. 争夺不到this对象锁,就blocked ,准备继续争夺
            //5. this对象锁是非公平锁.
     
            synchronized (this) {//
                //判断余额是否够
                if (money < 1000) {
                    System.out.println("余额不足");
                    break;
                }
     
                money -= 1000;
                System.out.println(Thread.currentThread().getName() + " 取出了1000 当前余额=" + money);
            }
     
            //休眠1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

知识点

1 面对异常的快捷键处理方式

(1)使用Ctrl+Alt+t 选择try/catch来进行抑制异常;

(2)使用Alt+Enter来选择try/catch或者throw抛出异常来抑制。

参考资料

本文第九节 wait和notify 参考 明天不吃。 的CSDN博客的【多线程】wait()和notify() 篇章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值