2024年最新版零基础详细Java知识笔记【多线程】⑨

第九章 多线程


前言

2024年最新版零基础Java笔记,最全最详细笔记,适用于零基础小白、软件设计师、就业、提升等,笔记会持续更新,共同进步,希望可以帮助更多的小伙伴!

感谢杜老师的超级无敌详细讲解,感谢动力节点,真的受益匪浅!第三次看老杜Java了,每次都有不一样的收获!由于老杜最新版Java没有md笔记,因此鄙人把所学所写的更新于此,共同进步,希望对大家有帮助!

笔记根据老杜《动力节点》视频进行编写,视频地址:动力节点Java零基础视频(下)
笔记有写的不好的地方,恳请在评论区指正批评,谢谢!



9.1 线程概述

  1. 什么是进程?什么是线程?它们的区别?
    1. 进程是指操作系统中的一段程序,它是一个正在执行中的程序实例,具有独立的内存空间和系统资源,如文件、网络端口等。在计算机程序执行时,先创建进程,再在进程中进行程序的执行。一般来说,一个进程可以包含多个线程。
    2. 线程是指进程中的一个执行单元,是进程的一部分,它负责在进程中执行程序代码。每个线程都有自己的栈和程序计数器,并且可以共享进程的资源。多个线程可以在同一时刻执行不同的操作,从而提高了程序的执行效率。
    3. 现代的操作系统是支持多进程的,也就是可以启动多个软件,一个软件就是一个进程。称为:多进程并发。
    4. 通常一个进程都是可以启动多个线程的。称为:多线程并发。
  2. 多线程的作用?
    1. 提高处理效率。(多线程的优点之一是能够使 CPU 在处理一个任务时同时处理多个线程,这样可以充分利用 CPU 的资源,提高 CPU 的利用效率。)
  3. JVM规范中规定:
    1. 堆内存、方法区 是线程共享的。
    2. 虚拟机栈、本地方法栈、程序计数器 是每个线程私有的。
  4. 关于Java程序的运行原理
    1. “java HelloWorld”执行后,会启动JVM,JVM的启动表示一个进程启动了。
    2. JVM程会首先启动一个主线程(main-thread),主线程负责调用main方法。因此main方法是在主线程中运行的。
    3. 除了主线程之外,还启动了一个垃圾回收线程。因此启动JVM,至少启动了两个线程。
    4. 在main方法的执行过程中,程序员可以手动创建其他线程对象并启动。

在这里插入图片描述

9.2 并发与并行

  1. 并发(concurrency)
    1. 使用单核CPU的时候,同一时刻只能有一条指令执行,但多个指令被快速的轮换执行,使得在宏观上具有多个指令同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干端,使多个指令快速交替的执行。
      在这里插入图片描述

    2. 如上图所示,假设只有一个CPU资源,线程之间要竞争得到执行机会。图中的第一个阶段,在A执行的过程中,B、C不会执行,因为这段时间内这个CPU资源被A竞争到了,同理,第二阶段只有B在执行,第三阶段只有C在执行。其实,并发过程中,A、B、C并不是同时进行的(微观角度),但又是同时进行的(宏观角度)。

    3. 在同一个时间点上,一个CPU只能支持一个线程在执行。因为CPU运行的速度很快,CPU使用抢占式调度模式在多个线程间进行着高速的切换,因此我们看起来的感觉就像是多线程一样,也就是看上去就是在同一时刻运行。

  2. 并行(parallellism)
    1. 使用多核CPU的时候,同一时刻,有多条指令在多个CPU上同时执行。
    2. 如图所示,在同一时刻,ABC都是同时执行(微观、宏观)。

在这里插入图片描述

并发编程与并行编程

  1. 在CPU比较繁忙(假设为单核CPU),如果开启了很多个线程,则只能为一个线程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。
  2. 在CPU资源比较充足的时候,一个进程内的多个线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。
  3. 至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所以,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。
  4. 总结:单核CPU上的多线程,只是由操作系统来完成多任务间对CPU的运行切换,并非真正意义上的并发。随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行,故而多线程技术得到广泛应用。
  5. 不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源,而我们使用多线程的目的就是为了提高CPU资源的利用率。

