JavaSE(多线程和线程池)知识点总结


JavaSE

多线程

1、什么是线程?

​ 线程就是操作系统能够进行运算调度的最小单位,它被包含在进程(进程是程序的基本执行实体,任务管理器种选择的进程就可以看到每个软件的进程)中,是进程中实际运作的单位。

​ 比如:360杀毒软件,包括木马查杀、电脑清理、系统修复功能。其中360杀毒软件就是进程,其中的功能就是线程。

​ 简单理解:应用软件种相互独立,可以同时运行的功能就是线程。

2、多线程的应用场景?

​ 拷贝迁移大文件、加载大量的资源文件

3、并发和并行

​ 并发:在同一时刻,有多个指令在单个cpu上交替执行

​ 并行:在同一时刻,有多个指令在多个cpu上同时执行

4、多线程的实现方式

4.1 继承Thread类的方式进行实现
public class MyThread extends Thread{

    /**
     * 重写run方法
     */
    @Override
    public void run() {
        //书写线程要执行的代码
        for (int count = 0; count < 100; count++) {
            System.out.println(getName() + "hello");
        }
    }
}


public class ThreadDemo {

    public static void main(String[] args) {

        /*
         * 多线程的第一种实现方式:
         *   1、自定义一个类继承Thread类
         *   2、重写run()方法
         *   3、创建子类对象,调用start()方法启动线程
         *
         * */

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("线程1:");
        t2.setName("线程2:");

        t1.start();
        t2.start();

    }
}


运行结果:
	线程2:hello
    线程2:hello
    线程1:hello
    线程2:hello
    线程1:hello
   	...

根据运行结果可以发现,线程1和线程2在同时执行.

4.2 实现Runable接口方式进行实现
public class MyRunable implements Runnable{

    /**
     * 重写Run方法
     */
    @Override
    public void run() {
        //书写线程要执行的代码
        for (int count = 0; count < 100; count++) {
            //Thread.currentThread():获取当前线程对象
            //.getName():获取当前线程对象的名字
            System.out.println(Thread.currentThread().getName() + "hello");
        }
    }
}



public class MyThread2 {

    public static void main(String[] args) {
        /*
         * 多线程的第二种实现方式:
         *   1、自定义一个类实现Runable接口
         *   2、重写run()方法
         *   3、创建自己的类的对象(任务对象)
         *   4、创建一个Thread类的对象,将自己的类放入进去,调用start()方法启动线程
         * */

        //创建自己的类的对象(任务对象)
        MyRunable mr = new MyRunable();

        //创建Thread类的对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        //给线程设置名字
        t1.setName("线程1:");
        t2.setName("线程2:");

        //启动线程
        t1.start();
        t2.start();
    }
}



运行结果:
	线程2:hello
    线程2:hello
    线程1:hello
    线程2:hello
    线程1:hello
   	...

根据运行结果可以发现,线程1和线程2在同时执行.
4.3 使用Callable接口和Future接口方式进行实现
public class MyCallable implements Callable<Integer> { //Callable<Integer> 其中Integer:返回值的类型

    /**
     * 重写call方法
     * @return
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        //书写线程要执行的代码

        //求1~100之间的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName());
            sum = sum + i;
        }
        return sum;
    }
}


public class MyThread3 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
         * 多线程的第三种实现方式:
         *   特点:可以获取到多线程运行的结果
         *
         *   1、自定义一个类实现Callable接口
         *   2、重写call()方法。(这个方法是有返回值的,返回值表示多线程运行的结果)
         *   3、创建自己的类的对象(任务对象)
         *   4、创建一个FutureTask类的对象。(作用:管理多线程的运行结果)
         *   5、创建一个Thread类的对象(表示线程),调用start()方法启动线程
         * */

        //创建自己的类的对象(任务对象)
        MyCallable mc = new MyCallable();

        //创建一个FutureTask类的对象。(作用:管理多线程的运行结果)
        FutureTask<Integer> ft = new FutureTask<>(mc);

        //创建一个Thread类的对象(表示线程)
        Thread t1 = new Thread(ft);
        Thread t2 = new Thread(ft);

        //设置名字
        t1.setName("线程1");
        t2.setName("线程2");

        //启动线程
        t1.start();
        t2.start();

        //获取多线程的运行结果
        Integer result = ft.get();
        System.out.println("运行结果:" + result);

    }
}


