Java核心技术 - 多线程



线程种类

  • 用户线程
  • 守护线程

什么是线程

操作系统在分配资源时是把资源分配给进程的,但是CPU资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。

在Java 中,当我们启动main函数时其实就启动了一个JVM的进程,而main 函数所在的线程就是这个进程中的一个线程,也称主线程。

进程和线程的关系如图1-1所示。

由图1-1可以看到,一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域

程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。那么为何要将程序计数器设计为线程私有的呢?前面说了线程是占用CPU执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行。那么如何知道之前程序执行到哪里了呢?其实程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。另外需要注意的是,如果执行的是native方法,那么pc计数器记录的是undefined地址,只有执行的是Java代码时 pc 计数器记录的才是下一条指令的地址。

另外每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。

堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例。

方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的

线程创建与运行

其实调用start方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦 run方法执行完毕,该线程就处于终止状态。

使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runable则没有这个限制。下面看实现Runnable接口的run方法方式。

构建带有返回值的线程任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CallerTask implements Callable<String> {
@Override
public String call() throws Exception {
return "caller value";
}

public static void main(String[] args) {
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
new Thread(futureTask).start();
try {
// 等待任务执行完毕,并返回结果
String result = futureTask.get();
System.out.println(result);
} catch(Exception e) {
e.printStackTrace();
}
}
}

如上代码中的CallerTask类实现了Callable接口的call()方法。在main函数内首先创建了一个 FutrueTask对象(构造函数为CallerTask的实例),然后使用创建的FutrueTask对象作为任务创建了一个线程并且启动它,最后通过 futureTask.get()等待任务执行完毕并返回结果。

并行计算

内容:在Java中利用Callable进行带返回结果的线程计算,利用Future表示异步计算的结果,分别计算不同范围的Long求和,类似的思想还能够借鉴到需要大量计算的地方。

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
public class Sums {

public static class Sum implements Callable<Long> {
private final Long from;
private final Long to;

public Sum(long from, long to) {
this.from = from;
this.to = to;
}

@Override
public Long call() throws Exception {
long ans = 0;
for (long i = from; i <= to; i++)
ans += i;

return ans;
}
}

public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(2);
List<Future<Long>> ans = executor.invokeAll(Arrays.asList(
new Sum(0, 1000), new Sum(10000, 100000), new Sum(1000000, 1000000)));
executor.shutdown();

long sum = 0;
for (Future<Long> i : ans) {
long tmp = i.get();
System.out.println(tmp);
sum += tmp;
}
System.out.println("sum : " + sum);
}
}

小结:使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过 set方法设置参数或者通过构造函数进行传递,而如果使用Runnable方式,则只能使用主线程里面被声明为final 的变量。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以。

常见创建方式

  • 继承Thread类
  • 实现Runnable接口
  • Java8新特性:new Thread(() -> {})

image-20210316154918045

Thread.java类中的start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run() 方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run)方法,也就是必须等run)方法中的代码执行完后才可以执行后面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test1 {
public static void main(String args[]) {

Thread t = new Thread() {
@Override
public void run() {
pong();
}
};

t.run(); // t.start();
System.out.print("ping");
}

static void pong() {
System.out.print("pong");
}
}

使用 t.run() 方法调用,同步的:输出 pongping

使用 t.start() 方法调用,异步的:输出 pingpong

1、getId()

作用:取得线程的唯一标识,如当前执行线程名称是main,那么获取的id值为 1。

2、yield()

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

3、join方法

作用:等待某个线程执行终止

4、sleep方法

作用:调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所持有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了。

5、暂停线程

意味着此线程还可以恢复运行。在Java多线程中,可以使用suspend()方法暂停线程,使用resume()方法恢复线程的执行

线程的优先级

1、概念

在JDK中,线程的优先级分为1~10这10个等级,且使用3个常量来预置定义优先级的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 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;

2、特性

  • 优先级具有继承性

比如父线程Thread1优先级是6,然后子线程默认也是的,且父线程优先级修改后,子线程也会相应改动。

  • 优先级具有规则性

CPU尽量将执行资源让给优先级比较高的线程。故优先级高的线程可以大部分先执行,但不能说明它就一定会先执行完。

  • 优先级具有随机性

优先级较高的线程不一定会每一次都先执行完

守护线程

1、概念

守护线程是一种特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。

当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。

用个比较通俗的比喻来解释一下“守护线程”:

任何一个守护线程都是整个JVM中所有非守护线程的“保姆”,只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就在工作,只有当最后一个非守护线程结束时,守护线程才随着JVM一同结束工作。

Daemon(守护线程)的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC (垃圾回收器),它就是一个很称职的守护者。

2、使用守护线程

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
public class DaemonDemo extends Thread{

public static int i = 0;

@Override
public void run() {
try {
for (int j = 0; j < 5; j++) {
i++;
System.out.print("i= " + i + "\t");
Thread.sleep(500);
}
} catch(Exception e) {
e.printStackTrace();
}
}

public static void showI(int i) {
i = 10;
System.out.println("final:" + "\t" + DaemonDemo.i + "\t" + i);
}

public static void main(String[] args) {
DaemonDemo daemonDemo = new DaemonDemo();
daemonDemo.setDaemon(true);
daemonDemo.start();
System.out.println("线程开始");
try {
Thread.sleep(5000);
showI(i); // 测试值传递对 i值的影响
} catch(Exception e) {
e.printStackTrace();
}
System.out.println("离开thread线程就不再打印了");
}
}

输出:
线程开始
i= 1 i= 2 i= 3 i= 4 i= 5
final: 5 10
离开thread线程就不再打印了

总结:如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。

使线程具有有序性

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
35
36
37
38
39
40
41
42
43
44
45
46
47
public class MyThread extends Thread {

Object lock;
private String showChar;
private int showNumPosition;
private int printCount = 0; //统计打印了几个字母
volatile private static int addNumber = 1;

/**
*
* @param lock 锁对象
* @param showChar 展示的字符
* @param showNumPosition 判断条件addNumber % 3 == this.showNumPosition
* @param name 线程名
*/
public MyThread(Object lock, String showChar, int showNumPosition, String name) {
this.lock = lock;
this.showChar = showChar;
this.showNumPosition = showNumPosition;
super.setName(name);
}

@Override
public void run() {
synchronized (this.lock) {
while (true) {
if (addNumber % 3 == this.showNumPosition) {
System.out.println("ThreadName=" + Thread.currentThread().getName() +
" runCount=" + addNumber + " showChar=" + this.showChar + " showNumPosition=" + this.showNumPosition);
lock.notifyAll();
addNumber++;
printCount++;
// 线程打印到第三次就跳出循环
if (this.printCount == 3) {
break;
}
} else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}

2、测试

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
Object obj = new Object();
Thread a = new MyThread(obj,"a", 1, "A");
Thread b = new MyThread(obj,"b", 2, "B");
Thread c = new MyThread(obj,"c", 0, "C");
a.start();
b.start();
c.start();
}
}

3、又输出可见线程间在有序打印

1
2
3
4
5
6
7
8
9
ThreadName=A runCount=1 showChar=a showNumPosition=1
ThreadName=B runCount=2 showChar=b showNumPosition=2
ThreadName=C runCount=3 showChar=c showNumPosition=0
ThreadName=A runCount=4 showChar=a showNumPosition=1
ThreadName=B runCount=5 showChar=b showNumPosition=2
ThreadName=C runCount=6 showChar=c showNumPosition=0
ThreadName=A runCount=7 showChar=a showNumPosition=1
ThreadName=B runCount=8 showChar=b showNumPosition=2
ThreadName=C runCount=9 showChar=c showNumPosition=0

SimpleDateFormat非线程安全

每个SimpleDateFormat实例里面都有一个Calendar对象, 后面我们就会知道, SimpleDateFormat 之所以是线程不安全的, 就是因为Calendar是线程不安全的。后者之所以是线程不安全的,是因为其中存放日期数据的变量都是线程不安全的,比如fields、time等。

下面从代码层面来看下parse方法做了什么事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Date parse(String text,  ParsePosition pos) 
// (1)解析日期字符串,并将解析好的数据放入CalendarBuilder的实例calb中
CalendarBuilder calb = new CalendarBuilder();
...
Date parsedDate;
try { // (2)使用calb中解析好的日期数据设置calendar
parsedDate = calb.establish(calendar).getTime();
...
} catch (IllegalArgumentException e) {
...
return null;
}
return parsedDate;
}

CalendarBuilder是一个建造者模式,用来存放后面需要的数据。

1
2
3
4
5
6
7
8
9
Calendar establish(Calendar cal) {
...
// (3) 重置日期对象 cal 的属性值
cal.clear();
// (4) 使用calb中的属性值设置cal
...
// (5) 返回设置好的cal对象
return cal;
}

1、创建日期格式化线程

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 MyThread extends Thread{

private SimpleDateFormat dateFormat;
private String dateStr;

public MyThread(SimpleDateFormat dateFormat, String dateStr) {
this.dateFormat = dateFormat;
this.dateStr = dateStr;
}

@Override
public void run() {
Date date = null;
try {
date = dateFormat.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
String format = dateFormat.format(date);
System.out.println(Thread.currentThread().getName() + "原始数据:" + this.dateStr
+ "日期类型date:" + date + "格式化后的format:" + format);
}
}

2、测试

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
String[] dates = {"2020-08-12", "2019-08-14", "2021-02-25", "2018-07-06", "2019-02-08", "2020-02-02"};
MyThread[] myThreads = new MyThread[6];
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd")

for (int i = 0; i < dates.length; i++) {
myThreads[i] = new MyThread(sdf, dates[i]);
}
for (int i = 0; i < dates.length; i++) {
myThreads[i].start();
}
}

