跳至主要內容

②JAVA 并发编程基础

holic-x...大约 33 分钟JAVA基础

②JAVA 并发编程基础

学习核心

  • 进程 VS 线程
    • 两者的概念和区别
  • 线程优先级、状态(线程运行分析)
  • 线程的创建、中断、终止
    • 1)线程创建的方式
    • 2)线程中断
    • 3)线程终止
  • 线程的基本用法(todo)
    • 线程休眠
    • 线程礼让
    • 终止线程
    • 守护线程
  • 线程间的通信和同步
  • ThreadLocal
    • 1)对ThrealLocal的理解和应用
    • 2)ThreadLocal为什么会造成内存泄漏?如何解决?

学习资料

线程基础

1.线程的基本概念

核心概念

常见概念对比

  • 单线程 VS 多线程

    • 单线程编程:顺序执行流,程序从main方法开始执行,依次从上往下执行,如果遇到了阻塞程序将会在该处停滞
    • 多线程编程:实际情况单线程的程序功能有限,需要通过多线程解决(例如在执行一个任务,这个任务被拆分为多个子任务,子任务之间的执行应该不会干扰,则可引入多线程并发执行任务,提升任务执行效率)
  • 并发 VS 并行 VS 串行

    • 并发:同一单位时间内,多个任务在同一个CPU执行(轮流执行不同的时间切片)
    • 并行:同一时刻,多个任务在不同的CPU上执行,是真正意义上的"同时执行"
    • 串行:多个任务按一定顺序执行

进程 VS 线程

  • 操作系统层面理解

    • 进程:一个在内存中独立运行的应用程序(是操作系统资源分配的基本单位)

    • 线程:进程中的一个执行任务(控制单元),负责在程序中独立执行(是操作系统资源调度的基本单位)

​ 一个进程中可以有多个线程,多个线程可共享数据。

  • Java程序层面理解
    • 启动一个Java程序,操作系统就会创建一个Java进程
    • 在一个进程中可以创建多个线程;在一个Java程序中可以自定义创建多个线程,这些线程拥有各自独立的计数器、堆栈、局部变量等属性,并且能够访问共享的内存变量

线程优先级

​ 操作系统基本采用时间分片的形式来分配处理器资源给线程运行,一个线程如果用完了一个时间片就会发生线程调度,即便线程还没执行完也需要等待下一次分配。所以线程能获得越多的时间片分配,也就能更多的使用处理器资源,而优先级就是一个可以指定线程应该多分还是少分时间片的属性。

​ Java线程可以通过setPriority()方法来设置优先级,默认是5,而可以设置的范围是1-10。针对频繁阻塞(频繁休眠或者IO操作较多)的线程应该 设置高优先级,而计算较多,耗CPU的线程则应该设置更低的优先级,避免处理器被独占。

线程的状态(生命周期)

Java中的线程状态在java.lang.Thread.State中有相应定义,源码参考如下所示

public class Thread {    
    public enum State {   
        /* 新建 */
        NEW , 

        /* 可运行状态 */
        RUNNABLE , 

        /* 阻塞状态 */
        BLOCKED , 

        /* 无限等待状态 */
        WAITING , 

        /* 计时等待 */
        TIMED_WAITING , 

        /* 终止 */
        TERMINATED;    
	}
    
    // 获取当前线程的状态
    public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }
}
  • 新建 (NEW):初始状态,线程被创建,但还没调用start()方法
  • 运行状态(RUNNABLE):Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
  • 阻塞状态(BLOCKED):表示线程阻塞于锁
  • 等待状态(WAITING):表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些等待动作(通知或中断)
  • 超时等待状态(TIME WAITING):该状态不同于WAITING状态,它可以在指定的时间自行返回
  • 终止状态(TERMINATED):表示当前线程已经执行完毕。
线程状态具体含义
NEW【初始状态|开始状态】一个尚未启动的线程的状态。线程刚被创建,但是还没调用start方法,并未启动。MyThread t = new MyThread()只有线程象,没有线程特征
RUNNABLE当调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,这个中间状态被称为可执行状态(RUNNABLE)即其具备执行的资格,但是并没有真正的执行起来而是在等待CPU调度
BLOCKED当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;
当该线程持有锁时,该线程将变成Runnable状态
WAITING【等待状态】一个正在等待的线程的状态,处于等待状态的线程正在等待其他线程去执行一个特定的操作
造成线程等待的原因有两种:分别为调用Object.wait()、join()方法
TIMED_WAITING【限时等待状态】一个在限定时间内等待的线程的状态。
造成线程限时等待状态的原因有三种:分别为调用Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED一个完全运行完成的线程的状态。也称之为终止状态、结束状态

