Java编程思想第21章并发读书笔记(上)

学习资料:

  • Java编程思想

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行

1. 并发的多面性

并发性,又称共行性,是指能处理多个同时性活动的能力。并发的实质是一个物理CPU(也可以多个物理CPU)在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率

摘自网络

顺序编程:程序中所有的事物在任意时刻都只能执行一个步骤

目前计算机都是多核处理器,但并发通常是提高运行在单处理器上的程序的性能

由于在运行期间增加了上下文切换的代价(从一个任务切换到另一任务),在单处理器运行的并发程序开销比该程序所有部分的都顺序执行的开销大

但由于阻塞的存在,并发就比顺序要高效稳定


通常一个线程有5个状态:创建,就绪,运行,阻塞,销毁

阻塞:在程序运行中,某一时刻,某一个任务由于一些控制范围之外的条件(通常是I/O)而导致不能继续执行,通常说任务或者线程阻塞

当发生阻塞时,顺序编程的整个程序就会停止,直至导致阻塞的外部条件发生改变;并发编程的程序,当一个任务发生阻塞时,程序中其他的任务还可以继续执行,整个程序依然可以保持继续向前执行

从性能的角度,如果任务不会发生阻塞,此任务在单处理器机器上使用并发就没有意义

使用并发的一个极佳需求便是:产生可响应的用户界面。在单核时代,程序需要连续执行任务,同时要求能够响应用户界面的控制,以便程序来响应用户,同时既要做这又要照顾那,并发会造成听起来CPU同时能够同时处于两个空间的错觉。但目前早以是多核时代,这种需求顺理成章


实现并发最直接的方式就是在操作系统级别使用进程,进程是运行在自己的地址空间内的自包容的程序

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体

狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的概念主要有两点:

第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上
——摘自百度百科

有一篇非常好,通俗易懂的博客:进程与线程的一个简单解释

多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程,但这导致每个进程看起来在运行期间总是歇歇停停。操作系统通常会将进程相互隔离开,彼此不会干涉

Java所使用的并发系统会 共享 诸如内存I/O这样的资源,编写多线程的困难在于协调并控制不同线程驱动的任务之间对与共享资源的使用,避免多个线程同时对一个资源进行操作

Java的线程机制是 抢占式 的。调度机制会周期性地中断某一正在运行的线程,将上下文(任务)切换到另一个线程,为每个线程都提供时间片,这样每一个线程都有机会分得一定的时间来执行任务。


2. 基本的线程机制

之前对线程有学习:Java——Thread线程基础知识学习

并发编程将程序分为多个分离的、独立运行的任务。通过多线程机制,每一个被分离的独立任务(子任务)都会有一个 执行线程 来执行。一个线程就是在进程中的一个单一的顺序控制流,一个进程可以拥有多个并发执行的任务

在使用多线程时,CPU将时间划分成片段分配给所有的任务,每个任务都会分配到时间片


2.1 Runnable定义任务

线程可以执行任务,任务则可以用Runnable来定义。实现Runnable接口,重写run()方法。run()方法则可以理解为具体执行任务的命令,任务的具体执行过程

定义LiftOff任务:

public class LiftOff implements Runnable {
    private int countDown = 10;
    private static int taskCount = 0;
    private final int id = taskCount ++;//一旦初始化后,不再改变,加final
    
    public LiftOff(){}

    public LiftOff(int countDown){
       this.countDown = countDown;
    }
    
    @Override
    public void run() {
       while (countDown -- > 0){
           System.out.println(status());
           Thread.yield();//声明任务完成,此刻切换其他线程
       }
    }

    private String status(){
        return "#" +id + " ---> " + (countDown > 0 ? countDown : "LiftOff!");
    }
}

通常run()内的命令是一个循环,这样可以确保任务一直被执行下去直到不再需要。根据任务需求,循环需要设置一些条件来跳出循环

Thread.yield() 是向线程调度器发出一个建议,声明:

我已经走完生命中最重要的部分,此刻正是切换其他任务执行的大好时机

这样做的目的是为了能够产生有趣的输出,很可能会看到任务换进换出的证据

执行任务:

public class MainThread {
    public static void main(String[] args) {
        LiftOff liftOff = new LiftOff();
        liftOff.run();
    }
}

运行结果:

