【Java 多线程编程 | 从0到1】线程机制

简介: 【Java 多线程编程 | 从0到1】线程机制

Java线程机制

1.1 线程是什么

对于语言层面的线程,Java开发人员再熟悉不过了。Java线程类为java.lang.Thread,当任务不能再当前线程中执行时,我们会创建一个Thread对象,然后启动该线程去工作。Java有三种创建线程的方法:

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程。

代码示例:

继承Thread类方式

创建一个类继承Thread类,重写run()方法,在主线程中创建此类的对象,调用对象的start()方法,可启动线程

public class Demo4 {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
    }
    static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("aaaaa");
        }
    }
}
实现Runnable接口

创建一个类实现Runnable接口,在主线程中创建这个类的对象实例,新创建一个线程并调用start()方法启动线程,需要先将实现Runnable接口的类的对象当做参数传给这个新的线程,这样就实现了线程的启动。

public class Demo5 {
    public static void main(String[] args) {
        newThread thread = new newThread();
        new Thread(thread).start();
    }
    static class newThread implements  Runnable{
        @Override
        public void run() {
            System.out.println("bbbb");
        }
    }
}
实现Callable接口

可以和使用实现Runnable接口方式创建线程方式比较,就是相当于多包装了一层,原来将Runnable对象直接丢到new Thread()中,也就是传入目标对象后调用start()方法启动线程,现在是不仅要创建实现Callable接口的对象,还需要util包下的concurrent下的FutureTask,创建FutureTask对象时需要传入实现Callable接口的对象到构造方法中,将FutureTask对象作为Thread的目标对象启动线程。

public class Demo6 {
    public static void main(String[] args) {
        callableThread ct = new callableThread();
        FutureTask task = new FutureTask(ct);
        new Thread(task).start();
    }
    static class callableThread implements Callable {
        @Override
        public Object call() throws Exception {
            System.out.println(Thread.currentThread().getName());
            return 2;
        }
    }
}

在早期的操作系统中,执行任务被抽象为进程,进程也是操作系统运行和调度的基本单元,然而随着计算机技术的不断发展,以进程为调度单元的方式逐渐产生了弊端,因为它的资源开销较大。于是人们在进程的基础上提出了线程。线程是进程里面的运行单位,可以把线程看成轻量级的进程。CPU会按某种策略为每个线程分配一定的时间片去执行。

比如图中有3个线程,它们分别定义了3个执行任务,CPU会轮着去执行这3个线程。

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


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

1.2 线程的映射

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

Java 语言编译后的字节码运行在JVM (Java 虚拟机)上,而JVM其实又是一个进程所以Java 属于应用程序层。我们在 Java 层通过new 关键词创建一个 Thread 对象,然后调用 start方法启动该线程,那么从线程角度来看其实就涉及 Java 层线程、JVM 层线程、操作系统层线程。IVM 主要由 C/C++实现,所以 Java 层线程终究还是要映射到JVM 层线程、而Java 层线程到操作系统层线程的映射就要看 JVM 的具体实现了。

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

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

1.2.1 多对一映射

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

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

1.2.2 一对一映射

一对一映射就是每个用户线程都被映射到一个内核线程上,用户线程的整个牛命周期都绑定到所映射的内核线程上。

1.2.3 多对多映射

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

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

1.3 Java线程的状态

与人类一样,线程也拥有自己的生命周期,一条线程从创建到死亡的过程就是线程的生命周期。在整个生命周期内,线程在不同时刻可能处于不同的状态。有些线程的任务简单,涉及的状态就少。而有些线程的任务复杂,涉及的状态就多。那么线程到底有多少种状态,不同状态之间又是如何转化的呢?