3、使用全局格式化变量

创建的时候可能会创建多个实例,在关闭连接的时候,就可能只关闭了一个对象的连接,造成其他连接没有被关闭,最后导致连接耗光系统不可用;

1
2
3
4
5
6
7
Exception in thread "Thread-3" Exception in thread "Thread-1" java.text.ParseException: Unparseable date: "2020-02-02"
at java.text.DateFormat.parse(DateFormat.java:366)
at SimpleDateFormat线程不安全.MyThread.run(MyThread.java:25)
java.text.ParseException: Unparseable date: "2021-02-25"
at java.text.DateFormat.parse(DateFormat.java:366)
at SimpleDateFormat线程不安全.MyThread.run(MyThread.java:25)
java.text.ParseException: Unparseable date: "2019-02-08"

4、异常解决办法:

使用ThreadLocal,创建工具类:使用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
public class DateTools {

private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};

public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}

public static String format(Date date) {
return threadLocal.get().format(date);
}

}

// 或者
class DateTools2 {
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();

public static DateFormat getDateFormat() {
DateFormat sdf = threadLocal.get();
if(sdf == null){
sdf = new SimpleDateFormat(DATE_FORMAT);
threadLocal.set(sdf);
}
return sdf;
}

public static String formatDate(Date date) throws ParseException {
return getDateFormat().format(date);
}

public static Date parse(String strDate) throws ParseException {
return getDateFormat().parse(strDate);
}

public static void remove() {
// 使用完毕后记得关闭,不然容易造成内存泄漏
threadLocal.remove();
}
}

修改自定义线程方法

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 MyThread extends Thread{

private String dateStr;

public MyThread(String dateStr) {
this.dateStr = dateStr;
}

@Override
public void run() {
Date date = null;
try {
date = DateTools.parse(this.dateStr);
} catch (ParseException e) {
e.printStackTrace();
} finally {
DateTools.remove();
}
String format = DateTools.format(date);
System.out.println(Thread.currentThread().getName() + "原始数据:" + this.dateStr
+ "日期类型date:" + date + "格式化后的format:" + format);
}
}

测试输出

1
2
3
4
5
6
Thread-1原始数据:2019-08-14日期类型date:Wed Aug 14 00:00:00 CST 2019格式化后的format:2019-08-14
Thread-5原始数据:2020-02-02日期类型date:Sun Feb 02 00:00:00 CST 2020格式化后的format:2020-02-02
Thread-2原始数据:2021-02-25日期类型date:Thu Feb 25 00:00:00 CST 2021格式化后的format:2021-02-25
Thread-4原始数据:2019-02-08日期类型date:Fri Feb 08 00:00:00 CST 2019格式化后的format:2019-02-08
Thread-0原始数据:2020-08-12日期类型date:Wed Aug 12 00:00:00 CST 2020格式化后的format:2020-08-12
Thread-3原始数据:2018-07-06日期类型date:Fri Jul 06 00:00:00 CST 2018格式化后的format:2018-07-06

或者也可以直接这么使用:

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
public class Test {

static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};

public static void main(String[] args) {
String[] dates = {"2020-08-12", "2019-08-14", "2021-02-25", "2018-07-06", "2019-02-08", "2020-02-02"};

for (int i = 0; i < dates.length; i++) {
int finalI = i;
new Thread(() -> {
try {
Date date = threadLocal.get().parse(dates[finalI]);
System.out.println("date:" + date);

System.out.println("format:" + threadLocal.get().format(date));
} catch (ParseException e) {
e.printStackTrace();
} finally {
// 使用完毕记得清楚,避免内存泄露
threadLocal.remove();
}
}).start();
}
}
}

创建了一个线程安全的SimpleDateFormat实例,首先使用get()方法获取当前线程下SimpleDateFormat的实例。在第一次调用ThreadLocal的 get() 方法时,会触发其initialValue方法创建当前线程所需要的SimpleDateFormat对象。另外需要注意的是使用完线程变量后,要进行清理,以避免内存泄漏。

线程中出现异常处理

1、自定义线程

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

private Integer count;

public MyThread(Integer count, String name) {
this.count = count;
super.setName(name);
}

@Override
public void run() {
int i = 10 / this.count;
System.out.println("线程" + Thread.currentThread().getName() + "的计算值:" + i);
}
}

2、使用 UncaughtExceptionHandler 类,对异常进行有效的捕捉

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 Main {
public static void main(String[] args) {
MyThread a = new MyThread(0, "A");
// 捕获异常
a.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.err.println("线程名:" + t.getName());
e.printStackTrace();
}
});
a.start();

MyThread b = new MyThread(1, "B");
b.start();
}
}

// 结果
线程名:A
java.lang.ArithmeticException: / by zero
at 捕获异常.MyThread.run(MyThread.java:18)
线程B的计算值:10

3、使用默认的 Thread.setDefaultUncaughtExceptionHandler 异常捕捉

1
2
3
4
5
6
7
8
9
10
11
12
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.err.println("默认的线程异常捕捉,线程名:" + t.getName());
e.printStackTrace();
}
});

// 结果
默认的线程异常捕捉,线程名:A
java.lang.NullPointerException
at 捕获异常.MyThread.run(MyThread.java:18)

线程组内出现异常

1、自定义关联线程的线程组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyThread extends Thread {

private Integer count;

public MyThread(Integer count, String name, MyThreadGroup threadGroup) {
super(threadGroup, name);
this.count = count;
}

@Override
public void run() {
int i = 10 / this.count;
System.out.println("线程" + Thread.currentThread().getName() + "的计算值:" + i);
// 通过是否中断判断
while (this.isInterrupted() == false) {
System.out.println("我不怕累,只想跑代码");
}
}
}

2、自定义子类线程组

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyThreadGroup extends ThreadGroup{

public MyThreadGroup(String name) {
super(name);
}

@Override
public void uncaughtException(Thread t, Throwable e) {
super.uncaughtException(t, e);
System.out.println("线程组" + t.getThreadGroup() + "\t 线程名" + t.getName() + e.getMessage());
this.interrupt();
}
}

3、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
MyThreadGroup threadGroup = new MyThreadGroup("我的线程组");

for (int i = 0; i < 5; i++) {
Thread thread = new MyThread(i+1, "第" + (i+1) + "个", threadGroup);
thread.start();
}

MyThread error = new MyThread(0, "报错线程", threadGroup);
error.start();
}
}

4、输出打印,可见捕获异常后就中断所有线程了,不然还是一直运行

1
2
3
4
5
6
7
8
9
线程第2个的计算值:5
我不怕累,只想跑代码
我不怕累,只想跑代码
我不怕累,只想跑代码
线程组线程组.捕获异常.MyThreadGroup[name=我的线程组,maxpri=10] 线程名报错线程/ by zero
我不怕累,只想跑代码
我不怕累,只想跑代码
Exception in thread "报错线程" java.lang.ArithmeticException: / by zero
at 线程组.捕获异常.MyThread.run(MyThread.java:18)

理解线程上下文切换

在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出 CPU的线程等下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。

线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。

线程死锁

什么是线程死锁

在图1-2中,线程A 已经持有了资源2,它同时还想申请资源1,线程B已经持有了资源1,它同时还想申请资源⒉,所以线程1和线程2就因为相互等待对方已经持有的资源,而进入了死锁状态。