运行结果:
    线程1
    线程1
    线程1
    线程1
    线程1
    线程1
    ...
    线程1
    运行结果:5050
    
    
   为什么线程1和线程2没有交替执行?
    	因为FutureTask只会执行一次Callable的call方法,并将结果缓存起来供后续调用。因为对于返回值总是不变的,我们总是要考虑把他缓存一下,下一次的时候,直接把值返回出去,就不用再进行计算了,大大的提高了效率。

4.4 多线程三种实现方式对比

在这里插入图片描述

5、多线程常见的成员方法

在这里插入图片描述

代码实现1(线程默认名字):
public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":执行了");
        }
    }
}

public class ThreadDemo {

    public static void main(String[] args) {

        //创建自己的类的对象(任务对象)
        MyRunnable mr = new MyRunnable();

        //创建Thread类的对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        //启动线程
        t1.start();
        t2.start();
    }
}

运行结果:
    Thread-0:执行了
    Thread-1:执行了
    Thread-0:执行了
    Thread-1:执行了
    Thread-1:执行了
    ...
    可以如果没有个线程起名字,则默认的名字为 Thread-线程的编号
代码实现2(main线程):
public static void main(String[] args) {
		
    	//获取当前线程名
        String name = Thread.currentThread().getName();
        System.out.println(name);

    }

运行结果:
    main
    可以发现我们所有的代码都是执行在main线程里面的。
代码实现3(设置线程名和获取线程名,线程睡眠):
public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
        	//获取当前线程对象并获取线程名称
            System.out.println(Thread.currentThread().getName() + ":执行了");
        }
    }
}

public class ThreadDemo {

    public static void main(String[] args) {

        //创建自己的类的对象(任务对象)
        MyRunnable mr = new MyRunnable();

        //创建Thread类的对象,并使用构造方法设置线程的名字
        Thread t1 = new Thread(mr,"线程1");
        Thread t2 = new Thread(mr,"线程2");
        
        //也可以使用setName给线程设置名字
        //t1.setName("线程1");
        //t2.setName("线程2");

        //启动线程
        t1.start();
        t2.start();
    }
}

运行结果:
    线程1:执行了
    线程2:执行了
    线程1:执行了
    线程2:执行了
    线程1:执行了
    线程2:执行了
    ...
    可以发现控制台先输出
    	线程1:执行了
    	线程2:执行了  
    两秒钟之后再输出
    	线程1:执行了
    	线程2:执行了
    说明睡了两秒。
代码实现4(设置线程优先级):
public class MyRunnable2 implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + "----------->" + i);
        }
    }
}

public class ThreadDemo2 {

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

        //创建自己的类的对象(任务对象)
        MyRunnable2 mr = new MyRunnable2();

        //创建Thread类的对象,并使用构造方法设置线程的名字
        Thread t1 = new Thread(mr, "线程1");
        Thread t2 = new Thread(mr, "线程2");

        //获取线程的优先级,发现默认线程的优先级为5
        int priority = t1.getPriority();
        int priority2 = t2.getPriority();
        System.out.println("默认线程1的优先级:" + priority + ",默认线程2的优先级:" + priority2);

        //给线程1设置优先级为1,线程2设置优先级为10
        t1.setPriority(1);
        t2.setPriority(10);

        //启动线程
        t1.start();
        t2.start();
        
        //睡3秒等线程都执行完之后再打印这句话
        Thread.sleep(3000);
        System.out.println("此时线程1的优先级为:" + t1.getPriority() +",线程2的优先级为:" + t2.getPriority());

    }
}

运行结果:
	默认线程1的优先级:5,默认线程2的优先级:5
    线程1----------->1
    线程2----------->1
    线程1----------->2
    线程2----------->2
    线程2----------->3
    线程2----------->4
    线程2----------->5
    线程2----------->6
    线程2----------->7
    线程2----------->8
    线程2----------->9
    线程2----------->10
    线程1----------->3
    线程1----------->4
    线程1----------->5
    线程1----------->6
    线程1----------->7
    线程1----------->8
    线程1----------->9
    线程1----------->10
    此时线程1的优先级为:1,线程2的优先级为:10
    
    从运行结果可以发现因为线程2的优先级比线程1的优先级高,所以线程2抢占cpu的概率比较高,所以线程2先执行完for循环。
    但是,虽然线程2比线程1的优先级高,并不一定代码每次执行都是线程2先执行完,也有线程1先执行完的情况。
代码实现5(设置守护线程):