线程的调度模型

  1. 如果多个线程被分配到一个CPU内核中执行,则同一时刻只能允许有一个线程能获得CPU的执行权,那么进程中的多个线程就会抢夺CPU的执行权,这就是涉及到线程调度策略。
  2. 分时调度模型
    • 所有线程轮流使用CPU的执行权,并且平均的分配每个线程占用的CPU的时间。
  3. 抢占式调度模型
    • 让优先级高的线程以较大的概率优先获得CPU的执行权,如果线程的优先级相同,那么就会随机选择一个线程获得CPU的执行权,而Java采用的就是抢占式调用。

9.4 实现线程

  1. 第一种方式:继承Thread
    1. 编写一个类继承java.lang.Thread,重写run方法。
    2. 创建线程对象:Thread t = new MyThread();
    3. 启动线程:t.start();
    /**
     * 在Java语言中,实现线程,有两种方式,第一种方式:
     *      第一步:编写一个类继承 java.lang.Thread
     *      第二步:重写run方法
     *      第三步:new线程对象
     *      第四部:调用线程对象的start方法来启动线程。
     */
    public class ThreadTest02 {
        public static void main(String[] args) {
            // 创建线程对象
            //MyThread mt = new MyThread();
            Thread t = new MyThread();
    
            // 直接调用run方法,不会启动新的线程。
            // java中有一个语法规则:对于方法体当中的代码,必须遵循自上而下的顺序依次逐行执行。
            // run()方法不结束,main方法是无法继续执行的。
            //t.run();
    
            // 调用start()方法,启动线程
            // java中有一个语法规则:对于方法体当中的代码,必须遵循自上而下的顺序依次逐行执行。
            // start()方法不结束,main方法是无法继续执行的。
            // start()瞬间就会结束,原因这个方法的作用是:启动一个新的线程,只要新线程启动成功了,start()就结束了。
            t.start();
    
            // 这里编写的代码在main方法中,因此这里的代码属于在主线程中执行。
            for (int i = 0; i < 100; i++) {
                System.out.println("main--->" + i);
            }
        }
    }
    
    // 自定义一个线程类
    // java.lang.Thread本身就是一个线程。
    // MyThread继承Thread,因此MyThread本身也是一个线程。
    class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("MyThread--->" + i);
            }
        }
    }
    
  2. 第二种方式:实现Runnable接口
    1. 编写一个类实现Runnable接口,实现run方法。
    2. 创建线程对象:Thread t = new Thread(new MyRunnable());
    3. 启动线程:t.start();
    /**
     * 在Java语言中,实现线程,有两种方式,第二种方式:
     *      第一步:编写一个类实现 java.lang.Runnable接口(可运行的接口)
     *      第二步:实现接口中的run方法。
     *      第三步:new线程对象
     *      第四部:调用线程的start方法启动线程
     *
     * 总结:实现线程两种方式:
     *      第一种:编写类直接继承Thread
     *      第二种:编写类实现Runnable接口
     *
     *      推荐使用第二种,因为实现接口的同时,保留了类的继承。
     */
    public class ThreadTest03 {
        public static void main(String[] args) {
            // 创建Runnable对象
            //Runnable r = new MyRunnable();
            // 创建线程对象
            //Thread t = new Thread(r);
    
            // 创建线程对象
            Thread t = new Thread(new MyRunnable());
            // 启动线程
            t.start();
            // 主线程中执行的。
            for (int i = 0; i < 100; i++) {
                System.out.println("main----->" + i);
            }
        }
    }
    
    // 严格来说,这个不是一个线程类
    // 它是一个普通的类,只不过实现了一个Runnable接口。
    class MyRunnable implements Runnable {
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("t----->" + i);
            }
        }
    }
    
  • 优先选择第二种方式:因为实现接口的同时,保留了类的继承。

  • 第二种方式也可以使用匿名内部类。

  • t.start()和t.run()的本质区别?

    • 本质上没有区别,都是普通方法调用。只不过两个方法完成的任务不同。
    • t.run()是调用run方法。执行run方法中的业务代码。
    • t.start()是启动线程,只要线程启动了,start()方法就执行结束了。
  • 线程常用的三个方法:

    • 实例方法:获取线程对象的名字String getName();,修改线程的名字void setName(String name);
    • 静态方法:获取当前线程对象的引用static Thread currentThread();

