JUC高并发编程-基础知识

JUC高并发编程

1.进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例,也有的程序只能启动一个实例进程。

线程

  • 一个进程之内可以分为一个到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
  • Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享。
  • 进程间通信比较复杂。
    • 同一台计算机的进程通信称为IPC
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。

并行与并发

并发(concurrent)

单核cpu下,线程实际还是串行执行的。操作系统中有一个组件叫任务调度器,将cpu的时间片分给不同的线程使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。总结一句话:微观串行,宏观并行。

一般会将这种线程轮流使用cpu的做法称为并发,concurrent。

并行(parallel)

多核cpu下,每个核(core)都可以调度运行线程,这时候线程可以是并行的。

例子

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做着多件事,这时就是并发。
  • 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)。
  • 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时就是并行。

应用

案例1:异步调用

从方法调用的角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能好继续运行就是异步

注意:同步在多线程中还有另外一层意思,是让多个线程步调一致。

  1. 设计

    多线程可以让方法执行变为异步的比如读取磁盘文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这5秒调用者什么都做不了,其代码都得暂停…

  2. 结论

    • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程。
    • tomcat的异步servlet也是类似的,让用户线程处理耗时较长的操作,避免阻塞tomcat的工作线程。
    • ui程序中,开线程进行其他操作,避免阻塞ui线程。

案例2:提高效率

充分利用多核cpu的优势,提高运行效率。想象下面的场景,执行3个计算,最后将计算结果汇总。

1
2
3
4
5
6
7
计算1 花费10ms

计算2 花费11ms

计算3 花费9ms

汇总需要1ms
  • 如果是串行执行,那么总共花费的时间是10ms+11ms+9ms+1ms=31ms

    • 如果是四核cpu,各个核心分别使用线程1执行计算1,线程2执行计算2,线程3执行计算3,那么3个线程是并行的,花费时间只取决于最长的那个线程运行的时间,即11ms最后加上汇总时间只会花费12ms。

    注意:需要在多核cpu才能提高效率,单核仍然是轮流执行。

    1. 设计

      案例1

    2. 结论

    • 单核cpu下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu,不至于一个线程总占用cpu,别的线程没法干活。
    • 多核cpu可以并行跑多个线程,但能否提高程序运行效率还是要分情况的。
    • IO操作不占用cpu,只是我们一般拷贝文件使用的是【阻塞IO】,这时相当于线程虽然不用cpu,但需要一直等待IO结束,没能充分利用线程。所有才有后面的【非阻塞IO】和【异步IO】优化。

2.Java线程

创建和运行线程

方法一:直接使用Thread

1
2
3
4
5
6
7
8
//创建线程对象
Thread t = new Thread(){
public void run(){
//要执行的任务
}
};
//启动线程
t.start();

例如:

1
2
3
4
5
6
7
8
9
//构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1"){
public void run(){
//要执行的任务
log.debug("hello");
}
};
//启动线程
t.start();

方法二:使用Runnable配合Thread

把【线程】和【任务】分开

  • Thread代表线程
  • Runnable代表可运行的任务(线程要执行的代码)
1
2
3
4
5
6
7
8
9
Runnable runnable = new Runnable(){
public void run(){
//要执行的任务
}
};
//创建线程对象
Thread t = new Thread(runnable);
//启动线程
t.start();

例如:

1
2
3
4
5
6
7
8
9
10
11
//创建任务对象
Runnable task2 = new Runnable(){
public void run(){
//要执行的任务
log.debug("hello");
}
};
//创建线程对象
Thread t = new Thread(task2,"t2");
//启动线程
t.start();

Java8以后可以使用lambda精简代码

1
2
3
4
5
//创建任务对象
Runnable task2 = ()->log.debug("hello");
//参数1是任务对象;参数2是线程名字,推荐
Thread t2 = new Thread(task2,"t2");
t2.start();

原理之Thread与Runnable的关系

分析Thread的源码,理清它与Runnable的关系

小结

  • 方法1是把线程和任务合并在了一起,方法2是把线程和任务分开了。
  • 用Runnable更容易与线程池等高级API结合。
  • 用Runnable让任务脱离了Thread继承体系,更灵活。

方法三:FutureTask配合Thread

FutureTask能够接收Callable;类型的参数,用来处理有返回结果的情况。

1
2
3
4
5
6
7
8
9
10
11
//创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(()->{
log.debug("hello");
return 100;
});

//参数1是任务对象;参数2是线程名字
new Thread(task3,"t3").start();
//主线程阻塞,同步等待task执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}",result);

观察多个线程同时运行

  • 交替执行
  • 谁先谁后,不由我们控制

查看进程线程的方法

Windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist查看进程
  • taskkill杀死进程

Linux

  • ps -ef查看所有进程
  • ps -fT -p查看某个进程(PID)的所有线程
  • kill杀死进程
  • top按大写H切换是否显示线程
  • top -H -p查看某个进程(PID)的所有线程

