Java多线程编程核心技术读书笔记

Chapter 1 Java多线程技能

1. 线程和进程

进程是受操作系统管理的基本运行单元。

线程是在进程中独立运行的子任务。

多线程的意义:在同一时间内运行更多不同种类的任务。CPU在这些任务之间不停的切换,由于切换的速度飞快,所以给使用者的感觉就是这些任务在同时运行。

多线程是异步的,使用多线程就是在使用异步。所以在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。

Thread类中的start方法通知“线程规划器”此线程已准备就绪,等待调用线程对象的run方法,其实这个过程就是让系统安排一个时间来调用run方法,具有异步的效果。如果在调用时直接调用了run方法则没有异步的效果,必须等run方法中的代码执行完毕才可以执行后面的代码。

实现多线程的两种方式:继承Thread类,实现Runnable接口。(后者较好,因为Java中不支持多继承)

2. 使用多线程
  1. Thread.currentThread()和this的区别

在线程不作为参数传入另一个线程时,this和Thread.currentThread()【表面上】代表的是同一个对象。而当线程作为参数传入时,this指向当前对象,即内部线程,而Thread.currentThread() 指向当前方法被哪个线程调用的那个对象,即外部线程。

  1. isAlive() sleep() getId()方法
3. 停止线程
  1. 停止线程

Java中有以下三种方法可以终止正在执行的线程:

1)使用退出标志,线程正常退出,也就是当run方法执行完成后终止线程。

2)使用stop方法强制终止线程,但是不推荐使用这个方法。因为stop和suspend及resume一样,都是作废过期的方法,使用它们可能产生不可预料的结果。

3)使用interrupt方法中断线程。

  1. interrupted() 和 isInterrupted() 方法

调用interrupt()方法只是给线程设置了一个中断标记,并不是立即停止当前线程,线程仍然会继续运行。

这两个方法的区别:

区别一:

interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能。换句话说,如果两次调用这个方法,第一次返回true,第二次返回false,因为第一次调用完成后清除了中断状态。

isInterrupted():测试线程Thread对象是否已经是中断状态,但不清除状态标志。

区别二:

interrupted()是静态方法:内部实现是调用的当前线程的isInterrupted(),并且会重置当前线程的中断状态。只能通过Thread.interrupted()来调用。

isInterrupted()是实例方法,是调用该方法的对象所表示的那个线程的isInterrupted(),不会重置当前线程的中断状态。

  1. 异常法停止线程继续向下执行

使用示例:

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
public class MyThread extends Thread {
@Override
public void run() {
super.run();
try {
for (int i = 1; i < 500000; i++) {
if (this.isInterrupted()) {
System.out.println("thread already interrupted!");
throw new InterruptedException();
}
System.out.println("i = " + i);
}
System.out.println("after for loop.");
} catch (InterruptedException e) {
System.out.println("InterruptedException occur!");
e.printStackTrace();
}

}

public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
TimeUnit.SECONDS.sleep(2);
thread.interrupt();
} catch (InterruptedException e) {
// 这句话不会打印,因为run方法已经把中断异常捕获了
System.out.println("main catch!");
e.printStackTrace();
}
System.out.println("main end!");
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
i = 268085
i = 268086
i = 268087
i = 268088
i = 268089
i = 268090
thread already interrupted!
InterruptedException occur!
main end!
java.lang.InterruptedException
at thread.MyThread.run(MyThread.java:21)

Process finished with exit code 0

说明:如果没有throw new InterruptedException();这句代码,那么MyThread线程不会终止运行,会一直将for循环执行完毕,并且after for loop.这句输出也会打印。

  1. sleep与interrupt

如果一个正在sleep的线程又执行到了interrupt方法,会抛出java.lang.InterruptedException: sleep interrupted异常,并且清除停止状态值,使之变为false。

如果一个正在interrupt的线程又执行到了interrupt方法,也会抛出java.lang.InterruptedException: sleep interrupted异常,并且清除停止状态值,使之变为false。

  1. stop方法

使用stop方法停止线程是非常暴力的,而且会抛出java.lang.ThreadDeath,但是在通常的情况下,该异常不需要显示的捕捉。而且使用stop方法可能还会引发以下问题:

1)有可能使一些清理性的工作得不到完成

2)对锁定的对象进行了“解锁“,导致数据得不到处理,出现数据不一致的问题