9.5 线程生命周期

  1. 线程生命周期指的是:从线程对象新建,到最终线程死亡的整个过程。

  2. 线程生命周期包括七个重要阶段:

    1. 新建状态(NEW)
    2. 就绪状态(RUNNABLE
    3. 运行状态(RUNNABLE
    4. 超时等待状态(TIMED_WAITING)
    5. 等待状态(WAITING)
    6. 阻塞状态(BLOCKED)
    7. 死亡状态/终止状态(TERMINATED)
      在这里插入图片描述
  3. 线程生命周期包括五个重要阶段:

    1. 新建状态
    2. 就绪状态(可运行状态)
    3. 运行状态
    4. 阻塞状态
    5. 死亡状态

9.6 线程的休眠与终止

  • 关于线程的sleep方法:
  1. static void sleep(long millis)
    静态方法,没有返回值,参数是一个毫秒。1秒 = 1000毫秒
  2. 这个方法作用是:
    让当前线程进入休眠,也就是让当前线程放弃占有的CPU时间片,让其进入阻塞状态。
    意思:你别再占用CPU了,让给其他线程吧。
    阻塞多久呢?参数毫秒为准。在指定的时间范围内,当前线程没有权利抢夺CPU时间片了。
  3. 怎么理解“当前线程”呢?
    Thread.sleep(1000); 这个代码出现在哪个线程中,当前线程就是这个线程。
  4. run方法在方法重写的时候,不能在方法声明位置使用 throws 抛出异常。
  5. sleep方法可以模拟每隔固定的时间调用一次程序。
  • 怎么中断一个线程的睡眠interrupt()。(怎么解除线程因sleep导致的阻塞,让其开始抢夺CPU时间片。)
    public class ThreadTest {
        public static void main(String[] args) {
            // 创建线程对象并启动
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "===> begin");
                    try {
                        // 睡眠一年
                        Thread.sleep(1000 * 60 * 60 * 24 * 365);
                    } catch (InterruptedException e) {
                        // 打印异常信息
                        //e.printStackTrace();
                        System.out.println("知道了,这就起床!");
                    }
                    // 睡眠一年之后,起来干活了
                    System.out.println(Thread.currentThread().getName() + " do some!");
                }
            });
    
            // 启动线程
            t.start();
    
            // 主线程
            // 要求:5秒之后,睡眠的Thread-0线程起来干活
            try {
                Thread.sleep(5 * 1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // Thread-0起来干活了。
            // 这行代码的作用是终止 t 线程的睡眠。
            // interrupt方法是一个实例方法。
            // 以下代码含义:t线程别睡了。
            // 底层实现原理是利用了:异常处理机制。
            // 当调用这个方法的时候,如果t线程正在睡眠,必然会抛出:InterruptedException,然后捕捉异常,终止睡眠。
            t.interrupt();
        }
    }
    
  • 一个线程 t 一直在正常的运行,如何终止 t 线程的执行,stop()从java2开始就不建议使用了,因为这种方式是强行终止线程。容易导致数据丢失。没有保存的数据,在内存中的数据一定会因为此方式导致丢失。
  • 如何合理的,正常的方式终止一个线程的执行?
    • 一般我们在实际开发中会使用打标记的方式,来终止一个线程的执行。
    public class ThreadTest {
        public static void main(String[] args) {
            // 创建线程
            MyRunnable mr = new MyRunnable();
            Thread t = new Thread(mr);
            t.setName("t");
            // 启动线程
            t.start();
    
            // 5秒之后终止线程t的执行
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //终止线程t的执行。
            mr.run = false;
        }
    }
    
    class MyRunnable implements Runnable {
        /**
         * 是否继续执行的标记。
         * true表示:继续执行。
         * false表示:停止执行。
         */
        boolean run = true;
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                if(run){
                    System.out.println(Thread.currentThread().getName() + "==>" + i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }else{
                    return;
    }}}}
    