​ 阻塞状态BLOCKED是线程阻塞在进入synchronized关键字修饰的方法或代码块时(获取锁)的状态,但是如果是阻塞在java.concurrent包中的Lock接口的线程状态却是等待状态WAITING,因为Lock接口的阻塞实现均使用的是LockSupport类中的相关方法;

image-20240526211022948

2.线程的创建

创建线程的方式

  • 1)继承Thread类
  • 2)实现Runnable接口
  • 3)实现Callable接口

​ 其中实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

一般场景下选择实现接口的方式创建线程,其具备以下优势:

  • Java是单继承机制:继承了Thread类就无法继承其他类,但是可以实现多个接口扩展功能
  • 继承Thread类开销过大,可能类只要求可执行即可

✨继承Thread类

​ 继承Thread类,重写run方法,调用start方法启动线程

class MyThread1 extends Thread {
    /**
     * 线程创建方式1:
     * a.继承Thread类
     * b.重写相应的run方法
     * c.调用start方法启动相应的线程
     */
    @Override
    public void run() {
        while(true){
            System.out.println("方式1创建线程......");
        }
    }
}

public class CreateThreadDemo1 {
    public static void main(String[] args) {
        // 方式1:继承Thread类(此类线程启动直接调用start方法)
        // 通过new关键字创建线程,此时线程ct1处于“新建new”状态
        MyThread1 mt = new MyThread1();
        // 线程ct1调用start方法,此时线程不是直接运行,而是处于“就绪ready”状态
        mt.start();
    }
}

✨实现Runnable接口

​ 实现Runnable接口,重写run方法,调用start方法启动线程

class MyThread2 implements Runnable {
    /**
     * 线程创建方式2:
     * a.实现Runnable接口
     * b.重写run方法
     * c.调用start方法启动线程(需要通过Thread进行封装)
     */
    @Override
    public void run() {
        while (true) {
            System.out.println("方式2创建线程......");
        }
    }
}

public class CreateThreadDemo2 {
    public static void main(String[] args) {
        // 方式2:实现Runnable接口(此类线程不能直接调用start方法,需要通过Thread类进行封装再调用start方法启动线程)
        MyThread2 mt = new MyThread2();
        Thread thread = new Thread(mt);
        thread.start();
    }
}

✨实现Callable接口

​ 实现Callable接口(从任务中产生返回值)

class MyThread3 implements Callable<String> {
    /**
     * 线程创建方式3:基于Callable
     * a.实现Callable接口
     * b.重写call方法
     * c.借助FutureTask对象创建间Thread对象,随后启动线程
     * d.调用get方法获取线程结束之后的结果
     */
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println("方式3:借助Callable创建线程" + i);
        }
        return "call线程";
    }
}

public class CreateThreadDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 线程开启之后需要执行里面的call方法
        MyThread3 mt = new MyThread3();
        // 构建FutureTask对象
        FutureTask<String> ft = new FutureTask<>(mt);
        //创建线程对象
        Thread t = new Thread(ft);
        // 开启线程
        t.start();
        System.out.println(ft.get());
    }
}

问题思考:线程调用start方法之后会如何?

​ 线程的启动主要基于CPU内部的调度(涉及CPU时间片的概念),此处案例执行结果显示,每次执行的线程顺序都有所不同,每个线程交替执行,一个线程执行一段时间之后便将CPU交付给下一个线程执行以此类推

public class ThreadStartDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("线程111111");
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("线程222222");
                }
            }
        }).start();
    }
}

// output
线程111111
线程111111
线程111111
线程222222
线程222222
线程222222
线程111111
线程111111
线程111111

问题思考: 一个线程两次调用start方法会怎样?

​ Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误

public class IllegalThreadStateExceptionDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {

            }
        });
        thread.start(); // 启动线程
        thread.start(); // 不允许,会抛出IllegalThreadStateException
    }
}
// output
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	at com.noob.multiThread.threadBase.IllegalThreadStateExceptionDemo.main(IllegalThreadStateExceptionDemo.java:12)

问题思考: 可以直接调用Thread类的run方法吗

  • 可以。但是如果直接调用 Threadrun 方法,它的行为就会和普通的方法一样。
  • 为了在新的线程中执行我们的代码,必须使用 Threadstart 方法

3.线程的基本用法

线程基本方法

方法描述
run线程的执行实体
start线程的启动方法
currentThread返回对当前正在执行的线程对象的引用
setName设置线程名称
getName获取线程名称
setPriority设置线程优先级。Java 中的线程优先级的范围是 [1,10],一般来说,高优先级的线程在运行时会具有优先权。可以通过 thread.setPriority(Thread.MAX_PRIORITY) 的方式设置,默认优先级为 5
getPriority获取线程优先级
setDaemon设置线程为守护线程
isDaemon判断线程是否为守护线程
isAlive判断线程是否启动
interrupt中断另一个线程的运行状态
interrupted测试当前线程是否已被中断。通过此方法可以清除线程的中断状态。换句话说,如果要连续调用此方法两次,则第二次调用将返回 false(除非当前线程在第一次调用清除其中断状态之后且在第二次调用检查其状态之前再次中断)
join可以使一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行
Thread.sleep静态方法。将当前正在执行的线程休眠
Thread.yield静态方法。将当前正在执行的线程暂停,让其他线程执行

✨线程休眠(sleep)

Thread.sleep 使当前正在执行的线程进入休眠状态

使用 Thread.sleep 需要向其传入一个整数值,这个值表示线程将要休眠的毫秒数。

Thread.sleep 方法可能会抛出 InterruptedException,因为异常不能跨线程传播回 main 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理

public class ThreadSleepDemo {

    public static void main(String[] args) {
        new Thread(new MyThread("线程A", 500)).start();
        new Thread(new MyThread("线程B", 1000)).start();
        new Thread(new MyThread("线程C", 1500)).start();
    }

    static class MyThread implements Runnable {

        /** 线程名称 */
        private String name;

        /** 休眠时间 */
        private int time;

        private MyThread(String name, int time) {
            this.name = name;
            this.time = time;
        }

        @Override
        public void run() {
            try {
                // 休眠指定的时间
                Thread.sleep(this.time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.name + "休眠" + this.time + "毫秒。");
        }

    }

}
// output
线程A休眠500毫秒。
线程B休眠1000毫秒。
线程C休眠1500毫秒。

✨线程礼让(yield)

Thread.yield 方法的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行

该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

// 线程礼让 demo
public class ThreadYieldDemo {

    public static void main(String[] args) {
        MyThread t = new MyThread();
        new Thread(t, "线程A").start();
        new Thread(t, "线程B").start();
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "运行,i = " + i);
                if (i == 2) {
                    System.out.print("线程礼让:");
                    Thread.yield();
                }
            }
        }
    }
}

// output
线程A运行,i = 0
线程B运行,i = 0
线程B运行,i = 1
线程A运行,i = 1
线程B运行,i = 2
线程礼让:线程A运行,i = 2
线程礼让:线程B运行,i = 3
线程A运行,i = 3
线程B运行,i = 4
线程A运行,i = 4

✨终止线程

​ Thread中的stop\suspend\resume方法已经被Java废弃,不建议使用

Thread 中的 stop 方法有缺陷,已废弃

​ 使用 Thread.stop 停止线程会导致它解锁所有已锁定的监视器(由于未经检查的 ThreadDeath 异常会在堆栈中传播,这是自然的结果)。 如果先前由这些监视器保护的任何对象处于不一致状态,则损坏的对象将对其他线程可见,从而可能导致任意行为。

​ stop() 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了。Thread.stop 的许多用法应由仅修改某些变量以指示目标线程应停止运行的代码代替。 目标线程应定期检查此变量,如果该变量指示要停止运行,则应按有序方式从其运行方法返回。如果目标线程等待很长时间(例如,在条件变量上),则应使用中断方法来中断等待。

​ 当一个线程运行时,另一个线程可以直接通过 interrupt 方法中断其运行状态

public class ThreadInterruptDemo {

