(Java线程)- 线程基础 | “朝闻道”知识分享大赛

头发保护计划 2022-10-28 19:04:48

这是我参加“朝闻道”知识分享大赛第1篇文章

线程机制

1.1线程是什么

​ 在早期的操作系统中,执行任务被抽象为进程,进程也是操作系统运行和调度的基本单元,但它的资源开销较大,因此人们在进程的基础上提出了线程。线程是进程里的运行单位,可以看作是轻量级的进程。CPU会按某种策略为每个线程分配一定的时间片去执行。

​ 进程是指程序的一次动态执行过程,计算机中正在执行的程序就是进程,每个程序都会各自对应着一个进程。一个进程包含了从代码加载到执行完成的一个完整过程,是操作系统资源分配的最小单元。

​ 线程则是比进程更小的执行单位,是CPU调度和分配的基本单位。每个进程都至少拥有一个线程,而一个线程却只能属于一个进程。线程可以对所属进程的所有资源进行调度和运算。线程既可以由操作系统内核来控制调度,也可以由用户程序来控制调度。

package Chapter1;

//线程的不同创建方式
public class JavaThreadTest {
    public static void main(String[] args) {
        //分别输出三个线程的名称
        System.out.println(Thread.currentThread().getName());
        new MyThread().start();
        new Thread(new MyThread2()).start();
    }
//直接继承java.lang.Thread类
    static class MyThread extends Thread{
        public void run(){
            System.out.println(Thread.currentThread().getName());
        }
    }
//实现java.lang.Runnable接口
    static class MyThread2 implements Runnable{
        public void run(){
            System.out.println(Thread.currentThread().getName());
        }
    }
}

1.2线程的映射

​ 现代计算机大体可以分为硬件和软件两大块。硬件是基础,而软件是运行在硬件之上的程序。其中软件可以分为操作系统和应用程序:操作系统专注于对硬件的交互管理并提供一个运行环境给应用程序使用;应用程序则是能实现若干功能且运行在操作系统环境中的软件。现代计算机的结构如图

​ Java语言编译后的字节码运行在JVM上,而JVM的本质就是一个进程,所以Java属于应用程序层。我们在Java层通过new关键词创建一个Thread对象,然后调用start方法启动该线程,在线程角度看就涉及Java层线程,JVM层线程,操作系统层线程。JVM主要由C/C++实现,所以Java层线程最终还是要映射至JVM层线程。

​ 线程按照操作系统和应用程序两个层次可以分为内核线程用户线程。所谓内核线程,就是直接由操作系统内核支持和管理的线程,线程的建立、启动、同步、销毁、切换等都由内核完成。而用户线程则是线程的管理工作在用户空间完成,它完全建立在用户空间的线程库上,由内核提供支持但不由内核管理,内核也无法感知到用户线程的存在。用户线程的建立、启动、同步、销毁、切换都在用户空间完成,无须切换到内核。

​ 我们可以将用户线程看成更高层面的线程,而内核线程则向用户线程提供支持。由此可以得出,用户线程和内核线程之间必然存在着一定的映射关系,不同的操作系统可能采取不同的映射方式,一般包括多对一映射(用户级方式)、一对一映射(内核级方式)和多对多映射(组合方式)这3种映射方式。

1.2.1多对一映射

​ 多对一映射就是多个用户线程被映射到一个内核线程上。每个进程都对应着一个内核线程,进程内的所有线程也都对应着该内核线程。如图所示

​ 多对一映射可在不支持线程的操作系统中由库来实现线程机制,用户线程创建、销毁、切换的代价比内核线程的小。但它也存在一个较大的风险,那就是当进程内的某个线程发生系统阻塞时将导致该进程中的所有线程都被阻塞。

1.2.2一对一映射

​ 一对一映射就是每个用户线程都被映射到一个内核线程上,用户线程的整个生命周期都绑定到所映射的内核线程上。在这种映射方式下,多个CPU能够并行执行同一个进程内的多个线程。如果进程内的某个线程被阻塞,就可以切换到该进程的其他线程继续执行,并且还能够切换执行其他进程的线程。但因为每个用户都需要对应一个内核线程,所以内核开销比多对一映射方式大。