那么为什么会产生死锁呢?学过操作系统的朋友应该都知道,死锁的产生必须具备以下四个条件。

  • 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件:指在发生死锁时,必然存在一个线程―资源的环形链,即线程集合{T0,T1,T2,…,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源。

并发编程的基础知识

并发和并行

并发是指同一个时间段内多个任务同时都在执行,并且都没有执行结束,而并行是说在单位时间内多个任务同时在执行并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行。 在单CPU的时代多个任务都是并发执行的,这是因为单个CPU同时只能执行一个任务。 在单CPU时代多任务是共享一个CPU的,当一个任务占用CPU运行时,其他任务就会被挂起,当占用CPU的任务时间片用完后,会把CPU让给其他任务来使用,所以在单CPU时代多线程编程是没有太大意义的,并且线程间频繁的上下文切换还会带来额外开销。

图2-1 所示为在单个CPU上运行两个线程,线程A和线程B是轮流使用CPU进行任务处理的,也就是在某个时间内单个CPU只执行一个线程上面的任务。 当线程A的时间片用完后会进行线程上下文切换,也就是保存当前线程A的执行上下文,然后切换到线程B来占用CPU运行任务。

图2-2所示为双CPU配置,线程A和线程B各自在自己的CPU上执行任务,实现了真正的并行运行。

而在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。

为什么要进行多线程并发编程

多核CPU时代的到来打破了单核CPU对多线程效能的限制。 多个CPU意味着每个线程可以使用自己的CPU运行,这减少了线程上下文切换的开销,但随着对应用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切的需求。

Java中的线程安全问题

谈到线程安全问题,我们先说说什么是共享资源。 所谓共享资源,就是说该资源被多个线程所持有或者说多个线程都可以去访问该资源。

线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题,如图2-3所示。

在图2-3 中, 线程A和线程B可以同时操作主内存中的共享变量,那么线程安全问题和共享资源之间是什么关系呢?是不是说多个线程共享了资源, 当它们都去访问这个共享资源时就会产生线程安全问题呢?答案是否定的, 如果多个线程都只是读取共享资源,而不去修改,那么就不会存在线程安全问题, 只有当至少一个线程修改共享资源时才会存在线程安全问题。最典型的就是计数器类的实现,计数变量count本身是一个共享变量,多个线程可以对其进行递增操作,如果不使用同步措施, 由于递增操作是获取一个计算一个保存三步操作, 因此可能导致计数不准确,如下所示。

假如当前count=0,在t1时刻线程A读取 count值到本地变量countA。然后在t2时刻递增countA 的值为1,同时线程B读取count的值0到本地变量countB,此时countB的值为0(因为countA 的值还没有被写入主内存)。在t3时刻线程A才把countA的值1写入主内存,至此线程A一次计数完毕,同时线程B递增CountB的值为1。在t4时刻线程B把countB的值1写入内存,至此线程B一次计数完毕。

这里先不考虑内存可见性问题,明明是两次计数,为何最后结果是1而不是2呢?

其实这就是共享变量的线程安全问题。那么如何来解决这个问题呢?这就需要在线程访问共享变量时进行适当的同步,在Java中最常见的是使用关键字synchronized进行同步,下面会有具体介绍。

Java中共享变量的内存可见性问题

谈到内存可见性, 我们首先来看看在多线程下处理共享变量时Java的内存模型,如图2-4所示。

Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量。 Java内存模型是一个抽象的概念,那么在实际实现中线程的工作内存是什么呢?请看图2-5。

图中所示是一个双核CPU系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有 CPU 都共享的二级缓存。那么 Java 内存模型里面的工作内存,就对应这里的 Ll 或者 L2 缓存或者 CPU 的寄存器。

当一个线程操作共享变量时, 它首先从主内存复制共享变量到自己的工作内存, 然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。

那么假如线程A和线程B同时处理一个共享变量, 会出现什么情况?我们使用图2-5所示CPU架构, 假设线程A和线程B使用不同CPU执行,并且当前两级Cache都为空,那么这时候由于Cache的存在,将会导致内存不可见问题, 具体看下面的分析。

  • 线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存, 线程A修改X的值为1,然后将其写入两级Cache, 并且刷新到主内存。 线程A操作完毕后,线程A所在的CPU的两级Cache 内和主内存里面的X的值都是1。
  • 线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1 ; 到这里一切都是正常的, 因为这时候主内存中也是X=1 。然后线程B修改X的值为2, 并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2; 到这里一切都是好的。
  • 线程A这次又需要修改X的值, 获取时一级缓存命中, 并且X=1 ,到这里问题就出现了,明明线程B已经把X的值修改为了2,为何线程A获取的还是1 呢?这就是共享变量的内存不可见问题, 也就是线程B写入的值对线程A不可见。

那么如何解决共享变量内存不可见问题?使用Java中的volatile关键字就可以解决这个问题, 下面会有讲解。

Java中的synchronized关键字

关键字介绍

synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用, 这些Java 内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait 系列方法时释放该内置锁。 内置锁是排它锁,也就是当一个线程获取这个锁后, 其他线程必须等待该线程释放锁后才能获取该锁

另外,由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换

方法中的变量不存在线程不安全问题,这是方法内部私有的特性造成的。但是访问一个对象中的实例,就可能存在线程不安全问题。

1、多个对象多个锁

1) A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。

2) A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需等待,也就是同步。

3) 哪个线程持有该方法的同步锁,那么其他线程只能呈等待状态,前提是多个线程访问的是用一个对象。

2、原理

简单来说就是:

A线程获得一个对象的锁,其他线程必须等A线程执行完毕才可以调用X方法和此对象里的其他声明了synchronized 关键字的非X方法。

摘自Java核心编程技术:

当A线程调用anyObject对象加入synchronized 关键字的X方法时,A线程就获得了x方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,而B线程如果调用声明了synchronized 关键字的非X方法时,必须等A线程将X方法执行完,也就是释放对象锁后才可以调用。这时A线程已经执行了一个完整的任务,也就是说username和password这两个实例变量已经同时被赋值,不存在脏读的基本环境。

3、锁重入

“可重人锁”的概念是:自已可以再次获取自己的内部锁。比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重人的话,就会造成死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LockDemo {
static synchronized void service1() {
System.out.println("service1");
service2();
}

static synchronized void service2() {
System.out.println("service2");
service3();
}

static synchronized void service3() {
System.out.println("service3");
}

public static void main(String[] args) {
new Thread(() -> {
service1();
}, "test").start();
}
}

输出:

service1
service2
service3

内存语义

进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。 退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内

其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存

除可以解决共享变量内存可见性问题外,synchronized经常被用来实现原子性操作。另外请注意,synchronized关键字会引起线程上下文切换并带来线程调度开销。

出现异常,锁自动释放

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 LockDemo {
static synchronized void service1() {
System.out.println("service1");
service2();
}

static synchronized void service2() {
System.out.println("service2");
int i = 1 / 0;
}

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.print(Thread.currentThread().getName() + ":\t");
service1();
}, "test").start();
Thread.sleep(500);
new Thread(() -> {
System.out.print(Thread.currentThread().getName() + ":\t");
service1();
}, "exception").start();
}
}

输出:

test: service1
service2
exception: service1
service2
Exception in thread “test” java.lang.ArithmeticException: / by zero
at LockDemo.service2(LockDemo.java:13) Exception in thread “exception” java.lang.ArithmeticException: / by zero
at LockDemo.service2(LockDemo.java:13)

注意:同步不具有继承性

synchronized代码块有volatile同步的功能

关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。同步synchronized不仅可以解决一 个线程看到对象处于不一致的状态,还可以保证进人同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果

静态同步方法

关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java 文件对应的Class类进行持锁,测试项目在synStaticMethod中,类文件Service.java 代码如下:

1
2
3
synchronized public static void printA() {
...
}

作用同

1
2
3
4
5
public void printA() {
synchronized(this.getClass()) {
...
}
}

而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
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
public class LockDemo {

/*
* 尽量避免使用String作锁,因为其存在常量池的特性
* 比如:String a = "A"; String b= "A"; 这两对象是相等的,就导致拿到同一个锁对象了。
*/
private Object key = new Object();

static synchronized void service1() throws InterruptedException {
System.out.println("service1:" + LocalDateTime.now());
Thread.sleep(2000);
}

static synchronized void service2() throws InterruptedException {
System.out.println("service2:" + LocalDateTime.now());
Thread.sleep(2000);
}

static synchronized void service3() {
System.out.println("service3");
}

public void testLockClass() throws InterruptedException {
synchronized (this.getClass()) {
System.out.println("testLockClass:" + LocalDateTime.now());
Thread.sleep(2000);
}
}

public void testNoLock() throws InterruptedException {
System.out.println("testNoLock:" + LocalDateTime.now());
}

public void test2LockKey() throws InterruptedException {
synchronized (key) {
System.out.println("test2LockKey:" + LocalDateTime.now());
}
Thread.sleep(2000);
}

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
System.out.print(Thread.currentThread().getName() + "\t");
// LockDemo.service1();
// LockDemo lockDemo = new LockDemo();
// lockDemo.testLockClass(); // synchronized (this.getClass())
// lockDemo.test2LockKey();
// lockDemo.testNoLock(); // test1 和 test2几乎同时进入
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "test").start();

new Thread(() -> {
try {
System.out.print(Thread.currentThread().getName() + "\t");
// LockDemo.service2();
// LockDemo lockDemo1 = new LockDemo();
// lockDemo1.testLockClass(); // synchronized (this.getClass())
// lockDemo1.test2LockKey();
// lockDemo1.testNoLock(); // test1 和 test2几乎同时进入
} catch(Exception e) {
e.printStackTrace();
}
}, "exception").start();

}
}

测试锁class对象后,new两个对象是否同步锁的

取消注释两个线程的前三行

1
2
3
LockDemo.service1(); // 静态同步方法
LockDemo lockDemo = new LockDemo();
lockDemo.testLockClass(); // synchronized (this.getClass())

由打印可见,以上都是锁住了当前的对象

1
2
3
4
service2:2021-03-17T10:10:43.620
service1:2021-03-17T10:10:45.621
testLockClass:2021-03-17T10:10:47.625
testLockClass:2021-03-17T10:10:49.627

测试锁对象后是否可以异步调用其他非同步方法

取消注释两个线程中的前两行和最后一行

1
2
3
LockDemo.service2(); // 静态同步方法 LockDemo.service1()
LockDemo lockDemo1 = new LockDemo();
lockDemo1.testNoLock()

由打印可见,以上锁住了当前的class对象,不能异步调用其他非同步方法>

1
2
3
4
service1:2021-03-17T10:12:38.260
service2:2021-03-17T10:12:40.261
testNoLock:2021-03-17T10:12:40.261
testNoLock:2021-03-17T10:12:42.270

锁住对象中的key或者字段,是否效果同锁住class对象

取消注释两个线程中的第二、四行

1
2
LockDemo lockDemo1 = new LockDemo();
lockDemo1.test2LockKey();

由打印可见,对多个对象而言是存在线程安全问题的。而且锁着key对象的时候就还可以调用其他非同步方法。

1
2
test2LockKey:2021-03-17T10:16:19.588
test2LockKey:2021-03-17T10:16:19.589

锁总结

  • 锁住Class对象,会阻塞非获取到锁资源的所有线程,就算调用非同步方法也不行,必须要等释放锁资源
  • 同一对象下,锁住key,会阻塞锁住同key的同步方法,不影响非同步方法调用和其他非同key锁的同步方法
  • 多个对象下,锁住同一个key也不会阻塞,因为对象不同导致不是同一个key被锁住

