Java并发学习(三)Java线程的实现原理

进程与线程

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换,因为线程只会共享同一进程的资源,到另一个进程当然需要再次加载资源,形成新的运行环境。

总结来说就是,进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同;线程是进程的一部分,线程主抓中央处理器执行代码的过程,其余的资源的保护和管理由整个进程去完成。

线程的实现

我们已经知道,线程是比进程更加轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,进程是资源分配的基本单位,线程是独立调度的基本单位;各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度。

   线程的实现主要有3种方式:使用内核线程实现、使用用户线程实现使用用户线程加轻量级进程混合实现。

使用内核线程实现:

内核线程(KLT,Kernel-Level Thread),直接由操作系统内核(Kernel,即内核)支持的线程。由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。
每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核叫做多线程内核(Multi-Threads Kernel)。
程序一般不会去直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),即通常意义上的线程*。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。轻量级进程与内核线程之间1:1关系称为一对一的线程模型。

使用用户线程实现:

广义上讲只要一个线程不是内核线程,就可以认为是用户线程(User Thread,UT)。

有关线程管理的所有工作都由应用程序完成,内核意识不到多线程的存在。用户级线程仅存在于用户空间中,此类线程的创建、撤销、线程之间的同步与通信功能,都无法利用系统调用来实现。这种进程与用户线程之间1:N的关系称为一对多线程模型。

应用程序需要通过使用线程库来控制线程。 通常,应用程序从单线程起始,在该线程中开始运行,在其运行的任何时刻,可以通过调用线程库中的派生创建一个在相同进程中运行的新线程。由于线程在进程内切换的规则远比进程调度和切换的规则简单,不需要进行用户态/核心态切换,所以切换速度快。但也正因为这样,没有系统内核的支援,所有的线程操作都需要用户程序自己处理,所以在面对“阻塞处理”等问题时将异常困难。所以现在使用用户线程的程序越来越少了,Java曾经使用过也放弃了。

使用用户线程加轻量级进程混合实现:

线程除了依赖内核线程和完全由用户自己程序实现外,还有一种将内核线程和用户线程一起使用的实现方式。

在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系。许多unix系列的操作系统,如Solaris、HP_UX等都提供了N:M的线程模型实现。

Java线程的实现

           对于Sun JDK来说,在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型(使用上述内核线程实现),即一条Java线程映射到一条轻量级进程之中;即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。

Java线程调度:

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调度和抢占式线程调度。 
协同式线程调度,线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。 
抢占式调度,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。 
Java线程调度就是抢占式调度。 
希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,线程调度最终还是取决于操作系统,所以程序的正确性不能依赖于线程的优先级高低。

线程的状态:

Java线程的生命周期中可处于以下6种状态。

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。

Daemon线程:

Daemon线程也叫守护线程,有时候你希望创建一个线程来执行一些辅助工作,但是又不希望这个线程阻碍JVM的关闭;这个情况下就需要使用守护线程。

线程可分为两种:普通线程和守护线程(后台线程)。在JVM启动时创建的所有线程中,除了主线程以外都是守护线程;守护线程主要用作程序中后台调度和支持性工作,如垃圾收集器等。

普通线程和守护线程之间的差异仅在于当线程退出时发生的操作。当一个线程退出时,JVM会检查其他正在运行的线程,如果JVM中不存在非守护线程时(即全都是守护线程),JVM将会正常退出,无视守护线程。

可通过调用Thread.setDaemon(true)将线程设置为守护线程,设置为守护线程的线程在JVM退出时会被直接抛弃,其finally块并不一定会执行。

多线程的具体的3种实现方式:

  • 继承Thread类
  • 实现Runnable接口
  • 使用ExecutorService、Callable、Future实现有返回结果的多线程

1.继承Thread类实现多线程

继承Thread类的方法尽管被列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:

public class MyThread extends Thread {
  public void run() {
   System.out.println("MyThread.run()");
  }
}
//启动线程即可
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();

2.实现Runnable接口方式实现多线程
如果自己的类已经extends另一个类,就无法直接继承Thread类,此时,必须实现一个Runnable接口,如下:

public class MyThread extends OtherClass implements Runnable {
  public void run() {
   System.out.println("MyThread.run()");
  }
}
//实例化一个Thread对象,传入实现了Runnable接口的自己的实例
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();

3、使用ExecutorService、Callable、Future实现有返回结果的多线程

ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程。
具体的实现方法较多,详细可参考这篇文章Java多线程实现的四种方式

线程状态转换

构造线程:

在运行线程之前首先要构造一个线程对象,线程对象在构造时需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否Daemon线程等信息。以下摘自java.lang.Thread类的线程初始化部分:

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    //当前线程是该线程的父线程
    Thread parent = currentThread();
    //如果线程组设置为空,则默认为父线程所属的线程组
    if (g == null) {
        g = parent.getThreadGroup();
    }
    this.group = g;
    //将父线程的daemon属性、priority属性设置给该线程
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    //设置加载上下文资源的类加载器
    this.contextClassLoader = parent.contextClassLoader;
    //设置run方法被调用的对象。
    this.target = target;
    //复制父线程的inheritableThreadLocal
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    //stackSize新线程的预期堆栈大小,为零时表示忽略该参数。
    this.stackSize = stackSize;
    //分配一个线程ID
    tid = nextThreadID();
}

在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而该child线程继承了parent线程是否为Daemon、线程优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识该线程。至此,一个能运行的线程对象就创建好了,在堆内存等待运行。

以上步骤完成后,直到调用start()方法启动该线程之前,线程都处于六大状态初始状态(NEW)。

启动线程

线程对象初始化完成后,调用start()方法就可以启动这个线程,此线程就进入就绪状态(ready)

start()方法的含义是:当前线程(即parent线程)同步告知JVM,只要线程规划器空闲,应立刻启动调用start()方法的线程。

但就绪状态仅仅是说明线程有资格运行,调度程序没有挑选到你,你就永远是就绪状态,直到线程调度程序从可运行池中选择一个线程作为当前线程时线程,即就绪状态的线程获取到了CPU执行时间片段,线程才算进入运行中状态(running),这也是线程进入运行中状态的唯一一种方式。

Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行状态”(RUNNABLE)

另外,线程启动需满足Java内存模型的happens-before八大原则中的线程启动原则:Thread对象的start()方法先行发生于此线程的任何一个动作。详细可见Java内存模型与happes-before原则

中断线程

在Java中没有办法立即停止一条线程(除了过时的stop方法,此方法在终结一个线程时不会保证线程的资源正常释放),但是线程的停止是极其重要的,因此Java提供了一种用于停止线程的机制-----线程中断;中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。

注意:此处的中断一个线程仅仅是指影响线程的某个标识属性,从而达到引起线程注意的目的;Java并没有任何语言方面的需求要求一个被中断的线程应该终止,被中断的线程可以自行选择如何对中断作出响应。

当其他线程对一个线程调用其interrupt()方法时,该线程的中断状态标识位将被置位。这是每一个线程的都具有的boolean标志。每个线程都应该时不时的检查这个标志,以判断线程是否被中断,线程通过其isInterrupted()方法来判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识进行复位。以下是Java API中对于三种线程中断相关方法的描述。

interrupt

public void interrupt()

中断线程。

如果当前线程没有中断它自己(这在任何情况下都是允许的),则该线程的 checkAccess 方法就会被调用,这可能抛出 SecurityException

如果线程在调用 Object 类的 wait()wait(long) 或 wait(long, int) 方法,或者该类的 join()join(long)join(long, int)sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException

如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException

如果该线程在一个 Selector 中受阻,则该线程的中断状态将被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用了选择器的 wakeup 方法一样。

如果以前的条件都没有保存,则该线程的中断状态将被设置。

中断一个不处于活动状态的线程不需要任何作用。

抛出:

SecurityException - 如果当前线程无法修改该线程


interrupted

public static boolean interrupted()

测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

线程中断被忽略,因为在中断时不处于活动状态的线程将由此返回 false 的方法反映出来。

返回:

如果当前线程已经中断,则返回 true;否则返回 false


isInterrupted

public boolean isInterrupted()

测试线程是否已经中断。线程的中断状态 不受该方法的影响。

线程中断被忽略,因为在中断时不处于活动状态的线程将由此返回 false 的方法反映出来。

返回:

如果该线程已经中断,则返回 true;否则返回 false

中断策略:

中断策略,即线程应该如何解释某个中断请求----当发现中断请求时,线程应该做哪些工作。

由于每个线程拥有各自的中断策略,因此除非你知道中断对于该线程的含义,否则就不应该中断这个线程;一般来说,只有线程的拥有者才能更准确的对中断做出合适的响应,因此对于线程的非拥有者的代码来说,应该小心的保存中断状态,这拥有线程的代码才能对中断做出响应,即使非拥有者代码也能做出响应。

这就是为什么大多数可阻塞的库函数只是抛出java.lang.InterruptedException作为中断响应,它们永远不会在某个由自己拥有的线程中运行,因此它们为任务或库代码实现了最合理的取消策略:尽快退出执行流程,并把中短信息传递给调用者,从而使得调用栈中的上层代码可以采取进一步的操作。

安全的停止线程:

上面已经提到,stop方法停止线程过于暴力,它会立即停止线程,不给任何资源释放的余地,因此被弃用了。