对于线程状态的分类实际上并没有严格的规定,只要能正确表示状态即可。我们来看一种常见的状态分类,如图 一个线程从创建到死亡期间可能会经历若干个状态,但在任意一个时间点上线程只能处于其中一种状态。线程总共包含 5 个状态:新建(new)、可运行(runnable )、运行(running )、不可运行(not runnable )、死亡( dead )。

  • 新建状态:
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  • 就绪状态:
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态:
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
  • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
  • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
  • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

1.4 Java 线程的调度

在Java 多线程环境中,为了保证所有线都能按照一定的策略执行,JVM 需要有一个线程调度器。这个调度器定义了线程调度的策略,通过特定的机制为多个线积分配 CPU 的使用权。线程调度器中一般包含多种调度策略算法,由这些算法来决定 CPU 的分配。此外,每个线程还有自己的优先级(比如有高、中、低级别),调度算法会通过这些优先级来实现优先机制。

所有的Java虚拟机都有一个线程调度器,用来确定那个时刻运行那个线程。主要包含两种:抢占式线程调度器和协作式线程调度器。

  • 抢占式调度:每个线程的执行时间和线程的切换都由调度器控制,调度器按照某种策略为每个线程分配执行时间。调度器可能会为每个线程都分配相同的执行时间,也可能为某些特定线程分配较长的执行时间,甚至在极端情况下还可能不给某些线程分配执行时间片,从而导致某些线程得不到执行。在抢占式调度机制下,一个线程的堵塞不会导致整个进程堵塞。
    行日态决控
  • 协同式调度:某一线程执行完后会主动通知调度器切换到下一个线程上执行。这种调度模式就像接力赛一样,一个人跑完自己的路程后就把接力棒交接给下一个人,下一个人继续往下跑。在这种模式下,线程的执行时间由线程本身控制,也就是说线程的切换点是可以预先知道的。但是它有一个致命弱点,即如果某一个线程的逻辑存在问题,则可能导致系统运行到一半就一直阻塞了,最终可能导致整个系统崩溃。

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

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

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

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

但是,优先级高的线程一定会先执行吗?我们能否在 Java 程序中通过优先级值的大小来控制线程的执行顺序呢? 答案是不能。这是因为影响线程优先级语义的因素有很多,具体如下.不同版本的操作系统和JM 都可能会产生不同的行为:

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

可以使用 Thread 类中的 setPriority() 方法来设置线程的优先级。语法如下:

public final void setPriority(int newPriority);

如果要获取当前线程的优先级,可以直接调用 getPriority() 方法。语法如下:

public final int getPriority();

使用优先级

举例:如何使用优先级。

  1. 分别使用 Thread 类和 Runnable 接口创建线程,并为它们指定优先级。
public class FirstThreadInput extends Thread
{
    public void run()
    {
        System.out.println("调用FirstThreadInput类的run()重写方法");    //输出字符串
        for(int i=0;i<5;i++)
        {
            System.out.println("FirstThreadInput线程中i="+i);    //输出信息
            try
            {
                Thread.sleep((int) Math.random()*100);    //线程休眠
            }
            catch(Exception e){}
        }
    }
}
  1. 创建实现 Runnable 接口的 SecondThreadInput 类,实现 run() 方法。代码如下:
public class SecondThreadInput implements Runnable
{
    public void run()
    {
        System.out.println("调用SecondThreadInput类的run()重写方法");    //输出字符串
        for(int i=0;i<5;i++)
        {
            System.out.println("SecondThreadInput线程中i="+i);    //输出信息
            try
            {
                Thread.sleep((int) Math.random()*100);    //线程休眠
            }
            catch(Exception e){}
        }
    }
}

创建 TestThreadInput 测试类,分别使用 Thread 类的子类和 Runnable 接口的对象创建线程,然后调用 setPriority() 方法将这两个线程的优先级设置为 4,最后启动线程。代码如下:

public class TestThreadInput
{
    public static void main(String[] args)
    {
        FirstThreadInput fti=new FirstThreadInput();
        Thread sti=new Thread(new SecondThreadInput());
        fti.setPriority(4);
        sti.setPriority(4);
        fti.start();
        sti.start();
    }
}
  1. 运行上述代码,运行结果如下所示。