1.2.3多对多映射

多对多映射也被称为组合方式,它是将多对一和一对一两种方式组合起来,通过综合两者的优点所形成的一种方式。该方式在用户空间创建、销毁、切换、调度线程,但进程中的多个用户线程会被映射到若干个内核线程上。如图所示

​ 多对多映射方式综合了多对一映射和一对一映射的优点,每个内核线程负责与之绑定的若干用户线程,进程中某个线程发生系统阻塞时并不会导致整个进程阻塞,而是阻塞该内核线程所对应的若干用户线程,其他线程仍然正常执行。同时因为用户线程的数量比内核数量多,所以也可以有效减少内核开销。

1.2.4Java层到内核层

1.3线程的状态

image-20220925172313088

1.4Java线程的调度

​ Java线程使用的是抢占式调度。Java的线程调度涉及JVM的实现,JVM规范中规定每个线程都有各自的优先级,且优先级越高,越优先执行。但优先级高并不代表能独自占用执行时间,可能是优先级越高,得到的执行时间越多。反之,优先级低的线程所分到的执行时间少,但不会不分配执行时间。

1.5Java线程的优先级与执行机制

Java线程的调度机制由JVM实现。假设有若干个线程,我们想让一些线程拥有更多的执行时间或者少分配点执行时间,就可以通过设置线程的优先级来实现。所有处于可执行状态的线程都在一个队列中且每个线程都有自己的优先级,JVM线程调度器会根据优先级来决定每次的执行时间和执行频率。

但是,优先级高的线程不一定会先执行。我们不能够在Java程序中通过优先级值的大小来控制线程的执行顺序。因为影响线程优先级语义的因素有很多,具体如下:

  • 不同版本的操作系统和JVM都可能会产生不同的行为
  • 优先级对于不同的操作系统调度器来说可能有不同的语义
  • 有些操作系统的调度器不支持优先级
  • 对于操作系统来说,线程的优先级存在“全局”和“本地”之分,不同进程的优先级一般相互独立
  • 不同的操作系统对优先级定义的值不一样,Java只定义了1~10
  • 操作系统常常会对长时间得不到运行的线程给予增加一定的优先级
  • 操作系统的线程调度器可能会对线程在发生等待时有一定的临时优先级调整策略。

​ JVM线程调度器的调度策略决定了上层多线程的运行机制,每个线程执行的时间都由它分配管理。调度器将按照线程优先级对线程的执行时间进行分配,优先级越高得到的CPU执行时间越长,执行频率也可能更大。Java把线程优先级分为10个级别,线程在创建时如果没有明确声明优先级,则使用默认优先级。Java定义了Thread.MIN_PRIORITYThread.NORM_PRIORITYThread.MAX_PRIORITY这3个常量,分别代表最小优先级值(1)、默认优先级值(5)和最大优先级值(10)

​ 此外,由于JVM的实现是以宿主操作系统为基础的,所以Java各优先级与不同操作系统的原生线程优先级必然存在着某种映射关系,这样才能够封装所有操作系统的优先级来提供统一的优先级语义。(例如:优先级110在Linux中可能要与-2019之间的优先级值进行映射,而Windows系统则有9个优先级要映射)

public class ThreadPriorityTest {
    //这里创建了两个线程并设置了不同的优先级值。每次运行的结果可能都不相同,所以并非优先级高就先执行
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.setPriority(10);
        t.setName("00");
        Thread t2 = new MyThread();
        t2.setPriority(8);
        t2.setName("11");
        t2.start();
        t.start();
    }
    static class MyThread extends Thread{
        public void run(){
            for (int i = 0; i < 5; i++) {
                System.out.println(this.getName());
            }
        }
    }
}

1.6Java线程的CPU时间

​ Java线程的执行由JVM进行管理,每个线程在从启动到结束的过程中都可能经历多种状态。多个线程执行则意味着线程的并发和并行,也就涉及CPU的执行时间。

