Java线程详解

并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务(也被称为子任务)中的每一个都将执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单个进程可以用于多个并发执行的任务,但是你的程序使得每个任务都好像有自己的CPU一样。其底层机制是切分CPU时间,但通常你不需要考虑它。

1. 线程的生命周期

线程的生命周期主要有五种状态:新建(New)、就绪(Runable)、运行(Running)、阻塞(Blocking)和死亡(Death)。 线程在运行过程中,不可能一直占用CPU,CPU会在多个线程之前来回切换,所以线程的状态也会随时在运行和阻塞状态切换:

  1. 新建状态,使用 New 关键字创建线程对象,线程就处于新建状态。此时仅由JVM分配内存空间,初始化其变量。
  2. 就绪状态,调用Thread#start方法,线程处于就绪状态。JVM虚拟机会为其创建方法调用栈和程序计数器,等待CPU调度进入运行状态。
  3. 运行状态,CPU准备就绪,调用Thread#run方法执行方法体,此时线程处于运行状态。
  4. 阻塞状态,处于运行状态的线程失去CPU为其分配的资源,线程进入阻塞状态。
  5. 死亡状态,线程执行正常结束或因抛出Exception或Error而终止,线程进入死亡状态。

2. 终止线程的三个方法

  1. 通过线程run方法中标志位实现
  2. 使用Thread#stop方法实现,这个方法不推荐使用。(因为与suspend、resume一样可能出现意料以外的情况。
  3. 使用Thread#interrupt方法终止线程

3. 线程池(ExecutorService Executors)

Thread.yield() 线程调度器(Java线程机制的一部分,可以将CPU从一个线程转移到另一个线程的一种建议),它在声明“我已执行完生命周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机”

CacheThreadPool:为每一个任务都创建一个线程。

1
ExecutorService executor = Executors.newCachedThreadPool();

FixedThradPool:创建有限的线程集执行任务。

1
ExecutorService executor = Executors.newFixedThreadPool(5);

使用FixedThreadPool,可以限制线程的数量,不必为每个任务都固定地付出创建线程的开销。

SingleThreadExecutor:是线程数量为1的FixedThreadPool。

1
ExecutorService executor = Executors.newSingleThreadExecutor();

即便向SingleThreadExecutor中提交了多个任务,这些任务依旧会排队执行,每个任务都会在下一个任务之前运行结束,所有的任务都使用相同的线程。

如何使用线程池执行任务?调用 ExecutorService#execute(Runable) 执行任务, ExecutorService#shutdown() 方法可以防止新任务提交给Executor,当前线程将继续执行 shutdown 方法被调用之前提交的任务,在任务完成时退出。

3.1 ThreadFactory 和 ThreadPoolExecutor 的概念

1
2
3
4
5
6
7
8
9
// 创建线程的工厂
public class DaemonThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
}
1
2
3
4
5
6
7
public class DaemonThreadPoolExecutor extends ThreadPoolExecutor {
public DaemonThreadPoolExecutor(){
super(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),new DaemonThreadFactory());

}
}

3.2 执行任务的返回值 Callable<T>

其实Callable 和 Runnable 的区别就是多一个返回值,看一下Callable类:

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}

执行任务的返回值调用 ExecutorService#submit(Callable)方法而不是 execute(Runable) 方法,返回值类型如下:

1
<T> Future<T> submit(Callable<T> task);

3.3 设置线程的优先级

1
Thread.currentThread().setPriority(priority);

常用 Thread.MIN_PRIORITY、Thread.NORM_PRIORITY 和 Thrad.MAX_PRIORITY。

###3.4 让步

Thread.yield() 方法是线程调度机制的一个暗示,表示所在线程的工作已经做得差不多了,可以让别的线程使用CPU了。这个暗示将通过调用yield()方法来作出(不过这只是一个暗示,没有任何机制保证它将会被采纳)。

3.5 后台线程

后台(daemon)线程是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序不可或缺的部分。因此,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。