守护线程特点:当其他非守护线程执行完毕后,守护线程会陆续结束。

public class MyRunnableDaemon1 implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + "----------->" + i);
        }
    }
}

public class MyRunnableDaemon2 implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i <= 99; i++) {
            System.out.println(Thread.currentThread().getName() + "----------->" + i);
        }
    }
}

public class ThreadDemo3 {

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

        //创建自己的类的对象(任务对象)
        MyRunnableDaemon1 mr = new MyRunnableDaemon1();

        //创建Thread类的对象,并使用构造方法设置线程的名字
        Thread t1 = new Thread(mr, "线程1");
        Thread t2 = new Thread(mr, "线程2");

        //设置线程2为守护线程
        t2.setDaemon(true);

        //启动线程
        t1.start();
        t2.start();
    }
}


执行结果:
    线程1----------->1
    线程2----------->1
    线程1----------->2
    线程1----------->3
    线程1----------->4
    线程1----------->5
    线程1----------->6
    线程1----------->7
    线程1----------->8
    线程1----------->9
    线程1----------->10
    线程2----------->2
    线程2----------->3
    线程2----------->4
    线程2----------->5
    线程2----------->6
    线程2----------->7
    线程2----------->8
    线程2----------->9
    线程2----------->10
    线程2----------->11
    线程2----------->12
    我们发现,原本来说线程2要循环100次,线程1执行10次,肯定比线程1要晚一点执行结束,但是将线程2设置为守护线程后发现,当线程1执行完成后,线程2(守护线程)也会陆续执行完毕。
代码实现6(礼让线程):
public class MyRunnable4 implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + "----------->" + i);

            //表示出让cpu的执行权
            Thread.yield();
        }
    }
}

public class ThreadDemo4 {

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

        //创建自己的类的对象(任务对象)
        MyRunnable4 mr = new MyRunnable4();

        //创建Thread类的对象,并使用构造方法设置线程的名字
        Thread t1 = new Thread(mr, "线程1");
        Thread t2 = new Thread(mr, "线程2");

        //启动线程
        t1.start();
        t2.start();
    }
}

执行结果:
    线程1----------->1
    线程2----------->1
    线程1----------->2
    线程2----------->2
    线程1----------->3
    线程2----------->3
    线程1----------->4
    线程1----------->5
    线程2----------->4
    线程2----------->5
    发现设置了礼让机制后,线程1和线程2尽可能的轮换抢夺cpu的执行权。尽可能让结果均匀一点。	
代码实现7(插入线程):
public class MyRunnable5 implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + "----------->" + i);
        }
    }
}

public class ThreadDemo5 {

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

        //创建自己的类的对象(任务对象)
        MyRunnable5 mr = new MyRunnable5();

        //创建Thread类的对象,并使用构造方法设置线程的名字
        Thread t1 = new Thread(mr, "线程1");

        //启动线程
        t1.start();

        //设置t1线程为插入线程
        //表示把t1这个线程,插入到当前线程之前
        //t1:线程1
        //当前线程:main线程
        t1.join();

        //这段循环是执行在main线程中的
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-----" + i);
        }
    }
}

执行结果:
    ...
    线程1----------->98
    线程1----------->99
    线程1----------->100
    main-----0
    main-----1
    main-----2
    main-----3
    main-----4
    main-----5
    main-----6
    main-----7
    main-----8
    main-----9
    因为把线程1设置为了插入线程,所以当线程1执行完后,main线程才会执行。

6、线程的生命周期

在这里插入图片描述

7、线程的安全问题

7.1 同步代码块

使用影院买票的案例来展示一下线程的安全问题:

public class MyThread extends Thread{

    //票号,使用了static关键字后,表示这个类所有的对象,都共享ticket数据。
    static int ticket = 0; // 0 ~ 99

    @Override
    public void run() {
        while (true){
            if (ticket < 100){
                try {
                    //睡1毫秒
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                ticket++;
                System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
            }else {
                break;
            }
        }
    }
}

public class ThreadDemo {