中断操作是一种简便的线程间交互方式,这种交互方式最适合取消或者停止任务;除此之外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。下面介绍这两种安全停止线程的方法:

public class Shutdown {
    public static void main(String[] args) throws Exception {
        Runner runner1 = new Runner();
        Thread countThread = new Thread(runner1,"countThread");
        countThread.start();
        //睡眠一秒,main线程对countThread线程进行中断,使它能感知中断而结束
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner runner2 = new Runner();
        countThread = new Thread(runner2,"countThread");
        countThread.start();
        //睡眠一秒,main线程对runner2进行取消,是的countThread能感知到on为false而结束
        TimeUnit.SECONDS.sleep(1);
        runner2.cancel();
    }
    private static class Runner implements Runnable {
        private long i;
        private volatile boolean on = true;
        @Override
        public void run() {
           while (on && !Thread.currentThread().isInterrupted()) {
               i++;
           }
           System.out.println("Count i = "+i);
        }
        public void cancel() {
            on = false;
        }
    }
}

在上述代码执行过程中,main线程能通过中断操作cancel()方法两种方式使得countThread得以终止;

上述两种方法本质一样,都是通过循环查看一个共享标记为来判断线程是否需要中断,他们的区别在于:runner2的标识位on是我们自己设定的,而runner1的标识位是Java提供的。除此之外,他们的实现方法是一样的。

上述两种方法之所以较为安全,是因为一条线程发出终止信号后,接收线程并不会立即停止,而是将本次循环的任务执行完,再跳出循环停止线程。此外,程序员又可以在跳出循环后添加额外的代码进行收尾工作,比如上面的打印出来。

线程间的通信

线程之间的通信是指线程之间以何种机制来交换信息。线程之间的通信机制有两种共享内存消息传递

共享内存的并发模型中,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。

消息传递的并发模型中,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。

虽然多线程之间通信原理上都可以看做是以上两种的衍生,但实际的实现方式还是有很多种的,下面就来介绍Java多线程通信的几种主要方式。

1、等待/通知机制

一个线程修改了变量的值,而另一个线程感知到了变化,然后进行相应的操作;整个过程开始于一个线程,而最终执行又是另一个线程;前者是生产者,后者是消费者,这种模式隔离了“做什么”“怎么做”,在功能层面实现了解耦,但是在Java中是如何实现的这样类似的功能呢?

以上面的题目为例,生产者线程改变了对象的值(打印1~52),消费者线程感知到了变化做出操作(对应打印A~Z),最简单的办法就是让消费者线程一直不断的循环检查变量是否符合预期(此处为是否连续打印了2个数字),如下代码,如果满足则退出while循环,然后完成消费者的工作(打印对应的字母)。

 while (value != desire) { //检查变量是否符合预期
     Thread.sleep(1000); //轮询的时间间隔
 }
 doSomething();  //消费者线程实际操作,如打印字母

这样做会有两个问题:在不停过通过轮询机制来检测判断条件是否成立的过程中,如果轮询时间的间隔太小会浪费CPU资源;而轮询时间的间隔太大,就不能及时的发现条件变更,从而不能及时的取到自己想要的数据。

Java提供的等待/通知相关方法就能很好解决这个问题,Java等待/通知的相关方法都是定义在java.lang.Object类上的,所有对象都具备这些方法。

Java等待/通知的相关方法
 方法名称  描述
notify()随机唤醒等待队列中等待同一共享资源的 “一个线程”,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个线程”
notifyAll()使所有正在等待队列中等待同一共享资源的 “全部线程” 退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现
wait()使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒
wait(long)  超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
wait(long,int)对于超时时间更细力度的控制,可以达到纳秒

等待/通知机制,是指一个线程A调用了对象obj的wait()/wait(long)/wait(long,int)方法进入等待状态(WAITING)超时等待状态(TIME_WAITING),而另一个线程B调用了对象obj的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。上诉两个线程通过对象obj来完成交互,而对象上的wait()notify()等方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

例:编写两个线程,一个线程打印1~52,另一个线程打印字母A~Z,打印顺序为12A34B56C……5152Z。

public class WaitNotify {
    private static final int MAX = 52;
    private static final String letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    static int flag = 0;
    static Object obj = new Object();