9.7 守护线程

  1. 在Java语言中,线程被分为两大类:
    • 第一类:用户线程(非守护线程)
    • 第二类:守护线程(后台线程)
  2. 在JVM当中,有一个隐藏的守护线程一直在守护者,它就是GC线程。
  3. 守护线程的特点:所有的用户线程结束之后,守护线程自动退出/结束。
  4. 如何将一个线程设置为守护线程?
    • t.setDaemon(true);
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.setName("t");

        // 在启动线程之前,设置线程为守护线程
        myThread.setDaemon(true);
        myThread.start();

        // 10s结束!
        // main线程中,main线程是一个用户线程。
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "===>" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
    }}}}

class MyThread extends Thread {
    @Override
    public void run() {
        int i = 0;
        while(true){
            System.out.println(Thread.currentThread().getName() + "===>" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
    }}}}

9.8 定时任务

  1. JDK中提供的定时任务:
    1. java.util.Timer 定时器
    2. java.util.TimerTask 定时任务
  2. 定时器 + 定时任务:可以帮我们在程序中完成:每间隔多久执行一次某段程序。
  3. Timer的构造方法:
    1. Timer()
    2. Timer(boolean isDaemon) isDaemon是true表示该定时器是一个守护线程。
public class ThreadTest {
    public static void main(String[] args) throws Exception{
        // 创建定时器对象(本质上就是一个线程)
        // 如果这个定时器执行的任务是一个后台任务,是一个守护任务,建议将其定义为守护线程。
        Timer timer = new Timer(true);

        // 指定定时任务
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2024-01-27 10:22:00");
        //timer.schedule(new LogTimerTask(), firstTime, 1000);

        // 匿名内部类的方式
        timer.schedule(new TimerTask() {
            int count = 0;
            @Override
            public void run() {
                // 执行任务
                Date now = new Date();
                String strTime = sdf.format(now);
                System.out.println(strTime + ": " + count++);
            }
        }, firstTime, 1000 * 5);

        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
        }
    }
}

定时任务类:专门记录日期的定时任务类。

public class LogTimerTask extends TimerTask {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
    int count = 0;

    @Override
    public void run() {
        // 执行任务
        Date now = new Date();
        String strTime = sdf.format(now);
        System.out.println(strTime + ": " + count++);
    }
}

9.9 线程的调度

9.9.1 线程的合并

  1. 调用join()方法完成线程合并
  2. join()方法是一个实例方法。(不是静态方法)t.join
  3. 假设在main方法(main线程)中调用了 t.join(),后果是什么?
    • t线程合并到主线程中。主线程进入阻塞状态。直到 t 线程执行结束。主线程阻塞解除。
  4. t.join()方法其实是让当前线程进入阻塞状态,直到t线程结束,当前线程阻塞解除。
  5. 和sleep方法有点类似,但不一样:
    1. 第一:sleep方法是静态方法,join是实例方法。
    2. 第二:sleep方法可以指定睡眠的时长,join方法不能保证阻塞的时长。
    3. 第三:sleep和join方法都是让当前线程进入阻塞状态。
    4. 第四:sleep方法的阻塞解除条件?时间过去了。 join方法的阻塞解除条件?调用join方法的那个线程结束了。
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.setName("t");
        t.start();
        System.out.println("main begin");
        // 合并线程
        // t合并到main线程中。
        // main线程受到阻塞(当前线程受到阻塞)
        // t线程继续执行,直到t线程结束。main线程阻塞解除(当前线程阻塞解除)。
        //t.join();

        // join方法也可以有参数,参数是毫秒。
        // 以下代码表示 t 线程合并到 当前线程,合并时长 10 毫秒
        // 阻塞当前线程 10 毫秒
        //t.join(10);

        // 调用这个方法,是想让当前线程受阻10秒
        // 但不一定,如果在指定的阻塞时间内,t线程结束了。当前线程阻塞也会解除。
        t.join(1000 * 10);

        // 当前线程休眠10秒。
        //Thread.sleep(1000 * 10);

        // 主线程
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "==>" + i);
        }

        System.out.println("main over");
    }
} 
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "==>" + i);
}}}