    public static void main(String[] args) {
        /*
            需求:
                某电影院正在上映国产大片,共100张票,而有3个窗口买票,请设计一个程序模拟该电影院卖票。
        */

        //创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

执行结果:
	...
	窗口3正在卖弟97张票!
    窗口1正在卖弟99张票!
    窗口3正在卖弟100张票!
    窗口2正在卖弟100张票!
    窗口1正在卖弟101张票!
    窗口3正在卖弟101张票!
    窗口2正在卖弟102张票!

	我们发现:
		1、同一张票卖了多次
		2、只有100张票,但是已经执行到102张票了
		所以线程引发的安全问题为:
			1、相同的票出现了多次
			2、出现了超出范围的票

引发上述问题的原因:

​ 因为当我们线程1(窗口1)线程抢到cpu的执行权后,会睡10毫秒,所以线程2(窗口2)会抢到cpu的执行权,也会睡10毫秒,线程3(窗口3)同理。当线程1醒了之后,在抢到cpu的执行权。所以会去将ticket加1,还没来得及打印的时候,cpu的执行权被线程2抢走了,线程2也会将ticket加1,线程3同理,所以就会出现了相同的票出现了多次的情况了。产生这一切的原因是:线程执行时,有随机性。

​ 出现了超出范围的票其实也跟上面一样,也是因为线程执行时,有随机性。

解决办法:

​ 当线程进去循环后,把操作共享数据的代码锁起来。

​ 格式:

​ synchronized(锁对象){

​ 操作共享数据的代码

​ }

​ 注意:锁对象一定要是唯一的,一般来用当前类的class文件。即:当前类.class

​ 特点1:锁默认打开,当有一个线程进去了,锁会自动关闭。

​ 特点2:里面的代码全部执行完毕后,线程出来,锁会自动打开。

代码实现:

public class MyThread extends Thread{

    //票号,使用了static关键字后,表示这个类所有的对象,都共享ticket数据。
    static int ticket = 0; // 0 ~ 99

    //锁对象,锁对象可以是任意的一个对象,但是一定要是唯一的,所以我们加了static关键字
    static Object object = new Object();

    @Override
    public void run() {
        while (true){
            //同步代码块
            synchronized (object){
                if (ticket < 100){
                    try {
                        //睡1毫秒
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    ticket++;
                    System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
                }else {
                    break;
                }
            }
        }
    }
}

public class MyThread extends Thread{

    //票号,使用了static关键字后,表示这个类所有的对象,都共享ticket数据。
    static int ticket = 0; // 0 ~ 99

    //锁对象,锁对象可以是任意的一个对象,但是一定要是唯一的,所以我们加了static关键字
    //static Object object = new Object();

    @Override
    public void run() {
        while (true){
            //同步代码块
            //synchronized (object){  可以这样写,只要确保object是唯一的就行。
            
            //使用这种锁对象,一般把当前类的字节码文件当作锁对象。因为在同一个文件夹中只有一个这个类(MyThread)的class文件,所以这个对象(MyThread)是				唯一的,所以就把它当作锁对象。
            synchronized (MyThread.class){
                if (ticket < 100){
                    try {
                        //睡1毫秒
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    ticket++;
                    System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
                }else {
                    break;
                }
            }
        }
    }
}

执行结果:
	...
	窗口3正在卖弟94张票!
    窗口3正在卖弟95张票!
    窗口3正在卖弟96张票!
    窗口3正在卖弟97张票!
    窗口3正在卖弟98张票!
    窗口3正在卖弟99张票!
    窗口3正在卖弟100张票!	
    我们发现,加了同步代码块之后,重复票没有了,超出范围的票也没有了。
7.2 同步方法

同步方法也就是将同步代码块中的内容放到了一个方法里面,如何写同步方法呢?

特点:

​ 1、同步方法锁住的是方法里面的所有代码。

​ 2、锁对象不能自己指定。(当前的对象如果是非静态的,则锁对象是this;如果当前的对象是静态的,则锁对象是当前类的字节码文件对象。)

格式: 修饰符 synchronized 返回值 方法名(方法参数){…}

技巧:

​ 如果不知道哪部分代码需要放到同步方法中,可以先写同步代码块,之后按快捷键 Alt+Shift+M即可将选中的内容自动生成在一个方法,再将方法返回值前面加上synchronized关键字即可变成同步方法。

public class MyRunnable implements Runnable{

    //票号  为什么不加static?因为我们MyRunnable对象只需要在实现类中创建一次就行,所以他是唯一的。
    int ticket = 0;  // 0 ~ 99

    @Override
    public void run() {
        //1.循环
        while (true){
            if (extracted()) {
                break;
            }
        }
    }
    
    //2.同步代码块(同步方法)
    //因为这个方法是非静态的,所以当前的锁对象是this,而this指的是MyRunnable对象,而MyRunnable对象是唯一的,所以锁对象也是唯一的。
    private synchronized boolean extracted() {
        //3.判断共享数据是否到了末尾,如果到了末尾
        if (ticket == 100){
            return true;
        }else {//4.判断共享数据是否到了末尾,如果没有到末尾
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
        }
        return false;
    }
}


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

        /*
            需求:
                某电影院正在上映国产大片,共100张票,而有3个窗口买票,请设计一个程序模拟该电影院卖票。
        */

        //创建我们自定义的任务对象
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.start();
        t2.start();
        t3.start();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
    }
}

运行结果:
    ...
    窗口1正在卖弟97张票!
    窗口1正在卖弟98张票!
    窗口2正在卖弟99张票!
    窗口3正在卖弟100张票!
7.3 StringBuffer和StringBuilder

​ 通过源码可以发现,在StringBuffer的所有成员方法上都有synchronized关键字,而StringBuilder都没有,所以StringBuffer是线程安全的,因为它里面所有的方法都是同步的。

​ 如何选择?

​ 如果你的代码是单线程的,不需要考虑数据的安全情况,则使用StringBuilder;

​ 如果你的代码是多线程的,需要考虑数据的安全情况,则使用StringBuffer。
在这里插入图片描述

7.4 Lock锁

在这里插入图片描述

public class MyRunnable implements Runnable{

    //票号  为什么不加static?因为我们MyRunnable对象只需要在实现类中创建一次就行,所以他是唯一的。
    int ticket = 0;  // 0 ~ 99

    //Lock锁对象是一个接口,所以要new它的实现类ReentrantLock  为什么不加static?因为我们MyRunnable对象只需要在实现类中创建一次就行,所以他是唯一的。
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        //1.循环
        while (true){
            //2.手动上锁
            lock.lock();
            try {
                //3.判断共享数据是否到了末尾,如果到了末尾
                if (ticket == 100){
                    break;
                }else {//4.判断共享数据是否到了末尾,如果没有到末尾
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(Thread.currentThread().getName() + "正在卖弟" + ticket + "张票!");
                }
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                //4.手动释放锁
                lock.unlock();
            }
        }
    }
}

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