1
2
3
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true);// Must call before start()
daemon.start();

如果一个线程是后台线程,那么由这个线程创建的所有线程都属于后台线程

后台线程中不执行 finally 语句的情况下就会终止其 run() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ADaemon implements Runnable{
@Override
public void run() {
System.out.println("Starting ADaemon");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("Exiting via InterruptException");
}finally {
System.out.println("This should always run?");
}
}
}
public class DaemonsDontRunFinally {
public static void main(String[] args) {
Thread thread = new Thread(new ADaemon());
thread.setDaemon(true);
thread.start();
}
}

3.6 join() 方法

当一个线程例如 Joiner 线程中调用另一个线程 Sleeper#join() 方法,那么 Joiner 线程将会被挂起,知道目标线程 Sleeper 执行完毕才恢复(即 Sleeper#isAlive() 方法返回值为 false )。join() 方法可以携带一个超时参数,若指定时间内目标线程没有执行完毕,会返回挂起的线程内执行。调用Thread#interrupt() 方法同样可以中断目标线程回到挂起线程。

3.7 线程的应用

Java使用线程的应用之一就是实现响应式界面,一边可以在线程中计算,一边可以响应用户输入或者操作界面。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class UnresponsiveUI{
private volatile double d = 1;
public UnresponsiveUI() throws IOException {
while (d>0)
d = d+(Math.PI+Math.E)/d;
System.in.read();
}
}
public class ResponsiveUI extends Thread {
private static volatile double d = 1;
public ResponsiveUI(){
setDaemon(true);
start();
}

@Override
public void run() {
while (true){
d = d +(Math.PI + Math.E) /d;
}
}

public static void main(String[] args) throws IOException {
new ResponsiveUI();
System.in.read();
System.out.println(d);
}
}

如果使用 UnresponsiveUI 类,那么界面会进入死循环,无法响应用户输入。而对于 ResponsiveUI 类来说,计算耗时操作会放在后台线程中,main 方法负责响应用户输入,在控制台输入任意字符,按下回车程序将自动退出。

3.8 线程异常捕获

线程异常无法通过 try…catch… 语句捕获,线程中的异常会直接输入到控制台,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ExceptionThread implements Runnable {
@Override
public void run() {
throw new RuntimeException();
}

public static void main(String[] args) {

try {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new ExceptionThread());
} catch (Exception e) {
System.out.println("Exception has been handled!");
}
}
}
/* outputs
Exception in thread "pool-1-thread-1" java.lang.RuntimeException
at concurrency.ExceptionThread.run(ExceptionThread.java:9)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0
*/

可以通过 Thread#setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler) 方法实现对异常的捕获

1
2
3
4
5
6
7
8
9
10
11
12
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
public static void main(String[] args) {
...
thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
...
}

4. 共享受限资源

Java的自增运算符自身需要多个步骤,并且在递增过程中任务可能会被线程机制挂起——也就是说 Java 的自增运算符不是原子性的操作。

4.1 线程锁

声明 synchronized 方法的方式:

1
2
synchronized void f(){/* ... */}
synchronized void g(){/* ... */}

所有的对象对自动含有单一的锁。当在对象上调用任意 synchronized 方法的时候,此对象会被加锁,这时该对象上的其他 synchronized 方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。对于前面的方法,如果某个任务对象调用了 f() ,对于同一个对象而言,就只有等到 f() 调用结束并释放了锁之后,其他任务才能调用 f() 和 g() 。所以,对于某个特定对象而言,其所有 synchronized 方法共享一个锁,这可以被用来防止多个任务同时访问被编码为对象内存。

注意,在使用并发时,将域设置为 private 是非常重要的,否则,synchronized 关键字就不能防止其他任务直接访问,这样就会产生冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EventGenerator extends IntGenerator {

private int currentEventValue = 0;

@Override
public synchronized int next() {
++currentEventValue;
++currentEventValue;
return currentEventValue;
}

public static void main(String[] args) {
EvenChecker.test(new EventGenerator());
}
}