#0 ---> 9
#0 ---> 8
#0 ---> 7
#0 ---> 6
#0 ---> 5
#0 ---> 4
#0 ---> 3
#0 ---> 2
#0 ---> 1
#0 ---> LiftOff!

main()方法中,liftOff任务并没有运行在一个单独的线程中,而是在main线程中

Runnablerun()方法并不会产生任何的线程能力,要实现线程的行为,Runnable必须显式地依附在一个线程之上


2.2 Thread类

通常把一个Runnable对象直接通过Thread的构造方法转换为一个工作任务

public class BasicThreads {
    public static void main(String[] args) {
        LiftOff liftOff = new LiftOff();
        Thread thread = new Thread(liftOff);
        thread.start();
        for(int i = 0 ; i < 10 ; i ++){
            System.out.println("Waiting for LiftOff");
        }

    }
}

其中的一种运行结果:

Waiting for LiftOff
Waiting for LiftOff
#0 ---> 9
Waiting for LiftOff
Waiting for LiftOff
Waiting for LiftOff
Waiting for LiftOff
#0 ---> 8
Waiting for LiftOff
Waiting for LiftOff
Waiting for LiftOff
Waiting for LiftOff
#0 ---> 7
#0 ---> 6
#0 ---> 5
#0 ---> 4
#0 ---> 3
#0 ---> 2
#0 ---> 1
#0 ---> LiftOff!

thread调用了start()方法后,新的线程在内部会调用Runnbalerun()方法。但LiftOff是在新的线程执行,此时main()线程仍然可以执行其他的操作

main()创建Thread对象t时,并没有捕获t的任何引用,但每个Thread对象注册了自己,确实存在一个t的引用,在任务完成run()方法结束,t死亡之前,回收器都无法回收


更多线程:

public class MoreBasicThreads {
    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            LiftOff liftOff = new LiftOff(2);
            Thread thread = new Thread(liftOff);
            thread.start();
        }
        System.out.println("Waiting for LiftOff");
    }
}

运行结果:

Waiting for LiftOff
#0 ---> 1, #1 ---> 1, #0 ---> LiftOff!, #2 ---> 1, #3 ---> 1,#4 ---> 1,
#1 ---> LiftOff!, #2 ---> LiftOff!, #3 ---> LiftOff!, #4 ---> LiftOff!, 

为了方便以后看,把运行结果做了调整

运行结果表明了不同任务执行时,换进换出混在了一起

结论:一个Thread对象创建出一个单独的执行线程,调用了start()方法后,Thread的对象仍旧会存在,直到内部的任务run()方法执行完毕,Thread的对象才会被回收


2.3 Executor 执行器

学习资料:

Executor 执行器,用于管理Thread对象,在客户端和任务执行之间提供了一个间接层。Excutor允许管理异步任务的执行,而无须显式地管理线程的生命周期

最简单的使用:

public class CachedThreadPool {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0 ; i < 3; i ++  ){
             executorService.execute(new LiftOff(3));
        }
        executorService.shutdown();//关闭线程池
    }

}

结果:

#1 ---> 2, #0 ---> 2, #2 ---> 2, #1 ---> 1, #0 ---> 1, #2 ---> 1, 
#1 ---> LiftOff!, #0 ---> LiftOff!, #2 ---> LiftOff!, 

Executor是一个接口,ExectorService接口继承之Executor,提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法

本例中使用的Executors.newCachedThreadPool()来生成ExectorService

ExectorService有3个生命周期状态: 运行,关闭,终止

executorService.shutdown()关闭线程池服务,便处于关闭状态,可以防止新任务再被提交到Executor。当前线程(本例中就是main()方法的线程)会继续运行shutdown()前提交的任务。这个程序将会在Exector中所有的任务完成之后尽快退出


Executors提供了一系列的实用的工厂方法用来创建线程池

newCachedThreadPool()方法源码:

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

四种线程池创建方法:

  • newCachedThreadPool
    可缓存的线程池。没有固定大小,如果线程池中的线程数量超过任务执行的数量,会回收60秒不执行的任务的空闲线程。当任务数量增加时,线程池自己会增加线程来执行任务。而能创建多少,就得看jvm能够创建多少

  • newFixedThreadPool
    固定线程数量大小的线程池,并发线程数量不会超过固定大小,超出的线程会在队列中等待。如果一个正在执行的线程出现异常结束,会创建一个显得线程来代替它

  • newScheduledThreadPool
    也是固定线程数量大小的线程池,可以延迟或者定时周期执行任务

  • newSingleThreadExecutor
    单例线程池。线程池中只有一个线程工作,出现异常会有有个新的线程来代替。线程池会保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

