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
线程中
Runnable
,run()
方法并不会产生任何的线程能力,要实现线程的行为,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()
方法后,新的线程在内部会调用Runnbale
的run()
方法。但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, 优先级)执行。
在任何一个线程池中,现有的线程都有可能会被自动复用
首先考虑使用newCachedThreadPool ,CachedTheadPool
在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程
四种类型的线程池都是通过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任务添加到线程池中时,按照如下顺序来处理:
如果线程池中的线程数量少于
corePoolSize
,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务如果线程池中的线程数量大于等于
corePoolSize
,但缓冲队列workQueue
未满,则将新添加的任务放到workQueue
中,按照FIFO
的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行)如果线程池中的线程数量大于等于
corePoolSize
,且缓冲队列workQueue
已满,但线程池中的线程数量小于maximumPoolSize
,则会创建新的线程来处理被添加的任务如果线程池中的线程数量等于了
maximumPoolSize
,有4种处理方式(该构造方法调用了含有5个参数的构造方法,并将最后一个构造方法为RejectedExecutionHandler类型,它在处理线程溢出时有4种方式)
总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize
,再看缓冲队列workQueue
是否满,最后看线程池中的线程数量是否大于maximumPoolSize
另外,当线程池中的线程数量大于corePoolSize
时,如果里面有线程的空闲时间超过了keepAliveTime
,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。
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)
- 参数不同,
execute()
参数只能为Runnable
,而submit()
支持两种类型 -
submit()
具有Future
的类型返回值 -
execute()
为Executor
的方法,submit()
为ExecutorService
的方法,Executor
为ExecutorService
的父接口
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()
方法便会返回true
;isCancel()
方法的返回值则由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次开销比较大的浮点运算。其中变量d
是volatile
类型的,以确保不进行任何的变量优化
书上作者说加浮点运算是为了更好的看到优先级高的线程先运行,作者在Winodws XP 下优先级最高的线程任务全部运行完,低优先级的才运行,而我测试的不是
volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。
volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。
也就是说,volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。
摘自Java中volatile的作用以及用法
volatile这个修饰符没有用过,查了下,暂时不理解
JDK
中,线程共有10个优先级,默认为 NORM_PRIORITY = 5 ,但与各个操作系统映射得不是很好。在Winodws
中有7个优先级并且不是固定的,所有优先级的映射关系也不是很准。具有可移植性的是: MAX_PRIORITY , NORM_PRIORITY , MIN_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()
内执行另外一个线程nt
的nt.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. 最后
这一章知识点好多,这才是上
,而且感觉想要理解都需要花费不少时间,感觉能吸收一半就不错了,以后得多看书,多看博客
本人很菜,有错误请指出
共勉 :)
上一篇: Nginx的反向代理配置实例
下一篇: php快速导出csv格式数据程序代码