        /*
            需求:
                某电影院正在上映国产大片,共100张票,而有3个窗口买票,请设计一个程序模拟该电影院卖票。
                使用JDK5的Lock锁实现手动上锁手动释放锁。
        */

        //创建我们自定义的任务对象
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.start();
        t2.start();
        t3.start();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
    }
}

运行结果:
    ...
    窗口1正在卖弟97张票!
    窗口1正在卖弟98张票!
    窗口2正在卖弟99张票!
    窗口3正在卖弟100张票!
7.5 死锁

死锁是一种错误,如何避免?写代码的时候不要让两个锁嵌套起来,否则会形成死锁。

8、等待唤醒机制

8.1 使用锁对象实现等待唤醒机制

在这里插入图片描述

生产者和消费者常见方法:
在这里插入图片描述

使用锁实现等待唤醒机制:

package MoreThread.waitAndNotify;

/**
 * @ClassName: 桌子
 * @Description:
 * @author: ZhangJian
 * @Create: 2024-08-06 14:47
 **/
public class Desk {

    /**
     * 作用:控制生产者和消费者的执行
     */


    //桌子上是否有面条  0:没有面条  1:有面条
    public static int foodFlag = 0;

    //总个数
    public static int count = 10;

    //锁对象
    public static Object lock = new Object();

}

package MoreThread.waitAndNotify;

/**
 * @ClassName: 生产者(厨师)
 * @Description:
 * @author: ZhangJian
 * @Create: 2024-08-06 14:48
 **/
public class Cook extends Thread{