    public static void main(String args[]) throws Exception {
        Thread numThread = new Thread(new Producer(),"printNumbers");
        Thread letterThread = new Thread(new Consumer(),"printLetters");
        numThread.start();
        letterThread.start();
    }
    //生产者,此处功能为打印数字
    static class Producer implements Runnable {
        @Override
        public void run() {
            synchronized (obj) {//加锁,获取obj对象的monitor
                for (int i = 1;i <= MAX;i++) {
                    while (flag >= 2) {//条件不满足,继续wait
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }//条件满足,执行功能代码
                    System.out.print(i);
                    flag++;
                    obj.notify(); //叫醒在等待的线程
                }
            }
        }
    }
    //消费者,此处功能为打印字母
    static class Consumer implements Runnable {
        @Override
        public void run() {
            synchronized (obj) {//加锁,获取obj对象的monitor
                for (int i = 0;i < letter.length();i++) {
                    while (flag < 2) {
                        try {
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(letter.charAt(i));
                    flag = 0;
                    obj.notify();
                }
            }
        }
    }
}

需要注意的是:

  • wait()和notify()等方法时需要先获得对象的锁
  • 调用wait()方法后,线程状态由运行状态RUNNING转变为WAITING等待状态(如果是wait(long)/wait(long,int)方法就会是超时等待状态TIME_WAITING),线程移也会移动到等待队列
  • 调用 notify() 方法会将等待队列中的线程移动到同步队列中,线程状态也会更新为 阻塞状态BLOCKED
  • 调用wait()方法的线程会释放锁,而调用notify()方法是不会立刻释放锁的,必须执行完notify()方法所在的synchronized代码块后才释放
  • 从 wait() 方法返回的前提是调用 notify() 方法的线程释放锁,wait() 方法的线程获得锁

等待/通知的经典范式:

//Thread A
synchronized(Object){
    while(条件){
        Object.wait();
    }
    //叫醒其他等待线程,但不释放锁
    Object.notify();
    //do something
     
}
//Thread B
synchronized(Object){
    条件=!条件;   //改变条件
    Object.notify();
    //do something
}

线程 A 作为消费者:

  1. 获取对象的锁。
  2. 进入 while(判断条件),并调用 wait() 方法。
  3. 当条件满足跳出循环执行具体处理逻辑。

线程 B 作为生产者:

  1. 获取对象锁。
  2. 更改与线程 A 共用的判断条件。
  3. 调用 notify() 方法。

2、使用Thread.join()方法

         在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

JDK中对join()方法解释为:“等待该线程终止”,换句话说就是:”当前线程等待子线程的终止“。也就是主线程中在子线程.join()后面的代码只有等到子线程结束了才能执行。

public class WaitNotify {
    private static final int MAX = 9;
    private static final String letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    public static void main(String args[]) throws Exception {
        Thread numThread = new Thread(new Producer(),"printNumbers");
        Thread letterThread = new Thread(new Consumer(),"printLetters");
        numThread.start();
        numThread.join();  //等到numThread线程终止
        letterThread.start();
    }

    static class Producer implements Runnable {
        @Override
        public void run() {
            for (int i = 1;i <= MAX;i++) {
                System.out.print(i);
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            for (int i = 0;i < letter.length();i++) {
                System.out.print(letter.charAt(i));
            }
        }
    }
}
//输出结果为顺序的123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
//若不使用join则会是乱序的123ABCDEFGHI456789JKLMNOPQRSTUVWXYZ

3、volatile共享内存

 flag 变量存放于主内存中,所以主线程和线程 A 都可以看到,且flag 采用 volatile 修饰,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。所以主线程在修改了标志位使得线程 A 立即停止,如果没有用 volatile 修饰,就有可能出现延迟。

public class Volatile implements Runnable{
    private static volatile boolean flag = true ;
    @Override
    public void run() {
        while (flag){
        }
        System.out.println(Thread.currentThread().getName() +"执行完毕");
    }
    public static void main(String[] args) throws InterruptedException {
        Volatile aVolatile = new Volatile();
        new Thread(aVolatile,"thread A").start();
        System.out.println("main 线程正在运行") ;
        Scanner sc = new Scanner(System.in);
        while(sc.hasNext()){
            String value = sc.next();
            if(value.equals("1")){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        aVolatile.stopThread();
                    }
                }).start();
                break ;
            }
        }
        System.out.println("主线程退出了!");
    }
    private void stopThread(){
        flag = false ;
    }
}

除了以上方式外,还可以采用CountDownLatch并发工具、CyclicBarrier并发工具、管道通信和线程池等方法能实现线程通信,由于篇幅问题,以及这几种方法需要一定的并发工具类和同步队列的知识,我们将在后续这些并发知识学习后,再作实践。

 

 

参考文章:

 《Java并发编程的艺术》

 《Core Java VolumeⅠ》

 《Java并发编程实践》

   https://crossoverjie.top/2018/03/16/java-senior/thread-communication/

   https://blog.csdn.net/u011480603/article/details/75332435

   https://blog.csdn.net/evankaka/article/details/44153709

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值