Java并发编程的艺术-学习笔记-4

Java并发编程的基础

现代操作系调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、栈和局部变量,并且能够访问共享变量。
处理器在这些线程上高速切换。
执行main()方法是一个名称为main的线程。

线程简介

线程优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待者下次分配。线程分配到的时间片多少决定了线程使用处理器资源的多少。

Java线程中,通过控制整形变量priority来控制优先级。
优先级高的线程分配的时间片数量要多于优先级低的线程。
频繁阻塞(睡眠或者I/O操作)的线程设蛰较高的优先级,而偏重计算(需要较多的CPU时间或者偏运算)的线程设置较低的优先级,确保处理器不会被独占。

线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。

线程的状态

状态名称 说明
NEW 线程初始状态,刚被创建没有调用start()方法
RUNNABLE 运行状态,包括操作系统中的”就绪”和”运行”两种状态
BLOCKED 阻塞状态,表示线程阻塞与锁
WAITING 等待状态,进入该状态的线程需要等待其他线程做一些特定动作(通知或者中断)
TIME_WAITING 超时等待状态,在等待时间结束返回
TERMINATED 终止状态,表示当前线程执行完毕

线程在自身的生命周期中,并不固定于某个状态,而是随着代码的执行在不同的状态之间进行切换。

点击加载

从图中可以看出,线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依赖其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间一过,就会返回到运行状态。当线程调用同步方法时候,在没有获取到锁的状态下,线程将会进入到阻塞状态。线程在执行Runable的run()方法之后,将会进入到终止状态。

Daemon线程

守护线程是一种支持型线程,因为它主要被用作在后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Deamon线程的时候,Java虚拟机将会退出。可以调用Thread.setDaemon(true)将线程设置为Daemon线程。

Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。

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
import java.util.concurrent.TimeUnit;
public class Daemon {
static class DaemonRunner implements Runnable{
@Override
public void run(){
try{
TimeUnit.SECONDS.sleep(3);
}catch (Exception e){
}finally {
System.out.println("finally in run()");
}
}
}
public static void main(String[] args) throws Exception{
Thread t = new Thread(new DaemonRunner(), "...");
//将main线程作为自己的守护线程
t.setDaemon(true);
t.start();
//main线程退出, java虚拟中没有非Daemon线程, 虚拟机退出
//DaemonRunner中的finally没有被执行.
//下面t.join()可让finnally块执行
//在main线程调用了t的join, 那么main线程就要等待t结束, 才能执行后续代码
// t.join();
// System.out.println("shit");
}
}

运行上述程序,可以看到在终端或者命令提示符上没有任何输出。main线程(非Daemon线程)在启动了线程DaemonRunner之后随着main方法执行完毕而终止,而此时Java虚拟机已经没有非Daemon线程,虚拟机退出。所有线程终止,所以DaemonRunner中的finnally块没有执行。
在构建Daemon线程时,不能依靠finnally块来保证资源的释放

启动和终止线程

构造线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc){
if(name == null){
throw new NullPointerException("name connot be null");
}
//当前线程就是新建线程的父线程
Thread parent = Thread.currentThread();
this.group = g;
this.target = target;
tid = nextThreadID(); //分配一个线程ID
//将daemon, priority属性设置为父线程的对应属性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
}

启动线程

上述的构造线程完成后,一个能够运行的线程对象就初始化了,在堆内存中等待着运行。

此时,调用start()方法就可以启动这个线程。线程starrt()方法含义是:当前线程(即parent线程)同步告知虚拟机,只要线程规划器空闲,应该立即调用start()方法的线程。

理解中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用线程的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该对象的isInterrupted()方法时,依旧会返回false。

过期的suspend()、resume()和stop()

不建议使用三个过期的函数的原因:
以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占着有资源进入睡眠状态,这样容易引发死锁问题。同样滴,stop()方法在终结一个线程时不会保证资源的正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定的状态。

安全地终止线程

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
import java.util.concurrent.TimeUnit;
public class ShutDown {
private static class Runner implements Runnable{
private long i;
private volatile boolean on = true;
public void cancel(){
on = false;
}
@Override
public void run(){
while( on && !Thread.currentThread().isInterrupted()){
++i;
}
System.out.println("Count i: " + i);
}
}
public static void main(String[] args) throws Exception{
Runner r1 = new Runner();
Runner r2 = new Runner();
Thread thread1 = new Thread(r1, "countThread1");
Thread thread2 = new Thread(r2, "countThread2");
//main线程睡眠1秒, 然后对thread1进行中断, 使得thread1可以感知中断而结束
thread1.start();
TimeUnit.SECONDS.sleep(1);
thread1.interrupt();
//main线程睡眠1秒, 然后thread2调用cancel(), 使得thread2可以感知 on == false而结束
thread2.start();
TimeUnit.SECONDS.sleep(1);
r2.cancel();
}
}

示例在执行过程中,main线程通过中断操作和cancel()方法都可以让Thread线程终止。这种通过标识位或者中断操作方式让线程在终止的时候可以有机会去清理资源,而不是直接武断地终止线程。

线程间的通信

volatile和synchronized关键字

volitale

volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量的访问的可见性。

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
class Pikaqiu{
public synchronized void n(){
}
}
public class SynchronizedLock {
private static Pikaqiu pikaqiu = new Pikaqiu();
public static void main(String []args){
//普通同步方法, 锁的对象是实例对象
pikaqiu.n();
//静态同步方法, 锁的对象是 SynchronizedLock Class类
m();
//同步方法块, 锁的对象是括号里的 Pikaqiu Class类
synchronized (Pikaqiu.class){
}
//同步方法块, 锁的对象是括号里的实例对象
synchronized (pikaqiu){
}
}
public static synchronized void m(){
}
}

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能够进入同步块或同步方法,而没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入阻塞状态。