综上来看,stop方法在功能上是有一定缺陷的,所以不建议在程序中使用该方法。

  1. 使用return停止线程

将方法interrupt()与return结合使用也能实现停止线程的效果。就是将[3]中的[异常法停止线程继续向下执行]使用示例中的throw new InterruptedException() 修改为return。不过还是建议使用[3]中的“抛异常”的方法来实现线程的停止,因为在catch块中还可以将异常向上抛,使线程停止的事件得以传播。

4. 暂停线程

在Java中,可以使用suspend方法暂停线程,resume方法恢复线程的执行。

suspend和resume方法都被标记为@Deprecated,是因为它们有如下缺点:

  1. 独占

如果在一个持有synchronized关键字的方法A里面调用了suspend方法,则该方法会被永远“锁住”,其它线程再也不会进入到A方法中。

  1. 不同步

使用suspend方法还可能会造成数据不同步的情况,比如做一个更新用户密码操作时,更新完用户名后执行了该方法,则密码没有更新,用户还得使用原来的密码,但是此时用户已经认为密码更新完成了。

5. yield方法

yield方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU的执行时间。但放弃的时间不确定,有可能执行yield的线程刚刚放弃,马上又获得了CPU时间片。

6. 线程的优先级

CPU会优先执行优先级较高的线程对象中的任务。

JDK中使用了三个常量来预置定义优先级的值(在Thread类中):

1
2
3
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

线程优先级的特性:

  1. 传递性(继承性):比如A线程启动B线程,则B线程的优先级是和A一样的。
  2. 规则性:CPU尽量让执行资源让给优先级比较高的线程。
  3. 随机性:线程的优先级与执行时打印的日志顺序无关,优先级高的线程并不一定每一次都先执行完run方法中的任务。
7. 守护线程

在Java中有两种线程:用户线程和守护线程。

当一个Java进程中不存在非守护线程了,则守护线程自动销毁。所以守护线程是一种“陪伴”的含义。典型的守护线程就是垃圾回收线程。

Chapter 2 对象及变量的并发访问

java语言支持的变量类型

  • 类变量:独立于方法之外的变量,用 static 修饰。
  • 局部变量:类的方法中的变量。
  • 实例变量(全局变量):独立于方法之外的变量,不过没有 static 修饰。
1. synchronized同步方法
  1. 方法内部的变量(局部变量)是方法私有的变量,不会存在线程安全问题。
  2. 实例变量是非线程安全的。
  3. 多线程访问多个对象时,JVM会创建多个锁。这是因为关键字synchronized取得的是对象锁,而不是把一段代码或方法当做锁。
  4. 只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。
2. 脏读

在读取实例变量时,此值已经被其它线程修改过了,这种现象称为脏读。

脏读是通过synchronized关键字来解决的,或者通过其它同步手段解决。

  1. 当线程A调用anyObject对象加入synchronized关键字的方法X时,A线程就获得了X方法所在对象的锁。所以其它线程必须等A线程执行完毕X方法才可以调用X方法,但B线程可以随意调用其它非同步方法。
  2. 当线程A调用anyObject对象加入synchronized关键字的方法X时,A线程就获得了X方法所在对象的锁。而B线程如果调用声明了synchronized关键字的非X方法,也必须等A线程将X方法执行完,也就是释放了对象锁之后才可以调用。
3. synchronized是一种可重入锁
  1. 可重入锁概念:自己可以再次获取自己的内部锁。比如有一个A线程获取了某个对象的锁,在A线程没有释放此对象锁之前,当其再次想要获取这个对象的锁的时候还是可以获取的。如果不可重入的话,就会造成死锁。
  2. 出现异常时,synchronized锁会自动释放
  3. 同步不具有继承性。即:假如B继承A,A中有一个synchronized的a方法,子类B中重写了这个a方法,但B中的a方法也必须添加synchronized才可以实现同步。
4. synchronized同步语句块
1
2
3
4
5
public void method(){
synchronized(this) {
// code
}
}

同步代码块存在的意义就是提醒程序员将该加同步的代码最细化。尽量不要将synchronized整个加在某一个方法上面,因为这样效率会很低。

和synchronized一样,synchronized(this)同步代码块也是锁定当前对象的。

5. Java中支持将任意对象作为对象监听器

