别指望一文读懂Java并发之从一个线程开始
Understanding concurrent programming is on the same order of difficulty as understanding object-oriented programming. If you apply some effort, you can fathom the basic mechanism, but it generally takes deep study and understanding to develop a true grasp of the subject.
———— Bruce Eckel
序言
对于一段并发程序而言,我们往往会按照顺序编程时的习惯编写具有确定性的代码,预想着输出可预见性的结果;但实际上这段程序却绝不会按照程序员的意愿执行,即我们的代码开始变得不可预见。回想我们的编程生涯,哪一个不是从顺序编程慢慢地走向并发编程呢?而我们编写的程序也从乖巧的婴孩成长为叛逆的青春期少年,不要被一时意外的结果搞得火冒三丈,慢慢理解并发编程的深切内涵,相信终有一天,并发会带给你一个又一个想不到的惊喜。
想要深刻理解并发编程不是一朝一夕所能完成的事,需要花费大量的时间和精力。如果不能理解并发,即使写出了看似正确的并发程序,那么在实际应用中,也会遇到各种各样的问题,而这些问题在测试中极难遇到。这时你可能会说,既然并发的概念理解起来那么晦涩,使用中又会遇到那么多问题,那干脆不用并发不就行了嘛?很遗憾,这种想法仅仅具有逻辑上的可行性,因为即使不编写并发程序,你使用的很多框架和系统也都或多或少的用到了并发的思想,所以理解并发终究还是我们必须要做的事情。
是线程而不是进程
实现并发最直接的方式是在操作系统级别使用进程。因为操作系统通常会将进程相互隔离开来,其彼此不会相互干涉,这也是并发编程的最理想的情况。但对于Java所使用的并发系统会共享如内存和I/O这样的资源,因此Java采用了另一种方式实现并发:在顺序型语言的基础上提供了对线程的支持。线程机制是由执行程序所代表的一个进程中创建任务,简言之就是线程的粒度比进程更细,生存于进程之内。所以在使用Java实现并发程序时,最基本也是最重要的难点就是如何协调不同线程对于资源的使用,以保证该资源不会被多个线程抢占。
此外因为有些操作系统并不支持多进程,Java的线程机制将并发模型绑定到单一进程的多个线程上,也在一定程度上履行了Java的"write once/run everywhere"
的诺言。
并发解决的两个问题
更快地执行
在编写程序时,除了要保证程序能正确合理地实现想要的功能,我们还会希望程序执行地尽可能快,而程序的执行速度大多数情况下和处理器有关。以现在的科技发展,摩尔定律或许有些过时了,单个处理器的时钟频率趋进天花板的时候,剩下的方式就是采用多个处理器同时运转,即并行计算,而并发是实现多处理器编程的基本工具。那么并发在单处理器上是不是就没什么作用了呢?
并发通常是提高运行在单处理器上的程序的性能。
注:在单处理器上的并发指的是多个任务在一段时间内各执行一小段时间,在频繁的切换过程中,多个任务可以看作在并发执行。
这似乎有点违背常理,因为并发执行所需要的时间应该等于程序真正执行的时间加上上下文切换的时间,应该比顺序执行所需要的时间更长才对。真实的情况是程序执行时往往会与外部交互(如I/O)而导致程序不能继续执行,这种情况叫做阻塞,所以顺序执行所需要的时间应该等于程序真正执行的时间加上阻塞的时间。采用并发编程可以大大降低由于阻塞所造成的时延,大大提高程序的性能。
还有一个比较常见的场景:计算机"卡住"的时候,此时无论是键盘输入还是鼠标点击,计算机都不能给出一个合理的响应。当然这个例子涉及到的知识较多,这里不做详述,我想说的是另一个相似的场景:对于一个可以接收用户输入的程序,如果采用顺序编程实现,那么程序执行耗时任务时,很容易进入"卡住"的状态,所以应该使用并发编程的方式,创建一个单独的线程用于接收用户输入,这样程序才能及时响应用户请求,才更容易在市场中存活下来。
改进代码设计
说到现在,你可能感觉并发存在的意义仅仅是提高程序执行效率,那我们再来考虑一种场景。在特效电影或游戏中,往往会同时出现很多个人物或其它元素,这里的每一个人物都是由一个处理器驱动。如果采用顺序编程实现,这基本不可能实现,因此必须为每个人物或元素分配独立的处理器进行驱动,即采用并发编程的方式。如果要处理元素的数量较多,以至于系统不能提供足够的线程,这时需要使用协作多线程机制来保证为每个元素都提供一个线程。
另外一个典型的场景是消息系统。由于消息系统涉及分布在整个网络中的多台计算机,因此必须采用并发编程的方式,才能保证整个消息系统不会丢失信息或在错误的时刻混进信息。
定义一个任务
并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过多线程机制,每一个独立的任务都将由执行线程驱动。在Java中,通过实现Runnable接口并编写run()方法就可以定义一个任务。如定义一个火箭发射倒计时的任务:
public class LiftOff implements Runnable {
private int countDown = 10;
private static int taskCount = 0;
//每创建一个对象,id增1,用于标识线程id
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String print() {
return "#" + id + "(" + (countDown > 0 ? countDown : "发射!" ) + "),";
}
//定义一个倒计时任务
@Override
public void run() {
while (countDown-- > 0) {
System.out.print(print());
}
}
}
执行这个任务
可以通过直接调用run()
来执行这个任务:
public class MainThread {
public static void main(String[] args) {
LiftOff launch = new LiftOff();
launch.run();
System.out.println("等我一下!");
}
}
/* 运行结果:
#0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(发射!),等我一下!
*/
但此时该任务并不是由单独的线程执行,而是由调用main()的线程执行,所以可以看到"等我一下!"
在发射之后打印。通过这种方式执行任务并没有达到我们的预期:多个线程并发执行!因此Java提供了Thread类和Executor接口来执行上面定义的任务。
Thread
将一个实现Runnable接口的任务对象作为构造器参数来创建Thread对象,然后就可以调用该对象的start()
来执行任务,以火箭发射倒计时任务为例:
public class BasicThreads {
public static void main(String[] args) {
Thread t = new Thread(new LiftOff());
t.start();
System.out.println("等我一下!");
}
}
/* 运行结果:
等我一下!
#0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(发射!),
*/
相比较与直接调用run(),可以看到此时"等我一下!"
在开始发射之前打印。造成这种结果的核心是start()
,查看其源码注释可以看到如下信息:
Causes this thread to begin execution; the Java Virtual Machine calls the
run
method of this thread.The result is that two threads are running concurrently: the current thread (which returns from the call to the
start
method) and the other thread (which executes itsrun
method).
start()会执行当前线程t,同时调用该线程的run()。此时就出现两个同时运行的线程,一个是调用start()方法的线程(在本例中就是main()线程),一个是执行run()的线程(即线程t)。
简言之,就是在主线程在执行main()时额外启动了一个新线程执行火箭发射倒计时任务,且主线程先于新线程执行完毕,所以"等我一下!"
在开始发射之前打印。
下面创建多个新线程执行火箭发射倒计时任务:
public class MoreBasicThreads {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new LiftOff()).start();
}
System.out.println("等我一下!");
}
}
/* 运行结果
等我一下!
#1(9),#0(9),#1(8),#1(7),#0(8),#0(7),#0(6),#0(5),#1(6),#0(4),#2(9),#2(8),#2(7),#2(6),#2(5),#0(3),#1(5),#1(4),#0(2),#2(4),#0(1),#1(3),#0(发射!),#2(3),#1(2),#3(9),#1(1),#2(2),#2(1),#2(发射!),#1(发射!),#3(8),#3(7),#3(6),#4(9),#3(5),#4(8),#3(4),#4(7),#3(3),#4(6),#3(2),#4(5),#3(1),#3(发射!),#4(4),#4(3),#4(2),#4(1),#4(发射!),
*/
可以看到多个线程交替执行任务,这种交替由线程调度机制决定,由于线程调度机制是非确定性的,因此每次执行时都会看到不同的结果。
Executor
除了调用Thread.start()执行火箭发射任务外,还可以使用Executor接口的execute()
方法来执行火箭发射任务,:
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new LiftOff());
System.out.println("等我一下!");
exec.shutdown();
}
}
/* 运行结果:
等我一下!
#0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(发射!),
*/
这时代码变得不像使用Thread时那么显而易见了,Executor
、Executors
和ExecutorService
三者又有什么关系呢?
来先从Executor说起,它的定义如下:
public interface Executor {
void execute(Runnable command);
}
Executor是一个接口,只定义了一个execute()方法,并没有给出execute()的实现。再看看ExecutorService:
public interface ExecutorService extends Executor {
...
}
ExecutorService则是继承了Executor接口的接口,同样没有给出execute()的实现。那executor()在哪里实现的呢?别着急,再来看看 Executors.newCachedThreadPool():
public class Executors {
···
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
}
还是没有给出execute()的实现,再去看看ThreadPoolExecutor:
public class ThreadPoolExecutor extends AbstractExecutorService {
...
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
}
终于在这里看到了execute()方法的实现,那么ThreadPoolExecutor又是如何与ExecutorService建立关联的呢?通过代码可以看到ThreadPoolExecutor继承自AbstractExecutorService,来看看AbstractExecutorService:
public abstract class AbstractExecutorService implements ExecutorService {}
AbstractExecutorService是一个实现了ExecutorService接口的抽象类。真相大白!我们通过调用Executors类的的newCachedThreadPool()静态方法,从而创建了一个实现了Executor接口的ThreadPoolExecutor对象,并实现了execute()方法。考虑到后期还需要关闭任务的方法,因此只能将ThreadPoolExecutor对象向上转型到ExecutorService接口,而不是Executor接口。
为保证任务可以程序可以正常结束,任务执行完毕后需要调用shutwdown()方法以终止新任务继续提交给Executor,否则程序将一直等待新任务而无法正常结束。
相比较于使用Thread每执行一个任务都需要显式地创建一个线程,单个Executor通过创建线程池可以用来创建和管理系统中所有的任务,大大降低由于频繁的创建线程所带来的性能损耗。Executor允许管理异步任务的执行,而无需显式地管理线程的生命周期,因此在执行任务时,首选Executor,不过考虑到使用Thread对象执行任务将更加直观,下文中的示例程序将主要使用Thread创建线程。
一个带有返回值的任务
在前面我一直用Runnable接口定义任务,查看Runnable接口本身的定义:
public interface Runnable {
public abstract void run();
}
很显然,通过Runnable接口定义任务无法带有返回值,那有没有办法可以让任务返回一些信息呢?可以实现Callable接口的并编写带有返回值的call()
方法,查看Callable的定义:
public interface Callable<V> {
V call() throws Exception;
}
可以看到call()方法的返回值的是一个泛型,这意味着可以返回任何类型的值。下面使用Callable重新编写火箭发射任务:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
class TaskWithResult implements Callable<String> {
//每创建一个对象,id增1
private int id;
private String string = "";
public TaskWithResult(int id) {
this.id = id;
}
@Override
public String call() throws Exception {
return "Result of " + Thread.currentThread().getName() + " " + id;
}
}
public class CallDemo {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
List<Future<String>> list = new ArrayList<>();
for (int i = 1; i < 10; i++) {
list.add(exec.submit(new TaskWithResult(i)));
}
try {
for (Future<String> s : list) {
System.out.println(s.get());
}
System.out.println("等我一下!");
} catch (InterruptedException e) {
e.printStackTrace();
return;
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
exec.shutdown();
}
}
}
/* 输出结果
Result of pool-1-thread-1 1
Result of pool-1-thread-2 2
Result of pool-1-thread-3 3
Result of pool-1-thread-4 4
Result of pool-1-thread-5 5
Result of pool-1-thread-6 6
Result of pool-1-thread-7 7
Result of pool-1-thread-8 8
Result of pool-1-thread-9 9
等我一下!
*/
submit()会返回Future对象,然后使用get()方法获取其返回值。这里需要注意的是,call()方法只能使用ExecutorService.submit()方法调用。
让步和休眠
在创建多个新线程执行火箭发射倒计时任务中,从结果可以看到先执行的线程更早地完成了火箭发射任务,不过既然都已经创建了多个线程,那么能不能让这些线程稍微考虑一下彼此,先执行的线程等一下后执行的线程,从而共同完成发射任务呢?当然是可以的,主要通过yield()
和sleep()
两种方法实现。
使用yield()可以完成线程的让步,告诉线程调度器“现在我的任务已经执行的差不多了,你可以让其它的线程来占有CPU了”,再由线程调度器来决定是否要让其它的线程执行。为了演示效果,简单修改火箭发射任务:
public class LiftOff implements Runnable {
private int countDown = 10;
private static int taskCount = 0;
//每创建一个对象,id增1,用于标识线程id
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String print() {
return "#" + id + "(" + (countDown > 0 ? countDown : "发射!" ) + "),";
}
//定义一个倒计时任务
@Override
public void run() {
while (countDown-- > 0) {
System.out.print(print());
//此处添加一个让步方法
Thread.yield();
}
}
}
/* 输出结果
等我一下!
#0(9),#1(9),#0(8),#2(9),#1(8),#0(7),#3(9),#2(8),#4(9),#1(7),#0(6),#3(8),#2(7),#4(8),#1(6),#0(5),#3(7),#2(6),#4(7),#1(5),#0(4),#3(6),#2(5),#4(6),#1(4),#0(3),#3(5),#2(4),#4(5),#1(3),#0(2),#3(4),#2(3),#4(4),#1(2),#0(1),#3(3),#2(2),#4(3),#1(1),#0(发射!),#3(2),#2(1),#4(2),#1(发射!),#3(1),#2(发射!),#4(1),#3(发射!),#4(发射!),
*/
与不使用让步的情况对比,可以看到此时线程执行的步调更为统一。
相比较与yield()向调度器申请让出自己占用的CPU,使用sleep()则是显式地强制线程停止运行一段时间。sleep()在Java SE5后作为TimeUnit类的一部分,可以显式指定停止的时间,为了演示效果,简单修改火箭发射任务:
public class LiftOff implements Runnable {
private int countDown = 10;
private static int taskCount = 0;
//每创建一个对象,id增1,用于标识线程id
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String print() {
return "#" + id + "(" + (countDown > 0 ? countDown : "发射!" ) + "),";
}
//定义一个倒计时任务
@Override
public void run() {
while (countDown-- > 0) {
System.out.print(print());
//线程执行到此处停止300ms
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/* 输出结果
等我一下!
#0(9),#1(9),#2(9),#3(9),#4(9),#3(8),#4(8),#1(8),#2(8),#0(8),#3(7),#4(7),#1(7),#2(7),#0(7),#2(6),#1(6),#3(6),#4(6),#0(6),#2(5),#4(5),#3(5),#1(5),#0(5),#1(4),#3(4),#4(4),#2(4),#0(4),#0(3),#2(3),#4(3),#3(3),#1(3),#2(2),#3(2),#1(2),#4(2),#0(2),#3(1),#0(1),#2(1),#1(1),#4(1),#2(发射!),#0(发射!),#3(发射!),#4(发射!),#1(发射!),
*/
可以看到线程执行的步调变得更为统一。需要注意的是sleep()会抛出InterruptedException,因为线程中的异常不能跨线程传回main(),所以需要在run()中显式地捕获该异常。
优先级
尽管不愿接受,多数时候“王侯将相有种乎”却是一个不争的事实。对于线程也一样,发射火箭的线程肯定要比看球赛的线程更重要,这里的重要程度在线程中被描述为优先级,线程调度器会倾向于让优先级高的任务先执行,但优先级低的线程也有机会执行,只不过机会更少而已,因为优先级不能导致死锁。
线程的优先级范围从1到10,默认为5:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
可以通过setPriority()显式修改优先级,并通过getPriority()获取当前线程的优先级,下面演示不同优先级线程的执行顺序:
public class SimplePriorities implements Runnable {
private int countDown = 3;
private volatile double d;
private int priorities;
@Override
public String toString() {
return Thread.currentThread() + ": " + countDown;
}
public SimplePriorities(int priorities) {
this.priorities = priorities;
}
@Override
public void run() {
Thread.currentThread().setPriority(priorities);
while (true) {
for (int i = 0; i < 10000000; i++) {
d += (Math.PI + Math.E) / (double)i;
if (i % 10000 == 0) {
Thread.yield();
}
}
System.out.println(this);
if (countDown == 0) {
System.out.println("哈哈,我的优先级是" + Thread.currentThread().getPriority() + ", 我执行完啦!");
}
if (countDown-- == 0) {
return;
}
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
exec.execute(new SimplePriorities(Thread.MIN_PRIORITY));
}
exec.execute(new SimplePriorities(Thread.MAX_PRIORITY));
exec.execute(new SimplePriorities(Thread.NORM_PRIORITY));
exec.shutdown();
}
}
/* 输出结果:
Thread[pool-1-thread-4,10,main]: 3
Thread[pool-1-thread-5,5,main]: 3
Thread[pool-1-thread-4,10,main]: 2
Thread[pool-1-thread-5,5,main]: 2
Thread[pool-1-thread-4,10,main]: 1
Thread[pool-1-thread-5,5,main]: 1
Thread[pool-1-thread-4,10,main]: 0
哈哈,我的优先级是10, 我执行完啦!
Thread[pool-1-thread-2,1,main]: 3
Thread[pool-1-thread-1,1,main]: 3
Thread[pool-1-thread-3,1,main]: 3
Thread[pool-1-thread-5,5,main]: 0
哈哈,我的优先级是5, 我执行完啦!
Thread[pool-1-thread-2,1,main]: 2
Thread[pool-1-thread-1,1,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-3,1,main]: 1
Thread[pool-1-thread-1,1,main]: 0
哈哈,我的优先级是1, 我执行完啦!
Thread[pool-1-thread-2,1,main]: 0
哈哈,我的优先级是1, 我执行完啦!
Thread[pool-1-thread-3,1,main]: 0
哈哈,我的优先级是1, 我执行完啦!
*/
可以看到即使优先级为10的线程和优先级为5的线程晚于优先级为1的线程开始执行,但后两个线程仍然先于优先级为1的任务执行结束。上面的输出中优先级5的线程仍然在和优先级为10的线程竞争资源,这是因为低优先级的任务较少,如果增加低优先级线程的数量,很容易看到优先级为5的线程将不再有任何机会和优先级为10的线程竞争。类比到生活中也是,一个组织的人越多,也才越能凸显其核心人物的地位。
需要注意的是JDK为线程设置了10个优先级,但一般的操作系统又有各自的优先级数量,因此在优先级映射的时候不能处理的很好,建议使用时只使用MIN_PRIORITY,NORM_PRIORITY,MAX_PRIORITY这三种级别。
守护线程
守护线程(Daemon Thread)的地位在线程界很尴尬,因为在程序运行中,JVM关闭的默认标志是当前执行的线程都是守护线程,也就是JVM根本不管其死活,守护线程能存活完全是依托于JVM中还有非守护进程在执行。这有点类似奴隶制社会里主人去世,奴隶殉葬的做法,极其违反人道主义精神。
使用setDaemon(true)
将线程设置为守护线程,且必须在该线程启动之前完成设置。下面通过try-catch-finally例子来看看守护进程到底被JVM忽视到何种地步:
class ADeamon implements Runnable {
@Override
public void run() {
try {
System.out.println("我是一个守护线程,但我随时都有可能消失:( ");
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("can u si me ?");
}
}
}
public class DaemonsDontRunFinaally {
public static void main(String[] args) {
Thread t = new Thread(new ADeamon());
t.setDaemon(true);
t.start();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {}
}
}
/* 输出结果:
我是一个守护线程,但我随时都有可能消失:(
*/
可以看到finally子句的内容没有被输出。考虑到守护线程关闭的不确定性,所以不建议使用守护线程。
自带任务的线程
前面介绍了通过实现Runnable接口定义一个任务的方式,也简单说了通过实现Callable接口定义一个带有返回值的任务。这些任务都是通用的任务,即任何一个线程都可以执行它们。但有时侯我们希望一些任务只被特定的线程执行,即这些线程的使命就是执行特定的任务,这时候可以通过直接从Thread继承并重写run()方法来定义一个有专属任务的线程:
class Smile extends Thread {
public Smile() {
start();
}
@Override
public void run() {
System.out.println("我是一个微笑线程 :)");
System.out.println("我是一个微笑线程 :)");
System.out.println("我是一个微笑线程 :)");
}
}
class Sad extends Thread {
public Sad() {
start();
}
@Override
public void run() {
System.out.println("我是一个悲伤线程 :(");
System.out.println("我是一个悲伤线程 :(");
System.out.println("我是一个悲伤线程 :(");
}
}
public class TaskThread {
public static void main(String[] args) {
new Smile();
new Sad();
}
}
/* 输出结果
我是一个微笑线程 :)
我是一个悲伤线程 :(
我是一个悲伤线程 :(
我是一个悲伤线程 :(
我是一个微笑线程 :)
我是一个微笑线程 :)
*/
创建一个微笑线程,即可执行微笑任务。
自执行的任务
使用上面的方式可以很方便地执行某些特定的任务,但由于定义线程时继承了Thread类,因此该线程不能再继承其他的类,导致线程的功能受到很大限制。可以通过定义一个自执行的任务来解除这种限制:
class SmileTask implements Runnable {
Thread t = new Thread(this);
public SmileTask() {
t.start();
}
@Override
public void run() {
System.out.println("我是一个微笑任务 :)");
Thread.yield();
System.out.println("我是一个微笑任务 :)");
Thread.yield();
System.out.println("我是一个微笑任务 :)");
}
}
class SadTask implements Runnable {
Thread t = new Thread(this);
public SadTask() {
t.start();
}
@Override
public void run() {
System.out.println("我是一个悲伤任务 :(");
Thread.yield();
System.out.println("我是一个悲伤任务 :(");
Thread.yield();
System.out.println("我是一个悲伤任务 :(");
}
}
public class AutoTask {
public static void main(String[] args) {
new SmileTask();
new SadTask();
}
}
/* 输出结果
我是一个微笑任务 :)
我是一个悲伤任务 :(
我是一个微笑任务 :)
我是一个悲伤任务 :(
我是一个微笑任务 :)
我是一个悲伤任务 :(
*/
此时通过使任务继承其它类,以拓展其功能。使用Thread.yield()确保可以看到两个任务在并发执行。
不管是启动一个自带任务的线程,还是定义一个自执行的任务,可以看到start()方法都是在类构造器里被调用的,这样带来的问题是构造器方法可能还未执行完,对象还未完成初始化,任务就已经开始执行,也就意味着任务能够访问处于不稳定状态的对象,从而产生意想不到的错误。这是优选Executor执行任务的的另一个原因。
使用join()为线程建立联系
前面创建的所有线程,总结起来就是各干各的,相互之间没有任何沟通。那如何为两个线程建立关联呢?使用join()
方法,它会等待调用它的线程消亡。一个线程在其它线程之上调用join()方法,使第二个线程等待第一个线程执行完毕才开始执行。
以饭前洗手为例,虽然没被写成法律条文,但很多人还是会在吃饭之前洗手,也就是说执行吃饭任务的线程应该等待执行洗手任务的线程完成,实现方法为:
import java.util.concurrent.TimeUnit;
class WashHand extends Thread {
public WashHand(String name) {
super(name);
start();
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(getName() + "过了3s,感觉有点饿了,先去洗个手");
} catch (InterruptedException e) {}
}
}
class Eat extends Thread {
WashHand washHand;
public Eat(String name, WashHand wh) {
super(name);
washHand = wh;
start();
}
@Override
public void run() {
try {
washHand.join();
System.out.println(getName() + "觉得今天的饭味道还不错 :)");
} catch (InterruptedException e) {}
}
}
public class JoinThread {
public static void main(String[] args) {
WashHand wh1 = new WashHand("wh1");
Eat e1 = new Eat("e1", wh1);
}
}
/* 输出结果
wh1过了3s,感觉有点饿了,先去洗个手
e1觉得今天的饭味道还不错 :)
*/
虽然需要等到3秒钟才去洗手,但是由于调用吃饭线程执行吃饭任务时,洗手线程调用了join()方法,即洗手线程在吃饭线程之上调用了join()方法,因此吃饭需要等待洗手完成。
可以使用join(long millis)方法设置最大等待时间,如果在这段时间结束后线程的isAlive()返回仍未true,将不再进行等待,直接执行后续任务。
捕获异常
由于异常不能跨线程传递,因此上述所有任务中的异常都需要在任务内部被捕获,而不能在执行该任务的main()方法中去捕获。为了解决这个问题,可以为每个线程附加一个异常处理器,统一由该异常处理器捕获异常,而无需在任务内部显式捕获异常:
class ExcepionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("捕获异常:" + e);
}
}
class ExceptionThread implements Runnable {
@Override
public void run() {
throw new RuntimeException();
}
}
public class CaughtInMain {
public static void main(String[] args) {
Thread t = new Thread(new ExceptionThread());
t.setUncaughtExceptionHandler(new ExcepionHandler());
t.start();
}
}
/* 输出结果:
捕获异常:java.lang.RuntimeException
*/
如果在任务内部显式地捕获了异常,那么异常处理器将不会发挥作用。
补充说明
这是Java并发系列文章的第一篇,内容出自Java编程思想(第4版)第二十一章:并发章节的的阅读笔记整理,参考内容为21.1~21.2,后续部分的笔记还在整理之中。
由于并发是一个比较难于掌握的概念,因此文中内容难免有理解上的偏差,如您有不同的看法,烦请指正,谢谢!
别指望一文读懂Java并发之从一个线程开始的更多相关文章
- 一文读懂Java动态代理
作者 :潘潘 日期 :2020-11-22 事实上,对于很多Java编程人员来说,可能只需要达到从入门到上手的编程水准,就能很好的完成大部分研发工作.除非自己强主动获取,或者工作倒逼你学习,否则我们好 ...
- 一文读懂JAVA多线程
背景渊源 摩尔定律 提到多线程好多书上都会提到摩尔定律,它是由英特尔创始人之一Gordon Moore提出来的.其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍 ...
- 一文读懂Java中的动态代理
从代理模式说起 回顾前文: 设计模式系列之代理模式(Proxy Pattern) 要读懂动态代理,应从代理模式说起.而实现代理模式,常见有下面两种实现: (1) 代理类关联目标对象,实现目标对象实现的 ...
- 一文读懂Java线程状态转换
前言 本文描述Java线程线程状态及状态转换,不会涉及过多理论,主要以代码示例说明线程状态如何转换. 基础知识 1. 线程状态 Thread源码中的状态说明: 线程可以有6种状态: New(新建) R ...
- 一文读懂Java类加载机制
Java 类加载机制 Java 类加载机制详解. @pdai Java 类加载机制 类的生命周期 类的加载:查找并加载类的二进制数据 连接 验证:确保被加载的类的正确性 准备:为类的静态变量分配内存, ...
- [No0000196]一文读懂Java 11的ZGC为何如此高效
导读:GC是大部分现代语言内置的特性,Java 11 新加入的ZGC号称可以达到10ms 以下的 GC 停顿,本文作者对这一新功能进行了深入解析.同时还对还对这一新功能带来的其他可能性做了展望.ZGC ...
- 夯实Java基础系列7:一文读懂Java 代码块和执行顺序
目录 Java中的构造方法 构造方法简介 构造方法实例 例 1 例 2 Java中的几种构造方法详解 普通构造方法 默认构造方法 重载构造方法 java子类构造方法调用父类构造方法 Java中的代码块 ...
- 夯实Java基础系列16:一文读懂Java IO流和常见面试题
本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...
- 一文读懂 Java 异常体系
写程序的时候,编辑器会提示错误,关键字拼错了,语法不符合规则,不符合泛型:程序编译的时候,编译器会提示错误,检查是否符合 Java 的语法规范,没有通过编译器检查的程序就无法编译,也就无法运行.这些都 ...
随机推荐
- Struts2第六篇【文件上传和下载】
前言 在讲解开山篇的时候就已经说了,Struts2框架封装了文件上传的功能--..本博文主要讲解怎么使用Struts框架来完成文件上传和下载 回顾以前的文件上传 首先,我们先来回顾一下以前,我们在we ...
- 高德地图markers生成和点击
因为自己平时上班也是比较忙,遇到什么写什么,希望能给现在的你一些帮助,都是自己在工作中遇到的问题,给自己一个提醒,也是分享 相信很多人在做高德地图开发的时候,对于新手,官方的demo解读单个marke ...
- JVM菜鸟进阶高手之路十一(eden survivor分配问题)
转载请注明原创出处,谢谢! 问题 这个Xmn设置为1G,,我用jmap -heap 看,这个Eden From To怎么不是一个整8:1:1的关系呢? 我看内存分配还是没变,我Xmn1g,感觉From ...
- String类的一些转换功能(6)
1:把字符串转换成字节数组 getBytes() 如: String s = "你好啊!" //编码 byte [] arr = s.getBytes();//这里默认编码格式是g ...
- Android Studio 字体和字号调整
点击File,Settings. 找到Editor-Colors&Fonts-Font 点击Save As... 改个名字点击OK. 1为字体,2为字号,3为行间距. 我认为字体设置为Cons ...
- Linux查找和筛选工具
本文为原创文章,转载请标明出处 目录 文件名通配符 单字符匹配元字符 ? 多字符匹配元字符 * 字符范围匹配符 [] 排除范围匹配符 [!] 命令中的正则表达式 单字符匹配符 . 单字符或字符串重复匹 ...
- VB.net DateTimePicker 初始化为空,选择后显示日期
目的:当某记录的日期数据为空的时候,DateTimePicker 不以默认当前时间显示. 优点:避免不规则的时间格式输入:符合平时遇到的时间输入习惯 缺点:设置要代码,没有textbox控件那么方便设 ...
- 【转】NAS 黑群晖 配置完成(不含硬盘),NAS能做什么?
在配黑群晖前,240元入手过一个艾美佳的NAS感受了下,功能倒还合适,就是配置太老,厂家固件也停止更新了,一直不太满意. 后来经常关注NAS1,发现现在X86的NAS也很好自己DIY了,就长草了,向女 ...
- Vue实现选项卡切换
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...
- Kindeditor JS 取值问题以及上传图片后回调等
KindEditor.ready(function (K) { var editor = K.create('#editor_id', { //上传管理 uploadJson: '/js/kinded ...