​ 在Java中,线程会按照优先级来分配CPU时间,那么在执行过程中线程何时会放弃CPU的使用权呢,主要分为以下三种情况

  • 线程死亡。即线程运行结束,也就是运行完了run()方法中的任务后整个线程的生命周期结束。
  • 线程主动放弃CPU。这里需要注意的是,基于时间片轮转调度的操作系统不会让线程永久放弃CPU,也就是说只是放弃本轮CPU时间片的执行权。比如在调用yield方法时,线程将放弃参与当前CPU时间片的分配。
  • 因等待放弃CPU,指线程因为进入阻塞等待状态,从而放弃CPU执行时间。进入等待状态的原因可能有很多种,比如磁盘I/O、网络I/O、主动睡眠,锁竞争和执行等待等等。

1.7Java线程的yield操作

​ 在讨论yield之前,必须先了解时间片这个概念。时间片是指在抢占式操作系统中线程或进程在被抢占前所能持续运行的时间。通俗的说就是每个线程轮流分配到的CPU执行时间。每个线程或进程都执行一定的时间,然后再切换到另一个线程或进程去执行一定的时间。

​ 时间片的长短由操作系统控制,它也可能与线程优先级相关。时间片太长会导致并发效果差,影响系统的交互表现。反之又会带来大量的切换成本,从而浪费CPU,导致真正用在线程或进程的执行时间变少。

​ 操作系统通过时间片的机制来执行线程,一般会通过一个就绪队列来维护待执行的线程,新创建的线程就绪后会加入到就绪队列中,然后CPU会以此执行就绪队列中的线程。

​ 实际上,除了完整地使用完时间片之外,还可以明确指定放弃时间片。放弃时间片就意味着将CPU的执行时间让给其他线程,放弃时间片的线程只有在下一轮才能再次分配到时间片,这个放弃CPU时间片的操作称为yield,Java中的线程对象提供了yield()方法。 如图所示

public class YieldThreadTest {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.setDaemon(true);//设置子线程为守护线程 
        mt.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主线程");
        }
    }

    //让出自已的CPU时间
    static class MyThread extends Thread{
        public void run(){
            while (true){
                System.out.println("让出线程CPU时间");
                Thread.currentThread().yield();
            }
        }
    }
}

​ 上述代码整个过程可以理解为:在运行该类后,主线程会创建一个MyThread对象并启动它,然后就一直输出“主线程”,而MyThread对象作为线程也会分到CPU时间片,它每次分到时间片后就输出“让出线程CPU时间”并放弃该轮CPU的使用权。

1.8Java线程的sleep操作

public class TestSleep2 {
    public static void main(String[] args) {
        System.out.println("当前线程睡眠3000ms");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("睡眠结束");
    }
}

在使用sleep方法时,应注意以下事项。

  • 该方法只针对当前线程,即让当前线程进入休眠状态。也就是说,哪个线程调用Thread.sleep,则哪个线程睡眠。
  • sleep方法传入的睡眠时间并不精准,这取决于操作系统的计时器和调度器。
  • 如果在synchronized块内进行sleep操作,或在已获得锁的线程中执行sleep操作,不会让线程失去锁,这点与Object.wait方法不同
  • 当前线程执行sleep操作进入睡眠状态后,其他线程能够中断当前线程,使其解除睡眠状态并抛出InterruptedException异常
public class TestSleep3 {
    //通过synchronized同步块实现锁机制
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread thread1 = new Thread(()->{
         synchronized (lock){
             System.out.println("thread1 gets the lock.");

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread1 releases the lock.");}
        });
        Thread thread2 = new Thread(()->{
            synchronized (lock){
                System.out.println("thread2 gets the lock 3 second later.");
            }
        });
        thread1.start();
        Thread.sleep(100);
        thread2.start();
    }
}
public class TestSleep4 {
    //中断机制
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            System.out.println("thread1 sleeps for 30 seconds.");
            try {
                Thread.sleep(30000);
            } catch (InterruptedException e) {
                System.out.println("thread1 is  interrupted by thread2");
            }
            });
        Thread thread2 = new Thread(()->{
            System.out.println("thread2 interrupts thread1.");
            thread1.interrupt();
        });
        thread1.start();
        Thread.sleep(2000);
        thread2.start();
    }
}

