深入理解Java多线程编程:从基础到高级应用

一、线程概述

1.什么是进程?什么是线程?它们的区别?

进程是指操作系统中的一段程序,它是一个正在执行中的程序实例,具有独立的内存空间和系统资源,如文件、网络端口等。在计算机程序执行时,先创建进程,再在进程中进行程序的执行。一般来说,一个进程可以包含多个线程。

线程是指进程中的一个执行单元,是进程的一部分,它负责在进程中执行程序代码。每个线程都有自己的栈和程序计数器,并且可以共享进程的资源。多个线程可以在同一时刻执行不同的操作,从而提高了程序的执行效率。

现代的操作系统是支持多进程的,也就是可以启动多个软件,一个软件就是一个进程。称为:多进程并发。

通常一个进程都是可以启动多个线程的。称为:多线程并发。

二、并发与并行

1.并发(concurrency)

使用单核CPU的时候,同一时刻只能有一条指令执行,但多个指令被快速的轮换执行,使得在宏观上具有多个指令同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干端,使多个指令快速交替的执行。

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

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

2.并行(parallellism)

使用多核CPU的时候,同一时刻,有多条指令在多个CPU上同时执行。

如图所示,在同一时刻,ABC都是同时执行(微观、宏观)。

三、并发编程与并行编程

在CPU比较繁忙(假设为单核CPU),如果开启了很多个线程,则只能为一个线程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。

在CPU资源比较充足的时候,一个进程内的多个线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。

至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所以,如果有人问我我所写的多线程是并发还是并行的?

我会说,都有可能。

**总结:**单核CPU上的多线程,只是由操作系统来完成多任务间对CPU的运行切换,并非真正意义上的并发。随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行,故而多线程技术得到广泛应用。

不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源,而我们使用多线程的目的就是为了提高CPU资源的利用率。

四、线程的调度策略

1.线程的调度模型

如果多个线程被分配到一个CPU内核中执行,则同一时刻只能允许有一个线程能获得CPU的执行权,那么进程中的多个线程就会抢夺CPU的执行权,这就是涉及到线程调度策略。

分时调度模型

所有线程轮流使用CPU的执行权,并且平均的分配每个线程占用的CPU的时间。

抢占式调度模型

让优先级高的线程以较大的概率优先获得CPU的执行权,如果线程的优先级相同,那么就会随机选择一个线程获得CPU的执行权,而Java采用的就是抢占式调用。

五、实现线程

注意:

实现线程就是会出现2个线程,因为main方法本身算一个线程

主线程

在 Java 中,当 JVM 启动执行一个 Java 程序时,会首先创建一个主线程,该线程会负责执行 main 方法里的代码。例如以下简单代码:

public class MainThreadExample {
    public static void main(String[] args) {
        System.out.println("主线程正在执行,线程名称: " + Thread.currentThread().getName());
    }
}

在这个例子中,main 方法是程序的入口,它在主线程里执行。Thread.currentThread().getName() 方法用于获取当前正在执行的线程的名称,默认情况下主线程的名称是 "main"。

创建并启动新线程

当你使用继承 Thread 类或者实现 Runnable 接口的方式创建并启动一个新线程时,就会额外生成一个独立的执行路径。例如:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("新线程正在执行,线程名称: " + Thread.currentThread().getName());
    }
}

public class TwoThreadsExample {
    public static void main(String[] args) {
        // 创建新线程对象
        MyThread myThread = new MyThread();
        // 启动新线程
        myThread.start();

        System.out.println("主线程正在执行,线程名称: " + Thread.currentThread().getName());
    }
}

在上述代码中:

主线程负责执行 main 方法中的代码,包括创建 MyThread 对象、调用 start() 方法启动新线程以及打印主线程信息。

MyThread 类继承自 Thread 类,重写了 run() 方法。调用 start() 方法后,会创建一个新线程并执行 run() 方法里的代码。

多线程并发执行

一旦新线程启动,它就会和主线程并发执行(在多核处理器上可以真正并行执行),也就是说这两个线程会同时推进各自的任务,就像两条并行的车道,车辆(线程中的任务)可以同时在上面行驶。因此,在运行上述代码时,输出的顺序可能会有所不同,因为两个线程的执行顺序是由操作系统的线程调度器决定的。

综上所述,实现并启动一个新线程后,程序里就会有两个线程同时运行,一个是主线程,另一个是你创建的新线程。当然,你还可以创建更多的线程,从而实现多线程并发编程。

1.第一种方式:继承Thread

编写一个类继承Thread,重写run方法。

创建线程对象:Thread t = new MyThread();

启动线程:t.start();

⑴.调用run方法的内存图

/**
 * 在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);
        }
    }
}

运行结果:

先把run方法执行输出,执行完后弹出run方法

再执行main方法,输出0-99,输出完main方法弹栈

⑵.调用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);
        }
    }
}

出现切换的原因:抢cpu时间片,用完就下一个

start方法的任务是启动一个新线程,分配一个新的栈空间,分完即结束,弹出

2.第二种方式:实现Runnable接口

编写一个类实现Runnable接口,实现run方法。

创建线程对象:Thread t = new Thread(new MyRunnable());

启动线程: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);
        }
    }
}

运行结果:

优先选择第二种方式:因为实现接口的同时,保留了类的继承。

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

/**
 * 采用匿名内部类的方式,少写一个Runnable接口的实现类。
 */