synchronized 同步控制块,在调用synchronized控制代码段时,必须获取到syncObject对象的锁。如果其他线程已经得到这个锁,那么就得等到这个锁释放之后才可以继续调用。

1
2
3
synchronized(syncObject){
// This code can be accessed by only one task at one time
}

使用同步控制块而不是对整个方法进行同步的原因是,同步代码块对象不加锁时间更长,使得其他线程更多地访问(在安全的情况下尽可能多)。

4.2 使用显式的 Lock 对象

Java SE5的java.util.concurrent类库还包含有定义在java.util.concurrent.locks中的显式的互斥机制。Lock对象必须被显式地创建、锁定和释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MutexEventGennerator extends IntGenerator {

private int currentEventValue = 0;
private Lock lock = new ReentrantLock();

@Override
public int next() {
lock.lock();
try {
++currentEventValue;
Thread.yield();
++currentEventValue;
return currentEventValue;
} finally {
// 在执行完return语句之后执行
lock.unlock();
}
}

public static void main(String[] args) {
EvenChecker.test(new MutexEventGennerator());
}
}

注意在使用synchronized关键字时不能尝试获取锁且最终会获取失败。

4.3 原子性与易变性

关于Java线程,经常的错误性认知“ 原子操作不需要进行同步控制 ”。原子操作是不能被线程调度机制中断的操作

volatile关键字确保了应用的可见性,如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作都可以看到这个修改。volatile 域会立即被写入到主存中(而不是各个线程的工作线程),而读取操作就发生在主存中。非volatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务看不到这个新值。同步操作synchronized也会导致在主存中刷新,因此如果一个域完全由synchronized方法或语句来防护,那就不必将其设置为volatile。

volatile 关键字不适用的场景:

  • 当一个域的值依赖于它之前的值时(例如递增一个计数器)
  • 当一个域的值受其他域的值的限制(例如Range类的lower和upper边界就必须遵循lower<=upper的限制)

推荐使用synchronized关键字保证并发的正确性,使用volatile关键字并不能保证所有操作的原子性。

4.4 原子类

AtomicInteger、AtomicLong、AtomicReference,可以通过使用AtomicInteger而消除synchronized 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class AtomicIntegerTest implements Runnable {

private AtomicInteger i = new AtomicInteger(0);
private int getValue(){return i.get();}

private void evenIncrement(){
i.addAndGet(2);
}

@Override
public void run() {
while (true)
evenIncrement();
}

public static void main(String[] args) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
System.err.println("Aborting");
System.exit(0);
}
},5000);
ExecutorService executorService = Executors.newCachedThreadPool();
AtomicIntegerTest at = new AtomicIntegerTest();
executorService.execute(at);
while (true){
int val = at.getValue();
if(val%2 !=0){
System.out.println(val);
System.exit(0);
}
}
}
}

4.5 线程本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。因此,如果你有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。主要是,它们使得你可以将状态与线程关联起来。创建和管理线程本地存储可以由java.lang.ThreadLocal类来实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Accessor implements Runnable{

private final int id;

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

@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}

@Override
public String toString() {
return "#"+id+": "+ThreadLocalVariableHolder.get();
}
}
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
private Random random = new Random(47);

protected synchronized Integer initialValue()
{
return random.nextInt(10000);
}
};


public static void increment(){
value.set(value.get()+1);
}

public static int get(){return value.get();}

public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i = 0; i<5;i++){
executorService.execute(new Accessor(i));
}
TimeUnit.SECONDS.sleep(3);
executorService.shutdownNow();
}
}

ThreadLocal对象通常用作静态域存储。在创建ThreadLocal时,你只能通过get() 和 set() 来访问该对象的内容。

5 终结任务

5.2 在阻塞时终结

线程状态:新建、就绪、阻塞、死亡

参考资料

https://www.cnblogs.com/sunddenly/p/4106562.html

https://www.cnblogs.com/sunrunzhi/p/3930297.html

# 线程
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×