9.9.2 线程的优先级

关于线程生命周期中的JVM调度:

  1. 优先级
  2. 线程是可以设置优先级的,优先级较高的,获得CPU时间片的总体概率高一些。
  3. JVM采用的是抢占式调度模型。谁的优先级高,获取CPU时间片的总体概率就高。
  4. 默认情况下,一个线程的优先级是 5.
  5. 最低是1,最高是10.
public class ThreadTest {
    public static void main(String[] args) {
        /*System.out.println("最低优先级:" + Thread.MIN_PRIORITY);
        System.out.println("最高优先级:" + Thread.MAX_PRIORITY);
        System.out.println("默认优先级:" + Thread.NORM_PRIORITY);
        // 获取main线程的优先级
        Thread mainThread = Thread.currentThread();
        System.out.println("main线程的优先级:" + mainThread.getPriority()); // 5

        // 设置优先级
        mainThread.setPriority(Thread.MAX_PRIORITY);
        System.out.println("main线程的优先级:" + mainThread.getPriority()); // 10*/

        // 创建两个线程
        Thread t1 = new MyThread();
        t1.setName("t1");

        Thread t2 = new MyThread();
        t2.setName("t2");

        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);

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

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + "==>" + i);
}}}

9.9.3 让位

关于JVM的调度:

  1. 让位
  2. 静态方法:Thread.yield()
  3. 让当前线程让位。
  4. 注意:让位不会让其进入阻塞状态。只是放弃目前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。
  5. 只能保证大方向上的,大概率,到了某个点让位一次。
public class ThreadTest {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        t1.setName("t1");

        Thread t2 = new MyThread();
        t2.setName("t2");

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

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            if(Thread.currentThread().getName().equals("t1") && i % 10 == 0){
                System.out.println(Thread.currentThread().getName() + "让位了,此时的i下标是:" + i);
                // 当前线程让位,这个当前线程一定是t1
                // t1会让位一次
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + "==>" + i);
        }
    }
}

9.10 线程安全问题

9.10.1 线程安全概念

  1. 什么情况下需要考虑线程安全问题?
    • 条件1:多线程的并发环境下
    • 条件2:有共享的数据
    • 条件3:共享数据涉及到修改的操作
  2. 一般情况下:
    • 局部变量不存在线程安全问题。(尤其是基本数据类型不存在线程安全问题【在栈中,栈不是共享的】,如果是引用数据类型,就另说了!)
    • 实例变量可能存在线程安全问题。实例变量在堆中。堆是多线程共享的。
    • 静态变量也可能存在线程安全问题。静态变量在堆中。堆是多线程共享的。
  3. 大家找一个现实生活中的例子,来说明一下,线程安全问题:比如同时取钱!
  4. 以上多线程并发对同一个账户进行取款操作的时候,有安全问题?怎么解决?
    • 让线程t1和线程t2排队执行。不要并发。要排队。
    • 我们把线程排队执行,叫做:线程同步机制。(t1和t2线程,t1线程在执行的时候必须等待t2线程执行到某个位置之后,t1线程才能执行。只要t1和t2之间发生了等待,就认为是同步。)
    • 如果不排队,我们将其称为:线程异步机制。(t1和t2各自执行各自的,谁也不需要等对方。并发的,就认为是异步)
    • 异步:效率高。但是可能存在安全隐患。
    • 同步:效率低。排队了。可以保证数据的安全问题。