    @Override
    public void run() {

        /**
         * 1、循环
         * 2、同步代码块
         * 3、判断共享数据是否到了末尾(到了末尾)
         * 4、判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
         */

        while (true){
            synchronized (Desk.lock){
                if (Desk.count == 0){
                    break;
                }else {
                    //判断桌子上是否有面条
                    if (Desk.foodFlag == 1){
                        //如果有面条,就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else {
                        //如果没有面条,就继续制作面条
                        System.out.println("厨师做了一碗面条");
                        //修改桌子上面条的状态
                        Desk.foodFlag = 1;
                        //叫醒等待的消费者开吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }

    }
}

package MoreThread.waitAndNotify;

import sun.security.krb5.internal.crypto.Des;

/**
 * @ClassName: 消费者(吃货)
 * @Description:
 * @author: ZhangJian
 * @Create: 2024-08-06 14:48
 **/
public class Foodie extends Thread{

    @Override
    public void run() {

        /**
         * 1、循环
         * 2、同步代码块
         * 3、判断共享数据是否到了末尾(到了末尾)
         * 4、判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
         */

        while (true){
            synchronized (Desk.lock){
                if (Desk.count == 0){
                    break;
                }else {
                    //先判断桌子上是否有面条
                    if (Desk.foodFlag == 0){
                        //没有面条,就等待
                        try {
                            //让当前线程跟锁对象进行绑定(让跟锁对象有关的线程进行等待)
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else {
                        //桌子上有面条

                        //面条总数-1
                        Desk.count--;
                        //如果有,就开吃
                        System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll(); //唤醒跟锁对象相关的线程的线程
                        //修改桌子是否有面条的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }

    }
}

package MoreThread.waitAndNotify;

/**
 * @ClassName:
 * @Description:
 * @author: ZhangJian
 * @Create: 2024-08-06 14:27
 **/
public class ThreadDemo {

    private static Cook c;

    public static void main(String[] args) {

        /**
         * 需求:
         *      完成生产者和消费者(等待唤醒机制)的代码。
         *      实现现成轮流交替执行的结果。
         */

        //创建线程对象
        c = new Cook();
        Foodie f = new Foodie();

        //给线程设置名字
        c.setName("厨师");
        f.setName("吃货");

        //开启线程
        c.start();
        f.start();

    }
}

运行结果:
厨师做了一碗面条
吃货在吃面条,还能再吃9碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃8碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃7碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃6碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃5碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃4碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃3碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃2碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃1碗!!!
厨师做了一碗面条
吃货在吃面条,还能再吃0碗!!!
8.2使用阻塞队列实现等待唤醒机制

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

使用阻塞队列实现等待唤醒机制:

package MoreThread.watiAndNotify2;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @ClassName: 生产者(厨师)
 * @Description:
 * @author: ZhangJian
 * @Create: 2024-08-06 15:22
 **/
public class Cook extends Thread{

    //创建阻塞队列的对象
    ArrayBlockingQueue<String> queue;

    //重写Cook的有参构造
    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){

            try {
                //不断把面条放入阻塞队列中
                //为什么不用加锁?
                //因为put方法底层已经加锁了,如果我们再加锁,会变成锁的嵌套,变成死锁。
                //给阻塞队列添加数据
                queue.put("面条");
                System.out.println("厨师放了一碗面条。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package MoreThread.watiAndNotify2;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @ClassName: 消费者(吃货)
 * @Description:
 * @author: ZhangJian
 * @Create: 2024-08-06 15:23
 **/
public class Foodie extends Thread{

    //创建阻塞队列的对象
    ArrayBlockingQueue<String> queue;

    //重写Foodie的有参构造
    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            try {
                //不断从阻塞队列中获取面条
                //为什么不用加锁?
                //因为take方法底层已经加锁了,如果我们再加锁,会变成锁的嵌套,变成死锁。
                //取出阻塞队列中的数据
                String food = queue.take();
                System.out.println("吃货从阻塞队列中获取到了一碗" + food);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package MoreThread.watiAndNotify2;

import MoreThread.watiAndNotify2.Cook;

import java.util.concurrent.ArrayBlockingQueue;

/**
 * @ClassName:
 * @Description:
 * @author: ZhangJian
 * @Create: 2024-08-06 15:21
 **/
public class ThreadDemo {

    public static void main(String[] args) {

        /**
         * 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码。
         * 细节:
         *      生产者和消费者必须使用同一个阻塞队列
         */

        //1、创建阻塞队列的对象并指定上限
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1);

        //2、创建线程对象,并把阻塞队列传递过去
        Cook c = new Cook(queue);
        Foodie f = new Foodie(queue);

        //3、开启线程
        c.start();
        f.start();
    }
}

运行结果:
厨师放了一碗面条。
厨师放了一碗面条。
吃货从阻塞队列中获取到了一碗面条
吃货从阻塞队列中获取到了一碗面条
厨师放了一碗面条。
厨师放了一碗面条。
吃货从阻塞队列中获取到了一碗面条
吃货从阻塞队列中获取到了一碗面条
...

为什么打印出来的来过不是顺序执行的?因为的put方法和take都是在底层加的锁,我们打印的话并没有在它们锁对象里面,
所以打印出来的结果不是顺序的,但是存的数据肯定是先往阻塞队列中存,然后从阻塞队列中取,取出来之后,才会再次从
阻塞队列中存,再取...  按照这样的顺序执行的。

9、线程的状态

方便理解的线程的状态:

在这里插入图片描述

Java定义的线程的6大状态:

在这里插入图片描述

多线程综合练习

综合练习1

一共有1000张电影票,可以在两个窗口处领取,假设每次领取的时间为3000毫秒。
要求:请用多线程模拟卖票过程并打印剩余电影票的数量

public class MyThread extends Thread{

    //电影票数量
    private static int ticketNum = 10;

    @Override
    public void run() {
        while (true){
            synchronized (MyThread.class){
                if (ticketNum == 0){
                    break;
                }else {
                    try {
                        //当前线程睡3000毫秒
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //电影票-1
                    ticketNum--;
                    System.out.println(Thread.currentThread().getName() + "售出一张电影票,还剩" + ticketNum + "张!");
                }
            }
        }

    }
}

public class ThreadDemo {

    /**
     * 一共有1000张电影票,可以在两个窗口处领取,假设每次领取的时间为3000毫秒。
     * 要求:请用多线程模拟卖票过程并打印剩余电影票的数量
     */

    public static void main(String[] args) {

        //创建线程
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        //设置线程名字
        t1.setName("窗口1");
        t2.setName("窗口2");

        //开启线程
        t1.start();
        t2.start();
    }
}

运行结果:
...
窗口1售出一张电影票,还剩4张!
窗口1售出一张电影票,还剩3张!
窗口2售出一张电影票,还剩2张!
窗口1售出一张电影票,还剩1张!
窗口2售出一张电影票,还剩0张!
综合练习2

有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不在送出。
利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来。

public class MyRunnable implements Runnable {

    //第二种方式实现多线程,测试类中MyRunable只创建一次,所以不需要加static
    int count = 100;

    @Override
    public void run() {
        while (true){
            synchronized (MyRunnable.class){
                if (count < 10){
                    System.out.println( "礼物还剩下" + count + "个,不在赠送。");
                    break;
                }else {
                    count--;
                    System.out.println(Thread.currentThread().getName() + "在赠送礼物,还剩下" + count + "个。");
                }
            }
        }
    }
}

public class ThreadDemo {

    /**
     * 有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不在送出。
     * 利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来。
     */

    public static void main(String[] args) {
        //创建任务对象
        MyRunnable mr = new MyRunnable();

        //创建线程对象
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        t1.setName("张三");
        t2.setName("李四");

        t1.start();
        t2.start();
    }
}

运行结果:
...
张三在赠送礼物,还剩下13个。
李四在赠送礼物,还剩下12个。
张三在赠送礼物,还剩下11个。
李四在赠送礼物,还剩下10个。
李四在赠送礼物,还剩下9个。
礼物还剩下9个,不在赠送。
礼物还剩下9个,不在赠送。
综合联系3

同时开启两个线程,共同获获取1-100之间的所有数字。
要求:将输出所有的奇数。

public class MyThread extends Thread{

    //因为需要共用number,所以需要加上static
    private static int number = 1;

    @Override
    public void run() {
        //循环
        while(true){
            //同步代码块
            synchronized (MyThread.class){
                //判断共享数据,到末尾
                if (number > 100){
                    break;
                }else {
                    //判断共享数据,没有到末尾
                    if (number % 2 == 1){ //判断奇数
                        System.out.println(Thread.currentThread().getName() + "打印奇数:" + number);
                    }
                    number++;
                }
            }
        }
    }
}

public class ThreadDemo {

    /**
     * 同时开启两个线程,共同获获取1-100之间的所有数字。
     * 要求:将输出所有的奇数。
     */

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

运行结果:
...
线程1打印奇数:95
线程2打印奇数:97
线程2打印奇数:99

线程池

线程池的作用:创建线程将线程放入到线程池中(可以确定线程池存放线程的数量),当执行完这个线程任务时,不会销毁。会放入到线程池中进行复用。

一、使用Executors类创建线程池

线程池主要核心原理:

​ 1、创建一个池子,池子中是空的。

​ 2、提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子下次再提交任务时,不需要创建新的线程,直接复用已有的线程即可。

​ 3、但是如果提交任务时,池子中没有空闲的线程,也无法创建新的线程,任务就会排队等待。
在这里插入图片描述

代码实现:

​ 1、创建没有上限的线程池。

public class MyThreadPoolDemo {

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

        //创建一个没有上限的线程池
        ExecutorService pool1 = Executors.newCachedThreadPool();

        //提交任务
        pool1.submit(new MyRunnable());
        Thread.sleep(1);
        pool1.submit(new MyRunnable());
        Thread.sleep(1);
        pool1.submit(new MyRunnable());
        Thread.sleep(1);

      	//销毁线程(线程池一般不用销毁)
        pool.shutdown();
    }
}


public class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "----");
    }
}


运行结果:
pool-1-thread-1----
pool-1-thread-1----
pool-1-thread-1----

发现公用的是一个线程(线程1),睡一秒是的目的是让线程执行完放回线程池中.
	

2、创建有上限的线程池(最大的线程数为3)。

public class MyThreadPoolDemo {

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

        //创建一个有上限的线程池(最大上限为3)
        ExecutorService pool2 = Executors.newFixedThreadPool(3);
       
        //提交任务
        pool2.submit(new MyRunnable());
       	pool2.submit(new MyRunnable());
        pool2.submit(new MyRunnable());
        pool2.submit(new MyRunnable());
        pool2.submit(new MyRunnable());

        //销毁线程
        pool.shutdown();
    }
}

public class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "----");
    }
}

运行结果:
	pool-2-thread-2----
    pool-2-thread-1----
    pool-2-thread-2----
    pool-2-thread-3----
    发现只有3个线程,当线程执行任务时,因为最多只有3个线程,所以会进入等待状态,当线程执行完时,才会去执行下一个.

二、自定义创建线程池

在这里插入图片描述

使用ThreadPoolExecutor类创建自定义线程的7个参数解释:

​ 核心元素一:正式员工数量 -------------------------------> 核心线程数量(不能小于0)

​ 核心元素二:餐厅最大员工数量 -------------------------------> 线程池中最大线程数量(最大数量>=核心线程数量)

​ 核心元素三:临时员工空闲多长时间被辞退 (值) -------------------------------> 空闲时间(值)(不能小于0)

​ 核心元素四:临时员工空闲多长时间被辞退 (单位) -------------------------------> 空闲时间(单位) (使用TimeUtil类中指定)

​ 核心元素五:排队的客户 -------------------------------> 阻塞队列(不能为null)

​ 核心元素六:从哪里招人 -------------------------------> 创建线程的方式(不能为null)

​ 核心元素七:当排队人数过多时,超出的顾客请下次在来(拒绝服务) -------------------------------> 要执行的任务过多时的解决方案(不能为null)
在这里插入图片描述

代码实现:

/**
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            核心线程数量,
            最大线程数量,
            空闲线程最大存活时间,
            空闲线程最大存活时间单位,
            任务队列,
            创建线程工厂,
            任务的拒绝策略
    );
**/

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

    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            3, //核心线程数量
            6, //最大线程数量
            60, //空闲线程存活时间
            TimeUnit.SECONDS, //空闲线程存活时间单位
            new ArrayBlockingQueue<>(3), //任务队列
            Executors.defaultThreadFactory(), //创建线程工厂
            new ThreadPoolExecutor.AbortPolicy() //任务的拒绝策略
    );

}

三、最大并行数设置多大合适?

最大并行数和自己的电脑有关系,可以通过代码来计算出java可以使用的并行数有几个。比如4核8线程。

public class GetAvaiableProcessors {

    //获取java虚拟机可以用的处理器的数量
    public static void main(String[] args) {
        int count = Runtime.getRuntime().availableProcessors();
        System.out.println(count);
    }
}

运行结果:8

四、线程池设置多大合适呢?

设置多大合适需要分两种情况来考虑:

​ CPU密集型(当该项目进行运算比较多,读取本地文件比较少,使用这种):最大并行数 + 1

​ I/O密集型(当项目读取本地文件比较多,或者操作数据库时,使用这种):

最大并行数 * 期望CPU利用率 * (总时间(CPU计算时间+等待时间)/CPU计算时间) CPU计算时间+等待时间可以通过工具 thread dump工具测试出来。

​ 例子:从本地文件中读取数据,读取两个数据,并进行相加。电脑的CPU是4核8线程的。

​ 操作1:读取两个数据(1秒钟)

​ 操作2:进行相加(1秒中)

​ 答: 8 * 100% * ( (2/1) ) = 16秒

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值