​ sleep方法传入的时间参数值必须大于等于0,正常情况下我们都会传入大于0的值,但有时我们也会传入0值。实际上sleep(0)并非指睡眠0s,它的意义是让出该轮CPU时间。也就是说,它的意义与yield方法相同,而JVM在实现时也可以用yield操作来代替。

public class TestSleep5 {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.setDaemon(true);
        mt.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("main thread");
        }

    }
    //此代码中,MyThread会不断让出自己的CPU时间,主线程则得到更多的执行时间
    static class MyThread extends Thread{
        public void run(){
            while (true){
                System.out.println("yield cpu time");
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

1.9Java线程的Interrupt操作

​ 中断(Interrupt)通常被定义为一个事件或者信号,它表示发生了某个事件需要进行关注。一个中断系统一般包括中断源,中断信号,中断控制器和中断处理器。中断源指发起中断信号的对象,中断信号就是一个信号标识,中断处理器则负责对中断信号进行管理,中断处理器负责在接收到中断信号后进行处理。

​ Java的中断机制使用一个中断变量作为标识,假如某个线程的中断变量被标记为true,那么该线程在适当的时机会抛出异常,我们在捕获异常后进行相应的处理。要实现Java中断机制,必须明白中断标识是如何检测和触发的。这并非直接在Java层写个for/while循环去不断地轮询检测,这样的效率太低,而且JVM层一般也不执行这类的轮询检测。实际上最高效的方式就是使用硬件层提供的信号触发机制。我们可以在需要中断机制的方法中通过本地调用来实现硬件层信号触发。比如Java的Thread线程类的sleep()方法被定义为本地方法,可直接使用C库提供的信号触发来实现中断检测触发。

​ 可以用一个布尔类型变量来表示中断标识,并把这个标识变量放入线程中。从Java应用开发的角度来看,我们可以自定义中断标识变量。从JDK的角度看,由于线程是由JVM维护的,我们可以在JVM层的线程中定义中断标识。

//在Java层中自定义中断标识变量
public class InterruptThreadDemo extends Thread{
    //为了保证其可见性,应该声明为volatile
    private volatile boolean isInterrupted = false;

    public void customInterrupt(){
        isInterrupted = true;
    }
//通过while循环不断检测isInterrupted变量,一旦该变量被设为true,就立即停止执行
    public void run(){
        while (!isInterrupted){
            System.out.println("Thread is running");
        }
        System.out.println("Interrupt thread");
    }

    public static void main(String[] args) {
        InterruptThreadDemo thread = new InterruptThreadDemo();
        thread.start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.customInterrupt();
    }
}
//使用JVM提供的中断标识
public class InterruptThreadDemo2 extends Thread{
    public void run(){
        while (!Thread.interrupted()){
            System.out.println("Thread is running");
        }
        System.out.println("Interrupt thread");
    }

    public static void main(String[] args) {
        InterruptThreadDemo2 thread = new InterruptThreadDemo2();
        thread.start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }
}

JDK预留了3个方法用于操作中断标识,如下所示。这三个方法依次用于设置线程为中断状态、判断线程状态是否中断,清除当前线程的中断状态并返回它之前的值。通过interrupt()方法设置中断标识,设置中断状态后线程并非立即停止,除非我们对自己的变量进行检查处理。而如果线程正在执行sleep()、wait()、join()等方法,则会抛出InterruptedException异常(由JVM实现)。需要注意的是,interrupted()不仅能让线程变成中断状态,它还会清除中断标识。

public class Thread{
     public void interrupt(){}
     public Boolean isInterrupted(){}
     public static Boolean interrupted(){}
}

1.9.1可运行状态的中断

​ 线程最常见的状态就是可运行(RUNNABLE)状态,在此状态下我们需要自己来检测中断标识,从而提供中断操作。

public class RunnableInterrupt {
    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            //通过while循环来不断检测中断标识
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("running");
            }
        });
        thread1.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //主线程睡眠2s后调用interrupt方法来将其线程中的中断标识标为true
        thread1.interrupt();
    }
}

1.9.2阻塞/等待状态的中断

​ 对于处于可运行状态的线程,我们需要通过while的形式来检测中断标识,而对于处于阻塞或等待状态的线程,则无需自己检测中断标识,我们要做的就是捕获中断异常并进行处理。