锁非this对象具有一定的优点:如果一个类中有很多个synchronized方法,这时虽然能实现同步,但是会受到阻塞,所以影响运行效率;如果此时使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序和同步方法(同步方法其实就是synchronized(this))是异步的,不与同步方法争抢this锁,可以大大提高程序运行效率。

6. 静态同步方法

synchronized加到static静态方法上是给Class类上锁,而synchronized加到非静态方法上是给对象上锁。

Class锁对当前的*.java文件对应的Class类进行持锁,对这个类的所有实例对象起作用。而对象锁只对自己的实例对象起作用。

7. 数据类型String的常量池特性

大多数情况下,同步代码块都不会使用String对象作为锁对象,而改用其它,比如new Object()实例化一个Object对象,因为它并不放入缓存中。

8. 多线程的死锁

死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能释放的锁,从而导致所有的任务都无法继续完成。在多线程中,“死锁”是必须避免的,因为这样会造成程序的“假死”。

使用JDK自带工具检测是否有死锁:

jstack -l pid

如果有死锁会有Found 1 deadlock字样。

如下是一个死锁代码示例:

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
49
50
51
52
53
54
55
56
57
58
59
60
public class DeadLockDemo implements Runnable {
public String username;

private Object lock1 = new Object();
private Object lock2 = new Object();

public void setUsername(String username) {
this.username = username;
}

@Override
public void run() {
if ("a".equals(username)) {
synchronized (lock1) {
try {
System.out.println("username = " + username);
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lock2) {
System.out.println("lock1 -> lock2");
}
}
}

if ("b".equals(username)) {
synchronized (lock2) {
try {
System.out.println("username = " + username);
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (lock1) {
System.out.println("lock2 -> lock1");
}
}
}
}

public static void main(String[] args) {
try {
DeadLockDemo thread = new DeadLockDemo();
thread.setUsername("a");
Thread t1 = new Thread(thread);
t1.start();

TimeUnit.SECONDS.sleep(1);

thread.setUsername("b");
Thread t2 = new Thread(thread);
t2.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
9. 锁对象

在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间就是同步的;如果分别获得锁对象,这些线程之间就是异步的。

10. volatile与synchronized的区别
  1. volatile是线程同步的轻量级实现,所以性能肯定要比synchronized好,并且volatile只能修饰变量,而synchronized可以修饰代码块和方法。随着JDK新版本的发布,synchronized在执行效率上得到了很大的提升,在开发中使用该关键字的概率还是比较大的。
  2. 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
  3. volatile可以保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
  4. synchronized同步代码块包含volatile同步的功能。

线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。

volatile非线程安全的原因是因为变量在内存中的工作过程如下:

  1. read和load阶段:从主内存复制变量到当前工作内存;
  2. use和assign阶段:执行代码,改变共享变量的值;
  3. store和write阶段:用工作内存数据刷新主内存对应变量的值。

使用原子类进行多线程下的i++操作。

Chapter 3 线程间通信

1. 不使用等待/通知机制实现线程间通信

方法是通过while语句轮询机制来检测某一个条件,这样会浪费CPU资源。

2. 等待/通知机制的实现

什么是等待/通知机制?

服务员和厨师的例子:厨师没有做完菜之前,服务员一直处于等待的状态。厨师做完菜后将菜放在“菜品传递柜”上,其实就相当于一种通知,这样服务员才可以拿到菜品并交给就餐者。

wait和notify方法介绍

wait和notify还有notifyAll都是Object类的方法。

  1. wait():在调用wait之前,线程必须获得该对象的对象级别锁,即只能在同步方法或者同步代码块中调用wait方法。在执行wait方法之后,当前线程释放锁。

    如果调用wait方法时没有持有适当的锁,则会抛出IllegalMonitorStateExeception,它是RuntimeException的一个子类,因此不需要try-catch。

  2. notify():在调用notify之前,线程必也须获得该对象的对象级别锁,即只能在同步方法或者同步代码块中调用notify方法。在执行notify方法之后,由线程规划器从所有wait这个对象锁的线程中随机挑选出一个,对其发出notify通知,并使它等待获取该对象的对象锁。

    如果调用notify方法时没有持有适当的锁,也会抛出IllegalMonitorStateExeception,它是RuntimeException的一个子类,因此不需要try-catch。

    需要说明的时,在执行notify方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify方法的线程将程序执行完,也就是退出synchronized代码块之后,当前线程才释放锁。

    当第一个获得了该对象锁的wait线程执行完毕后,它会释放掉对象锁。但此时如果该对象没有再次使用notify语句,即便该对象已经空闲,其它wait线程由于错过了第一次的notify通知还是会继续阻塞在wait状态,直到这个对象发出一个notify或者notifyAll。

3. wait notify notifyAll方法说明

wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。

notify()方法可以随机唤醒等待队列中等待同一个共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态,也就是notify仅通知“一个”线程。可以在代码中多次调用notify唤醒多个,调用几次就唤醒几个。

notifyAll()方法可以使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,线程优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM的状态。

4. 线程状态切换示意图

avatar

每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等到CPU的调度;反之,一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒。

5. 当interrupt方法遇到wait方法

线程呈wait状态时,调用线程对象的interrupt方法会出现InterruptedException异常。

6. wait(long)

wait(long)方法的功能是等待某一段时间内是否有线程对锁进行唤醒,如果没有则超过这个时间自动唤醒。

7. 使用等待/通知时的注意事项
  1. 假如在使用wait和notify时,还没有线程执行wait,某个线程就执行了notify,就是通知过早。此时会造成再执行wait的线程一直在wait,还在等待notify呢。
  2. 在使用wait/notify时,还要注意一种情况。假如有两个线程呈wait状态,wait之后的操作是对一个List进行remove(0)操作,当List中只有一个元素时,此时其它持有同一个对象锁的线程执行了notifyAll,则两个wait状态的线程只有一个可以remove成功,另一个出现索引溢出的异常。解决此问题的办法是remove时先判断一下有没有元素,这也是自己写代码时的一个习惯问题。
8. 生产者/消费者模式实现

使用wait/notify实现生产者/消费者模式的思路:

目标是向一个List中存取元素,List的大小是1。生产者线程要有while true判断当前list大小,如果是1,则wait;否则put一个元素并notify。消费者线程要有while true判断当前list大小,如果是0,则wait;否则get并remove一个元素并notify。时刻保证List的大小不会超过1。

单生产单消费、单生产多消费、多生产单消费、多生产多消费

9. 通过管道进行线程间的通信

字节流:PipedInputStream、PipedOutputStream

字符流:PipedReader、PipedWriter

10. 使用等待/通知机制实现交叉备份
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
public class DbTools {
private volatile boolean prevIsA = false;

public synchronized void backupA() {
try {
while (prevIsA) {
wait();
}

for (int i = 0; i < 5; i++) {
System.out.println("AAAAA");
}

prevIsA = true;
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public synchronized void backupB() {
try {
while (!prevIsA) {
wait();
}

for (int i = 0; i < 5; i++) {
System.out.println("BBBBB");
}

prevIsA = false;
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class BackupA extends Thread{
private DbTools dbTools;

public BackupA(DbTools dbTools) {
super();
this.dbTools = dbTools;
}

@Override
public void run() {
dbTools.backupA();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BackupB extends Thread {

private DbTools dbTools;

public BackupB(DbTools dbTools) {
super();
this.dbTools = dbTools;
}

@Override
public void run() {
dbTools.backupB();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
DbTools dbTools = new DbTools();
for (int i = 0; i < 20; i++) {
BackupA backupA = new BackupA(dbTools);
backupA.start();

BackupB backupB = new BackupB(dbTools);
backupB.start();
}
}
}
11. join方法的使用

join方法的作用:使所属的线程对象x正常执行run方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。

方法join具有使线程排队的作用,有些类似同步的运行效果。join与synchronized的区别是:在join内部使用wait方法进行等待,而synchronized关键字使用的是“对象监视器”原理作为同步。

当join方法遇到interrupt方法时,会抛出InterruptedException异常。

join(long)方法:参数设定的是等待的时间。

12. join(long)和sleep(long)的区别

由于join内部实现是使用的wait方法,所以join具有释放锁的特点,当前执行join方法的线程的锁被释放,那么其他线程就可以调用此线程中的同步方法了。

Thread.sleep(long)方法只是等待,没有释放锁的特点。

13. ThreadLocal类的使用

当所有的线程都需要共享一个变量时,可以使用public static变量的形式。

但如果每个线程都想拥有一个自己的共享变量呢?就可以用ThreadLocal来实现。

ThreadLocal类的作用:ThreadLocal变量用来存储每个线程自己的共享变量。可以将ThreadLocal类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有数据。

此类解决的就是变量在不同线程间的隔离性,也就是不同线程拥有自己的值,不同线程的值是可以放入ThreadLocal类中来进行保存的。

ThreadLocal的使用:

  1. 如果只是new一个ThreadLocal变量,那么第一次get获取值的时候是null,给ThreadLocal变量赋默认值的方法为:
1
2
3
4
5
6
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "defaultValue";
}
};

或者用withInitial()方法加lambda表达式简写:

1
ThreadLocal<String> stringThreadLocal = ThreadLocal.withInitial(() -> "defaultValue");
  1. 主线程为main,主线程下有10个子线程,则只需要在主线程中定义一个ThreadLocal变量即可,这样10个子线程各自的变量值之间就是相互隔离的,各自用各自的,不用每个子线程都定义一个ThreadLocal变量。
14. 拓展:InheritableThreadLocal

使用此类可以让子线程取得父线程继承下来的值。取得之后还可以对值进行进一步的处理。

但需要注意的一点是:在子线程继承到父线程的值之后,若父线程对值进行了更改,子线程是接收不到这个讯息的,使用的还是第一次继承得到的值。

Chapter 4 Lock的使用

1. ReentrantLock的使用
  1. 使用ReentrantLock可以实现和synchronized同样的效果,调用lock方法的线程就持有了“对象监视器”,其它线程只有等待锁被释放时才可以再次争抢。
  2. 使用Condition实现等待/通知机制

使用notify/notifyAll方法进行通知时,被通知的线程是由JVM随机选择的。使用ReentrantLock结合Condition是可以实现“选择性通知”的。也就是说一个Lock对象里面可以创建多个Condition(即对象监视器)实例,线程对象可以在注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更灵活

而synchronized就相当于整个Lock对象只有一个单一的Condition,所有的线程都注册在它一个对象的身上,线程开始notify或者notifyAll时,没有选择权,只能是随机。

Object类中的wait()方法相当于Condition类中的await()方法。

Object类中的wait(long timeout)方法相当于Condition类中的await(long timeout, TimeUnit unit)方法。

Object类中的notify()方法相当于Condition类中的signal()方法。

Object类中的notifyAll()方法相当于Condition类中的signalAll()方法。

使用一个Condition对象来实现等待/通知机制示例:

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
public class MyService {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();

public void await() {
try {
lock.lock();
System.out.println("await time: " + System.currentTimeMillis());
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public void signal() {
try {
lock.lock();
System.out.println("signal time: " + System.currentTimeMillis());
condition.signal();
} finally {
lock.unlock();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadA extends Thread {
private MyService service;

public ThreadA(MyService service) {
this.service = service;
}

@Override
public void run() {
service.await();
}
}
1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) throws InterruptedException {
MyService myService = new MyService();
ThreadA threadA = new ThreadA(myService);
threadA.start();
TimeUnit.SECONDS.sleep(3);
myService.signal();
}
}
  1. 几个示例

一个Condition实现等待/通知机制 – 示例如上

多个Condition实现通知部分线程

生产者/消费者模式:一对一交替打印

生产者/消费者模式:多对多交替打印

  1. 公平锁与非公平锁

使用ReentrantLock可以实现公平锁和非公平锁。使用方式为初始化锁时构造函数传入的参数true为公平锁,false或者不传(默认)为非公平锁。

公平锁:线程获取锁的顺序是按照线程加锁的顺序来分配的,即FIFO。

非公平锁:一种获取锁的抢占机制,是随机获取锁的,这种方式可能造成某些线程一直拿不到锁,所以就是非公平。

  1. getHoldCount()、getQueueLength()、getWaitQueueLength()方法说明

int getHoldCount():查询当前线程保持此锁定的个数,也就是调用lock方法的次数。

int getQueueLength():返回正等待获取此锁定的线程估计数,比如有5个线程,1个线程首先执行await方法,那么调用getQueueLength()方法后返回值是4,说明有4个线程在等待lock的释放。

int getWaitQueueLength(Condition condition):返回等待与此锁定相关的给定条件condition的线程估计数,比如有5个线程,每个线程都执行了同一个condition的await方法,则返回值为5。

  1. hasQueueThread()、hasQueueThreads()、hasWaiters()方法说明

boolean hasQueueThread(Thread thread):查询指定的线程是否在等待获取此锁定。

boolean hasQueueThreads():查询是否有线程正在等待获取此锁定。

boolean hasWaiters(Condition condition):查询是否有线程正在等待与此锁定有关的condition条件。

  1. isFair()、isHeldByCurrentThread()、isLocked()方法说明

boolean isFair():判断是不是公平锁。

boolean isHeldByCurrentThread():查询当前线程是否保持此锁定。

boolean isLocked():查询此锁定是否有线程在保持。

  1. lockInterruptibly()、tryLock()、tryLock(long timeout, TimeUnit unit)方法说明

void lockInterruptibly():如果当前线程未被中断,则获取锁定;如果已中断则抛出异常。

boolean tryLock():仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。

boolean tryLock(long timeout, TimeUnit unit):如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未中断,则获取该锁定。

  1. awaitUninterruptibly()、awaitUntil()方法说明

awaitUninterruptibly():

线程在调用condition.await()后处于await状态,此时调用thread.interrupt()会抛出中断异常。
但是使用condition.awaitUninterruptibly()后,调用thread.interrupt()则不会报错。

awaitUntil(Date deadline):

调用此方法的线程在等待时间deadline到来之前可以被其他线程提前唤醒,否则等时间到了自动唤醒。意思就是调用这个方法的线程等一段时间后假如没有唤醒它的线程,则自动唤醒。

  1. 使用condition可以实现对线程执行的业务进行排序规划

示例:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class ConditionTest {
private static ReentrantLock lock = new ReentrantLock();
private final static Condition conditionA = lock.newCondition();
private final static Condition conditionB = lock.newCondition();
private final static Condition conditionC = lock.newCondition();
private volatile static int nextPrintWho = 1;

public static void main(String[] args) {
Thread threadA = new Thread() {
@Override
public void run() {
super.run();
try {
lock.lock();
while (nextPrintWho != 1) {
conditionA.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadA " + (i + 1));
}
nextPrintWho = 2;
conditionB.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};

Thread threadB = new Thread() {
@Override
public void run() {
try {
lock.lock();
while (nextPrintWho != 2) {
conditionB.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadB " + (i + 1));
}
nextPrintWho = 3;
conditionC.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};

Thread threadC = new Thread() {
@Override
public void run() {
try {
lock.lock();
while (nextPrintWho != 3) {
conditionC.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadC " + (i + 1));
}
nextPrintWho = 1;
conditionA.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};

Thread[] aArray = new Thread[5];
Thread[] bArray = new Thread[5];
Thread[] cArray = new Thread[5];

for (int i = 0; i < 5; i++) {
aArray[i] = new Thread(threadA);
bArray[i] = new Thread(threadB);
cArray[i] = new Thread(threadC);

aArray[i].start();
bArray[i].start();
cArray[i].start();
}
}
}

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ThreadA 1
ThreadA 2
ThreadA 3
ThreadB 1
ThreadB 2
ThreadB 3
ThreadC 1
ThreadC 2
ThreadC 3
ThreadA 1
ThreadA 2
ThreadA 3
ThreadB 1
ThreadB 2
ThreadB 3
ThreadC 1
ThreadC 2
ThreadC 3
...
2. ReentrantReadWriteLock的使用

读写锁:读操作相关的锁,也成为共享锁;写操作相关的锁,也叫排他锁。也就是说读读之间不互斥,读与写互斥,写与写之间也互斥。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。

总结

完全可以使用Lock对象将synchronized关键字替换掉,而且Lock对象的功能更加丰富一些。掌握Lock有助于学习并发包中源码的实现原理,在并发包中大量的类使用了Lock接口作为同步的处理方式。

Chapter 5 定时器Timer

  1. Timer的缺陷

Timer计时器可以定时(指定时间执行任务)、延迟(延迟5秒执行任务)、周期性地执行任务(每隔个1秒执行任务),但是,Timer存在一些缺陷。

1.首先Timer对调度的支持是基于绝对时间的,而不是相对时间,所以它对系统时间的改变非常敏感。

2.其次Timer线程是不会捕获异常的,如果TimerTask抛出的了未检查异常则会导致Timer线程终止,同时Timer也不会重新恢复线程的执行,他会错误的认为整个Timer线程都会取消。同时,已经被安排单尚未执行的TimerTask也不会再执行了,新的任务也不能被调度。故如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。

  1. 使用ScheduledThreadPoolExecutor代替Timer

Timer是基于绝对时间的,对系统时间比较敏感,而ScheduledThreadPoolExecutor 则是基于相对时间;Timer是内部是单一线程,而ScheduledThreadPoolExecutor内部是个线程池,所以可以支持多个任务并发执行。

Timer是JDK1.5出现的,因此JDK1.5之后几乎没有理由再使用Timer了,而是使用ScheduledThreadPoolExecutor来代替。

Chapter 6 单例模式与多线程

本章目的:如何使单例模式遇到多线程时是安全的、正确的。

1. 单例模式的实现方式
  1. “饿汉”模式
  2. DCL双检锁“懒汉”模式
  3. 使用静态内部类实现单例
  4. 序列化和反序列化的单例实现

静态内部类可以实现单例模式,但是当反序列化时,反序列化出来的还是多例的,解决办法是在反序列化的对象类中添加readResolve()方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyObject implements Serializable {
private static final long serialVersionUID = 1L;

private static class MyObjectHandler {
private static final MyObject OBJECT = new MyObject();
}

private MyObject() {
}

public static MyObject getInstance() {
return MyObjectHandler.OBJECT;
}

protected Object readResolve() throws ObjectStreamException {
return MyObjectHandler.OBJECT;
}
}
  1. 使用静态代码块实现单例
  2. 使用枚举类实现单例

Chapter 7 拾遗增补

1. 线程的状态

线程的状态信息存在于Thread.State枚举类中,有以下六个状态:

NEW:线程实例化后还未执行start()方法时的状态。

RUNNABLE:正在JVM中执行的线程处于这种状态。

BLOCKED:受阻塞并等待某个监视器锁的线程处于这种状态。

WAITING:线程执行了Object.wait()方法后处于这种状态。

TIMED_WAITING:执行了sleep方法后处于这种状态。

TERMINATED:线程被销毁时处于这种状态。

2. 线程组
  1. 线程组的作用:批量管理线程或线程组对象,有效地对线程或线程组对象进行组织。线程组中可以有线程对象,也可以有线程组。
  2. 线程组的自动归属特性:在实例化一个ThreadGroup线程组x时如果不指定所属的线程组,则x线程组自动归到当前线程对象所属的线程组中,也就是隐式地在一个线程组中添加了一个子线程组。
  3. JVM的根线程组:JVM的根线程组是system,再取其父线程组则出现空异常。
  4. 线程组内的线程批量停止:通过将线程添加到线程组中,当调用线程组ThreadGroup的interrupt()方法时,可以将该组中所有正在运行的线程批量停止。
  5. 递归与非递归获取线程组内对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 递归
ThreadGroup[] listGroupA = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
Thread.currentThread().getThreadGroup().enumerate(listGroupA, true);
for (ThreadGroup threadGroup : listGroupA) {
if (threadGroup != null) {
System.out.println(threadGroup.getName());
}
}

// 非递归
ThreadGroup[] listGroupB = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
Thread.currentThread().getThreadGroup().enumerate(listGroupB, false);
for (ThreadGroup threadGroup : listGroupB) {
if (threadGroup != null) {
System.out.println(threadGroup.getName());
}
}
3. 使线程具有有序性

正常情况下,线程在运行时多个线程之间执行任务的时机是无序的,可以通过改造代码使它们的运行具有有序性。

4. SimpleDateFormat非线程安全

可以使用JDK8中的DateTimeFormatter 代替 SimpleDateFormat,或者使用common-lang3包的DateFormatUtils.format(),线程安全,底层基于FastDateFormat实现的。

5. 线程中出现异常的处理方式

Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler eh):给指定线程对象设置异常处理器。Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh):给指定线程类的所有线程对象设置默认的异常处理器。

6. 线程组中出现异常的处理方式

默认情况下,一个线程组内的某个线程发生异常后并不影响组内的其它线程正常运行,但如果想要一个组内只要有一个线程出现异常则组内所有线程全部停止,则需要如下这么做:

自定义一个线程组,实现uncaughtException()方法,并加this.interrupt();这句代码。

1
2
3
4
5
6
7
8
9
10
11
public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);
}

@Override
public void uncaughtException(Thread t, Throwable e) {
super.uncaughtException(t, e);
this.interrupt();
}
}
坚持原创技术分享,您的支持将鼓励我继续创作!

------本文结束 感谢您的阅读------