public class ThreadTest04 {
    public static void main(String[] args) {
        /*// 创建线程对象
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("t ---> " + i);
                }
            }
        });

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

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("t ---> " + i);
                }
            }
        }).start();

        for (int i = 0; i < 100; i++) {
            System.out.println("main ---> " + i);
        }
    }
}

六、线程常用的三个方法:

实例方法:String getName();

void setName(String name);

静态方法:static Thread currentThread();

String getName() 功能:该实例方法用于获取线程的名称。每个线程在创建时都可以被赋予一个特定的名称,通过调用getName方法可以获取到这个名称,方便在程序中对不同的线程进行识别和跟踪。

void setName(String name) 功能:此实例方法用于设置线程的名称。可以在创建线程后,根据需要为其设置一个有意义的名称,以便更好地管理和区分线程。

static Thread currentThread() 功能: 这是一个静态方法,用于获取当前正在执行的线程对象的引用。在多线程程序中,不同的线程可能会执行到同一个代码块或方法,通过currentThread方法可以获取到当前正在执行该代码的线程实例,进而可以对当前线程进行一些操作,如获取或设置线程的属性等

/**
 * 关于线程中常用方法:
 *      实例方法:
 *          String getName();  获取线程对象的名字
 *          void setName(String threadName); 修改线程的名字
 *      静态方法:
 *          static Thread currentThread(); 获取当前线程对象的引用。
 */
public class ThreadTest {
    public static void main(String[] args) {

        // 获取当前线程对象
        Thread mainThread = Thread.currentThread();

        // 获取当前线程的名字
        System.out.println("主线程的名字:" + mainThread.getName()); // 主线程的名字:main

        // 创建线程对象
        Thread t = new MyThread("tt");
        // 修改线程的名字
        t.setName("t");
        // 启动线程
        t.start();

        // 创建线程对象
        Thread t1 = new MyThread("tt1");
        // 修改线程名字
        t1.setName("t1");
        // 启动线程
        t1.start();
    }
}

class MyThread extends Thread{

    public MyThread(String threadName){
        super(threadName);
    }

    @Override
    public void run() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取当前线程对象的名字
        System.out.println("分支线程的名字:" + t.getName()); // 分支线程的名字:Thread-0
    }
}

运行结果:

七、线程生命周期

1.线程生命周期指的是:

从线程对象新建,到最终线程死亡的整个过程。

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

新建状态(NEW)

就绪状态(RUNNABLE)

运行状态(RUNNABLE)

超时等待状态(TIMED_WAITING)

等待状态(WAITING):

阻塞状态(BLOCKED)

死亡(终止)状态(TERMINATED)

八、线程休眠与终止

1.线程的sleep

`Thread.sleep()`是Java中的一个静态方法,可以让当前线程暂停指定的时间。

该方法会将当前线程暂停指定的毫秒数,让其他可用的线程获得执行机会,但是当前线程的锁并不会被释放。因此,在sleep期间,其他线程如果请求获得了该线程占有的锁,那么这些线程也无法执行。

2.Thread.sleep()`方法的常见用法有:

  1. 模拟耗时操作,例如网络请求等场景。使用`Thread.sleep()`可以模拟网络请求的等待时间,从而测试应用在高并发场景下的表现等。

  2. 控制线程执行的顺序。在多线程编程中,可能需要确保某个线程先于其他线程执行,使用`Thread.sleep()`方法可以使当前线程暂停一定的时间,等待其他线程执行完毕。

以下是`Thread.sleep()`的示例代码:

在这个示例中,`main`方法中的for循环使用了`Thread.sleep()`方法,让当前线程暂停1秒钟,从而可以控制输出的每一行之间的时间间隔。

通过这个示例,我们可以看到`Thread.sleep()`方法让当前线程暂停指定的时间,并在暂停结束后继续执行。

/**
 * 关于线程的sleep方法:
 *      1. static void sleep(long millis)
 *          静态方法,没有返回值,参数是一个毫秒。1秒 = 1000毫秒
 *      2. 这个方法作用是:
 *          让当前线程进入休眠,也就是让当前线程放弃占有的CPU时间片,让其进入阻塞状态。
 *          意思:你别再占用CPU了,让给其他线程吧。
 *          阻塞多久呢?参数毫秒为准。在指定的时间范围内,当前线程没有权利抢夺CPU时间片了。
 *      3. 怎么理解"当前线程"呢?
 *          Thread.sleep(1000); 这个代码出现在哪个线程中,当前线程就是这个线程。
 *      4. run方法在方法重写的时候,不能在方法声明位置使用 throws 抛出异常。
 *      5. sleep方法可以模拟每隔固定的时间调用一次程序。
 */
public class ThreadTest {
    public static void main(String[] args) {
        try {
            // 让当前线程睡眠5秒
            // 这段代码出现在主线程中,所以当前线程就是主线程
            // 让主线程睡眠5秒
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        for (int i = 0; i < 10; i++) {//主线程睡眠5秒后,再输出主线程名字
            System.out.println(Thread.currentThread().getName() + "===>" + i);
        }

        // 启动线程
        Thread t = new Thread(new MyRunnable());
        t.setName("t");
        t.start();
    }
}

class MyRunnable implements Runnable {

    @Override
    public void run(){
        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);
            }
        }
    }
}

运行结果:

3.sleep相关面试题

关于sleep的面试题:以下程序中,是main线程休眠5秒,还是分支线程休眠5秒?

/**
 * 关于sleep的面试题:以下程序中,是main线程休眠5秒,还是分支线程休眠5秒?
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.setName("t");
        t.start();

        try {
            // 这行代码并不是让t线程睡眠,而是让当前线程睡眠。
            // 当前线程是main线程。
            t.sleep(1000 * 5); // 等同于:Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

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

class MyThread extends Thread {
    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "===>" + i);
        }
    }
}
  1. sleep方法的本质
    Thread.sleep()静态方法 ,它的行为与调用它的对象实例无关 。即使通过实例t调用(如t.sleep()),实际上等同于Thread.sleep(),且只会让当前执行的线程(即调用该方法的线程)休眠。

  2. 代码执行逻辑

    • main线程启动分支线程t后,执行t.sleep(1000 * 5)

    • 由于sleep是静态方法,此时main线程(而非t)会休眠5秒。

    • 分支线程t启动后立即执行run()方法,不会受main线程休眠的影响,会继续执行自己的循环输出。

  3. 输出结果分析

    • main线程休眠的5秒内,分支线程t可能已经完成或部分完成其run()中的循环输出。

    • main线程苏醒后,才会继续执行自己的循环输出。

  4. 常见误区

    误以为t.sleep()会让t线程休眠,但实际上静态方法调用与对象无关,休眠的始终是当前线程

结论

程序中的sleep使main线程休眠5秒,分支线程t不受影响,会正常执行。

4.如何中断线程的休眠

/**
 * 怎么中断一个线程的睡眠。(怎么解除线程因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();

    }
}

运行结果;

5.强行终止一个线程的执行

复制代码
如何合理的,正常的方式终止一个线程的执行?
*      一般我们在实际开发中会使用打标记的方式,来终止一个线程的执行。
/**
 * 如何合理的,正常的方式终止一个线程的执行?
 *      一般我们在实际开发中会使用打标记的方式,来终止一个线程的执行。
 */
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;
            }
        }
    }
}

运行结果:

九、守护线程

1.概念

守护线程是为其他线程提供服务的线程,也被称为 "服务线程" 或 "后台线程"。它的主要目的是在程序运行期间为其他线程提供一些辅助性的支持和服务,比如垃圾回收线程就是一种典型的守护线程,它负责在程序运行过程中自动回收不再使用的内存空间,为其他线程的正常运行提供保障。

2.特点

**生命周期依赖:**守护线程的生命周期依赖于其他非守护线程。当 Java 虚拟机中所有的非守护线程都执行完毕后,无论守护线程是否完成任务,Java 虚拟机会自动退出,守护线程也会随之结束。

**不可独立运行:**守护线程不能单独作为程序的核心线程来运行,它必须依赖于至少一个非守护线程的存在。如果没有非守护线程在运行,守护线程也就没有存在的意义了。

**优先级较低:**一般情况下,守护线程的优先级相对较低,这是为了确保它不会影响到非守护线程的正常执行。在 CPU 资源分配时,操作系统会优先分配给非守护线程,只有在非守护线程不需要使用 CPU 资源或者处于等待状态时,守护线程才可能有机会执行。

3.使用场景

**垃圾回收:**在 Java 程序运行过程中,垃圾回收线程作为守护线程,会不断地检查和回收不再被使用的内存对象,释放内存空间,以确保程序有足够的内存来运行。它在后台默默地运行,不影响其他业务逻辑线程的执行,保证了系统的稳定性和资源的有效利用。

**日志记录:**可以创建一个守护线程来负责日志的记录工作。在程序运行过程中,各个线程可能会产生各种日志信息,守护线程可以在后台实时地将这些日志信息写入到日志文件中,而不会影响其他线程的主要业务逻辑执行。这样可以实现日志的异步记录,提高系统的整体性能和响应速度。

**定时任务调度:**比如定时备份数据、定时清理缓存等任务,都可以使用守护线程来实现。守护线程可以按照设定的时间间隔,定期执行相应的任务,为系统提供定时服务,而不干扰其他核心业务线程的正常运行。

复制代码
1. 在Java语言中,线程被分为两大类:
*      第一类:用户线程(非守护线程)
*      第二类:守护线程(后台线程)
*
* 2. 在JVM当中,有一个隐藏的守护线程一直在守护者,它就是GC线程。
*
* 3. 守护线程的特点:所有的用户线程结束之后,守护线程自动退出/结束。

Java中的守护线程是一种特殊的线程,当所有的非守护线程都结束时,守护线程会自动退出。在Java中,GC线程就是一种典型的守护线程。

创建守护线程的方法很简单,只需要将线程的setDaemon()方法设置为true即可。

例如:

Thread myThread = new Thread(new MyRunnable());

myThread.setDaemon(true);

myThread.start();

在这个示例中,创建了一个守护线程myThread,并将其设置为守护线程。当程序中所有的非守护线程结束时,myThread会自动退出。

需要注意的是,只有在启动守护线程之前将其设置为守护线程才有效。如果在线程已经运行的情况下将其设置为守护线程,是无法将其设置为守护线程的。

另外,守护线程并不是每次运行都会执行完成,因此不能依赖守护线程完成关键任务。

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

        // 在启动线程之前,设置线程为守护线程
        // 守护线程的特点是当所有用户线程(非守护线程)结束时,守护线程会自动结束
        myThread.setDaemon(true);

        // 启动守护线程,使其开始执行 run 方法中的代码
        myThread.start();

        // 主线程中,main 线程是一个用户线程。
        // 主线程执行一个循环,循环 10 次,每次循环打印当前线程名称和循环次数
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "===>" + i);
            try {
                // 主线程休眠 1 秒,模拟耗时操作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 如果线程在休眠过程中被中断,抛出运行时异常
                throw new RuntimeException(e);
            }
        }
    }
}

// 自定义线程类,继承自 Thread 类
class MyThread extends Thread {
    @Override
    public void run() {
        int i = 0;
        // 守护线程进入一个无限循环,不断打印当前线程名称和循环次数
        while(true){
            System.out.println(Thread.currentThread().getName() + "===>" + (++i));
            try {
                // 守护线程休眠 1 秒,模拟耗时操作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 如果线程在休眠过程中被中断,抛出运行时异常
                throw new RuntimeException(e);
            }
        }
    }
}

运行结果:

4.为什么输出会交叉出现 、

输出交叉出现是因为守护线程和主线程是并发执行的。在多线程环境中,操作系统的线程调度器负责分配 CPU 时间片给各个线程。当一个线程的时间片用完后,线程调度器会暂停该线程的执行,切换到另一个线程继续执行。

在这段代码中,主线程和守护线程都在执行循环,并且都在每次循环中休眠 1 秒。由于线程调度器的调度是不确定的,它可能会在主线程休眠时让守护线程执行,也可能在守护线程休眠时让主线程执行,因此会出现两个线程的输出交叉出现的情况。

十、定时器和定时任务

Timer类是 Java 提供的一个定时器工具,它可以用来安排一个或多个定时任务在未来的某个时间点执行,或者按照固定的时间间隔重复执行。

TimerTask 是一个抽象类,它实现了 Runnable 接口,我们需要继承 TimerTask 类并实现其 run() 方法,在 run() 方法中编写具体的定时任务逻辑。

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 定时任务类:专门记录日期的定时任务类。
 */
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++);
    }
}

/**
 * 1. JDK中提供的定时任务:
 *      java.util.Timer         定时器
 *      java.util.TimerTask     定时任务
 * 2. 定时器 + 定时任务:可以帮我们在程序中完成:每间隔多久执行一次某段程序。
 * 3. Timer的构造方法:
 *      Timer()
 *      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");

        // 使用 LogTimerTask 类的方式执行定时任务
        // 第一个参数是定时任务对象,第二个参数是任务开始的时间,第三个参数是任务执行的时间间隔(单位:毫秒)
        // timer.schedule(new LogTimerTask(), firstTime, 1000);

        // 匿名内部类的方式
        // 使用匿名内部类创建一个 TimerTask 对象,并将其传递给定时器的 schedule 方法
        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);

        // 主线程休眠 10 秒,让定时器有时间执行任务
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
        }
        // 取消定时器,释放资源
        timer.cancel();
    }
}

1.优化方案

可以只保留 LogTimerTask 类,避免使用匿名内部类来实现相同的功能,这样能提高代码的复用性和可维护性。以下是优化后的代码:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 定时任务类:专门记录日期的定时任务类。
 */
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++);
    }
}

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");

        // 使用 LogTimerTask 类的方式执行定时任务
        timer.schedule(new LogTimerTask(), firstTime, 1000 * 5);

        // 主线程休眠 10 秒,让定时器有时间执行任务
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
        }
        // 取消定时器,释放资源
        timer.cancel();
    }
}

2.示例解释

假设任务在 t = 0 时刻开始执行:

第 1 次执行:t = 0 秒,输出第一次结果。

第 2 次执行:t = 5 秒,距离第 1 次执行过去了 5 秒,输出第二次结果。

第 3 次执行:t = 10 秒,距离第 2 次执行又过去了 5 秒,输出第三次结果。

到 t = 10 秒时,主线程休眠结束,由于定时器是守护线程,主线程结束会导致定时器线程也结束,任务不再继续执行

十一、线程的调度

1.调用join()方法完成线程合并

复制代码
* 线程合并
*      1. 调用join()方法完成线程合并。
*
*      2. join()方法是一个实例方法。(不是静态方法) t.join
*
*      3. 假设在main方法(main线程)中调用了 t.join(),后果是什么?
*          t线程合并到主线程中。主线程进入阻塞状态。直到 t 线程执行结束。主线程阻塞解除。
*
*      4. t.join()方法其实是让当前线程进入阻塞状态,直到t线程结束,当前线程阻塞解除。
*
*      5. 和sleep方法有点类似,但不一样:
*          第一:sleep方法是静态方法,join是实例方法。
*          第二:sleep方法可以指定睡眠的时长,join方法不能保证阻塞的时长。
*          第三:sleep和join方法都是让当前线程进入阻塞状态。
*          第四:sleep方法的阻塞解除条件?时间过去了。 join方法的阻塞解除条件?调用join方法的那个线程结束了。
/**
 * 线程合并
 *      1. 调用join()方法完成线程合并。
 *
 *      2. join()方法是一个实例方法。(不是静态方法) t.join
 *
 *      3. 假设在main方法(main线程)中调用了 t.join(),后果是什么?
 *          t线程合并到主线程中。主线程进入阻塞状态。直到 t 线程执行结束。主线程阻塞解除。
 *
 *      4. t.join()方法其实是让当前线程进入阻塞状态,直到t线程结束,当前线程阻塞解除。
 *
 *      5. 和sleep方法有点类似,但不一样:
 *          第一:sleep方法是静态方法,join是实例方法。
 *          第二:sleep方法可以指定睡眠的时长,join方法不能保证阻塞的时长。
 *          第三:sleep和join方法都是让当前线程进入阻塞状态。
 *          第四: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);
        //过多,t线程已经结束,交个main线程
        //过少,阻塞完,由cpu重新分配时间片

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

        // 主线程
        for (int i = 0; i < 10; 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 < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "==>" + i);
        }
    }
}

运行结果:

2.线程优先级

线程优先级可以用来指定线程的执行优先级,优先级高的线程会优先获得CPU资源,执行速度也会更快。然而,线程优先级并不是让我们可以依赖的东西,因为它受到底层操作系统调度的影响,不同操作系统调度方式不同,在不同的操作系统上表现也不同。此外,线程优先级也很容易引起线程死锁等问题,因此在线程编程时要慎用。

⑴.线程优先级的范围

Java 中线程优先级的范围是 1 到 10,其中:

**Thread.MIN_PRIORITY:**值为 1,表示最低优先级。

Thread.NORM_PRIORITY:值为 5,是线程的默认优先级。当创建一个新线程时,如果没有显式设置优先级,默认就是这个值。

**Thread.MAX_PRIORITY:**值为 10,表示最高优先级。

⑵.设置与获取线程优先级的方法

在 Java 中,Thread 类提供了设置和获取线程优先级的方法:

**setPriority(int newPriority):**用于设置线程的优先级,参数 newPriority 是一个整数,表示要设置的优先级。

**getPriority():**用于获取线程的当前优先级,返回一个整数。

public class Main {
    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
    }
}

运行结果:

优先级高的抢占的时间片多一些

/**
 * 关于线程生命周期中的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("t2222");

        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 < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "==>" + i);
        }
    }
}

⑶.线程让位

线程让位是指当前正在执行的线程主动放弃CPU资源,让其他线程先执行。

线程让位的主要作用是

调整线程执行顺序,使得高优先级的线程能够更快地执行完任务,从而提高程序的响应性和效率。

复制代码
* 关于JVM的调度:
*      1. 让位
*      2. 静态方法:Thread.yield()
*      3. 让当前线程让位。
*      4. 注意:让位不会让其进入阻塞状态。只是放弃目前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。
*      5. 只能保证大方向上的,大概率,到了某个点让位一次。

注意事项

**不保证让位成功:**yield() 方法只是一个建议,线程调度器可以选择忽略这个建议,继续让当前线程执行。因此,调用 yield() 方法并不能保证当前线程一定会让出 CPU 资源。

**只影响相同或更高优先级线程:**yield() 方法只会让当前线程将执行机会让给具有相同或更高优先级的线程。如果没有其他相同或更高优先级的线程处于可运行状态,当前线程会继续执行。

**不要过度依赖 yield():**在编写多线程程序时,不应该过度依赖 yield() 方法来控制线程的执行顺序。可以使用同步机制(如 synchronized 关键字、Lock 接口等)和线程间通信机制(如 wait()、notify()、notifyAll() 等)来实现更可靠的线程协作。

与 sleep() 方法的区别

**yield():**只是建议让出 CPU 资源,让其他相同或更高优先级的线程有机会执行,当前线程可能马上又会获得 CPU 资源继续执行。

sleep():会让当前线程暂停执行指定的时间,在这段时间内,该线程不会参与 CPU 竞争,直到休眠时间结束才会重新进入可运行状态。

/**
 * 关于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);
        }
    }
}

十二、线程安全问题

1.synchronized 关键字介绍

synchronized 关键字是 Java 中用于实现线程同步的重要工具,它可以保证在同一时刻只有一个线程能够访问被其修饰的代码块或方法,从而避免多线程访问共享资源时可能出现的数据不一致等线程安全问题。以下从基本用法、原理、特性、优缺点等方面详细介绍 synchronized 关键字。

⑴.基本用法

1. 同步实例方法

当 synchronized 修饰实例方法时,该方法在同一时刻只能被一个线程访问,锁对象是调用该方法的实例对象。

public class SynchronizedInstanceMethodExample {
    private int count = 0;

    // 同步实例方法
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

在上述代码中,increment 方法被 synchronized 修饰,当多个线程同时调用同一个 SynchronizedInstanceMethodExample 实例的 increment 方法时,只有一个线程能够获得该实例对象的锁并执行该方法,其他线程需要等待锁被释放后才能继续执行。

2. 同步静态方法

当 synchronized 修饰静态方法时,该方法在同一时刻只能被一个线程访问,锁对象是该类的 Class 对象。

public class SynchronizedStaticMethodExample {
    private static int count = 0;

    // 同步静态方法
    public static synchronized void increment() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

这里的 increment 方法是静态方法,多个线程同时调用该方法时,会竞争该类的 Class 对象的锁,同一时刻只有一个线程能获得锁并执行方法。

3. 同步代码块

synchronized 还可以用于修饰代码块,锁对象可以是任意对象。

public class SynchronizedBlockExample {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        // 同步代码块
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

在这个例子中,使用 synchronized 修饰代码块,锁对象是 lock 对象。只有获得 lock 对象锁的线程才能进入该代码块执行 count++ 操作。

⑵.原理

在 Java 中,每个对象都有一个与之关联的监视器(Monitor),也称为锁。当一个线程访问被 synchronized 修饰的代码块或方法时,它需要先获取该对象的监视器锁。如果锁已经被其他线程持有,那么当前线程会进入阻塞状态,直到锁被释放。当线程执行完 synchronized 代码块或方法后,会自动释放锁。

⑶.特性

1. 可重入性synchronized 是可重入的,即一个线程在持有某个对象的锁时,可以再次获取该对象的锁而不会被阻塞。例如:

public class ReentrantExample {
    public synchronized void method1() {
        System.out.println("Method 1");
        method2();
    }

    public synchronized void method2() {
        System.out.println("Method 2");
    }

    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        example.method1();
    }
}

在上述代码中,method1 方法调用了 method2 方法,由于 synchronized 的可重入性,线程在持有对象锁进入 method1 方法后,可以再次获取该对象的锁进入 method2 方法。

2. 互斥性synchronized 保证了同一时刻只有一个线程能够访问被其修饰的代码块或方法,从而实现了线程之间的互斥访问。

3. 可见性 synchronized 还能保证线程之间的可见性。当一个线程释放锁时,会将工作内存中的数据刷新到主内存中;当另一个线程获取锁时,会从主内存中读取最新的数据。

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

2.演示线程安全问题

以下程序存在安全问题。t1和t2线程同时对act一个账号进行取款操作。数据是错误的。

两个账号同时取款1000,应剩8000

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

// 取款的线程类
class Withdraw implements Runnable {

    // 实例变量(共享数据)
    private Account act;

    public Withdraw(Account act) {
        this.act = act;
    }

    @Override
    public void run() {
        act.withdraw(1000);
    }
}

// 银行账户
class Account {
    /**
     * 账号
     */
    private String actNo;
    /**
     * 余额
     */
    private double balance;

    public Account(String actNo, double balance) {
        this.actNo = actNo;
        this.balance = balance;
    }

    public String getActNo() {
        return actNo;
    }

    public void setActNo(String actNo) {
        this.actNo = actNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    /**
     * 取款的方法
     * @param money 取款额度
     */
    public void withdraw(double money){
        // 想要演示出多线程并发带来的安全问题,这里建议分为两步去完成取款操作。
        // 第一步:读取余额
        double before = this.getBalance();
        System.out.println(Thread.currentThread().getName() + "线程正在取款"+money+",当前"+this.getActNo()+"账户余额" + before);

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

        // 第二步:修改余额
        this.setBalance(before - money);
        System.out.println(Thread.currentThread().getName() + "线程取款成功,当前"+this.getActNo()+"账户余额" + this.getBalance());
    }
}

运行结果:

3.同步代码块

复制代码
* 使用线程同步机制,来保证多线程并发环境下的数据安全问题:
*      1. 线程同步的本质是:线程排队执行就是同步机制。
*      2. 语法格式:
*          synchronized(必须是需要排队的这几个线程共享的对象){
\* // 需要同步的代码
\* }
*
*          “必须是需要排队的这几个线程共享的对象” 这个必须选对了。
*          这个如果选错了,可能会无故增加同步的线程数量,导致效率降低。
*      3. 原理是什么?
*          synchronized(obj){
*              // 同步代码块
*          }
*          假设obj是t1 t2两个线程共享的。
*          t1和t2执行这个代码的时候,一定是有一个先抢到了CPU时间片。一定是有先后顺序的。
*          假设t1先抢到了CPU时间片。t1线程找共享对象obj的对象锁,找到之后,则占有这把锁。只要能够占有obj对象的对象锁,就有权利进入同步代码块执行代码。
*          当t1线程执行完同步代码块之后,会释放之前占有的对象锁(归还锁)。
*          同样,t2线程抢到CPU时间片之后,也开始执行,也会去找共享对象obj的对象锁,但由于t1线程占有这把锁,t2线程只能在同步代码块之外等待。
*
*      4. 注意同步代码块的范围,不要无故扩大同步的范围,同步代码块范围越小,效率越高。
/**
 * 使用线程同步机制,来保证多线程并发环境下的数据安全问题:
 *      1. 线程同步的本质是:线程排队执行就是同步机制。
 *      2. 语法格式:
 *          synchronized(必须是需要排队的这几个线程共享的对象){
 *              // 需要同步的代码
 *          }
 *
 *          "必须是需要排队的这几个线程共享的对象" 这个必须选对了。
 *          这个如果选错了,可能会无故增加同步的线程数量,导致效率降低。
 *      3. 原理是什么?
 *          synchronized(obj){
 *              // 同步代码块
 *          }
 *          假设obj是t1 t2两个线程共享的。
 *          t1和t2执行这个代码的时候,一定是有一个先抢到了CPU时间片。一定是有先后顺序的。
 *          假设t1先抢到了CPU时间片。t1线程找共享对象obj的对象锁,找到之后,则占有这把锁。只要能够占有obj对象的对象锁,就有权利进入同步代码块执行代码。
 *          当t1线程执行完同步代码块之后,会释放之前占有的对象锁(归还锁)。
 *          同样,t2线程抢到CPU时间片之后,也开始执行,也会去找共享对象obj的对象锁,但由于t1线程占有这把锁,t2线程只能在同步代码块之外等待。
 *
 *      4. 注意同步代码块的范围,不要无故扩大同步的范围,同步代码块范围越小,效率越高。
 */
public class ThreadTest {
    public static void main(String[] args) {
        // 创建账户对象
        Account act = new Account("act-001", 10000);
        // 创建线程对象1
        Thread t1 = new Thread(new Withdraw(act));
        // 创建线程对象2
        Thread t2 = new Thread(new Withdraw(act));
        // 启动线程
        t1.start();
        t2.start();
    }
}

// 取款的线程类
class Withdraw implements Runnable {

    // 实例变量(共享数据)
    private Account act;

    public Withdraw(Account act) {
        this.act = act;
    }

    @Override
    public void run() {
        act.withdraw(1000);
    }
}

// 银行账户
class Account {

    private static Object obj = new Object();

    /**
     * 账号
     */
    private String actNo;
    /**
     * 余额
     */
    private double balance;

    public Account(String actNo, double balance) {
        this.actNo = actNo;
        this.balance = balance;
    }

    public String getActNo() {
        return actNo;
    }

    public void setActNo(String actNo) {
        this.actNo = actNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    /**
     * 取款的方法
     * @param money 取款额度
     */
    public void withdraw(double money){
        // this是当前账户对象
        // 当前账户对象act,就是t1和t2共享的对象。
        synchronized (this){
        //synchronized (obj) {
            // 第一步:读取余额
            double before = this.getBalance();
            System.out.println(Thread.currentThread().getName() + "线程正在取款"+money+",当前"+this.getActNo()+"账户余额" + before);

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

            // 第二步:修改余额
            this.setBalance(before - money);
            System.out.println(Thread.currentThread().getName() + "线程取款成功,当前"+this.getActNo()+"账户余额" + this.getBalance());
        }
    }
}

运行结果:

4.同步实例方法

复制代码
* 在实例方法上也可以添加 synchronized 关键字:
*      1. 在实例方法上添加了synchronized关键字之后,整个方法体是一个同步代码块。
*      2. 在实例方法上添加了synchronized关键字之后,共享对象的对象锁一定是this的。
*
* 这种方式相对于之前所讲的局部同步代码块的方式要差一些:
*      synchronized(共享对象){
*          // 同步代码块
*      }
*
*      这种方式优点:灵活
*          共享对象可以随便调整。
*          同步代码块的范围可以随便调整。
/**
 * 在实例方法上也可以添加 synchronized 关键字:
 *      1. 在实例方法上添加了synchronized关键字之后,整个方法体是一个同步代码块。
 *      2. 在实例方法上添加了synchronized关键字之后,共享对象的对象锁一定是this的。
 *
 * 这种方式相对于之前所讲的局部同步代码块的方式要差一些:
 *      synchronized(共享对象){
 *          // 同步代码块
 *      }
 *
 *      这种方式优点:灵活
 *          共享对象可以随便调整。
 *          同步代码块的范围可以随便调整。
 */
public class ThreadTest {
    public static void main(String[] args) {
        // 创建账户对象
        Account act = new Account("act-001", 10000);
        // 创建线程对象1
        Thread t1 = new Thread(new Withdraw(act));
        // 创建线程对象2
        Thread t2 = new Thread(new Withdraw(act));
        // 启动线程
        t1.start();
        t2.start();
    }
}

// 取款的线程类
class Withdraw implements Runnable {

    // 实例变量(共享数据)
    private Account act;

    public Withdraw(Account act) {
        this.act = act;
    }

    @Override
    public void run() {
        act.withdraw(1000);
    }
}

// 银行账户
class Account {

    private static Object obj = new Object();

    /**
     * 账号
     */
    private String actNo;
    /**
     * 余额
     */
    private double balance;

    public Account(String actNo, double balance) {
        this.actNo = actNo;
        this.balance = balance;
    }

    public String getActNo() {
        return actNo;
    }

    public void setActNo(String actNo) {
        this.actNo = actNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    /**
     * 取款的方法
     *
     * @param money 取款额度
     */
    public synchronized void withdraw(double money) {
        // 第一步:读取余额
        double before = this.getBalance();
        System.out.println(Thread.currentThread().getName() + "线程正在取款" + money + ",当前" + this.getActNo() + "账户余额" + before);

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

        // 第二步:修改余额
        this.setBalance(before - money);
        System.out.println(Thread.currentThread().getName() + "线程取款成功,当前" + this.getActNo() + "账户余额" + this.getBalance());
    }
}

运行结果:

5.同步机制面试题

⑴.线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?

/**
 * 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
 *      不需要。
 */
public class ThreadTest {
    public static void main(String[] args) {
        // 创建 MyClass 类的一个实例
        MyClass mc = new MyClass();
        // 创建一个新线程 t1,将 MyRunnable 任务交给它执行,并且传入之前创建的 MyClass 实例
        Thread t1 = new Thread(new MyRunnable(mc));
        // 创建另一个新线程 t2,同样将 MyRunnable 任务交给它执行,传入相同的 MyClass 实例
        Thread t2 = new Thread(new MyRunnable(mc));

        // 给线程 t1 设置名称为 "t1",方便后续区分和调试
        t1.setName("t1");
        // 给线程 t2 设置名称为 "t2",方便后续区分和调试
        t2.setName("t2");

        // 启动线程 t1,使其进入就绪状态,等待获取 CPU 时间片开始执行
        t1.start();

        try {
            // 让当前正在执行的线程(这里是主线程)暂停执行 1000 毫秒(即 1 秒)
            // Thread.sleep 是一个静态方法,它会使当前线程进入阻塞状态,
            // 在指定的时间内不会参与 CPU 时间片的竞争,
            // 直到休眠时间结束后,线程会重新进入就绪状态,等待获取 CPU 资源继续执行。
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 当线程在休眠过程中被其他线程调用 interrupt() 方法中断时,
            // 会抛出 InterruptedException 异常。
            // 这里捕获该异常,并将其包装成 RuntimeException 异常重新抛出,
            // 这意味着程序在遇到线程中断时,会以运行时异常的形式将问题暴露出来,
            // 调用者需要处理这个运行时异常。
            throw new RuntimeException(e);
        }

        // 启动线程 t2,使其进入就绪状态,等待获取 CPU 时间片开始执行
        t2.start();
    }
}

// 实现 Runnable 接口,定义一个可被线程执行的任务类
class MyRunnable implements Runnable {
    // 持有一个 MyClass 类的引用
    private MyClass mc;

    // 构造函数,用于接收 MyClass 类的实例
    public MyRunnable(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        // 判断当前执行的线程名称是否为 "t1"
        if("t1".equals(Thread.currentThread().getName())){
            // 如果是 "t1" 线程,调用 MyClass 实例的 m1 方法
            mc.m1();
        }
        // 判断当前执行的线程名称是否为 "t2"
        if("t2".equals(Thread.currentThread().getName())){
            // 如果是 "t2" 线程,调用 MyClass 实例的 m2 方法
            mc.m2();
        }
    }
}

// 定义一个普通的类 MyClass
class MyClass {
    // 同步实例方法 m1,使用 synchronized 关键字修饰
    // 意味着同一时刻只能有一个线程访问该方法,锁对象是调用该方法的 MyClass 实例
    public synchronized void m1(){
        // 打印 m1 方法开始执行的信息
        System.out.println("m1 begin");
        try {
            // 让当前线程(执行 m1 方法的线程)休眠 5 秒
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            // 捕获线程休眠被中断的异常,并将其包装成 RuntimeException 重新抛出
            throw new RuntimeException(e);
        }
        // 打印 m1 方法执行结束的信息
        System.out.println("m1 over");
    }

    // 普通实例方法 m2,没有使用 synchronized 关键字修饰
    public void m2(){
        // 打印 m2 方法开始执行的信息
        System.out.println("m2 begin");
        // 打印 m2 方法执行结束的信息
        System.out.println("m2 over");
    }
}

运行结果:

解释:

两个线程的执行情况

t1 线程

t1 线程启动后,会进入 MyRunnable 的 run 方法,由于其名称为 "t1",所以会调用 mc.m1() 方法。m1 方法被 synchronized 修饰,t1 线程会获取 mc 对象的锁,接着打印 "m1 begin",然后进入 5 秒的休眠状态。在这 5 秒内,mc 对象的锁一直被 t1 线程持有。

t2 线程

主线程休眠 1 秒后启动 t2 线程,t2 线程进入 MyRunnable 的 run 方法,因为其名称为 "t2",所以会调用 mc.m2() 方法。m2 方法没有被 synchronized 修饰,不需要获取 mc 对象的锁,因此 t2 线程可以直接执行 m2 方法,打印 "m2 begin" 和 "m2 over"。

线程之间的关系

虽然 t1 和 t2 是两个独立的线程,但它们共享同一个 MyClass 实例 mc。不过,由于 m1 和 m2 方法的同步特性不同,t2 线程调用 m2 方法时不受 t1 线程执行 m1 方法的影响,也就是 m2 方法执行时不需要等待 m1 方法结束。

代码中锁的情况分析

m1 方法

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");
}

m1 方法被 synchronized 修饰,这意味着当一个线程调用 m1 方法时,它会获取调用该方法的 MyClass 实例的锁。例如,当 t1 线程调用 mc.m1() 时,t1 线程会获取 mc 这个对象的锁。

在 m1 方法内部,线程会休眠 5 秒,在这 5 秒内,mc 对象的锁一直被 t1 线程持有,其他线程如果想要调用 m1 方法,就需要等待 t1 线程执行完 m1 方法并释放锁。

m2 方法

public void m2(){
    System.out.println("m2 begin");
    System.out.println("m2 over");
}
  • m2 方法没有被 synchronized 修饰,这意味着调用 m2 方法不需要获取 MyClass 实例的锁。所以,无论 m1 方法是否正在执行,t2 线程都可以直接调用 m2 方法,不会受到 m1 方法锁的影响。

代码执行流程分析

  1. t1 线程启动 :在 main 方法中,首先启动 t1 线程,t1 线程会调用 mc.m1() 方法。此时,t1 线程获取 mc 对象的锁,打印 m1 begin,然后开始休眠 5 秒。
  2. 主线程休眠 1 秒main 方法中的主线程休眠 1 秒,这是为了确保 t1 线程有足够的时间进入 m1 方法并开始执行。
  3. t2 线程启动 :1 秒后,主线程启动 t2 线程,t2 线程会调用 mc.m2() 方法。由于 m2 方法不需要获取 mc 对象的锁,所以 t2 线程可以直接执行 m2 方法,打印 m2 beginm2 over
  4. t1 线程继续执行t2 线程执行完 m2 方法后,t1 线程可能还在休眠中。5 秒后,t1 线程休眠结束,打印 m1 over,并释放 mc 对象的锁。

总结

由于 m2 方法没有使用 synchronized 关键字修饰,它的执行不需要获取 MyClass 实例的锁,所以 m2 方法在执行时不需要等待 m1 方法结束。这就是为什么 m2 方法可以在 m1 方法执行期间正常执行的原因。

⑵.线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?

/**
 * 线程同步机制的面试题:分析以下程序 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");
    }
}

运行结果:

⑶.2个对象

/**
 * 线程同步机制的面试题:分析以下程序 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");
    }
}

运行结果:

⑷.在静态方法上添加synchronized之后,线程会占有类锁

类锁就一把

/**
 * 线程同步机制的面试题:分析以下程序 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");
    }
}

运行结果:

6.死锁的概念

死锁是多线程(或多进程)编程中一种严重的问题。当多个线程在执行过程中,由于互相持有对方继续执行所需的资源,从而陷入一种无限期的互相等待状态,使得所有涉及的线程都无法继续推进,程序就进入了死锁状态。

⑴.死锁产生的原因

资源竞争:系统中的资源是有限的,当多个线程同时竞争有限的资源时,就可能导致死锁。例如,两个线程都需要访问两个共享资源,并且各自已经持有了其中一个资源,同时又在等待对方持有的另一个资源,就容易产生死锁。

**线程推进顺序不当:**线程在执行过程中,如果对资源的请求和释放顺序不合理,也可能导致死锁。例如,线程 A 先请求资源 X 再请求资源 Y,而线程 B 先请求资源 Y 再请求资源 X,在某些情况下就可能出现死锁。

⑵.死锁产生的必要条件(Coffman 条件)

**互斥条件:**进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。

**请求和保持条件:**进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

**不剥夺条件:**进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

**环路等待条件:**在发生死锁时,必然存在一个进程 ------ 资源的环形链,即进程集合 {P0,P1,P2,・・・,Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,......,Pn 正在等待已被 P0 占用的资源。

⑶.死锁示例代码

/**
 * 写一个死锁。
 */
public class ThreadTest {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        Thread t1 = new Thread(new MyRunnable(o1, o2));
        Thread t2 = new Thread(new MyRunnable(o1, o2));
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

class MyRunnable implements Runnable {

    private Object o1;
    private Object o2;

    public MyRunnable(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            synchronized (o1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (o2){

                }
            }
        }else if("t2".equals(Thread.currentThread().getName())){
            synchronized (o2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (o1){

                }
            }
        }

    }
}

运行结果:

7.模拟三个窗口卖票。

/**
 * 模拟三个窗口卖票。
 */
public class SellTicket {
    public static void main(String[] args) {

        // 创建一个对象,这样让多个线程共享同一个对象
        MyRunnable mr = new MyRunnable();

        // 创建三个线程,模拟三个窗口
        Thread t1 = new Thread(mr);
        t1.setName("1");
        Thread t2 = new Thread(mr);
        t2.setName("2");
        Thread t3 = new Thread(mr);
        t3.setName("3");

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

class MyRunnable implements Runnable {

    // 实例变量(多线程共享)
    private int ticketTotal = 100;

    @Override
    public void run() {
        // 实现核心业务:卖票
        while(true){
            // synchronized 是线程同步机制。
            // synchronized 又被称为互斥锁!
            synchronized (this){
                if(ticketTotal <= 0){
                    System.out.println("票已售完...");
                    break; // 停止售票
                }
                // 票还有(ticketTotal > 0)
                // 一般出票都需要一个时长,模拟一下
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 出票了
                System.out.println("窗口"+ Thread.currentThread().getName() +"售出一张票,还剩" + (--ticketTotal) + "张票");
            }
        }
    }
}

/*
题目描述:两个线程交替输出
t1-->1
t2-->2
t1-->3
t2-->4
t1-->5
t2-->6
t1-->7
t2-->8
t1-->9
t2-->10
t1-->11
t2-->12
t1-->13
t2-->14
....
 */

运行结果:

十三、线程间的通信

复制代码
/*
题目描述:两个线程交替输出
t1-->1
t2-->2
t1-->3
t2-->4
t1-->5
t2-->6
t1-->7
t2-->8
t1-->9
t2-->10
t1-->11
t2-->12
t1-->13
t2-->14
....
 */

1.线程通信的三个方法:wait()、notify()、notifyAll()

/**
 * 1. 内容是关于:线程通信。
 *
 * 2. 线程通信涉及到三个方法:
 *      wait()、notify()、notifyAll()
 *
 * 3. 以上三个方法都是Object类的方法。
 *
 * 4. 其中wait()方法重载了三个:
 *      wait():调用此方法,线程进入"等待状态"
 *      wait(毫秒):调用此方法,线程进入"超时等待状态"
 *      wait(毫秒, 纳秒):调用此方法,线程进入"超时等待状态"
 *
 * 5. 调用wait方法和notify相关方法的,不是通过线程对象去调用,而是通过共享对象去调用。
 *
 * 6. 例如调用了:obj.wait(),什么效果?
 *      obj是多线程共享的对象。
 *      当调用了obj.wait()之后,在obj对象上活跃的所有线程进入无期限等待。直到调用了该共享对象的 obj.notify() 方法进行了唤醒。
 *      而且唤醒后,会接着上一次调用wait()方法的位置继续向下执行。
 *
 * 7. obj.wait()方法调用之后,会释放之前占用的对象锁。
 *
 * 8. 关于notify和notifyAll方法:
 *      共享对象.notify(); 调用之后效果是什么?唤醒优先级最高的等待线程。如果优先级一样,则随机唤醒一个。
 *      共享对象.notifyAll(); 调用之后效果是什么?唤醒所有在该共享对象上等待的线程。
 *
 *
 * 新题目:
 * t1-->A
 * t2-->B
 * t3-->C
 * t1-->A
 * t2-->B
 * t3-->C
 * ....
 * t1-->A
 * t2-->B
 * t3-->C
 */


public class ThreadTest {
    public static void main(String[] args) {
        // 共享对象
        MyRunnable mr = new MyRunnable();

        // 两个线程
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

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

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

class MyRunnable implements Runnable {

    // 实例变量,多线程共享的。
    private int count = 0;

    private Object obj = new Object();

    @Override
    public void run() {
        while(true){
            //synchronized (this){
            synchronized (obj) {

                // 记得唤醒t1线程
                // t2线程执行过程中把t1唤醒了。但是由于t2仍然占用对象锁,所以即使t1醒了,也不会往下执行。
                //this.notify();
                obj.notify();

                if(count >= 100) break;
                // 模拟延迟
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 程序执行到这里count一定是小于100
                System.out.println(Thread.currentThread().getName() + "-->" + (++count));

                try {
                    // 让其中一个线程等待,这个等待的线程可能是t1,也可能是t2
                    // 假设是t1线程等待。
                    // t1线程进入无期限的等待,并且等待的时候,不占用对象锁。
                    //this.wait();
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果:

⑴.执行流程示例

假设 t1 线程先获取到 obj 对象的锁,进入同步代码块:

  1. t1 调用 obj.notify(),但此时没有线程在等待,所以此操作无实际效果。
  2. 检查 count 小于 100,继续执行。
  3. t1 休眠 50 毫秒。
  4. t1 输出 t1-->1,并将 count 递增为 1。
  5. t1 调用 obj.wait(),进入等待状态并释放 obj 对象的锁。

此时 t2 线程获取到 obj 对象的锁,进入同步代码块:

  1. t2 调用 obj.notify(),唤醒 t1 线程,但 t2 仍然持有 obj 对象的锁,所以 t1 无法立即执行。
  2. 检查 count 小于 100,继续执行。
  3. t2 休眠 50 毫秒。
  4. t2 输出 t2-->2,并将 count 递增为 2。
  5. t2 调用 obj.wait(),进入等待状态并释放 obj 对象的锁。

以此类推,两个线程交替执行,直到 count 达到 100 时,线程退出循环,结束执行。

⑵.wait()和sleep的区别?

2.实现三个线程交替输出

复制代码
* 新题目:
* t1-->A
* t2-->B
* t3-->C
* t1-->A
* t2-->B
* t3-->C
* ....
* t1-->A
* t2-->B
* t3-->C
*/
 /* 新题目:
 * t1-->A
 * t2-->B
 * t3-->C
 * t1-->A
 * t2-->B
 * t3-->C
 * ....
 * t1-->A
 * t2-->B
 * t3-->C
  */

public class ThreadTest {

    // 共享对象(t1 t2 t3线程共享的一个对象,都去争夺这一把锁)
    private static final Object lock = new Object();
    // 给一个初始值,这个初始值表示第一次输出的时候,t1先输出。
    private static boolean t1Output = true;
    private static boolean t2Output = false;
    private static boolean t3Output = false;

    public static void main(String[] args) {
        // 创建三个线程

        // t1线程:负责输出A
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    for (int i = 0; i < 10; i++) {
                        while(!t1Output){ // 只要不是t1线程输出
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        // 程序到这里说明:该t1线程输出了,并且t1线程被唤醒了。
                        System.out.println(Thread.currentThread().getName() + " ---> A");
                        // 该布尔标记的值
                        t1Output = false;
                        t2Output = true;
                        t3Output = false;
                        // 唤醒所有线程
                        lock.notifyAll();
                    }
                }
            }
        }).start();

        // t2线程:负责输出B
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    for (int i = 0; i < 10; i++) {
                        while(!t2Output){
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + " ---> B");
                        // 该布尔标记的值
                        t1Output = false;
                        t2Output = false;
                        t3Output = true;
                        // 唤醒所有线程
                        lock.notifyAll();
                    }
                }
            }
        }).start();

        // t3线程:负责输出C
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    for (int i = 0; i < 10; i++) {
                        while(!t3Output){
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + " ---> C");
                        // 该布尔标记的值
                        t1Output = true;
                        t2Output = false;
                        t3Output = false;
                        // 唤醒所有线程
                        lock.notifyAll();
                    }
                }
            }
        }).start();

    }
}

运行结果:

十四、懒汉式单例模式的线程安全问题

1.可重入锁(ReentrantLock)

⑴.可重入锁的概念及作用

可重入锁允许同一个线程在持有锁的情况下,再次进入被该锁保护的代码块。这意味着线程可以多次获取同一把锁而不会被阻塞,避免了因线程自己锁死自己而导致的死锁情况。同时,它也提高了代码的灵活性,例如在递归调用或者嵌套同步代码块的场景中,使用可重入锁可以让代码编写更加自然。

⑵.示例代码及详细解释

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        // 创建第一个线程
        Thread thread1 = new Thread(() -> {
            // 第一次获取锁
            lock.lock();
            try {
                System.out.println("Thread 1: 第一次获取锁");
                // 可重入,再次获取锁
                lock.lock();
                try {
                    System.out.println("Thread 1: 第二次获取锁");
                } finally {
                    // 释放第二次获取的锁
                    lock.unlock();
                    System.out.println("Thread 1: 释放第二次获取的锁");
                }
            } finally {
                // 释放第一次获取的锁
                lock.unlock();
                System.out.println("Thread 1: 释放第一次获取的锁");
            }
        });

        // 创建第二个线程
        Thread thread2 = new Thread(() -> {
            // 获取锁
            lock.lock();
            try {
                System.out.println("Thread 2: 获取锁");
            } finally {
                // 释放锁
                lock.unlock();
                System.out.println("Thread 2: 释放锁");
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();
    }
}

⑶.代码解释

ReentrantLock 对象的创建:private static final ReentrantLock lock = new ReentrantLock(); 创建了一个 ReentrantLock 类型的静态常量 lock,用于多个线程之间的同步。

thread1 线程:

首先调用 lock.lock() 第一次获取锁,并输出相应信息。

接着再次调用 lock.lock() 进行可重入操作,再次获取同一把锁,然后输出信息。

在嵌套的 try...finally 块中,先释放第二次获取的锁,再释放第一次获取的锁,确保锁的正确释放。

thread2 线程:

调用 lock.lock() 获取锁并输出信息。

在 try...finally 块中释放锁并输出信息。

线程启动:

通过 thread1.start() 和 thread2.start() 启动两个线程,它们会竞争 lock 锁。

⑷.ReentrantLock 与 synchronized 的区别

1**. 锁的获取和释放方式**

**ReentrantLock:**需要手动调用 lock() 方法获取锁,在 try...finally 块中手动调用 unlock() 方法释放锁,以确保锁最终会被释放,避免死锁。

synchronized:是 Java 内置的关键字,当线程进入被 synchronized 修饰的方法或代码块时自动获取锁,当线程退出该方法或代码块时自动释放锁,无需手动操作。

2. 灵活性

ReentrantLock: 提供了更多的灵活性,例如可以实现公平锁(ReentrantLock(true)),让线程按照请求锁的顺序依次获取锁;还可以使用 tryLock() 方法尝试获取锁,避免线程长时间阻塞。 **synchronized:**相对比较固定,缺乏这些额外的功能。

3. 性能

ReentrantLock:在高并发场景下,如果需要使用其特有的功能(如公平锁、尝试获取锁等),性能可能会优于 synchronized;但如果只是简单的同步需求,synchronized 经过 JVM 优化后性能也不错,并且 ReentrantLock 手动管理锁的过程可能会带来一定的性能开销。

synchronized:早期版本性能较差,但在 JDK 1.6 及以后进行了大量优化,引入了偏向锁、轻量级锁等机制,性能有了很大提升。

import java.util.concurrent.locks.ReentrantLock;

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){//如果为空,就不锁了,直接返回singleton
            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;
    }
}

十五、实现线程的另外两种方法

1.第三种方式:Callable实现线程

复制代码
实现线程的第三种方式:实现Callable接口,实现call方法。
* 这种方式实现的线程,是可以获取到线程返回值的。
/**
 * 实现线程的第三种方式:实现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() {

    }
}*/

运行结果:

import java.util.concurrent.*;

// 定义一个实现 Callable 接口的类,指定返回值类型为 Integer
class CallableDemo implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        // 计算 1 到 10 的整数和
        for (int i = 1; i <= 10; i++) {
            sum += i;
        }
        return sum;
    }
}

public class CallableWithoutThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个 Callable 任务实例
        CallableDemo callableTask = new CallableDemo();
        // 使用 FutureTask 包装 Callable 任务
        FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
        // 创建一个新线程,将 FutureTask 作为任务传递给线程
        Thread thread = new Thread(futureTask);
        // 启动线程,开始执行任务
        thread.start();

        try {
            // 调用 FutureTask 的 get() 方法获取任务的执行结果
            // 该方法会阻塞当前线程,直到任务执行完成并返回结果
            Integer result = futureTask.get();
            System.out.println("计算结果: " + result);
        } catch (InterruptedException | ExecutionException e) {
            // 处理可能出现的异常
            // InterruptedException 表示线程在等待结果时被中断
            // ExecutionException 表示任务执行过程中抛出了异常
            e.printStackTrace();
        }
    }
}

运行结果:

2.线程池实现线程

复制代码
* 创建线程的第四种方式:使用线程池技术。
* 线程池本质上就是一个缓存:cache
* 一般都是服务器在启动的时候,初始化线程池,
* 也就是说服务器在启动的时候,创建N多个线程对象,
* 直接放到线程池中,需要使用线程对象的时候,直接从线程池中获取。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 创建线程的第四种方式:使用线程池技术。
 * 线程池本质上就是一个缓存: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();
    }
}

运行结果:

相关推荐
m0_7482554122 分钟前
适用于IntelliJ IDEA 2024.1.2部署Tomcat的完整方法,以及笔者踩的坑,避免高血压,保姆级教程
java·tomcat·intellij-idea
奋斗的小方24 分钟前
Springboot基础篇(3):Bean管理
java·spring boot·后端
容器( ु⁎ᴗ_ᴗ⁎)ु.。oO29 分钟前
仿12306购票系统(3)
java·前端
Y_3_735 分钟前
30 分钟从零开始入门 CSS
开发语言·前端·css·人工智能·python·tensorflow
新时代丘鸣山38 分钟前
Idea java项目结构介绍
java·ide·intellij-idea
2403_875180951 小时前
AI数字人开发,引领科技新潮流
java·大数据·数据结构·人工智能·科技·前端框架
扫地僧0091 小时前
JVM 高级面试题及答案整理,最新面试题
java·jvm·面试
2401_882726481 小时前
组态软件在物联网中的应用
java·物联网·struts·低代码·web
johnny2332 小时前
Locale+Jackson导致Controller接口StackOverflowError异常解决
java
weixin_399264292 小时前
QT C++ QtConcurrent::run 异步任务 简单例子
开发语言·c++