//Java线程睡眠与等待的中断情况
public class WaitingInterrupt {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread thread1 = new Thread(()->{
            try {
                System.out.println("thread1 is running...");
                Thread.currentThread().sleep(200000);
            } catch (InterruptedException e) {
                System.out.println("thread1 has stopped!");
            }
        });
        Thread thread2 = new Thread(()->{
            try {
                System.out.println("thread2 is running...");
                synchronized (lock){
                    //thread1睡眠200s而线程二进入等待状态
                    lock.wait();
                }
            } catch (Exception e) {
                System.out.println("thread2 has stopped!");
            }
        });
        thread1.start();
        thread2.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //当都调用此方法后,都抛出InterruptedException异常,捕获后输出,如果不对thread2中断,那么它会一直等待下去
        thread1.interrupt();
        thread2.interrupt();
    }
}

1.9.3经典中断实现方式

​ 我们知道,对于可运行状态的线程我们需要自己维护中断状态标识,而对于阻塞和等待状态的线程则需要捕获中断异常并处理。通常情况下,线程既可能处于可执行状态又可能处于阻塞/等待状态,那么我们应该如下处理

//检测中断标识
public class RunningWaitingInterrupt {
    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("running...");
                try {
                    Thread.currentThread().sleep(200);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().isInterrupted());
                    Thread.currentThread().interrupt();
                    System.out.println("thread1 has stopped");
                }
            }
        });
        thread1.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread1.interrupt();
    }
}

​ 在这段代码中,通过while(!Thread.currentThread().isInterrupted())来不断检测中断标识。在这个循环中,线程除了可运行状态还会进入睡眠阻塞状态,也就是说在这两种状态下都要支持中断。睡眠操作是由JVM层面支持中断的,所以当thread1调用interrupt()方法时,它会中断睡眠并抛出InterruptedException异常。需要注意的是,当捕获异常后要再次调用Thread.currentThread().interrupt(),因为sleep()方法被中断后,它会将中断标识清理掉,然后抛出中断异常。因此需要再次设置中断标识,以便能跳出while循环,结束整个thread1线程的执行。

1.9.4park的特殊中断

​ park的中断方式比较特殊。在此方式阻塞下的中断并不会抛出InterruptedException异常,但中断标识会被设置为true。

//使用park方式进行阻塞
public class ParkInterrupt {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("thread1 is running...");
            LockSupport.park();
        });
        thread1.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread1.interrupt();
       //主线程调用thread1的interrupt()方法来中断线程1的阻塞,此时虽然已经结束阻塞,但是thread1的中断标识已经被设为了true
        System.out.println(thread1.isInterrupted());
    }
}

1.10Java线程的阻塞与唤醒

​ Java提供了多种方式来对线程进行阻塞和唤醒操作,比如suspend与resume、wait与notify以及park与unpark等。那么我们为什么需要阻塞和唤醒操作呢。

​ 简单来说,主要是为了控制线程在某些关键节点的先后执行顺序。线程之间是相互独立的,它们之间需要某种机制才能相互通信,Java为此引入了共享变量。某个线程将信息写到变量中,另一个线程通过该变量读取信息,这样便能实现线程之间的通信。

//线程之间的通信
public class ThreadCommunicationDemo {
    private static String message;
    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            while (message==null){
                
            }
            System.out.println(message);
        });
        Thread thread2 = new Thread(()->{
            message = "i am thread2";
        });
        thread1.start();
        thread2.start();
    }
}

​ 我们可以使用suspend和resume方法对线程进行阻塞和唤醒,它们能够在代码中控制阻塞和唤醒的时间节点。比如线程1启动后在某个时间点需要让它挂起,此时可以调用suspend方法,而线程2则可以通过调用resume方法唤醒线程1继续往下执行,如下代码

public class SuspendResumeDemo {
    private static String message;
    static Thread thread1 = new Thread1();
    static Thread thread2 = new Thread2();

    public static void main(String[] args) {
        thread1.start();
        thread2.start();
    }
    static class Thread1 extends Thread{
        public void run(){
            if(message == null){
                suspend();
                System.out.println(message);
            }
        }
    }
    static class Thread2 extends Thread{
        public void run(){
            message = "i am thread2";
            thread1.resume();
        }
    }
}
//如果thread1先执行并发现message为null,那么它会调用suspend来阻塞自己。后面thread2对message变量赋值后会调用thread1的resume方法来唤醒thread1,然后其正确输出“i am thread2”