    public static void main(String[] args) {
        MyThread mt = new MyThread(); // 实例化Runnable子类对象
        Thread t = new Thread(mt, "线程"); // 实例化Thread对象
        t.start(); // 启动线程
        try {
            Thread.sleep(2000); // 线程休眠2秒
        } catch (InterruptedException e) {
            System.out.println("3、main线程休眠被终止");
        }
        t.interrupt(); // 中断线程执行
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            System.out.println("1、进入run()方法");
            try {
                Thread.sleep(10000); // 线程休眠10秒
                System.out.println("2、已经完成了休眠");
            } catch (InterruptedException e) {
                System.out.println("3、MyThread线程休眠被终止");
                return; // 返回调用处
            }
            System.out.println("4、run()方法正常结束");
        }
    }
}

​ 如果一个线程的 run 方法执行一个无限循环,并且没有执行 sleep 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt 方法就无法使线程提前结束。

​ 但是调用 interrupt 方法会设置线程的中断标记,此时调用 interrupted 方法会返回 true。因此可以在循环体中使用 interrupted 方法来判断线程是否处于中断状态,从而提前结束线程

安全地终止线程有两种方法:

  • 定义 volatile 标志位,在 run 方法中使用标志位控制线程终止
  • 使用 interrupt 方法和 Thread.interrupted 方法配合使用来控制线程终止

【示例】使用 volatile 标志位控制线程终止

public class ThreadStopDemo2 {

    public static void main(String[] args) throws Exception {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "MyTask");
        thread.start();
        TimeUnit.MILLISECONDS.sleep(50);
        task.cancel();
    }

    private static class MyTask implements Runnable {

        private volatile boolean flag = true;

        private volatile long count = 0L;

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " 线程启动");
            while (flag) {
                System.out.println(count++);
            }
            System.out.println(Thread.currentThread().getName() + " 线程终止");
        }

        /**
         * 通过 volatile 标志位来控制线程终止
         */
        public void cancel() {
            flag = false;
        }

    }

}

【示例】使用 interrupt 方法和 Thread.interrupted 方法配合使用来控制线程终止

public class ThreadStopDemo3 {

    public static void main(String[] args) throws Exception {
        MyTask task = new MyTask();
        Thread thread = new Thread(task, "MyTask");
        thread.start();
        TimeUnit.MILLISECONDS.sleep(50);
        thread.interrupt();
    }

    private static class MyTask implements Runnable {

        private volatile long count = 0L;

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " 线程启动");
            // 通过 Thread.interrupted 和 interrupt 配合来控制线程终止
            while (!Thread.interrupted()) {
                System.out.println(count++);
            }
            System.out.println(Thread.currentThread().getName() + " 线程终止");
        }
    }
}

✨守护线程

什么是守护线程?

  • 守护线程(Daemon Thread)是在后台执行并且不会阻止 JVM 终止的线程当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程
  • 与守护线程(Daemon Thread)相反的,叫用户线程(User Thread),也就是非守护线程。

为什么需要守护线程?

  • 守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。典型的应用就是垃圾回收器。

如何使用守护线程?

  • 可以使用 isDaemon 方法判断线程是否为守护线程
  • 可以使用setDaemon 方法设置线程为守护线程
    • 正在运行的用户线程无法设置为守护线程,所以 setDaemon 必须在 thread.start 方法之前设置,否则会抛出 llegalThreadStateException 异常;
    • 一个守护线程创建的子线程依然是守护线程。
    • 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑
public class ThreadDaemonDemo {

    public static void main(String[] args) {
        Thread t = new Thread(new MyThread(), "线程");
        t.setDaemon(true); // 此线程在后台运行
        System.out.println("线程 t 是否是守护进程:" + t.isDaemon());
        t.start(); // 启动线程
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + "在运行。");
            }
        }
    }
}

4.线程中断和终止

✨理解线程中断

​ 中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。其他线程通过调用该线程的interrupt方法进行中断操作。反过来,线程通过isInterrupt()方法来判断自己是否被执行的中断操作,并做出响应。

while (!Thread.currentThread().isInterrupted()&& more work to do)
{
    do more work
}

✨InterruptedException

​ 如果 sleep、wait 等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常。在抛出InterruptException之前,Java虚拟机会将该线程的中断标记位清除。然后抛出InterruptException,此时调用isInterrunpted()方法将会返回false。

class MyThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(20000);
            System.out.println("MyThread run");
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("catch:" + Thread.currentThread().isInterrupted());
        }

    }
}

/**
 * InterruptedException demo
 */
public class InterruptedExceptionDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        thread.start();
        TimeUnit.SECONDS.sleep(2);
        thread.interrupt();
    }
}

// output
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.noob.multiThread.threadBase.MyThread.run(InterruptedExceptionDemo.java:10)
catch:false

✨为什么不应该强制停止?

​ 基于中断的停止模式:A线程如果想要停止B线程,则A 线程需要给B线程发送一个中断信号,B线程通过中断标志位判断自己是否有被中断,最后自行决定如何响应中断(例如:可以停止,可以延后,也可以直接忽略)

​ 为什么要让线程拥有响应中断的自主权,而不可以强制停止呢?(例如为什么不让A强制停止B)

​ 可以设想一下,很多工作并不是可以贸然停止的,例如A想要停止B时,B线程正在写入一个文件,可能文件正写入一半,如果立即停止那数据就是不完整的。但是对于A来说,它是感知不到B进行到什么阶段的,所以也没办法选择一个最佳时机来停止B,要想安全稳妥的停止B线程,确实只能B线程自行决策

​ 再看几种停止线程的错误方法。比如 stop(),suspend()和 resume(),这些方法已经被 Java 直接标记为 @Deprecated。如果再调用这些方法,IDE 会友好地提示"不应该再使用它们"。是因为 stop()会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题

✨interrupted和isInterrupted方法的区别?

​ interrupt:用于中断线程。调用该方法的线程的状态为将被置为“中断“状态。

​ 注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。

​ interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。

​ isInterrupted也是可以返回当前中断信号是true还是false,与interrupted最大的差别是并不会清除中断信号。

5.线程之间的通信和同步

​ 在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步。

​ 通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

线程之间的通信:共享内存、消息传递

​ 现实的多线程模式下,每一个线程并不是独立的执行就完事了,更常见的是线程之间需要相互协作才能更好的完成一项任务。协作的过程中,就免不了线程之间需要相互通信。

​ 以下简单介绍Java提供的一些支持线程间通信的机制

(1)volatile:引入volatile关键字修饰字段解决共享变量的可见性问题

​ 当两个线程A,B共同使用一个普通共享变量的时候,线程A对变量进行了修改,另外一个线程B是不能保证一定能看到最新值的,这就导致了线程之间的可见性问题,并发程序基于此运行是会出错的。为了解决这个问题,Java提供了Volatile关键字,被该关键字修饰的变量不会存在B线程读不到最新值的情况

(2)synchronized:引入synchronized关键字修饰方法或代码块解决共享变量的可见性问题

​ 它可以修饰方法或者同步块被修饰之后能够确保同一时间只有一个线程可以处于方法或者同步块中,所以在方法和同步块中去访问共享变量,可以保证可见性和排他性

(3)wait/notify:提供“等待/通知”的机制来进行线程间的协作运行

理解通知机制:例如A,B两个人此时需要协作完成“文档编写“和“文档打印“工作。A负责“编写”,B负责”打印”。很明显,这两个工作存在关联性,文档只有先编写完成才能去打印。 此时对于B来说可以采用多种工作方式,可以感受下哪种是最合适的。

  • 方式1:B不停的询问A写好了没,写好了就去打印了;(分析:B不仅很累也可能会被A打)
  • 方式2:B每间隔10分钟去问次A,好了没;(分析:极端情况会存在10分钟的延时)
  • 方式3:B先去睡一觉,等A写完了叫醒B,再去打印;(分析:最及时也最舒服的)

Java的等待通知机制:将上面案例中的AB换成线程去理解。java在的等待/通知方法被定义在了0bject类上

​ 一个线程B调用了对象O的wait()方法进入等待状态WAITING或者TIMED WAITING,而另外一个线程A调用了对象O的的notify或者notifyAlI方法,线程B从wait方法返回,然后执行后续操作。

  • notify:通知一个在对象上等待的线程
  • notifyAll:通知所有等待在该对象上的线程
  • wait():线程调用该方法进入等待(WAITING)状态,返回需要等待另外的线程通知或者被中断,另外注意线程调用wait方法后会释放对象的锁(能调用wait方法的前提也是获取到了对象的锁)
  • wait(long):线程调用之后会进入超时等待(TIMED_WAITING)状态,多一种返回方式,就是如果没有通知,也会在等待n毫秒后返回
  • wait(long,int):超时时间更细,到纳秒