调用FirstThreadInput类的run()重写方法

调用SecondThreadInput类的run()重写方法

FirstThreadInput线程中i=0
SecondThreadInput线程中i=0
FirstThreadInput线程中i=1
FirstThreadInput线程中i=2
SecondThreadInput线程中i=1
FirstThreadInput线程中i=3
SecondThreadInput线程中i=2
FirstThreadInput线程中i=4
SecondThreadInput线程中i=3
SecondThreadInput线程中i=4

由于该例子将两个线程的优先级都设置为 4,因此它们交互占用 CPU ,宏观上处于并行运行状态。


1.6 Java 线程的 CPU 时间

Java 线程的执行由JVM 进行管理,每个线程在从启动到结束的过程中都可能经历多种状态。多个线程执行则意味着线程的并发和并行。也就涉及 CPU 的执行时间。图 1.13 是 3个线程分配到的 CPU 执行时间的示意图,3 个线程除了直正执行阶段,从启动到结束的过程中还包含了等待阶段。

在一个线程从启动到结束的过程中,有两个时间概念需要理解: 线程的真正执行时间和总消耗时间。总消耗时间等于真正执行时间与等待时间的和。在图 1.14 中可以很清晰地看到这两者之间的关系,T1 得到了多个 CPU 执行时间,即真正的执行时间,而 T1 的总消耗时间还包括T1 执行期间 CPU分配给其他线程执行的时间,所以总消耗时间总是大于等于真正执行时间。

在 Java 中,线程会按其优先级来分配 CPU 时间,那么在执行过程中线程何时会放弃 CPU的使用权呢?其实可以归类成以下 3 种情况。

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

1.7 Java 线程的 yield 操作

Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)

yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

public class TestYield {
  public static void main(String [] args){
    MyThread t1 = new MyThread("t1");
    MyThread t2 = new MyThread("t2");
    t1.start();
    t2.start();
  }
}
class MyThread extends Thread{
  MyThread(String s){
  super(s);
  }
  public void run(){
    for(int i = 0; i <= 30; i ++){
      System.out.println(getName()+":"+i);
      if(("t1").equals(getName())){
        if(i == 0){
          yield();
        }
      }
    }
  }
}

运行的结果是变化的。

Java 中 vield 操作的具体语义取决于JM 的实现,而JVM 的实现又依赖于不同的操作系统。所以不同的操作系统语义可能不相同,即使相同操作系统的不同版本也可能不同。我们没有必要去深究每种操作系统、每个版本的线程调度算法的实现,而是可以通过经典的基于时间片的调度策略来理解 vield 操作的含义。实际上,现代操作系统已经发展出很多不同的调度算法,实现更加复杂,考虑的因素也更多。


1.8 Java 线程的 sleep 操作

sleep 操作是一个经常使用的操作,特别是在开发环境中进行调试的时候,我们为了模拟长时间的执行而使用 sleep。该操作对应着 java.lang.Thread 类的 sleep 本地方法,它能使当前线程睡眠指定的时间,如图

public class ThreadTest06 {
    public static void main(String[] args) {
        //让当前线程进入休眠,睡眠5秒
        //当前线程是主线程!!!
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    //5秒之后执行这里的代码
        System.out.println("hello world!");
    }
}

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

  • 该方法只针对当前线程,即让当前线程进入休眠状态。也就是说,哪个线程调用Thread.sleep,则哪个线程睡眠。
  • sleep 方法传入的睡眠时间并不精准,这取决于操作系统的计时器和调度器如果在synchronized块内进行 sleep 操作,或在已获得锁的线程中执行 sleep 操作,都不会让线程失去锁,这点与 Object.wait 方法不同。
  • 当前线程执行 sleep操作进入睡眠状态后,其他线程能够中断当前线程,使其解除睡眠状态并抛出InterruptedException异常。