在任何一个线程池中,现有的线程都有可能会被自动复用

首先考虑使用newCachedThreadPoolCachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程

四种类型的线程池都是通过ThreadFactory接口来创建的线程


2.3.1 ThreadPoolExecutor

自定义线程池,学习了解参数:

public class ThreadPools {
    public static void main(String[] args) {
        //创建等待队列
        BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(10);
        //创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,//核心线程数,包含空闲线程
                5,//最大线程数
                50,//当线程数大于最大线程数时,空闲线程等待新任务的最大时间
                TimeUnit.MILLISECONDS,//前一个参数,等待时间的单位
                blockingQueue//任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务
        );
        for (int i = 0; i < 5; i++) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " ---> 正在执行");
                }
            });

//          threadPoolExecutor.execute(() -> System.out.println(Thread.currentThread().getName() + " ---> 正在执行"));

            //关闭线程池
            threadPoolExecutor.shutdown();
        }

    }
}

运行结果:

pool-1-thread-1 ---> 正在执行
pool-1-thread-2 ---> 正在执行
pool-1-thread-1 ---> 正在执行
pool-1-thread-2 ---> 正在执行
pool-1-thread-1 ---> 正在执行

源码:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
}

根据ThreadPoolExecutor源码前面大段的注释,我们可以看出,当试图通过excute方法讲一个Runnable任务添加到线程池中时,按照如下顺序来处理:

  1. 如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务

  2. 如果线程池中的线程数量大于等于corePoolSize,但缓冲队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行)

  3. 如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务

  4. 如果线程池中的线程数量等于了maximumPoolSize,有4种处理方式(该构造方法调用了含有5个参数的构造方法,并将最后一个构造方法为RejectedExecutionHandler类型,它在处理线程溢出时有4种方式)

总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize

另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。

以上这段,摘自【Java并发编程】之十九:并发新特性—Executor框架与线程池


2.4 Callable,从任务产生返回值

TaskWithResult代码:

public class TaskWithResult implements Callable<String> {

    private int id;

    public TaskWithResult(int id) {
        this.id = id;
    }

    @Override
    public String call() throws Exception {
        return "TaskWithResult ---> " + id;
    }
}

实现Callable<>接口,重写call()方法。call()的返回值就是异步线程所想要返回的任务结果

CallableDemo代码:

public class CallableDemo {
    public static void main(String[]args){
        test();
    }


    private static void test() {
        //创建线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Future集合
        ArrayList<Future<String>> list = new ArrayList<>();
        for (int i = 0; i < 5;i++){
            Future<String> future = executorService.submit(new TaskWithResult(i));
            list.add(future);
        }
        //遍历
        for (Future<String> future : list){
            try {
                    System.out.println(future.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            } finally {
                //记得关闭线程池
                executorService.shutdown();
            }
        }
    }    }
}

运行结果:

TaskWithResult ---> 0
TaskWithResult ---> 1
TaskWithResult ---> 2
TaskWithResult ---> 3
TaskWithResult ---> 4

提交任务用的是:executorService.submit()

submit()方法会产生Future对象,并用Callble的返回结果作为泛型。可以使用Future对象的isDone()来查看Future是否完成,当任务完成时,会具有一个结果,可以使用get()方法来获取结果

不用isDone()判断,而直接使用get()方法来获取结果时,get()会阻塞任务线程,直到能够获取结果。get()方法又一个具有设置超时的重载方法


submit() 和 execute() 直观区别:

  • execute(Runnable command)
  • <T> Future<T> submit(Callable<T> task),Future<?> submit(Runnable task)
  1. 参数不同,execute()参数只能为Runnable,而submit()支持两种类型
  2. submit()具有Future的类型返回值
  3. execute()Executor的方法,submit()ExecutorService的方法,ExecutorExecutorService的父接口

2.4.1 Future未来

单词的意思是: 未来,将来时

源码中的注释说明:

A {@code Future} represents the result of an asynchronous
computation.  Methods are provided to check if the computation is
complete, to wait for its completion, and to retrieve the result of
the computation.  The result can only be retrieved using method
{@code get} when the computation has completed, blocking if
necessary until it is ready.  Cancellation is performed by the
{@code cancel} method.  Additional methods are provided to
determine if the task completed normally or was cancelled. Once a
computation has completed, the computation cannot be cancelled.
If you would like to use a {@code Future} for the sake
of cancellability but not provide a usable result, you can
declare types of the form {@code Future<?>} and
return {@code null} as a result of the underlying task.

个人理解:

一个异步任务结果容器。并具有对任务结果操作的能力。可以查看结果是否完成,取消结果,获取结果

  • boolean isDone()

    如果任务结束,任务正常结束或者任务被取消了,就返回true

  • boolean isCancelled()

    任务在完成返回结果之前被取消,会返回true

  • boolean cancel(boolean mayInterruptIfRunning)

    取消正在执行的任务。如果要取消的任务目标已经完成,或者已经被取消过,或者由于其他原因无法取消,取消操作便会失败,返回false。如果一个任务在执行前进行了取消操作,这个任务便再不会被执行。如果任务线程已经开始,mayInterruptIfRunning将决定是否中断任务,true就会将正在执行的任务取消。false时,已经开始的任务便可以继续执行

    返回值为false时,代表取消失败,任务已经完成,结果已经返回;返回值为true时,表示任务取消,返回的Future容器中并没有任何结果

    调用这个方法后有了返回值之后,被取消的任务返回的Future对象,调用isDone()方法便会返回trueisCancel()方法的返回值则由cancel()的值来决定

  • V get() throws InterruptedException, ExecutionException

    必要的时候会发生阻塞,直到任务完成,返回结果

* @return the computed result
* @throws CancellationException if the computation was cancelled
* 返回任务结果时,被取消了
* @throws ExecutionException if the computation threw an exception
* 任务结果自身发生异常
* @throws InterruptedException if the current thread was interrupted while waiting
* 在阻塞等待结果时,任务线程被中断

这个方法抛出3个异常


2.5 休眠与让步

代码:

public class SleepingTask extends LiftOff{
    @Override
    public void run() {
        super.run();
        while (countDown-- > 0){
            System.out.println(status());
            //1.5前
            // Thread.sleep(100);
            //1.5后
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0 ; i < 3; i ++){
            executorService.execute(new SleepingTask());
        }
        executorService.shutdown();
    }
}

Java SE5中添加了更加显示的sleep()方法版本,直接通过TimeUtil来指定休眠的时间单位,sleep(long timeout)调用会线程处于阻塞状态

yield()方法是让步,调用后会给线程调度器一个暗示,可以让别的线程来使用cpu。但也仅仅是个暗示,没有办法保证这个暗示一定会被采纳,只是建议让拥有相同优先级的其他线程运行


2.6 优先级

线程优先级的作用就是将线程的重要性告诉调度器。自然而然,优先级高的线程就越是有机会优先执行。优先级低并不意味着得不到执行,优先权并不会导致死锁。

简单Dome:

public class SimplePriorities implements Runnable {
    private int countDown = 2;
    private volatile double d;
    private int priority;

    public SimplePriorities(int priority) {
        this.priority = priority;
    }

    @Override
    public String toString() {
        return Thread.currentThread() + " --> " + countDown;
    }

    @Override
    public void run() {
        Thread.currentThread().setPriority(priority);
        while (true) {
            for (int i = 1; i < 100000; i++) {
                d += (Math.PI + Math.E) / (double) i;
                if (i % 1000 == 0) {
                    Thread.yield();
                }
            }
            System.out.println(this);
            if (--countDown == 0) return;
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
           executorService.execute(new SimplePriorities(Thread.MIN_PRIORITY)); //最低优先级
        }
        executorService.execute(new SimplePriorities(Thread.MAX_PRIORITY));//最高优先级
        executorService.shutdown();
    }

}

运行结果:

Thread[pool-1-thread-1,1,main] --> 2
Thread[pool-1-thread-2,1,main] --> 2
Thread[pool-1-thread-4,10,main] --> 2
Thread[pool-1-thread-3,1,main] --> 2
Thread[pool-1-thread-2,1,main] --> 1
Thread[pool-1-thread-1,1,main] --> 1
Thread[pool-1-thread-4,10,main] --> 1
Thread[pool-1-thread-3,1,main] --> 1