在这里插入图片描述

9.10.2 同步机制

使用线程同步机制,来保证多线程并发环境下的数据安全问题:

  1. 线程同步的本质是:线程排队执行就是同步机制。
  2. 语法格式:
    synchronized(必须是需要排队的这几个线程共享的对象){
        // 需要同步的代码
    }
    
    • 必须是需要排队的这几个线程共享的对象” 这个必须选对了。
    • 这个如果选错了,可能会无故增加同步的线程数量,导致效率降低。
  3. 原理是什么?
    synchronized(obj){
        // 同步代码块
    }
    
    • 假设obj是t1 t2两个线程共享的。
    • t1和t2执行这个代码的时候,一定是有一个先抢到了CPU时间片。一定是有先后顺序的。
    • 假设t1先抢到了CPU时间片。t1线程找共享对象obj的对象锁,找到之后,则占有这把锁。只要能够占有obj对象的对象锁,就有权利进入同步代码块执行代码。
    • 当t1线程执行完同步代码块之后,会释放之前占有的对象锁(归还锁)。
    • 同样,t2线程抢到CPU时间片之后,也开始执行,也会去找共享对象obj的对象锁,但由于t1线程占有这把锁,t2线程只能在同步代码块之外等待。
  4. 注意同步代码块的范围,不要无故扩大同步的范围,同步代码块范围越小,效率越高

实例方法上也可以添加 synchronized 关键字:

  1. 在实例方法上添加了synchronized关键字之后,整个方法体是一个同步代码块。
  2. 在实例方法上添加了synchronized关键字之后,共享对象的对象锁一定是this的。
    这种方式相对于之前所讲的局部同步代码块的方式要差一些:
    synchronized(共享对象){
        // 同步代码块
    }
    
  3. 这种方式优点:
    1. 灵活
    2. 共享对象可以随便调整。
    3. 同步代码块的范围可以随便调整。

9.10.3 同步机制面试题