同步synchronized方法无限等待与解决

1
2
3
4
5
6
7
8
9
10
11
public class LockDemo() {
synchronized public void methodA() {
while (true) {
System.out.println("methodA");
}
}

synchronized public void methodB() {
System.out.println("methodB");
}
}

当调用两个线程后会存在锁死问题,导致methodB方法一直进不去,我们可以用过锁住key对象来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Object key1 = new Object();
public void methodC() throws InterruptedException {
synchronized (this.key1) {
System.out.println("methodC" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println("methodC" + System.currentTimeMillis());
}
}

private Object key2 = new Object();
public void methodD() throws InterruptedException {
synchronized (this.key2) {
System.out.println("methodD" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println("methodD" + System.currentTimeMillis());
}
}

由打印可见,两个线程是同时进入同步方法的,这样就没有无线等待了。

1
2
3
4
methodD	1615948867325
methodC 1615948867325
methodC 1615948868330
methodD 1615948868330

锁对象的改变

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

线程A和B持有的锁都是“123”, 虽然将锁改成了“456”, 但结果还是同步的,因为A和B共同争抢的锁是“123”” 。

还需要提示一下,只要对象不变,既使对象的属性被改变,运行的结果还是同步

锁升级

锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)

(1)偏向锁:

为什么要引入偏向锁?

因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

偏向锁的升级

当线程1访问代码块并获取锁对象时,会在Java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

偏向锁的取消

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;

(2)轻量级锁

为什么要引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放

轻量级锁什么时候升级为重量级锁?

线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;

如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。

但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

*注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态

(3)这几种锁的优缺点(偏向锁、轻量级锁、重量级锁)

锁粗化

按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。 锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作

锁消除

Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间

Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级

Java中的volatile关键字

关键字介绍

主要作用是使变量在多个线程间可见,而且它是强制线程每次从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值(可见性)。但是它也存在致命的痛点是在多个线程间不具备同步性,也就是不支持原子性(不支持原子性)。

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
35
36
public class VolatileDemo extends Thread{

private volatile boolean bool = true;

@Override
public void run() {
printMethod();
}

private void printMethod() {
System.out.println("进入了printMethod方法");
while (bool) {
System.out.println(Thread.currentThread().getName() + "printMethod-ing");
}
System.out.println("将要退出printMethod方法");
}

public void setBool(boolean bool) {
System.out.println("赋值前:" + this.isBool());
this.bool = bool;
System.out.println("赋值后:" + this.isBool());
}

public boolean isBool() {
return bool;
}

public static void main(String[] args) throws InterruptedException {
VolatileDemo volatileDemo = new VolatileDemo();
// volatileDemo.printMethod(); // 单独调用此方法就会一直打印
volatileDemo.start();
Thread.sleep(100);
volatileDemo.setBool(false);
}

}

由此可见volatile可以强制方法从公共堆栈中取得变量的值

1
2
3
4
5
6
7
Thread-0printMethod-ing
Thread-0printMethod-ing
Thread-0printMethod-ing
赋值前:true
Thread-0printMethod-ing
赋值后:false
将要退出printMethod方法

2、非原子性

用图来演示一下使用关键字volatile时出现非线程安全的原因。变量在内存中工作的过程如图2-80所示。由上,我们可以得出以下结论。

  1. read和load阶段:从主存复制变量到当前线程工作内存;

  2. use 和assign阶段:执行代码,改变共享变量值;

  3. store 和write阶段:用工作内存数据刷新主存对应变量的值。

​ 在多线程环境中,use 和assign是多次出现的,但这-操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全问题。

​ 对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如线程1和线程2在进行read和load的操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。也就是说,volatile 关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。

3、可见性

该关键字可以确保对一个变量的更新对其他线程马上可见。 当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。 当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

内存语义

volatile 的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。

引申

所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

  前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

  下面这段话摘自《深入理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

不保证原子性

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
class MyData {
/**
* volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
*/
volatile int number = 0;

public void addTo60() {
this.number = 60;
}

/**
* 注意,此时number 前面是加了volatile修饰
*/
public void addPlusPlus() {
number ++;
}
}

/**
* 验证volatile的可见性
* 1、 假设int number = 0, number变量之前没有添加volatile关键字修饰
* 2、添加了volatile,可以解决可见性问题
*
* 验证volatile不保证原子性
* 1、原子性指的是什么意思?
*/
public class VolatileDemo {

public static void main(String args []) {

MyData myData = new MyData();

// 创建10个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}

// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
while(Thread.activeCount() > 2) {
// yield表示不执行
Thread.yield();
}

// 查看最终的值
// 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);

}
}

最终结果我们会发现,number输出的值并没有20000,而且是每次运行的结果都不一致的,这说明了volatile修饰的变量不保证原子性

不保证原子性(需要加载-可能加载后数据被修改了,操作,赋值刷新到主内存),但是保证数据的可见性,多线程下会导致脏读,但是最后获取到的数据还能是最新的,虽然不一定是正确的。

所以使用场景中说:写入变量值不依赖、变量的当前值时。 就不能用比如 num++这种,会有脏读。

1
2
3
4
5
private volatile boolean bool = true;

while (bool) {
System.out.println(Thread.currentThread().getName() + "printMethod-ing");
}

指令重排

1
2
3
4
5
6
public void mySort() {
int x = 11;
int y = 12;
x = x + 5;
y = x * x;
}

按照正常单线程环境,执行顺序是 1 2 3 4

但是在多线程环境下,可能出现以下的顺序:

  • 2 1 3 4
  • 1 3 2 4

上述的过程就可以当做是指令的重排,即内部执行顺序,和我们的代码顺序不一样

但是指令重排也是有限制的,即不会出现下面的顺序

  • 4 3 2 1

因为处理器在进行重排时候,必须考虑到指令之间的数据依赖性

因为步骤 4:需要依赖于 y的声明,以及x的声明,故因为存在数据依赖,无法首先执行

这样造成的结果,和最开始的就不一致了,这就是导致重排后,结果和最开始的不一样,因此为了防止这种结果出现,volatile就规定禁止指令重排,为了保证数据的一致性

使用场景

那么一般在什么时候才使用volatile关键字呢?

  • 写入变量值不依赖、变量的当前值时。 因为如果依赖当前值,将是获取一计算一写入三步操作,这三步操作不是原子性的,而volatile 不保证原子性。 状态标记量,Java 中的双重检查(Double-Check)。
  • 读写变量值时没有加锁。 因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile 的。

并发组件ConcurrentHashMap使用注意事项

这里借用直播的一个场景,在直播业务中,每个直播间对应一个topic,每个用户进入直播间H才会把自己设备的ID绑定到这个topic上, 也就是一个topic对应一堆用户设备。可以使用map来维护这些信息, 其中key为topic, value为设备的list。下面使用代码来模拟多用户同时进入直播间时map信息的维护。

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
public class ConcurrentHashMapTest {
// (1) 创建map,key为topic,value为设备列表
static ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>();

public static void main(String[] args) {
// (2) 进入直播间topic1,线程1
Thread threadOne = new Thread(() -> {
List<String> list1 = new ArrayList<>();
list1.add("device1");
list1.add("device2");

map.put("topic1", list1);
System.out.println(Thread.currentThread().getName() + ":\t" + map.toString());
});

Thread threadTwo= new Thread(() -> {
List<String> list2 = new ArrayList<>();
list2.add("device11");
list2.add("device22");

map.put("topic1", list2);
System.out.println(Thread.currentThread().getName() + ":\t" + map.toString());
});

// (2) 进入直播间topic2,线程3
Thread threadThree = new Thread(() -> {
List<String> list3 = new ArrayList<>();
list3.add("device111");
list3.add("device222");

map.put("topic2", list3);
System.out.println(Thread.currentThread().getName() + ":\t" + map.toString());
});

threadOne.start();
threadTwo.start();
threadThree.start();
}
}

输出结果:

1
2
3
Thread-0:	{topic1=[device1, device2]}
Thread-1: {topic1=[device11, device22]}
Thread-2: {topic1=[device11, device22], topic2=[device111, device222]}

可见,topic1房间中的用户会丢失一部分, 这是因为put 方法如果发现map里面存在这个key,则使用value覆盖该key对应的老的value值。 而putlfAbsent方法则是,如果发现己经存在该key则返回该key对应的value, 但并不进行覆盖,如果不存在则新增该key,并且判断和写入是原子性操作。使用putlfAbsent替代put方法后的代码如下。

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
public class ConcurrentHashMapOptimization {
// (1) 创建map,key为topic,value为设备列表
static ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>();

public static void main(String[] args) {
// (2) 进入直播间topic1,线程1
Thread threadOne = new Thread(() -> {
List<String> list1 = new ArrayList<>();
list1.add("device1");
list1.add("device2");

// 判断key是否存在和放入是原子性操作
// 添加新设备列表,如果topic1 在map中不存在,则将topic1 和对应设备列表放入map。
// 如果返回不为null,那就是此key下存在值,即我们需要调用addAll尾部添加进去
List<String> topic1 = map.putIfAbsent("topic1", list1);
if (topic1 != null) {
topic1.addAll(list1);
}
System.out.println(Thread.currentThread().getName() + ":\t" + map.toString());
});

Thread threadTwo= new Thread(() -> {
List<String> list2 = new ArrayList<>();
list2.add("device11");
list2.add("device22");

List<String> topic1 = map.putIfAbsent("topic1", list2);
if (topic1 != null) {
topic1.addAll(list2);
}
System.out.println(Thread.currentThread().getName() + ":\t" + map.toString());
});

// (2) 进入直播间topic2,线程3
Thread threadThree = new Thread(() -> {
List<String> list3 = new ArrayList<>();
list3.add("device111");
list3.add("device222");

List<String> topic2 = map.putIfAbsent("topic2", list3);
if (topic2 != null) {
topic2.addAll(list3);
}
System.out.println(Thread.currentThread().getName() + ":\t" + map.toString());
});

threadOne.start();
threadTwo.start();
threadThree.start();
}
}

输出结果:

1
2
3
Thread-2:	{topic1=[device1, device2], topic2=[device111, device222]}
Thread-0: {topic1=[device1, device2], topic2=[device111, device222]}
Thread-1: {topic1=[device1, device2, device11, device22], topic2=[device111, device222]}

总结: put(Kkey, V value) 方法判断如果key己经存在,则使用value覆盖原来的值并返回原来的值,如果不存在则把value放入并返回null。而putlfAbsent(Kkey, V value) 方法则是如果key己经存在则直接返回原来对应的值并不使用value覆盖,如果key不存在则放入value并返回null,另外要注意, 判断key是否存在和放入是原子性操作。

Java中的CAS操作

在Java中, 锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起, 这会导致线程上下文的切换和重新调度开销。 Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题, 这在一定程度上弥补了锁带来的开销问题,但是volatile 只能保证共享变量的可见性,不能解决读一改一写等的原子性问题。 CAS 即Compare and Swap,其是JDK提供的非阻塞原子性操作, 它通过硬件保证了比较 更新操作的原子性。 JDK里面的Unsafe类提供了一系列的 compareAndSwap*方法, 下面以compareAndSwapLong方法为例进行简单介绍。

boolean compareAndSwapLong(Object obj,long valueOffset,long expect, long update)方法: 其中compareAndSwap的意思是比较并交换。CAS有四个操作数, 分别为: 对象内存位置、 对象中的变量的偏移量、 变量预期值和新的值。 其操作含义是, 如果对象obj 中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。 这是处理器提供的一个原子性指令。

关于CAS操作有个经典的ABA问题, 具体如下:假如线程I使用CAS修改初始值为A的变量X, 那么线程I会首先去获取当前变量X的值(为A), 然后使用CAS操作尝试修改X的值为B, 如果使用CAS操作成功了, 那么程序运行一定是正确的吗?

其实未必,这是因为有可能在线程I获取变量X的值A后,在执行CAS前,线程II使用CAS修改了变量X的值为B,然后又使用CAS修改了变量X的值为A。 所以虽然线程I执行CAS时X的值是A, 但是这个A己经不是线程I获取时的A了。 这就是ABA问题

ABA问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从A到B,然后再从B到A。如果变量的值只能朝着一个方向转换,比如A到B, B到C, 不构成环形,就不会存在问题。JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳, 从而避免了ABA问题的产生。

Unsafe类

Unsafe类中的重要方法

JDK的此jar包中的Unsafe类提供了硬件级别的原子性操作, Unsafe类中的方法都是native方法,它们使用 JNI 的方式访问本地 C++ 实现库。 下面我们来了解一下Unsafe提供的几个主要的方法以及编程时如何使用Unsafe类做一些事情。

long objectFieldOffset(Field field)方法: 返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时使用。 如下代码使用Unsafe类获取变量value在AtomicLong对象中的内存偏移。

1
2
3
4
5
6
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

int arrayBaseOffset(Class arrayClass) 方法:获取数组中第一个元素的地址。


int arrayIndexScale(Class arrayClass)方法: 获取数组中一个元素占用的字节。


boolean compareAndSwapLong(Object obj, long offset, long expect, long update)方法: 比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。


public native long getLongvolatile(Object obj,long offset)方法: 获取对象obj中偏移量为offset的变量对应volatile 语义的值。


void putLongvolatile(Object obj,long offset,long value)方法: 设置obj对象中offset偏移的类型为long 的 field的值为 value,支持volatile语义。


void putOrderedLong(Object obj, long offset, long value)方法: 设置obj对象中offset偏移地址对应的long型 field 的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。


void park(boolean isAbsolute, long time)方法: 阻塞当前线程,其中参数isAbsolute等于false 且 time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒,这个time是个相对值,是个增量值,也就是相对当前时间累加time后当前线程就会被唤醒。如果 isAbsolute等于true,并且time大于0,则表示阻塞的线程到指定的时间点后会被唤醒,这里time是个绝对时间,是将某个时间点换算为ms后的值。 另外,当其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时, 当前线程也会返回, 而当其他线程调用了unPark方法并且把当前线程作为参数时当前线程也会返回。


void unpark(Object thread) 方法: 唤醒调用park后阻塞的线程。下面是JDK8新增的函数, 这里只列出Long类型操作。


long getAndSetLong(Object obj, long offset, long update) 方法: 获取对象obj 中偏移量为offset的变量volatilei吾义的当前值, 并设置变量volatilei吾义的值为update。

1
2
3
4
5
6
7
8
public final long getAndSetLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var4));