(4)Thread.join():A B两线程,A调用B.join(),表示A需要等待B完全执行完成,才会从B.join()处返回继续执行。当然也支持join(long)和 join(long,int)两种超时返回。调用join后,A线程会处于等待(WAITING)或 超时等待(TIMED_WAITING)状态

✨案例分析

生产者、消费者模式是 waitnotifynotifyAll 的一个经典使用案例

public class ProductConsumerDemo {

    private static final int QUEUE_SIZE = 10;
    private static final PriorityQueue<Integer> queue = new PriorityQueue<>(QUEUE_SIZE);

    public static void main(String[] args) {
        new Producer("生产者A").start();
        new Producer("生产者B").start();
        new Consumer("消费者A").start();
        new Consumer("消费者B").start();
    }

    static class Consumer extends Thread {

        Consumer(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == 0) {
                        try {
                            System.out.println("队列空,等待数据");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notifyAll();
                        }
                    }
                    queue.poll(); // 每次移走队首元素
                    queue.notifyAll();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 从队列取走一个元素,队列当前有:" + queue.size() + "个元素");
                }
            }
        }
    }

    static class Producer extends Thread {

        Producer(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == QUEUE_SIZE) {
                        try {
                            System.out.println("队列满,等待有空余空间");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notifyAll();
                        }
                    }
                    queue.offer(1); // 每次插入一个元素
                    queue.notifyAll();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 向队列取中插入一个元素,队列当前有:" + queue.size() + "个元素");
                }
            }
        }
    }
}

join

​ 在线程操作中,可以使用 join 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行

public class ThreadJoinDemo {

    public static void main(String[] args) {
        MyThread mt = new MyThread(); // 实例化Runnable子类对象
        Thread t = new Thread(mt, "mythread"); // 实例化Thread对象
        t.start(); // 启动线程
        for (int i = 0; i < 50; i++) {
            if (i > 10) {
                try {
                    t.join(); // 线程强制运行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Main 线程运行 --> " + i);
        }
    }

    static class MyThread implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 50; i++) {
                System.out.println(Thread.currentThread().getName() + " 运行,i = " + i); // 取得当前线程的名字
            }
        }
    }
}

管道

​ 管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。 管道输入/输出流主要包括了如下 4 种具体实现:PipedOutputStreamPipedInputStreamPipedReaderPipedWriter,前两种面向字节,而后两种面向字符