/**
 * 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyClass mc = new MyClass();
        Thread t1 = new Thread(new MyRunnable(mc));
        Thread t2 = new Thread(new MyRunnable(mc));

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.start();
    }
}

class MyRunnable implements Runnable {

    private MyClass mc;

    public MyRunnable(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            mc.m1();
        }
        if("t2".equals(Thread.currentThread().getName())){
            mc.m2();
        }
    }
}
class MyClass {
    public synchronized void m1(){
        System.out.println("m1 begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("m1 over");
    }
    public void m2(){
        System.out.println("m2 begin");
        System.out.println("m2 over");
    }
}

答案:不需要

/**
 * 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyClass mc = new MyClass();
        Thread t1 = new Thread(new MyRunnable(mc));
        Thread t2 = new Thread(new MyRunnable(mc));

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.start();
    }
}

class MyRunnable implements Runnable {

    private MyClass mc;

    public MyRunnable(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            mc.m1();
        }
        if("t2".equals(Thread.currentThread().getName())){
            mc.m2();
        }
    }
}

class MyClass {
    public synchronized void m1(){
        System.out.println("m1 begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("m1 over");
    }

    public synchronized void m2(){
        System.out.println("m2 begin");
        System.out.println("m2 over");
    }
}

答案:需要

/**
 * 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyClass mc1 = new MyClass();
        MyClass mc2 = new MyClass();
        Thread t1 = new Thread(new MyRunnable(mc1));
        Thread t2 = new Thread(new MyRunnable(mc2));
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.start();
    }
}
class MyRunnable implements Runnable {
    private MyClass mc;
    public MyRunnable(MyClass mc) {this.mc = mc;}
    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){mc.m1();}
        if("t2".equals(Thread.currentThread().getName())){mc.m2();}
    }
}
class MyClass {
    public synchronized void m1(){
        System.out.println("m1 begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("m1 over");
    }
    public synchronized void m2(){
        System.out.println("m2 begin");
        System.out.println("m2 over");
    }
}

答案:不需要

**
 * 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
 *      需要等待。
 *
 * 在静态方法上添加synchronized之后,线程会占有类锁。
 * 类锁是,对于一个类来说,只有一把锁。不管创建了多少个对象,类锁只有一把。
 *
 * 静态方法上添加synchronized,实际上是为了保证静态变量的安全。
 * 实例方法上添加synchronized,实际上是为了保证实例变量的安全。
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyClass mc1 = new MyClass();
        MyClass mc2 = new MyClass();
        Thread t1 = new Thread(new MyRunnable(mc1));
        Thread t2 = new Thread(new MyRunnable(mc2));
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.start();
}}
class MyRunnable implements Runnable {
    private MyClass mc;
    public MyRunnable(MyClass mc) {this.mc = mc;}
    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            mc.m1();
        }
        if("t2".equals(Thread.currentThread().getName())){
            mc.m2();
}}}

class MyClass {
    public static synchronized void m1(){
        System.out.println("m1 begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("m1 over");
    }
    public static synchronized void m2(){
        System.out.println("m2 begin");
        System.out.println("m2 over");
    }
}

9.11 线程间的通信

  1. 内容是关于:线程通信。
  2. 线程通信涉及到三个方法:wait()、notify()、notifyAll()
    1. wait(): 线程执行该方法后,进入等待状态,并且释放对象锁。
      2. notify(): 唤醒优先级最高的那个等待状态的线程。【优先级相同的,随机选一个】。被唤醒的线程从当初wait()的位置继续执行。
    2. notifyAll(): 唤醒所有wait()的线程
    3. 需要注意的:
      • 以上三个方法在使用时,必须在同步代码块中或同步方法中。
      • 调用这三个方法的对象必须是共享的锁对象。
      • 这三个方法都是Object类的方法。
  3. 其中wait()方法重载了三个:
    • wait():调用此方法,线程进入“等待状态”
    • wait(毫秒):调用此方法,线程进入“超时等待状态”
    • wait(毫秒, 纳秒):调用此方法,线程进入“超时等待状态”
  4. 调用wait方法和notify相关方法的,不是通过线程对象去调用,而是通过共享对象去调用。
  5. 例如调用了:obj.wait(),什么效果?
    • obj是多线程共享的对象。
    • 当调用了obj.wait()之后,在obj对象上活跃的所有线程进入无期限等待。直到调用了该共享对象的 obj.notify() 方法进行了唤醒。而且唤醒后,会接着上一次调用wait()方法的位置继续向下执行。
  6. obj.wait()方法调用之后,会释放之前占用的对象锁。
  7. 关于notify和notifyAll方法:
    • 共享对象.notify(); 调用之后效果是什么?唤醒优先级最高的等待线程。如果优先级一样,则随机唤醒一个。
    • 共享对象.notifyAll(); 调用之后效果是什么?唤醒所有在该共享对象上等待的线程。
  8. wait()和sleep的区别
    • 相同点:都会阻塞。
    • 不同点:
      • wait是Object类的实例方法。sleep是Thread的静态方法。
      • wait只能用在同步代码块或同步方法中。sleep随意。
      • wait方法执行会释放对象锁。sleep不会。
      • wait结束时机是notify唤醒,或达到指定时间。sleep结束时机是到达指定时间。

9.12 单例模式的线程安全问题

class SingletonTest {
    // 静态变量
    private static Singleton s1;
    private static Singleton s2;
    public static void main(String[] args) {
        // 获取某个类。这是反射机制中的内容。
        /*Class stringClass = String.class;
        Class singletonClass = Singleton.class;
        Class dateClass = java.util.Date.class;*/
        // 创建线程对象t1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                s1 = Singleton.getSingleton();
            }
        });

        // 创建线程对象t2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                s2 = Singleton.getSingleton();
            }
        });

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

        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 判断这两个Singleton对象是否一样。
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);

    }
}

