JUC高并发编程-基础知识
JUC高并发编程
1.进程与线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例,也有的程序只能启动一个实例进程。
线程
- 一个进程之内可以分为一个到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
- Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。
二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享。
- 进程间通信比较复杂。
- 同一台计算机的进程通信称为IPC
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。
并行与并发
并发(concurrent)
单核cpu下,线程实际还是串行执行的。操作系统中有一个组件叫任务调度器,将cpu的时间片分给不同的线程使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。总结一句话:微观串行,宏观并行。
一般会将这种线程轮流使用cpu的做法称为并发,concurrent。
并行(parallel)
多核cpu下,每个核(core)都可以调度运行线程,这时候线程可以是并行的。
例子
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做着多件事,这时就是并发。
- 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)。
- 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时就是并行。
应用
案例1:异步调用
从方法调用的角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能好继续运行就是异步
注意:同步在多线程中还有另外一层意思,是让多个线程步调一致。
设计
多线程可以让方法执行变为异步的比如读取磁盘文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这5秒调用者什么都做不了,其代码都得暂停…
结论
- 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程。
- tomcat的异步servlet也是类似的,让用户线程处理耗时较长的操作,避免阻塞tomcat的工作线程。
- ui程序中,开线程进行其他操作,避免阻塞ui线程。
案例2:提高效率
充分利用多核cpu的优势,提高运行效率。想象下面的场景,执行3个计算,最后将计算结果汇总。
1 | 计算1 花费10ms |
如果是串行执行,那么总共花费的时间是10ms+11ms+9ms+1ms=31ms
- 如果是四核cpu,各个核心分别使用线程1执行计算1,线程2执行计算2,线程3执行计算3,那么3个线程是并行的,花费时间只取决于最长的那个线程运行的时间,即11ms最后加上汇总时间只会花费12ms。
注意:需要在多核cpu才能提高效率,单核仍然是轮流执行。
设计
案例1
结论
- 单核cpu下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu,不至于一个线程总占用cpu,别的线程没法干活。
- 多核cpu可以并行跑多个线程,但能否提高程序运行效率还是要分情况的。
- IO操作不占用cpu,只是我们一般拷贝文件使用的是【阻塞IO】,这时相当于线程虽然不用cpu,但需要一直等待IO结束,没能充分利用线程。所有才有后面的【非阻塞IO】和【异步IO】优化。
2.Java线程
创建和运行线程
方法一:直接使用Thread
1 | //创建线程对象 |
例如:
1 | //构造方法的参数是给线程指定名字,推荐 |
方法二:使用Runnable配合Thread
把【线程】和【任务】分开
- Thread代表线程
- Runnable代表可运行的任务(线程要执行的代码)
1 | Runnable runnable = new Runnable(){ |
例如:
1 | //创建任务对象 |
Java8以后可以使用lambda精简代码
1 | //创建任务对象 |
原理之Thread与Runnable的关系
分析Thread的源码,理清它与Runnable的关系
小结
- 方法1是把线程和任务合并在了一起,方法2是把线程和任务分开了。
- 用Runnable更容易与线程池等高级API结合。
- 用Runnable让任务脱离了Thread继承体系,更灵活。
方法三:FutureTask配合Thread
FutureTask能够接收Callable;类型的参数,用来处理有返回结果的情况。
1 | //创建任务对象 |
观察多个线程同时运行
- 交替执行
- 谁先谁后,不由我们控制
查看进程线程的方法
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频繁发生会影响性能
常见方法
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
- 调用sleep会让当前线程从Running进入TimedWaiting状态。
- 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException。
- 睡眠结束后的线程未必会立刻得到执行。
- 建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。
1 | public static void main(String[] args){ |
yield
- 调用yield会让当前线程从Running进入Runnable状态,然后调度执行其它同优先级的进程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果。
- 具体的实现依赖于操作系统的任务调度器。
线程优先级
- 线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。
- 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用。
不使用yield
1 | public static void main(Stirng[] args){ |
案例-防止CPU占用100%
sleep实现
在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或sleep来让出cpu的使用权给其它程序。
1 | while(true){ |
- 可以用wait或条件变量达到类似的结果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般使用于要进行同步的场景
- sleep适用于无需锁同步的场景
join方法详解
1 | static int r = 0; |
1 | [main]-开始 |
分析:
- 因为主线程和线程t1是并行执行的,t1线程需要1秒后才能计算出r=10
- 而主线程一开始就要打印r的结果,所以只能打印出r=0
join方法加在start方法之后即可。
应用之同步(案例1)
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
1 | static int r1 = 0; |
输出:
1 | //因为线程是同步的,01开始t1和t2同时启动 |
有时效的join
等够时间
1 | static int r1 = 0; |
输出:
1 | r1:10 r2:0 cost:1010 |
interrupt方法详解
打断sleep,wait,join的线程
阻塞
打断sleep的线程,会清空打断状态,以sleep为例
1 | private static void test1() throws InterruptedException{ |
设计模式之两阶段终止–interrupt
Two Phase Termination
在一个线程T1如何优雅终止线程T2?
错误思路
使用线程对象的stop()方法停止线程
stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁。
使用System.exit(init)方法停止线程
目的仅是停止一个线程,但这种做法会让整个程序都停止。
设计模式之两阶段终止–interrupt-分析
设计模式之两阶段终止–interrupt-实现
1 | public class Test{ |
打断park线程
打断park线程,不会清空打断状态
1 | private static void test3() throws InterruptedException{ |
如果打断标记已经是true,则park会失效。
1 | log.debug("打断状态:{}",Thread.currentThread().isInterrupted()); |
不推荐的方法
还有一些不推荐的方法,这些方法已经过时,容易破坏同步代码块,造成线程死锁。
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂停)线程运行 | |
resume() | 恢复线程运行 |
守护线程
默认情况下,java进程需要等待所有线程运行结束,才会结束。有一种特殊的线程叫守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
1 | public static void main(String[] args){ |
例如:
1 | log.debug("开始运行..."); |
输出:
1 | [main]-开始运行 |
注意:
- 垃圾回收器线程就是一种守护线程
- Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令后,不会等待它们处理完当前请求。
五种状态
从操作系统层面描述
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联。
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行。
【运行状态】指获取了CPU时间片运行中的状态。
当CPU时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换。
【阻塞状态】
- 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】。
- 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】。
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们。
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态。
从Java API层面描述
根据Thread.State枚举,分为六种状态
- NEW线程刚被创建,但是还没有调用start()方法
- RUNNABLE当调用了start()方法之后,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可行的)
- BLOCKED、WAITING、TIME_WAITING都是Java API层面对【阻塞状态】的细分。
- TERMINATED当线程代码运行结束
1 | public class TestState{ |
案例-应用之统筹(烧水泡茶)
1 | public class Test{ |