return var6;
}

由以上代码可知, 首先(1) 处的getLongvolatile获取当前变量的值, 然后使用CAS原子操作设置新值。这里使用while循环是考虑到,在多个线程同时调用的情况下CAS失败时需要重试。


long getAndAddLong(Object obj, long offset, long addValue) 方法: 获取对象。同中偏移量为offset的变量volatile语义的当前值, 并设置变量值为原始值+addValue

1
2
3
4
5
6
7
8
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

return var6;
}

如何使用Unsafe类

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
public class TestUnSafe {

// 获取Unsafe的实例 (2.2.1)
// static final Unsafe unsafe = Unsafe.getUnsafe();
static final Unsafe unsafe;

// 记录变量state在类TestUnSafe中的偏移值 (2.2.2)
static final long stateOffset;

// 变量(2.2.3)
private volatile long state = 0;

static {
try {
// 获取state变量在类TestUnsafe中的偏移量(2.2.4):不是由Bootstrap类加载器加载,而是AppClassLoader
// java.lang.ExceptionInInitializerError Caused by: java.lang.SecurityException: Unsafe
// stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));

// 使用反射获取Unsafe的成员变量theUnsafe
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 设置为可存取
field.setAccessible(true);
// 获取该变量的值
unsafe = (Unsafe) field.get(null);
// 获取state在TestUnSafe中的偏移量
stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
} catch(Exception e) {
System.out.println(e.getLocalizedMessage());
throw new Error(e);
}
}

public static void main(String[] args) {
// 创建实例,并且设置state值为1(2.2. 5)
TestUnSafe test = new TestUnSafe();
// (2.2.6)
boolean b = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(b);
}

}

运行后输出结果如下

1
2
Connected to the target VM, address: '127.0.0.1:50398', transport: 'socket'
true

Java指令重排序

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
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Main {
private static int num = 0;

private static boolean ready = false;

public static void main(String[] args) {
try {
WriteThread writeThread = new WriteThread();
writeThread.start();

ReadThread readThread = new ReadThread();
readThread.start();

Thread.sleep(1000);
readThread.interrupt();
} catch(Exception e) {
e.printStackTrace();
}
}

static class WriteThread extends Thread {
@Override
public void run() {
num = 2; // (3)
ready = true; // (4)
System.out.println("WriteThread set over...");
}
}

static class ReadThread extends Thread {
@Override
public void run() {
if (!Thread.currentThread().isInterrupted()) {
if (ready) {
System.out.println(num + num);
}
}
System.out.println("ReadThread read over...");
}
}
}

首先这段代码里面的变量没有被声明为volatile 的,也没有使用任何同步措施, 所以在多线程下存在共享变量内存可见性问题。 这里先不谈内存可见性问题,因为通过把变量声明为volatile 的本身就可以避免指令重排序问题。

这里先看看指令重排序会造成什么影响,如上代码在不考虑、内存可见性问题的情况下一定会输出4? 答案是不一定,由于代码(1) (2) (3) (4)之间不存在依赖关系, 所以写线程的代码(3) (4)可能被重排序为先执行(4)再执行(3),那么执行(4)后, 读线程可能已经执行了(1)操作, 并且在(3)执行前开始执行(2)操作, 这时候输出结果为 0。而不是4。

重排序在多线程下会导致非预期的程序执行结果,而使用volatile 修饰ready就可以避免重排序和内存可见性问题

写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。 读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。

伪共享

介绍

为了解决计算机系统中主内存与CPU之间运行速度差问题,会在CPU与主内存之间添加一级或者多级高速缓冲存储器(Cache)。这个Cache一般是被集成到CPU内部的,所以也叫CPU Cache,图2-6所示是两级Cache结构。

在Cache内部是按行存储的,其中每一行称为一个Cache行。Cache行(如图2-7所示)是Cache与主内存进行数据交换的单位,Cache行的大小一般为 2 的幕次数字节。

当CPU访问某个变量时,首先会去看CPU Cache内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个Cache行大小的内存复制到Cache中。 由于存放到Cache行的是内存块而不是单个变量,所以可能会把多个变量存放到一个Cache行中。 当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享,如图2-8所示。