/**
 * 懒汉式单例模式
 */
public class Singleton {
    private static Singleton singleton;

    private Singleton() {
        System.out.println("构造方法执行了!");
    }

    // 非线程安全的。
    /*public static Singleton getSingleton() {
        if (singleton == null) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            singleton = new Singleton();
        }
        return singleton;
    }*/

    // 线程安全的:第一种方案(同步方法),找类锁。
    /*public static synchronized Singleton getSingleton() {
        if (singleton == null) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            singleton = new Singleton();
        }
        return singleton;
    }*/

    // 线程安全的:第二种方案(同步代码块),找的类锁
    /*public static Singleton getSingleton() {
        // 这里有一个知识点是反射机制中的内容。可以获取某个类。
        synchronized (Singleton.class){
            if (singleton == null) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                singleton = new Singleton();
            }
        }
        return singleton;
    }*/

    // 线程安全的:这个方案对上一个方案进行优化,提升效率。
    /*public static Singleton getSingleton() {
        if(singleton == null){
            synchronized (Singleton.class){
                if (singleton == null) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }*/

    // 使用Lock来实现线程安全
    // Lock是接口,从JDK5开始引入的。
    // Lock接口下有一个实现类:可重入锁(ReentrantLock)
    // 注意:要想使用ReentrantLock达到线程安全,假设要让t1 t2 t3线程同步,就需要让t1 t2 t3共享同一个lock。
    // Lock 和 synchronized 哪个好?Lock更好。为什么?因为更加灵活。
    private static final ReentrantLock lock = new ReentrantLock();

    public static Singleton getSingleton() {
        if(singleton == null){
            try {
                // 加锁
                lock.lock();
                if (singleton == null) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    singleton = new Singleton();
                }
            } finally {
                // 解锁(需要100%保证解锁,怎么办?finally)
                lock.unlock();
            }
        }
        return singleton;
    }
}

9.13 Callable实现线程

  • 实现线程的第三种方式:实现Callable接口,实现call方法。
  • 这种方式实现的线程,是可以获取到线程返回值的。
public class ThreadTest {
    public static void main(String[] args) {
        // 创建“未来任务”对象
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                // 处理业务......
                Thread.sleep(1000 * 5);
                return 1;
            }
        });

        // 创建线程对象
        Thread t = new Thread(task);
        t.setName("t");

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

        try {
            // 获取“未来任务”线程的返回值
            // 阻塞当前线程,等待“未来任务”结束并返回值。
            // 拿到返回值,当前线程的阻塞才会解除。继续执行。
            Integer i = task.get();
            System.out.println(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/*class MyRunnable implements Runnable {

    @Override
    public void run() {

    }
}

class MyThread extends Thread {
    @Override
    public void run() {

    }
}*/

9.14 线程池实现线程

  • 创建线程的第四种方式:使用线程池技术。
  • 线程池本质上就是一个缓存:cache
  • 一般都是服务器在启动的时候,初始化线程池,
  • 也就是说服务器在启动的时候,创建N多个线程对象,
  • 直接放到线程池中,需要使用线程对象的时候,直接从线程池中获取。
public class ThreadTest {
    public static void main(String[] args) {

        // 创建一个线程池对象(线程池中有3个线程)
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 将任务交给线程池(你不需要触碰到这个线程对象,你只需要将要处理的任务交给线程池即可。)
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                }
            }
        });

        // 最后记得关闭线程池
        executorService.shutdown();
    }
}

  • 每章一句:“ 人生没有如果,只有后果和结果。少问别人为什么,多问自己凭什么。”
  • 恭喜你已阅读完第九章!点个赞证明你已经挑战成功,进入第十章关卡【反射机制】吧【更新中……】!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值