任意线程对Object(Object由synchronized保护)的访问,首先要获取Object的监视器,如果获取失败,线程进入同步队列,线程状态变为阻塞。
当访问前驱(获取了锁的线程)释放了锁,该释放操作唤醒阻塞在同步队列中的线程,使其尝试获取监视器。

等待/通知机制

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象的O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进行后续的操作。
上述的两个线程通过对象O来完成交互,而对象上的wait()和notify()的关系就如开关信号一样,用来完成等待方和通知方之间的交互工作。

调用上述方法的细节:

  1. 使用wait()、notify()和notifyAll()需要先对调用对象加锁
  2. 调用wait()方法后,将当前线程放置到对象的等待队列。
  3. notify()/notifyAll()调用后,等待线程依旧不会从wait()返回,需要调用notfify()/notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回。
  4. notify()将等待队列中的一个等待线程从等待队列移到同步队列,而notifyAll()方法将等待队列中的所有线程全部移到同步队列。
  5. 从wait()方法中返回的前提是获得了对象的锁。

等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()返回时能够感知到通知线程对变量做出的改变。

如上图,WaitThread首先获取了对象的锁,然后调用了对象的wait()方法,从而放弃了锁并进入了等待队列WaitQueue中,进入等待状态。由于WaitThread释放了锁,NotifyThread随后获取了对象的锁,在业务逻辑处理后,调用了对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread状态变为阻塞状态。NotifyThread释放了锁之后,从SynchronizedQueue取出一个线程来获取锁,这个WaitThread再次获取锁,并从wait()方法返回继续执行。

等待/通知的经典范式

等待方(消费者)、通知方(生产者)

等待方遵循的规则

  1. 获取对象的锁
  2. 如果条件不满足,调用对象的wait()方法,被通知后再次检查条件
  3. 条件满足执行后续操作
1
2
3
4
5
6
synchronized(对象){
while(条件不满足){
对象.wait();
}
后续的逻辑处理;
}

通知方的规则

  1. 获取对象的锁
  2. 改变条件
  3. 通知所有等待在对象上的线程
1
2
3
4
synchronized(对象){
改变条件;
对象.notifyAll();
}

管道输入/输出流

管道输入/输出流与普通文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。

PipedReader和PipedWriterr面向字符。

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
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
public class Piped {
public static void main(String []args) throws Exception{
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
//将输出和输入的流进行连接, 否则在使用时会抛出IOException
out.connect(in);
Thread thread = new Thread(new Print(in), "PrintThread");
thread.start();
int receive = 0;
try{
while((receive = System.in.read()) != -1){
out.write(receive);
}
}catch (IOException ioe){
}finally {
out.close();
}
}
static class Print implements Runnable{
private PipedReader in;
public Print(PipedReader in){
this.in = in;
}
@Override
public void run(){
int receive = 0;
try{
while((receive = in.read()) != -1){
System.out.print((char)receive);
}
}catch (IOException ioe){
}
}
}
}

ThreadLocal的使用

ThreadLock:线程本地变量,是一个以ThreadLock对象为键,任意对象为值的存储结构。这个结果被附带在线程上,也就是说一个线程可以根据一个ThreadLock对象查询到绑定到一个线程上的值。
可以通过get()和set()方法来获取和设置值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.concurrent.TimeUnit;
public class Profiler {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void begin(){
threadLocal.set(System.currentTimeMillis());
}
public static Long end(){
return System.currentTimeMillis() - threadLocal.get();
}
public static void main(String[] args) throws Exception{
Profiler.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println("cost: " + Profiler.end() + "mills");
}
}

若使用上述的Profiler的好处有,两个方法的调用不用在同一个方法或者类中。
例如在AOP中,可以在方法的调用前的切入点执行begin()方法,而在方法的调用之后执行切入点end()方法。

Thread.join()的使用

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
import java.util.concurrent.TimeUnit;
public class Join {
static class Catalina implements Runnable{
private Thread thread;
public Catalina(Thread thread){
this.thread = thread;
}
@Override
public void run(){
try{
thread.join();
}catch (InterruptedException e){
e.getMessage();
}
System.out.println(Thread.currentThread().getName() + "terminate.");
}
}
public static void main(String[] args) throws Exception{
System.out.println("Start main");
Thread previous = Thread.currentThread();
for(int i = 0; i < 10; ++i){
//每个线程拥有上一个线程的一个引用, 需要等待上一个线程的终止, 才能从等待中返回
Thread thread = new Thread(new Catalina(previous), "Thread(" + i + ")");
thread.start();
previous = thread;
}
TimeUnit.SECONDS.sleep(2);
System.out.println("Thread: " + Thread.currentThread().getName());
}
}

每个线程的终止的前提是,前驱线程的终止,每个线程等待前驱线程终止后,才从join()返回,这里涉及了等待/通知机制(等待前驱线程终止,接受前驱线程结束通知)。
当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。

比如有两个线程,mainThread和ThreadA,在线程ThreadA中获取mainThread线程,并且调用mainThread.join(),那么线程A的终止前提条件是,mainThread终止。

Thread.yield()方法的使用

参考博客:点击跳转

使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。

Java线程中有一个Thread.yield( )方法,很多人翻译成线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。

打个比方:现在有很多人在排队上厕所,好不容易轮到这个人上厕所了,突然这个人说:“我要和大家来个竞赛,看谁先抢到厕所!”,然后所有的人在同一起跑线冲向厕所,有可能是别人抢到了,也有可能他自己有抢到了。我们还知道线程有个优先级的问题,那么手里有优先权的这些人就一定能抢到厕所的位置吗? 不一定的,他们只是概率上大些,也有可能没特权的抢到了。