public class Piped {

    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();
        int receive = 0;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }

    static class Print implements Runnable {

        private PipedReader in;

        Print(PipedReader in) {
            this.in = in;
        }

        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

// output
控制台输入数据\打印数据

6.ThreadLocal

✨什么是ThreadLocal?

​ ThreadLocal叫线程变量(ThreadLocal中的变量属于当前线程(是当前线程独有的变量),该变量对其他线程是隔离的)。ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。

存储结构以ThreadLocal对象为Key,任意对象为值,其底层是在线程里维护了一个和Map类似的结构,它的key就是各种ThreadLocal对象,当一个Key-Value值被存储之后,会一直附带在线程上,所以可以在线程执行的任何位置再通过这个ThreadLocal对象取到存入的一个值。

​ 设定或修改值的方式是SET(T)、获取值的方式是get(),key默认就是对应的ThreadLocal对象,在调用方法的时候不需要指定key值

案例分析:通过threadLocal 实现打印一段代码的运行时间

// 通过ThreadLocal实现打印一段代码的运行时间
class PrintCodeRunTime{

    // 1.创建一个ThreadLocal对象
    private static final ThreadLocal<Long> TIME_THREADLOCAL_OBJECT = new ThreadLocal<>();

    // 2.启动计时
    public static final void begin(){
        // set方法的key默认是TIME_THREADLOCAL_OBJECT对象,只需要设置value值
        TIME_THREADLOCAL_OBJECT.set(System.currentTimeMillis());
    }

    // 3.结束计时
    public static final long end(){
        // get方法无需指定key,TIME_THREADLOCAL_OBJECT对象会作为key
        long duration = System.currentTimeMillis() - TIME_THREADLOCAL_OBJECT.get();
        // 返回运行时间
        return duration;
    }
}

public class ThreadLocalPrintTimeDemo {
    public static void main(String[] args) throws InterruptedException {
        // 测试自定义基于ThreadLocal的计时器
        PrintCodeRunTime.begin();
        TimeUnit.SECONDS.sleep(2);
        long duration = PrintCodeRunTime.end();
        System.out.println("系统运行时间:" + duration + "毫秒");
    }
}

✨ThreadLocal原理分析

​ 一般在使用ThreadLocal的时候都是为了解决线程中存在的变量竞争问题。解决这类问题,通常也会使用synchronized来加锁解决

案例分析:SimpleDateFormat的线程安全问题

​ 例如在解决SimpleDateFormat的线程安全的时候。SimpleDateFormat是非线程安全的,它里面无论的是format()方法还是parse()方法,都有使用它自己内部的一个Calendar类的对象,format方法是设置时间,parse()方法里面是先调用Calendar的clear()方法,然后又调用了Calendar的set()方法(赋值),如果一个线程刚调用了set()进行赋值,这个时候又来了一个线程直接调用了clear()方法,那么这个parse()方法执行的结果就会有问题的。

解决办法1:将使用SimpleDateformat的方法加上synchronized,这样虽然保证了线程安全,但却降低了效率,同一时间只有一个线程能使用格式化时间的方法。

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static synchronized String formatDate(Date date){
    return simpleDateFormat.format(date);
}

解决办法2:将SimpleDateFormat的对象,放到ThreadLocal里面,这样每个线程中都有一个自己的格式对象的副本了。互不干扰,从而保证了线程安全。

private static final ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static String formatDate(Date date){
   return simpleDateFormatThreadLocal.get().format(date);
原理分析

ThreadLocal使用

ThreadLocal<Integer> threadLocal99 = new ThreadLocal<Integer>();
threadLocal99.set(3);
int num = threadLocal99.get();
System.out.println("数字:"+num);
threadLocal99.remove();
System.out.println("数字Empty:"+threadLocal99.get());

// output
数字:3
数字Empty:null

​ 将变量放到ThreadLocal里面,在线程执行过程中就可以取到,当执行完成后在remove掉,只要没有调用remove()当前线程在执行过程中都是可以拿到变量数据的。 因为是放到了当前执行的线程中,所以ThreadLocal中的变量值只能当前线程来使用,从而保证的了线程安全(当前线程的子线程其实也是可以获取到的)

ThreadLocal原理分析

(1)通过ThreadLocalMap存储:key为ThreadLocal对象,value为对应存储的变量

​ ThreadLocalMap是ThreadLocal的一个内部类,它的结构和Map类似(内部是一个Entry数组)

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {

        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    ......
}

​ ThreadLocalMap内部其实是一个Entry数组,Entry是ThreadLocalMap中的一个内部类,继承自WeakReference,并将ThreadLocal类型的对象设置为了Entry的Key,以及对Key设置成弱引用。 ThreadLocalMap的内部数据结构:基于key,value组成的Entry的数组集合(结构和map类似)

image-20240526222703542

​ 它和真正的Map还是有区别的,没有链表,这样在解决key的hash冲突的时候措施肯定就和HashMap不一样了。 一个线程中是可以创建多个ThreadLocal对象的,多个ThreadLocal对象就会存放多个数据,那么在ThreadLocalMap中就会以数组的形式存放这些数据。

(2)ThreadLocal的set方法

public void set(T value) {
   // 获取当前线程
   Thread t = Thread.currentThread();
   // 获取ThreadLocalMap
   ThreadLocal.ThreadLocalMap map = getMap(t);
   // ThreadLocalMap 对象是否为空,不为空则直接将数据放入到ThreadLocalMap中
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value); // ThreadLocalMap对象为空,则先创建对象,再赋值。
}

// map.set(this, value);
private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

处理逻辑分析:

  • 获取数据在当前数组中的位置

  • 判断当前位置是否为空

    • 如果为空:初始化一个Entry对象放到当前位置

    • 如果不为空:判断当前Entry中的key是否和传入的key一致

      • 如果一致:覆盖当前位置的数据
      • 如果不一致:则继续找下一个空位置,然后将数据存放到空位置(数据超出长度后会进行扩容)

(3)get方法分析

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

// map.getEntry(this)    
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}   