​ 然而该组合将出现很多问题,例如最经典的死锁问题。

public class ThreadSuspend {
    public static void main(String[] args) {
        Thread mt = new Thread(()->{
            while (true){
                System.out.println("running...");
            }
        });
        mt.start();
        try {
            Thread.currentThread().sleep(100);
        } catch (InterruptedException e) {

        }
        mt.suspend();//程序运行到此将永远卡住
        //因为println被声明为一个同步方法,执行时将对System类的out单例属性加同步锁。而suspend方法挂起线程又不释放锁,当mt线程被挂起后,主线程调用System.out.println时同样需要获取Systemout对象的同步锁才能打印。于是主线程一直在等待同步锁而mt线程又不释放锁,就导致了死锁
        System.out.println("can you get here?");
        mt.resume();
    }
}

​ 此时我们有另一种阻塞唤醒方案——wait和notify组合,即利用Object类的wait()和notify()方式实现线程阻塞。这与前者有些许区别,前者只需要在线程内直接调用就能完成阻塞和唤醒,若改用wait和notify形式,则是以某个Object为信号,线程1和线程2调用object的wait方法后都将被阻塞,接着线程3调用object的notify方法将会唤醒线程1或线程2。

​ 使用wait和notify组合能在一定程度上避免死锁问题,但并不能完全避免,因此必须在编程过程中避免死锁,在使用过程中需要注意以下几点。

  • wait和notify方法是针对对象的,调用任意对象的wait方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁。相应地,调用任意对象的notify方法则随机解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行。
  • wait和notify方法必须在synchronized块或方法中调用,并且要保证同步块或方法的锁对象与调用wait与notify方法的对象是同一个。如此一来,当前线程在调用wait之前就已经成功获取某对象的锁,执行wait阻塞后就将之前获取的对象锁释放。(如果不按照此约束编程,运行时将抛出IllegalMonitorStateException异常)
  • notify用于随机唤醒一个阻塞中的线程并让其获取对象锁,进而往下执行,而notifyAll则是唤醒阻塞中的所有线程,让其去竞争该对象锁,获取到锁的线程才能往下执行。
//改造上述代码的死锁问题
public class  ThreadWaitNotify {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt = new MyThread();
        mt.start();
        Thread.sleep(100);
        mt.suspendThread();
        System.out.println("can you get here?");
        Thread.sleep(3000);
        mt.resumeThread();
        
    }
//核心思想就是在MyThread中添加一个标识变量,一旦变量改变就相应地调用wait和notify阻塞和唤醒线程。由于在执行wait后释放synchronized(this)锁住的对象锁,此时System.out.println的锁也释放完毕,就解决了死锁问题 
    static class MyThread extends Thread{
        public boolean isSuspend = false;

        public void run(){
            while (true){
                synchronized (this){
                    System.out.println("running...");
                    if(isSuspend) {
                        try {
                            wait();
                        } catch (InterruptedException e) {
                        }
                    }
                }
            }
        }
        public void suspendThread(){
            this.isSuspend=true;
        }
        public void resumeThread(){
            synchronized (this){
                this.isSuspend=false;
                notify();
            }
        }
    }
}

​ 接着使用wait和notify组合来实现两个线程通信的例子

public class WaitNotifyDemo {
    private static String message;