通过synchronized同步块实现锁机制

中断机制

public class ThreadTest07 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunable2());
        t.setName("t");
        t.start();
        //希望5秒以后,t线程醒来(5秒以后主线程的活干完了)
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
        t.interrupt();//干扰,一盆冷水过去!!
    }
}
class MyRunable2 implements Runnable{
    //重点:run()当中的异常不能throws,只能try..catch
    //因为run()方法在父类中没有抛出异常,子类不能比父类抛出更多异常。
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "--> begin");
        try {
            //睡眠1年
            Thread.sleep(1000*60*60*24*365);
        } catch (InterruptedException e) {
            //打印异常信息
            e.printStackTrace();
        }
        //一年之后才会执行这里
        System.out.println(Thread.currentThread().getName() + "--> end");
    }
}

1.9 Java 线程的 interrupt操作

中断(Interrupt)一个线程意味着在该线程完成任务之前停止其正在进行的一切,有效地中止其当前的操作。线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序。虽然初次看来它可能显得简单,但是,你必须进行一些预警以实现期望的结果。你最好还是牢记以下的几点告诫。

首先,忘掉Thread.stop方法。虽然它确实停止了一个正在运行的线程,然而,这种方法是不安全也是不受提倡的,这意味着,在未来的JAVA版本中,它将不复存在。


使用 interrupt() + InterruptedException来中断线程

  线程处于阻塞状态,如Thread.sleep、wait、IO阻塞等情况时,调用interrupt方法后,sleep等方法将会抛出一个InterruptedException:

public static void main(String[] args) {
        Thread thread = new Thread() {
            public void run() {
                System.out.println("线程启动了");
                try {
                    Thread.sleep(1000 * 100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程结束了");
            }
        };
        thread.start();
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();//作用是:在线程阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态
    }

使用 interrupt() + isInterrupted()来中断线程  

  this.interrupted():测试当前线程是否已经中断(静态方法)。如果连续调用该方法,则第二次调用将返回false。在api文档中说明interrupted()方法具有清除状态的功能。执行后具有将状态标识清除为false的功能。

this.isInterrupted():测试线程是否已经中断,但是不能清除状态标识。

public static void main(String[] args) {
        Thread thread = new Thread() {
            public void run() {
                System.out.println("线程启动了");
                while (!isInterrupted()) {
                    System.out.println(isInterrupted());//调用 interrupt 之后为true
                }
                System.out.println("线程结束了");
            }
        };
        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
        System.out.println("线程是否被中断:" + thread.isInterrupted());//true
    }

但是当线程被阻塞的时候,比如被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞时。调用它的interrput()方法。可想而知,没有占用CPU运行的线程是不可能给自己的中断状态置位的。这就会产生一个InterruptedException异常。

/*  
    * 如果线程被阻塞,它便不能核查共享变量,也就不能停止。这在许多情况下会发生,例如调用 
    * Object.wait()、ServerSocket.accept()和DatagramSocket.receive()时,他们都可能永 
    * 久的阻塞线程。即使发生超时,在超时期满之前持续等待也是不可行和不适当的,所以,要使 
    * 用某种机制使得线程更早地退出被阻塞的状态。很不幸运,不存在这样一种机制对所有的情况 
    * 都适用,但是,根据情况不同却可以使用特定的技术。使用Thread.interrupt()中断线程正 
    * 如Example1中所描述的,Thread.interrupt()方法不会中断一个正在运行的线程。这一方法 
    * 实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更 
    * 确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么, 
    * 它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。因此, 
    * 如果线程被上述几种方法阻塞,正确的停止线程方式是设置共享变量,并调用interrupt()(注 
    * 意变量应该先设置)。如果线程没有被阻塞,这时调用interrupt()将不起作用;否则,线程就 
    * 将得到异常(该线程必须事先预备好处理此状况),接着逃离阻塞状态。在任何一种情况中,最 
    * 后线程都将检查共享变量然后再停止。下面示例描述了该技术。 
    * */  
    package Concurrency.Interrupt;  
    class Example3 extends Thread {  
    volatile boolean stop = false;  
    public static void main(String args[]) throws Exception {  
    Example3 thread = new Example3();  
    System.out.println("Starting thread...");  
    thread.start();  
    Thread.sleep(3000);  
    System.out.println("Asking thread to stop...");  
    /* 
    * 如果线程阻塞,将不会检查此变量,调用interrupt之后,线程就可以尽早的终结被阻  
    * 塞状 态,能够检查这一变量。 
    * */  
    thread.stop = true;  
    /* 
    * 这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退 
    * 出阻 塞的状态 
    * */  
    thread.interrupt();  
    Thread.sleep(3000);  
    System.out.println("Stopping application...");  
    System.exit(0);  
    }  
    public void run() {  
    while (!stop) {  
    System.out.println("Thread running...");  
    try {  
    Thread.sleep(2000);  
    } catch (InterruptedException e) {  
    // 接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态  
    System.out.println("Thread interrupted...");  
    }  
    }  
    System.out.println("Thread exiting under request...");  
    }  
    }  
    /* 
    * 把握几个重点:stop变量、run方法中的sleep()、interrupt()、InterruptedException。串接起 
    * 来就是这个意思:当我们在run方法中调用sleep(或其他阻塞线程的方法)时,如果线程阻塞的 
    * 时间过长,比如10s,那在这10s内,线程阻塞,run方法不被执行,但是如果在这10s内,stop被 
    * 设置成true,表明要终止这个线程,但是,现在线程是阻塞的,它的run方法不能执行,自然也就 
    * 不能检查stop,所 以线程不能终止,这个时候,我们就可以用interrupt()方法了:我们在 
    * thread.stop = true;语句后调用thread.interrupt()方法, 该方法将在线程阻塞时抛出一个中断 
    * 信号,该信号将被catch语句捕获到,一旦捕获到这个信号,线程就提前终结自己的阻塞状态,这 
    * 样,它就能够 再次运行run 方法了,然后检查到stop = true,while循环就不会再被执行,在执 
    * 行了while后面的清理工作之后,run方法执行完 毕,线程终止。 
    * */

当代码调用中须要抛出一个InterruptedException, 你可以选择把中断状态复位, 也可以选择向外抛出InterruptedException, 由外层的调用者来决定.

不是所有的阻塞方法收到中断后都可以取消阻塞状态, 输入和输出流类会阻塞等待 I/O 完成,但是它们不抛出 InterruptedException,而且在被中断的情况下也不会退出阻塞状态.

尝试获取一个内部锁的操作(进入一个 synchronized 块)是不能被中断的,但是 ReentrantLock 支持可中断的获取模式即 tryLock(long time, TimeUnit unit)。

可运行状态中断

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

阻塞/等待状态的中断

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

1.10 Java线程的阻塞与唤醒

线程的阻塞和唤醒在多线程并发过程中是一个关键点,当很多线程参与并发时可能会带俩很多隐蔽问题。如何正确地暂停一个线程,暂停后又如何在某个节点恢复运行,Java提供了多种方法来对线程进行阻塞和唤醒操作,比如suspend与resume、wait与notify以及park与unpark等。

方式1:早期JAVA采用suspend()、resume()对线程进行阻塞与唤醒,但这种方式产生死锁的风险很大,因为线程被挂起以后不会释放锁,可能与其他线程、主线程产生死锁,如:

View Code

方式2:wait、notify形式通过一个object作为信号,object的wait()方法是锁门的动作,notify()、notifyAll()是开门的动作,某一线程一旦关上门后其他线程都将阻塞,直到别的线程打开门。notify()准许阻塞的一个线程通过,notifyAll()允许所有线程通过。如下例子:主线程分别启动两个线程,随后通知子线程暂停等待,再逐个唤醒后线程抛异常退出。

View Code

wait、notify使用要点:

  • 1、对象操作都需要加同步synchronized;
  • 2、线程需要阻塞的地方调用对象的wait方法;

存在的不足:面向对象的阻塞是阻塞当前线程,而唤醒的是随机的一个线程或者所有线程,偏重线程间的通信;同时某一线程在被另一线程notify之前必须要保证此线程已经执行到wait等待点,错过notify则可能永远都在等待。

方式3:LockSupport提供的park和unpark方法,提供避免死锁和竞态条件,很好地代替suspend和resume组合。

View Code

park与unpark方法控制的颗粒度更加细小,能准确决定线程在某个点停止,进而避免死锁的产生。

park与unpark引入了许可机制,许可逻辑为:

①park将许可在等于0的时候阻塞,等于1的时候返回并将许可减为0;

②unpark尝试唤醒线程,许可加1。根据这两个逻辑,对于同一条线程,park与unpark先后操作的顺序似乎并不影响程序正确地执行,假如先执行unpark操作,许可则为1,之后再执行park操作,此时因为许可等于1直接返回往下执行,并不执行阻塞操作。

park与unpark组合真正解耦了线程之间的同步,不再需要另外的对象变量存储状态,并且也不需要考虑同步锁,wait与notify要保证必须有锁才能执行,而且执行notify操作释放锁后还要将当前线程扔进该对象锁的等待队列,LockSupport则完全不用考虑对象、锁、等待队列等问题。

总结:suspend()、resume()已经被deprecated,不建议使用。wait、notify需要对对象加同步,性能有折扣。LockSupport则完全不用考虑对象、锁、等待队列。


1.11 Java线程的join进操作

计算机为了提升 CPU 使用效率和交互性而引人了并发机制,执行的任务也抽象成了线稳从宏观上看,多个线程就像是同时执行一样。但并发同样也使得线程的执行顺序不容易控制实际工程中的很多场景都会涉及某个线程需要依赖另外一个或几个线程的执行结果,这就要被依赖的线程必须先执行完毕,此时就需要 join 操作。比如在图 1.24 中,假如要计算 A+B结果且A和B的计算都比较耗时,那么我们将 B 的计算分给线2,而线程1 则负责A的算。如果线程 1先执行完,则它要等待线程 2,直到线 2 计算出 B 的结果后,线程1才继线往下执行,去计算 A+B。

join 操作类似于前面讲解的线程的 wait 与 notify 功能,某个线程可以通过调用join 操作来等待另外一个线程的执行,直到另外一个线程执行完毕。我们根据图 1.25 看一下join 的过程线程t1首先创建了线程2并启动该线,接着线tl继续创建线程t3,然后线程调用2.join和 3.;join0后进人等待状态。自此线 t1 进人等待状态,而线程 2 和线 3 则一直执行,等它们都执行完毕后线程 t1 才会继续往下执行。

注:
join的操作具有中断机制
join的操作具有超时机制

join方法实现原理

其实,join方法是通过调用线程的wait方法来达到同步的目的的。例如,A线程中调用了B线程的join方法,则相当于A线程调用了B线程的wait方法,在调用了B线程的wait方法后,A线程就会进入阻塞状态,具体看下面的源码:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

从源码中可以看到:join方法的原理就是调用相应线程的wait方法进行等待操作的,例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。

目录
相关文章
|
20天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
11天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
6天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
11天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
42 1
|
19天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
40 6
|
19天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
18天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
Java
Java多线程编程核心技术(三)多线程通信(下篇)
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。
686 0
|
Java
Java多线程编程核心技术(三)多线程通信(上篇)
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。
2562 0
|
Java 安全
Java多线程编程核心技术(二)volatile关键字
关键字volatile的主要作用是使变量在多个线程间可见。
890 0