main()方法中,前3个线程的优先级为最低,最后一个线程为最高

注意:尽量不要构造方法中,设置优先级。上面的代码是在run()方法开头进行设置,此时Executor此时还没开始执行任务

run()方法中执行了100000次开销比较大的浮点运算。其中变量dvolatile类型的,以确保不进行任何的变量优化

书上作者说加浮点运算是为了更好的看到优先级高的线程先运行,作者在Winodws XP 下优先级最高的线程任务全部运行完,低优先级的才运行,而我测试的不是

volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。

volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。

也就是说,volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。

摘自Java中volatile的作用以及用法
volatile这个修饰符没有用过,查了下,暂时不理解

JDK中,线程共有10个优先级,默认为 NORM_PRIORITY = 5 ,但与各个操作系统映射得不是很好。在Winodws中有7个优先级并且不是固定的,所有优先级的映射关系也不是很准。具有可移植性的是: MAX_PRIORITYNORM_PRIORITYMIN_PRIORITY


2.7 后台线程

后台(daemon)线程:在程序运行的时候在后台提供了一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分。

当所有的非后台线程运行结束了,程序也就终止了,同时还会杀死所有的后台线程。反过来说,只要任何非后台线程还在运行,程序就不会终止

简单案例:

public class SimpleDaemons implements Runnable{
    @Override
    public void run() {
        while(true){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() +"  "+this);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0 ; i < 5;i ++){
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            
            daemon.start();
        }
        System.out.println("后台线程已全部启动");
        try {
            TimeUnit.MILLISECONDS.sleep(175);//可修改之来观察打印结果
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

关键也就一句 daemon.setDaemon(true)

可以通过isDaemon()来判断线程是否为一个后台线程。若为一个后台线程,那它创建的任何线程都将被自动设置为后后台线程

后台线程没有使用过,也不知道啥场景适合使用。。。


2.8 加入一个线程

一个线程t在自己的run()内执行另外一个线程ntnt.join()方法,t线程便会被挂起,nt线程结束后,也就是nt.isAlive()false,第一个线程t才会继续执行

可以在调用join()方法时带上一个超时参数,单位可以为毫秒,纳秒,当目标线程nt在设置的超时时间到期后还没有结束,join()方法便会返回,t也就不再会执行

简单案例:

public class Joining {
    public static void main(String[] args) {
        OtherThread otherThread = new OtherThread("OtherThread",1500);
        OneThread oneThread = new OneThread("OneThread",otherThread);
//        System.out.println("--------");
//        OtherThread interrupt = new OtherThread("InterruptThread",1500);
//        OneThread oneThread_2 = new OneThread("OneThread_2",interrupt);
//        interrupt.interrupt();

    }
}

class OtherThread extends Thread {
    private int duration;

    public OtherThread(String name, int sleepTime) {
        super(name);
        this.duration = sleepTime;
        start();
    }

    @Override
    public void run() {
        try {
            sleep(duration);
        } catch (InterruptedException e) {
            System.out.println(getName() + " was interrupted, " +
                               " isInterrupted(): "+isInterrupted());
            return;
        }
        System.out.println(getName() + " --> has awakened");
    }
}

class OneThread extends Thread{
    private OtherThread otherThread;

    public OneThread(String name,OtherThread otherThread) {
        super(name);
        this.otherThread = otherThread;
        start();
    }

    @Override
    public void run() {
        try {
            otherThread.join();//把otherThread线程加入进来
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getName()+" --> OneThread complete");
    }
}

运行结果:

OtherThread --> has awakened and complete
OneThread --> OneThread complete

个人理解:
个人感觉join()方法就是加塞,具有优先执行的特权,谁调用了join(),谁就有了特权,可以优先执行。有特权的线程执行完成后,没特权的线程才开始执行

CyclicBarrier工具类比join()方法更适合用来加入一个线程


3. 最后

这一章知识点好多,这才是,而且感觉想要理解都需要花费不少时间,感觉能吸收一半就不错了,以后得多看书,多看博客

本人很菜,有错误请指出

共勉 :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容