在该图中,变量x和y同时被放到了CPU的一级和二级缓存, 当线程1使用CPU1对变量x进行更新时,首先会修改CPU1 的一级缓存变量x所在的缓存行,这时候在缓存一致性协议下,CPU2中变量x对应的缓存行失效。那么线程2在写入变量x时就只能去二级缓存里查找,这就破坏了一级缓存。而一级缓存比二级缓存更快,这也说明了多个线程不可能同时去修改自己所使用的CPU中相同缓存行里面的变量。更坏的情况是,如果CPU只有一级缓存,则会导致频繁地访问主内存。

自我理解:缓存一致性协议应该不是及时可见性的,可能存在延时通知,不然需要volatile干嘛,也就不会存在那种所谓的脏读不保证原子性。

为何会出现伪共享

伪共享的产生是因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中不同的变量。那么为何多个变量会被放入一个缓存行呢?其实是因为缓存与内存交换数据的单位就是缓存行, 当CPU要访问的变量没有在缓存中找到时,根据程序运行的局部性原理, 会把该变量所在内存中大小为缓存行的内存放入缓存行。

1
2
3
4
long a; 
long b;
long c;
long d;

如上代码声明了四个long变量,假设缓存行的大小为32字节, 那么当CPU访问变量a时, 发现该变量没有在缓存中,就会去主内存把变量a以及内存地址附近的b、 c、 d放入缓存行。也就是地址连续的多个变量才有可能会被放到一个缓存行中。当创建数组时,数组里面的多个元素就会被放入同一个缓存行。那么在单线程下多个变量被放入同一个缓存行对性能有影响吗?其实在正常情况下单线程访问时将数组元素放入一个或者多个缓存行对代码执行是有利的,因为数据都在缓存中,代码执行会更快。

如何避免伪共享

在JDK8之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中,

例如如下代码。

1
2
3
4
public final static class FilledLong {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6;
}

假如缓存行为64字节,那么我们在FilledLong类里面填充了6个long类型的变量, 每个long类型变量占用8字节,加上value变量的8字节总共56字节。另外, 这里FilledLong是一个类对象,而类对象的字节码的对象头占用8字节,所以一个FilledLong对象实际会占用64字节的内存,这正好可以放入一个缓存行。

JDK 8提供了一个sun.misc. Contended注解,用来解决伪共享问题。将上面代码修改为如下。

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Contended {
String value() default "";
}

在这里注解用来修饰类,当然也可以修饰变量,比如在Thread类中。

1
2
3
4
5
6
7
8
9
10
11
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;

/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;

Thread类里面这三个变量默认被初始化为0,这三个变量会在ThreadLocalRandom类中使用。

需要注意的是,在默认情况下,@Contended注解只用于Java核心类,比如此包下的类。如果用户类路径下的类需要使用这个注解,则需要添加 JVM 参数:-XX:-RestrictContended。填充的宽度默认为128,要自定义宽度则可以设置-XX:ContendedPaddingWidth参数。

线程间的通信

等待/通知机制的实现

wait()方法:作用是使当前执行代码的线程进行等待,wait() 方法是Object类的方法,该方法用来将当前线程置入“预执行队列”中,并且在wait()所在的代码行处停止执行,直到接到通知或被中断为止。在调用wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。在执行wait()方法后,当前线程释放锁。在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放而此线程对象会进入线程等待池中,等待被唤醒。注意当线程呈 wait() 状态时,调用线程对象的 interrupt() 方法会出现InterruptedException 异常。

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

方法notify():也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify()时没有持有适当的锁,也会抛出legalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈 wait状态的线程,对其发出通知notify,并使它等待获取该对象的对象锁。需要说明的是,在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁

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

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

用一句话来总结一下wait和notify:wait使线程停止运行,而notify使停止的线程继续运行。

还有需要注意的是:方法wait会释放锁,方法notify不释放锁,而是必须要等待所在的同步方法synchronized代码块后才释放锁

线程状态

1 )新创建一个新的线程对象后,再调用它的start()方法,系统会为此线程分配CPU资源,使其处于Runnable (可运行)状态,这是一个准备运行的阶段。如果线程抢占到CPU资源,此线程就处于Running(运行)状态

2 ) Runnable状态和Running状态可相互切换,因为有可能线程运行一段时间后, 有其他高优先级的线程抢占了CPU资源,这时此线程就从Running状态变成Runnable状态。

线程进人Runnable状态大体分为如下5种情况:

  • 调用sleep()方法后经过的时间超过了指定的休眠时间。
  • 线程调用的阻塞IO已经返回,阻塞方法执行完毕。
  • 线程成功地获得了试图同步的监视器。
  • 线程正在等待某个通知,其他线程发出了通知。
  • 处于挂起状态的线程调用了resume 恢复方法。

3) Blocked 是阻塞的意思,例如遇到了一个IO操作,此时CPU处于空闲状态,可能会转而把CPU时间片分配给其他线程,这时也可以称为“暂停”状态。Blocked 状态结束后,进人Runnable状态,等待系统重新分配资源。

出现阻塞的情况大体分为如下5种:

  • 线程调用sleep方法,主动放弃占用的处理器资源。
  • 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
  • 线程等待某个通知。
  • 程序调用了suspend方法将该线程挂起。此方法容易导致死锁,尽量避免使用该方法。