// getEntryAfterMiss(key, i, e);
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

​ 在get的时候也是类似的逻辑,先通过传入的ThreadLocal的hashcode获取在Entry数组中的位置,然后拿当前位置的Entry的Key和传入的ThreadLocal对比,相等的话,直接把数据返回,如果不相等就去判断和数组中的下一个值的key是否相等

ThreadLocal的数据保存

​ ThreadLocal是保存在单个线程中的数据,每个线程都有自己的数据,但是实际ThreadLocal里面的真正的对象数据,其实是保存在堆里面的,而线程里面只是存储了对象的引用而已。 并且在使用的时候通常需要在上一个线程执行的方法的上下文共享ThreadLocal中的变量。

​ 例如主线程是在某个方法中执行代码,但是这个方法中有一段代码时新创建了一个线程,在这个线程里面还使用了这个正在执行的方法里面的定义的ThreadLocal里面的变量。这个时候,就是需要从新线程里面调用外面线程的数据,这个就需要线程间共享了。这种子父线程共享数据的情况,ThreadLocal也是支持的

ThreadLocal threadLocalMain = new InheritableThreadLocal();
 threadLocalMain.set("主线程变量");
 Thread t = new Thread() {
     @Override
     public void run() {
         super.run();
         System.out.println( "现在获取的变量是 =" + threadLocalMain.get());
     }
 };
 t.start();

// output
现在获取的变量是 =主线程变量

​ 上面这样的代码就能实现子父线程共享数据的情况,重点是使用InheritableThreadLocal来实现的共享。 那么它是怎么实现数据共享的呢? 在Thread类的init()方法中有这么一段代码:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

​ 这段代码的意思是,在创建线程的时候,如果当前线程的inheritThreadLocals变量和父线程的inheritThreadLocals变量都不为空的时候,会将父线程的inheritThreadLocals变量中的数据,赋给当前线程中的inheritThreadLocals变量。

✨为什么ThreadLocal会造成内存泄漏?如何解决?

​ ThreadLocal中的ThreadLocalMap里面的Entry对象是继承自WeakReference类的,说明Entry的key是一个弱引用

​ 弱引用是用来描述那些非必须的对象,弱引用的对象,只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

​ ThreadLocalMap中的key为ThreadLocal的弱引用,value为强引用。如果ThreadLocal对象没有被外部强引用,垃圾回收时key会被清理掉,但value不会。这时key=null,而value不为null,如果不做处理,value将永远不会被GC掉,就有可能发生内存泄漏。

​ ThreadLocalMap的实现中考虑了这个问题,在调用get/set/remove时会清理掉key为null的entry。在编程时如果意识到当前编写的run方法里不再会使用ThreadLocal对象了,最好手动调用remove把key和value的空间都释放了

问题思考:既然容易产生内存泄漏,为什么还要设置成弱引用的呢?

​ 如果正常情况下应该是强引用,但是强引用只要引用关系还在就一直不会被回收,所以如果线程被复用了,那么Entry中的Key和Value都不会被回收,这样就造成了Key和Value都会发生内存泄漏了;

​ 但是设置成弱引用,当ThreadLocal对象,没有被强引用后,就会被回收,回收后,Entry中的key就会被设置成null了,如果Thread被重复使用,只要还会用ThreadLocal存储数据,那么就会调用ThreadLocal的,set、get等方法,在调用set、get、等方法的时候,是会扫描Entry中key为null的数据的。 当发现Entry中,有key为null的数据时,会将value也设置为null,这样就将value的值也进行了回收,能进一步防止内存泄漏了,并且在进行rehash的时候,也是先清除掉key是null的数据后,如果空间还不够,才进行扩容的。

​ 但是虽然将key设置了弱引用,但是如果一个线程被重复利用,执行完任务后,再也不使用ThreadLocal了,那么最后value值会一直存在,最终也是会导致内存泄漏的,所以使用ThreadLocal的时候,最后一定要执行remove()方法,确保吧key、value空间释放

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3