    public static void main(String[] args) {
        Object lock = new Object();
        Thread thread1 = new Thread(()->{
            synchronized (lock){
                //如果线程1先得到锁,那么message肯定为null,此时会调用wait进入阻塞状态
                if(message==null){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
            System.out.println(message);
        });
        Thread thread2 = new Thread(()->{
            synchronized (lock){
                message = "i am thread2";
                lock.notify();
            }
        });
        thread1.start();
        thread2.start();
    }
}

​ wait和notify组合的方式仍存在一点不足,那就是wait必须在notify之前执行,如若先执行notify,再执行wait,将导致线程永远无法被唤醒。此外,它面向的主体是object,阻塞的是执行wait方法的线程,而唤醒的是某个随机的线程或所有线程。

​ 此外我们拥有第三种方式——park与unpark组合。该方式可以很好地规避死锁和竞态条件,从而代替suspend与resume组合

//使用park与unpark实现线程通信
public class ParkUnparkDemo {
    private static String message;
//假如线程1执行得更快,则该线程会执行park操作进入阻塞,等到线程2对message赋值后执行unpark操作才能唤醒线程1继续往下执行。
//假如线程2执行得更快,则对message赋值后会对线程1进行unpark操作,那么线程1在执行park操作时就能直接通过。
    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            LockSupport.park();
            System.out.println(message);
        });
        Thread thread2 = new Thread(()->{
            message = "i am thread2";
            LockSupport.unpark(thread1);
        });
        thread1.start();
        thread2.start(); 
    }
}

​ 相较于wait和notify组合对执行顺序的敏感性,park和unpark则通过许可机制避免了这个问题,这就是park与unpark使用许可机制的优势。另外,许可是一次性的,也就是说不管是先unpark一次还是100次,只要使用一次park许可就没有了,下次要得到许可就得重新unpark。如下代码。先对当前线程unpark了5次,然后第一次park就被许可使用掉了,那么第二个park就阻塞了,主线程被线程阻塞无法往下执行。++

public class ParkUnparkDemo2 {
    public static void main(String[] args) {
        LockSupport.unpark(Thread.currentThread());
        LockSupport.unpark(Thread.currentThread());
        LockSupport.unpark(Thread.currentThread());
        LockSupport.unpark(Thread.currentThread());
        LockSupport.unpark(Thread.currentThread());
        LockSupport.unpark(Thread.currentThread());
        LockSupport.park();
        LockSupport.park();
    }
}

​ park和unpark组合在竞争条件问题上具有wait和notify无可比拟的优势。在使用wait和notify组合时,某一线程被另一线程notify之前必须要保证此线程已经执行到wait等待点,若错过notify则可能永远在等待,另外notify也不能保证唤醒指定的某个线程。反观LockSupport,由于park和unpark引入了许可机制,所以能很好解决该问题。许可机制的逻辑为:

  • park操作将在许可为0的时候阻塞,而许可为1则可以直接返回并将许可置为0;
  • unpark操作则将许可置为1,并尝试唤醒线程。

​ 根据这两个逻辑,对于同一线程,park与unpark操作的顺序并不影响程序正确地执行。假设先执行unpark操作,则许可被置为1,之后再执行park操作,此时因为许可等于1而直接返回并往下执行,不会进入阻塞。

park与unpark组合真正解耦了线程之间的同步,不需要考虑同步锁。而wait与notify要保证有锁才能执行,而且执行notify操作释放锁后当前线程会重新进入等待队列来等待获取锁,LockSupport则完全不会考虑锁,等待队列等问题。

...全文
39 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

1,040

社区成员

发帖
与我相关
我的任务
社区描述
中南民族大学CSDN高校俱乐部聚焦校内IT技术爱好者,通过构建系统化的内容和运营体系,旨在将中南民族大学CSDN社区变成校内最大的技术交流沟通平台。
经验分享 高校 湖北省·武汉市
社区管理员
  • c_university_1575
  • WhiteGlint666
  • wzh_scuec
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

欢迎各位加入中南民族大学&&CSDN高校俱乐部社区(官方QQ群:908527260),成为CSDN高校俱乐部的成员具体步骤(必填),填写如下表单,表单链接如下:
人才储备数据库及线上礼品发放表单邀请人吴钟昊:https://ddz.red/CSDN
CSDN高校俱乐部是给大家提供技术分享交流的平台,会不定期的给大家分享CSDN方面的相关比赛以及活动或实习报名链接,希望大家一起努力加油!共同建设中南民族大学良好的技术知识分享社区。

注意:

1.社区成员不得在社区发布违反社会主义核心价值观的言论。

2.社区成员不得在社区内谈及政治敏感话题。

3.该社区为知识分享的平台,可以相互探讨、交流学习经验,尽量不在社区谈论其他无关话题。

试试用AI创作助手写篇文章吧