4) run() 方法运行结束后进人销毁阶段,整个线程执行完毕。

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

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

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
35
36
37
class WriteData {
public void writeMethod(PipedWriter out) {
try {
System.out.println("write:");
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < 300; i++) {
String outData = UUID.randomUUID().toString().replace("-", "") + (i + 1);
stringBuffer.append(outData);
out.write(outData);
}
System.err.println(stringBuffer);
System.out.println();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

class ReadData {
public void readMethod(PipedReader input) {
try {
System.out.println("read:");
char[] byteArray = new char[20];
int read = input.read(byteArray);
while (read != -1) {
String newData = new String(byteArray, 0, read);
System.out.println("newData:" + newData);
read = input.read(byteArray);
}
System.out.println();
input.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

2、构造两个线程

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
public class WriteThread implements Runnable{

private PipedWriter writer;
private WriteData writeData;

public WriteThread(WriteData writeData, PipedWriter writer) {
this.writeData = writeData;
this.writer = writer;
}

@Override
public void run() {
writeData.writeMethod(writer);
}
}

public class ReadThread extends Thread{

private PipedReader reader;
private ReadData readData;

public ReadThread(ReadData readData, PipedReader reader) {
this.readData = readData;
this.reader = reader;
}

@Override
public void run() {
readData.readMethod(reader);
}
}

3、测试通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws InterruptedException, IOException {
PipedReader reader = new PipedReader();
PipedWriter out = new PipedWriter();
// out.connect(reader); // 必须关联连接
reader.connect(out);

WriteData writeData = new WriteData();
ReadData readData = new ReadData();

ReadThread readThread = new ReadThread(readData, reader);
readThread.start();

Thread.sleep(1000);

WriteThread writeThread = new WriteThread(writeData, out);
writeThread.run();
}

4、输出打印

1
2
3
4
5
6
7
8
9
10
read:
write:
newData:f5602610c6c54cf3a9ae
newData:b9bec2d451031f1b0a7a
newData:52417418d86a72526af3
newData:33ee62b5a25a34af8a49
newData:94806c61faaa3597ac3b
newData:3f4698414d246db8904e
newData:cd709c73412458cc2636
...打印内容同上文 stringBuffer 的打印

5、字节流通信也基本一样,使用:

1
2
3
PipedInputStream inputStream = new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream();
inputStream.connect(outputStream);

6、记得关流,不然会报错:Write end dead

等待/通知之交叉备份

创建两个线程,并调用打印方法

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 ThreadA extends Thread{

private PrintClass a;

public ThreadA(PrintClass a) {
this.a = a;
}

@Override
public void run() {
try {
a.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public class ThreadB extends Thread{

private PrintClass a;

public ThreadB(PrintClass a) {
this.a = a;
}

@Override
public void run() {
try {
a.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

创建输出方法

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 PrintClass {

private volatile boolean bool = false;

public synchronized void printA() throws InterruptedException {
while (bool) {
this.wait();
}
for (int i = 0; i < 5; i++) {
System.out.println("AAAAA");
}
this.bool = true;
this.notifyAll();
}

public synchronized void printB() throws InterruptedException {
while (!bool) {
this.wait();
}
for (int i = 0; i < 5; i++) {
System.out.println("BBBBB");
}
this.bool = false;
this.notifyAll();
}
}

调用,循环打印

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
PrintClass printClass = new PrintClass();

for (int i = 0; i < 5; i++) {
ThreadA threadA = new ThreadA(printClass);
ThreadB threadB = new ThreadB(printClass);

threadA.start();
threadB.start();
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
AAAAA
AAAAA
AAAAA
AAAAA
AAAAA
BBBBB
BBBBB
BBBBB
BBBBB
BBBBB
AAAAA
.....

方法join

1、基本概念

在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。

再具体点就是:使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。方法join具有使线程排队运行的作用,有些类似同步的运行效果。

join与synchronized的区别是: join在内部使用wait()方法进行等待,而sychronized关键字使用的是“对象监视器”原理做为同步。

方法join()与interrupt()方法如果彼此遇到,则会出现异常,不过有其他继续运行的线程就不会退出程序。

2、源代码

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 final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
// 线程是活的为true
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

方法join(long)和sleep(long)的区别:

从源代码中可以了解到join方法内部是使用 wait(long) 方法来实现的,所以具备释放锁的特点。当执行 wait(long) 方法后,当前线程的锁被释放,那么其他线程就可以调用此线程中的同步方法了。而Thread.sleep(long)方法却不释放锁

类ThreadLocal

使用示例

提供 get和 set等接口或方法,这些方法为每一个使用这个变量的线程都存有一份独立的副本,因此 get总是返回由当前线程在调用 set时设置的最新值。

1、验证线程变量的隔离性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*工具类*/
public class Tool {

/**
* 子线程和父线程保留不同的变量副本
*/
public static ThreadLocal threadLocal = new ThreadLocalSon();

{
System.out.println("代码块:new 实例后,在构造方法前加载");
}

public Tool() {
System.out.println("Tool对象创建了");
}

static {
System.out.println("静态代码块");
}
}
1
2
3
4
5
6
7
8
9
/*解决get()方法获取初始化值为 null问题*/
public class ThreadLocalSon extends ThreadLocal {
/*默认初始值为null*/
@Override
protected Object initialValue() {
return System.currentTimeMillis();
}

}
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
/*创建线程*/
public class ThreadA extends Thread{

@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("ThreadA 线程中取值:" + Tool.threadLocal.get());
Thread.sleep(1000);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

public class ThreadB extends Thread{

@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("ThreadB 线程中取值:" + Tool.threadLocal.get());
Thread.sleep(1000);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

运行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
try {
for (int i = 0; i < 5; i++) {
System.out.println("在Main线程中取值 =" + Tool.threadLocal.get());
Thread.sleep(1000);
}
ThreadA a = new ThreadA();
a.start();
ThreadB a = new ThreadB();
b.start();
} catch(Exception e) {
e.printStackTrace();
}
}
}

由输出打印可见,每个线程变量间都有隔离性

1
2
3
4
5
6
在Main线程中取值 =1616033755782
在Main线程中取值 =1616033755782
ThreadA 线程中取值:1616033760835
ThreadB 线程中取值:1616033760836
ThreadA 线程中取值:1616033760835
ThreadB 线程中取值:1616033760836

实现原理

首先看一下 ThreadLocal 相关类的类图结构,如图1-5 所示。

由该图可知,Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal 的set或者get方法时才会创建它们。

其实每个线程的本地变量不是存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal 就是一个工具壳,它通过 set方法把 value值放入调用线程的 threadLocals 里面并存放起来,当调用线程调用它的get方法时,再从当前线程的 threadLocals 变量里面将其拿出来使用。

如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。

另外,Thread里面的threadLocals为何被设计为map结构?很明显是因为每个线程可以关联多个ThreadLocal变量

下面简单分析ThreadLocal 的 set、get及 remove方法的实现逻辑。

1、void set(T value)

1
2
3
4
5
6
7
8
9
10
11
12
13
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 将当前线程作为key,去查找对应的线程变量,找到则设置
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 第一次调用就创建当前线程对应的HashMap
createMap(t, value);
}

ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到,getMap(t)的作用是获取线程自己的变量threadLocals, threadlocals变量被绑定到了线程的成员变量上。

如果getMap(t)的返回值不为空,则把 value值设置到 threadLocals 中,也就是把当前变量值放入当前线程的内存变量 threadLocals 中。threadLocals是一个HashMap结构,其中key就是当前ThreadLocal的实例对象引用value是通过set方法传递的值

如果getMap(t)返回空值则说明是第一次调用set方法,这时创建当前线程的threadLocals变量。下面来看createMap(t, value)做什么。

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

它创建了当前线程的 threadLocals 变量。

2、T get()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public T get() {
// (4) 获取当前线程
Thread t = Thread.currentThread();
// (5) 获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
// (6) 如果threadLocals不为null,则返回对应本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 返回此线程局部变量的当前ThreadLocal中的值。如果threadLocals为空则初始化当前线程的threadLocals成员变量
return setInitialValue();
}

代码(4)首先获取当前线程实例,如果当前线程的threadLocals变量不为null,则直接返回当前线程绑定的本地变量,否则执行代码(7)进行初始化。setInitialValue()的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private T setInitialValue() {
// (8)初始化为null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// (9)如果当前线程的threadLocals变量不为空
if (map != null)
map.set(this, value);
else
// (10)如果当前线程的threadLocals变量为空
createMap(t, value);
return value;
}

protected T initialValue() {
return null;
}

如果当前线程的threadLocals变量不为空,则设置当前线程的本地变量值为null,否则调用createMap方法创建当前线程的createMap变量。

1
2
3
4
5
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

如以上代码所示,如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal 实例的本地变量。

总结: 如图1-6所示,在每个线程内部都有一个名为threadLocals 的成员变量,该变量的类型为HashMap,其中 key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals 中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。在后续要讲解的JUC包里面的ThreadLocalRandom,就是借鉴ThreadLocal 的思想实现的,后面会具体讲解。

ThreadLocal不支持继承性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestThreadLocal {

private static final ThreadLocal<String> local = new ThreadLocal<>();

public static void main(String[] args) {
local.set("Hello World!");

new Thread(() -> {
System.out.println(local.get());
}).start();

System.out.println("main: " + local.get());
}
}

输出:

1
2
main: Hello World!
null

也就是说,同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。根据上节的介绍,这应该是正常现象,因为在子线程 thread 里面调用get方法时当前线程为 thread 线程,而这里调用set方法设置线程变量的是main线程,两者是不同的线程,自然子线程访问时返回null。那么有没有办法让子线程能访问到父线程中的值?答案是有。

类InheritableThreadLocal

实现原理

InheritableThreadLocal 继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。 下面看一下InheritableThreadLocal 的代码。

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 InheritableThreadLocal<T> extends ThreadLocal<T> {
/** (1)
* 在启动子线程之前,从父线程中调用此方法。
*/
protected T childValue(T parentValue) {
return parentValue;
}

/** (2)
* Get the map associated with a ThreadLocal.
*/
ThreadLocalMap getMap(Thread t) {
// ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
return t.inheritableThreadLocals;
}

/** (3)
* Create the map associated with a ThreadLocal.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

由如上代码可知,InheritableThreadLocal 继承了 ThreadLocal,并重写了三个方法。由代码(3)可知,InheritableThreadLocal重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的 inheritableThreadLocals 变量的实例而不再是threadLocals。由代码(2)可知,当调用get方法获取当前线程内部的map变量时,获取的是 inheritableThreadLocals 而不再是 threadLocals。

综上可知,在 InheritableThreadLocal 的世界里,变量inheritableThreadLocals替代了threadLocals。

下面我们看一下重写的代码(1)何时执行,以及如何让子线程可以访问父线程的本地变量。这要从创建Thread的代码说起,打开Thread类的默认构造函数,代码如下。

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 Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
this.name = name;
// (4)获取当前线程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
...
if (g == null) {
/* If there is a security manager, ask the security manager what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
// (5)如果父线程的inheritThreadLocals变量不为null, Thread parent = currentThread()
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// (6)设置子线程中的inheritThreadLocals变量
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;

/* Set thread ID */
tid = nextThreadID();
}

如上代码在创建线程时,在构造函数里面会调用init 方法。代码(4)获取了当前线程( 这里是指main函数所在的线程,也就是父线程 ), 然后代码(5)判断main函数所在线程里面的inheritableThreadLocals属性是否为null, 前面我们讲了InheritableThreadLocal 类的get和set方法操作的是inheritableThreadLocals,所以这里的inheritableThreadLocal变量不为null, 因此会执行代码(6)。 下面看一下createInheritedMap的代码。

1
2
3
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}

可以看到,在 createInheritedMap 内部使用父线程的 inheritableThreadLocals 变量作为构造函数创建了一个新的ThreadLocalMap变量, 然后赋值给了子线程的 inheritableThreadLocals 变量。 下面我们看看在ThreadLocalMap的构造函数内部都做了什么事情。

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
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];

for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// (7)调用重写的方法,返回 parentValue
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}

protected T childValue(T parentValue) {
return parentValue;
}

在该构造函数内部把父线程的 inheritableThreadLocals 成员变量的值复制到新的ThreadLocalMap对象中,其中 代码(7)调用了InheritableThreadLocals 类重写的代码(1 )。

总结:InheritableThreadLocal 类通过重写代码。(2)getMap(Thread t) 和(3)createMap(Thread t, T firstValue)让本地变量保存到了具体线程的 inheritableThreadLocals 变量里面,那么线程在通过 InheritableThreadLocal 类实例的set或者get方法设置变量时,就会创建当前线程的 inheritableThreadLocals 变量。当父线程创建子线程时,构造函数会把父线程中 inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals 变量里面。

使用示例

使用类 InheritableThreadLocal 可以在子线程中取得父线程继承下来的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Tool {

/**
* 子线程可以拿到父线程的值
*/
public static ThreadLocal threadLocal = new InheritableThreadLocalSon();
}

public class InheritableThreadLocalSon extends InheritableThreadLocal{

/*默认初始值为null*/
@Override
protected Object initialValue() {
return UUID.randomUUID().toString().replace("-", "");
}
}

运行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
try {
for (int i = 0; i < 5; i++) {
System.out.println("在Main线程中取值 =" + Tool.threadLocal.get());
Thread.sleep(1000);
}
ThreadA a = new ThreadA();
a.start();
Tool.threadLocal.set("main线程修改ThreadB线程的值");
ThreadB b = new ThreadB();
b.start();
} catch(Exception e) {
e.printStackTrace();
}
}
}

由打印可见,如果子线程在取得值的同时,主线程将 InheritableThreadLocal 中的值进行修改,那么后面子线程取到的值也会相应改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
静态代码块
在Main线程中取值 =cd6ff6b35d6349d6bc75723fc12d0744
在Main线程中取值 =cd6ff6b35d6349d6bc75723fc12d0744
在Main线程中取值 =cd6ff6b35d6349d6bc75723fc12d0744
在Main线程中取值 =cd6ff6b35d6349d6bc75723fc12d0744
在Main线程中取值 =cd6ff6b35d6349d6bc75723fc12d0744
ThreadA 线程中取值:cd6ff6b35d6349d6bc75723fc12d0744
ThreadB 线程中取值:main线程修改ThreadB线程的值
main线程修改值后:main线程修改ThreadB线程的值
ThreadA 线程中取值:cd6ff6b35d6349d6bc75723fc12d0744
ThreadB 线程中取值:main线程修改ThreadB线程的值
ThreadB 线程中取值:main线程修改ThreadB线程的值
ThreadA 线程中取值:cd6ff6b35d6349d6bc75723fc12d0744
ThreadA 线程中取值:cd6ff6b35d6349d6bc75723fc12d0744
ThreadB 线程中取值:main线程修改ThreadB线程的值
...

定时器Timer

1、基本介绍

在 JDK 库中 Timer 类主要负责计划人物的功能,也就是在指定的时间开始执行某一个任务。

1
2
3
4
5
6
7
8
9
private final TaskQueue queue = new TaskQueue();

private final TimerThread thread = new TimerThread(queue);

public Timer(String name, boolean isDaemon) {
thread.setName(name);
thread.setDaemon(isDaemon);
thread.start();
}

TimerTask 是以队列的方式一个个被顺序执行的,所以执行的时间有可能和预期的时间不一致,因为前面的任务有可能消耗的时间较长,则后面的任务运行的时间也会被延迟。

2、定时调用

1
2
3
4
5
6
public class MyTask extends TimerTask {
@Override
public void run() {
System.out.println("运行了,时间为:" + new Date());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
// 推荐:ScheduledExecutorService,false代表不开启守护线程
private static Timer timer = new Timer(false);

public static void main(String[] args) throws ParseException {
MyTask myTask = new MyTask();

String str = "2021-03-18 15:09:30";
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = formatter.parse(str);
timer.schedule(myTask, date);
}
}

运行后线程会一直运行,直到Timer调用后

1
2
3
运行了,时间为:Thu Mar 18 15:09:30 CST 2021

Process finished with exit code -1

不过倘若开启守护线程后,程序运行后会迅速结束当前的进程,并且 TimerTask 中的任务也不再被运行,因为进程已经结束了。

注意:当一个Timer运行多个TimerTask时,只要其中一个TimerTask在执行中向run方法外抛出了异常,则其他任务也会自动终止。

线程组

可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程。这样的组织结构有些类似于树的形式,

如图7-8所示。线程组的作用是,可以批量的管理线程或线程组对象,有效地对线程或线程组对象进行组织。

线程对象关联线程组:1级关联

所谓的1级关联就是父对象中有子对象,但并不创建子孙对象。这种情况经常出现在开发中,比如创建一些线程时, 为了有效地对这些线程进行组织管理,通常的情况下是创建一个线程组,然后再将部分线程归属到该组中。这样的处理可以对零散的线程对象进行有效的组织与规划。

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
public class ThreadA extends Thread{

@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ":ThreadA");
Thread.sleep(1000);
} catch(Exception e) {
e.printStackTrace();
}
}
}

public class ThreadB extends Thread{

@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ":ThreadB");
Thread.sleep(1000);
} catch(Exception e) {
e.printStackTrace();
}
}
}

2、案例测试,加入线程组中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static void main(String[] args) {
ThreadA a = new ThreadA();
ThreadB b = new ThreadB();
// 创建线程组
ThreadGroup group = new ThreadGroup("Lauy的线程组");

Thread threadA = new Thread(group, a, "A线程");
Thread threadB = new Thread(group, b, "B线程");
threadA.start();
threadB.start();
System.out.println("活动名称的线程数:" + group.activeCount());
System.out.println("线程组的名称:" + group.getName());
}
}