Java

  • jps命令查看所有Java进程
  • jstack查看某个Java进程(PID)的所有线程状态
  • jconsole来查看某个java进程中线程的运行情况(图形界面)

原理之线程运行

栈与栈帧

Java虚拟机栈

JVM中由堆、栈、方法区锁组成,其中栈内存是给谁用的?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占的内存
  • 每个线程只能有一个活动栈,对应着当前正在执行的那个方法

线程上下文切换

因为以下一些原因导致cpu不在执行当前的线程,转而执行另一个线程的代码

  • 线程的cpu时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法程序

当Context Switch发生时,需要操作系统保存当前线程的状态,并恢复另一个线程的状态,java中对应的概念是程序计数器,它的作用是记住下一条jvm指令的执行地址,是线程私有的。

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch频繁发生会影响性能

常见方法

img44

img48

start与run

public static void main(String[] args){
Thread t1 = new Thread(){
@Override
public void run(){
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
}
t1.run();
log.debug(“do other things……”);

sleep与yield

sleep

  1. 调用sleep会让当前线程从Running进入TimedWaiting状态。
  2. 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException。
  3. 睡眠结束后的线程未必会立刻得到执行。
  4. 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args){
Thread t1 = new Thread("t1"){
@Override
public void run(){
try{
Thread.sleep(2000);
}catch(InterruptedException){
e.printStackTrace();
}
}
};
t1.start();
log.debug("t1的状态:{}"+t1.getState());

try{
Thread.sleep(500);
}catch(InterruptException e){
e.printStackTrace();
}
log.debug("t1的状态:{}"+t1.getState());
}

yield

  1. 调用yield会让当前线程从Running进入Runnable状态,然后调度执行其它同优先级的进程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果。
  2. 具体的实现依赖于操作系统的任务调度器。
线程优先级
  • 线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。
  • 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用。

不使用yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(Stirng[] args){
Runnable task1 = ()->{
int count = 0;
for(;;){
System.out.println("----->1" + count++);
}
};
Runnable task2 = ()->{
int count = 0;
for(;;){
Thread.yield();
System.out.prinln("------>2" + count++);
}
};
Thread t1 = new Thread(task1,"t1");
Thread t2 = new Thread(task2,"t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
案例-防止CPU占用100%

sleep实现

在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或sleep来让出cpu的使用权给其它程序。

1
2
3
4
5
6
7
while(true){
try{
Thread.sleep(50);
}catch(InterruptedExcetion e){
e.printStackTrace();
}
}
  • 可以用wait或条件变量达到类似的结果
  • 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般使用于要进行同步的场景
  • sleep适用于无需锁同步的场景

join方法详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int r = 0;
public static void main(String[] args){
test1();
}
private static final test1() throws InterruptedException{
log.debug("开始");
Thread t1 = new Thread({
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join();
log.debug("结果为:{}",r);
log.debug("结束");
}
1
2
3
4
5
[main]-开始
[t1]-开始
[main]-结果为:0
[main]-结束
[t1]-结束

分析:

  • 因为主线程和线程t1是并行执行的,t1线程需要1秒后才能计算出r=10
  • 而主线程一开始就要打印r的结果,所以只能打印出r=0

join方法加在start方法之后即可。

应用之同步(案例1)

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

img45

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
static int r1 = 0;
static int r2 = 0;

public static void main(String[] args) throws InterruptedException{
test2();
}

public static void test3() throws InterruptedException{}

private static void test2() throws InterruptedException{
Thread t1 = new Thread(()->{
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(()->{
sleep(2);
r1 = 20;
});
long start = System.currentTimeMills();
t1.start();
t2.start();
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
long end = System.currentTimeMills();
log.debug("r1:{} r2:{} cost:{}",r1,r2,end-start);
}
private static void test1() throws InterruptedException{}

输出:

1
2
3
4
5
//因为线程是同步的,01开始t1和t2同时启动
01--join begin
02--t1 join end
03--t2 join end
04--r1::10 r2:20 cost:2002

img46

有时效的join

等够时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args){
test3();
}
public static void test3() throws InterruptedException{
Thread t1 = new Thread(()->{
sleep(1);
r1 = 10;
});
long start = System.currentTimeMills();
t1.start();
//线程执行结束会导致join结束
long end = System.currentTimeMills();
log.debug("r1:{} r2:{} cost:{}",r1,r2,end-start);
}

输出:

1
r1:10 r2:0 cost:1010

interrupt方法详解

打断sleep,wait,join的线程

阻塞

打断sleep的线程,会清空打断状态,以sleep为例

1
2
3
4
5
6
7
8
9
10
private static void test1() throws InterruptedException{
Thread t1 = new Thread(()->{
sleep(1);
},"t1");
t1.start();

sleep(0.5);
t1.interrupt();
log.debug("打断状态:{}",t1.isInterrupted());
}
设计模式之两阶段终止–interrupt

Two Phase Termination

在一个线程T1如何优雅终止线程T2?

错误思路

  • 使用线程对象的stop()方法停止线程

    stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。

  • 使用System.exit(init)方法停止线程

    目的仅是停止一个线程,但这种做法会让整个程序都停止。

设计模式之两阶段终止–interrupt-分析

img47

设计模式之两阶段终止–interrupt-实现
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
public class Test{
public static void main(String[] args){
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();

Thread.sleep(3500);
tpt.stop();
}
}

class TwoPhaseTermination{
private Thread monitor;

//启动监控线程
public void start(){
monitor = new Thread(()->{
while(true){
Thread current = Thread.currentThread();
if(current.isInterrupted()){
log.debug("料理后事");
break;
}
try{
Thread.sleep(1000);//情况1
log.debug("执行监控记录");//情况2
}catch(InterrputedException e){
e.printStackTrace();
//重新设置打断标记
current.interrupt();
}
}
});
monitor.start();
}

//停止监控线程
public void stop(){
monitor.interrupt();
}
}
打断park线程

打断park线程,不会清空打断状态

1
2
3
4
5
6
7
8
9
10
11
12
private static void test3() throws InterruptedException{
Thread t1 = new Thread(()->{
log.debug("park....");
LockSupport.park();
log.debug("unpark....");
log.debug("打断状态:{}",Thread.currentThread().isInterrupted());
},"t1");
t1.start();

sleep(1);
t1.interrupt();
}

如果打断标记已经是true,则park会失效。

1
log.debug("打断状态:{}",Thread.currentThread().isInterrupted());

不推荐的方法

还有一些不推荐的方法,这些方法已经过时,容易破坏同步代码块,造成线程死锁。

方法名 static 功能说明
stop() 停止线程运行
suspend() 挂起(暂停)线程运行
resume() 恢复线程运行

守护线程

默认情况下,java进程需要等待所有线程运行结束,才会结束。有一种特殊的线程叫守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args){
Thread t1 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
break;
}
}
log.debug("结束");
},"t1");
t1.setDaemon(true);//守护线程
t1.start();

Thread.sleep(1000);
log.debug("结果");
}

例如:

1
2
3
4
5
6
7
8
9
10
11
12
log.debug("开始运行...");
Thread t1 = new Thread(()->{
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
},"daemon");
//设置该线程为守护线程
t1.setDaemon(true);
t1.start();

sleep(1);
log.debug("运行结束...");

输出:

1
2
3
[main]-开始运行
[daemon]-开始运行
[main]-运行结束

注意:

  • 垃圾回收器线程就是一种守护线程
  • Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求。

五种状态

从操作系统层面描述

img49

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联。

  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行。

  • 【运行状态】指获取了CPU时间片运行中的状态。

    当CPU时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换。

  • 【阻塞状态】

    • 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】。
    • 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】。
    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们。
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态。

从Java API层面描述

根据Thread.State枚举,分为六种状态

img50

  • NEW线程刚被创建,但是还没有调用start()方法
  • RUNNABLE当调用了start()方法之后,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可行的)
  • BLOCKED、WAITING、TIME_WAITING都是Java API层面对【阻塞状态】的细分。
  • TERMINATED当线程代码运行结束
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
public class TestState{
public static void main(String[] args){
Thread t1 = new Thread("t1"){
public void run(){
log.debug("running....");
}
};

Thread t2 = new Thread("t2"){
public void run(){
while(true){

}
}
};
t2.start();

Thread t3 = new Thread("t3"){
public void run(){
log.debug("running....");
}
};
t3.start();

Thread t4 = new Thread("t4"){
public void run(){
synchronized(TestState.class){
try{
Thread.sleep(1000000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
};
t4.start();

Thread t5 = new Thread("t5"){
public void run(){
try{
t2.join();
}catch(InterruptedException e){
e.printStackTrace();
}
}
};
t5.start();

Thread t6 = new Thread("t6"){
public void run(){
synchronized(TestState.class){
try{
Thread.sleep(1000000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
};
t6.start();

try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
log.debug("t1 state()",t1.getState());
log.debug("t2 state()",t2.getState());
log.debug("t3 state()",t3.getState());
log.debug("t4 state()",t4.getState());
log.debug("t5 state()",t5.getState());
log.debug("t6 state()",t6.getState());
}
}

案例-应用之统筹(烧水泡茶)

img51

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
public class Test{
public static void main(String[] args){
Thread t1 = new Thread(()->{
log.debug("洗水壶");
sleep(1);
log.debug("烧开水");
sleep(5);
},"老王");

Thread t2 = new Thread(()->{
log.debug("洗茶壶");
sleep(1);
log.debug("洗茶杯");
sleep(2);
log.debug("拿茶叶");
sleep(1);
try{
t1.join
}catch(InterruptedException e){
e.printStackTrace();
}
},"小王");

t1.start();
t2.start();
}
}
-------------本文结束感谢您的阅读-------------