3、结果输出

1
2
3
4
活动名称的线程数:2
线程组的名称:Lauy的线程组
A线程:ThreadA
B线程:ThreadB

线程对象关联线程组:多级关联

所谓的多级关联就是父对象中有子对象,子对象中再创建子对象,也就是出现子孙对象的效果了。但是此种写法在开发中不太常见,如果线程树结构设计得非常复杂反而不利于线程对象的管理,但JDK却提供了支持多级关联的线程树结构。

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 Main {
public static void main(String[] args) {
// 系统环境的一个快照
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
// 在main组中添加一个线程组 A
ThreadGroup group = new ThreadGroup(mainGroup, "A");
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("runMethod!");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
};
// 然后在 A组中添加 线程组,这个就是main组的子孙线程对象
Thread newThread = new Thread(group, runnable);
newThread.setName("子孙线程");
newThread.start(); //线程必须启动然后才归到组A中

// 子对象组,方法 activeGroupCount() 取得当前线程组对象中的子线程组数量
ThreadGroup[] threadGroups = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
// 方法 enumerate() 的作用是将线程组中的子线程组以复制的形式拷贝到 ThreadGroup[]数组对象中
Thread.currentThread().getThreadGroup().enumerate(threadGroups);
System.out.println("main线程中有多少个子线程组:" + threadGroups.length + "\t 名字为:" + threadGroups[0].getName());

// 子孙对象组
Thread[] listThread = new Thread[threadGroups[0].activeCount()];
threadGroups[0].enumerate(listThread);
System.out.println(listThread[0].getName());
}
}

输出结果

1
2
3
main线程中有多少个子线程组:1	 名字为:A
子孙线程
runMethod!

线程组自动归属特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();

System.out.println("A处线程:" + mainThread.getName() + "\t 所属的线程组名为:" + mainThread.getThreadGroup().getName() +
"\t 中有线程组数量:" + mainThread.getThreadGroup().activeGroupCount());

new ThreadGroup("新的组,自动加入main组中");

System.out.println("B处线程:" + mainThread.getName() + "\t 所属的线程组名为:" + mainThread.getThreadGroup().getName() +
"\t 中有线程组数量:" + mainThread.getThreadGroup().activeGroupCount());

ThreadGroup[] threadGroups = new ThreadGroup[(mainThread.getThreadGroup().activeGroupCount())];
mainThread.getThreadGroup().enumerate(threadGroups);
for (int i = 0; i < threadGroups.length; i++) {
System.out.println(threadGroups[i].getName());
}
}
}

由输出可见,在实例化一个ThreadGroup线程组x时如果不指定所属的线程组,则x线程组自动归到当前线程对象所属的线程组中,也就是隐式地在一个线程组中添加了一个子线程组,所以在控制台中打印的线程组数量值由0变成了1。

1
2
3
A处线程:main	 所属的线程组名为:main	 中有线程组数量:0
B处线程:main 所属的线程组名为:main 中有线程组数量:1
新的组,自动加入main组中

获取根线程组

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
// java.lang.ThreadGroup[name=main,maxpri=10]
System.out.println("线程名:" + Thread.currentThread().getName() +
"\t 所在线程组名:" + Thread.currentThread().getThreadGroup());
// java.lang.ThreadGroup[name=system,maxpri=10]
System.out.println("main线程所在的线程组的父线程组的名称是:" + Thread.currentThread().getThreadGroup().getParent());
// java.lang.ThreadGroup[name=system,maxpri=10]
System.out.println("main线程所在的线程组的父线程组的父线程组的名称是:" + Thread.currentThread().getThreadGroup().getParent());
// null
System.out.println(Thread.currentThread().getThreadGroup().getParent().getParent());
}
}

运行结果说明 JVM 的 根线程 就是 system

组内的线程组批量停止

1、创建我的线程组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyThread extends Thread{

public MyThread(ThreadGroup group, String name) {
super(group, name);
}

@Override
public void run() {
System.out.println("线程组" + Thread.currentThread().getThreadGroup() + "内,线程" + Thread.currentThread().getName() + "开始死循环");

while (!this.isInterrupted()) {

}
System.out.println("ThreadName" + Thread.currentThread().getName() + "结束了");
}
}

2、创建线程组并添加线程,测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {
try {
ThreadGroup threadGroup = new ThreadGroup("我的线程组");
for (int i = 0; i < 5; i++) {
MyThread myThread = new MyThread(threadGroup, "第" + (i + 1) + "个");
myThread.start();
}
Thread.sleep(5000);
threadGroup.interrupt();
System.out.println("调用了 interrupt 方法");
} catch(Exception e) {
e.printStackTrace();
}
}
}

3、输出

1
2
3
4
5
6
7
8
9
10
11
12
13
线程组java.lang.ThreadGroup[name=我的线程组,maxpri=10]内,线程第1个开始死循环
线程组java.lang.ThreadGroup[name=我的线程组,maxpri=10]内,线程第4个开始死循环
线程组java.lang.ThreadGroup[name=我的线程组,maxpri=10]内,线程第2个开始死循环
线程组java.lang.ThreadGroup[name=我的线程组,maxpri=10]内,线程第3个开始死循环
线程组java.lang.ThreadGroup[name=我的线程组,maxpri=10]内,线程第5个开始死循环
调用了 interrupt 方法
ThreadName第1个结束了
ThreadName第3个结束了
ThreadName第4个结束了
ThreadName第2个结束了
ThreadName第5个结束了

Process finished with exit code 0
打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  1. © 2020-2021 Lauy    湘ICP备20003709号

请我喝杯咖啡吧~

支付宝
微信