初识Java并发编程(一)
笔记是看某教学视频总结和记录的,有一些地方有待完善。总体来说看的学习视频开头部分讲的还是有点简略的,不过好的地方是有实际操作,我觉得可以当作入门视频来看。之前学并发看的对某一个点进行深入剖析的视频和一些书籍,再重新过一遍基础,对掌握整个体系还是挺有帮助的,有些地方老师讲的还是挺不错的。
文章目录
1.进程与线程
1.1进程与线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载到CPU,数据加载到内存。指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
- 当一个程序被执行,从磁盘加载这个程序的代码到内存,这时候就开启了一个进程。
- 进程就可以看作一个实例,大部分程序可以同时运行多个实例进程(比如说我可以开启好几个360浏览器窗口),但有的程序只能开启一个实例进程(比如网易云音乐不能开2个窗口)。
线程
- 一个进程之内可以分为1~多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
- Java中,线程作为最小调度单位,进程作为资源分配的最小单位,在Windows里面进程不活动,只是线程的容器
对比
-
进程基本上相互独立,而线程存在于进程内,是进程的子集
-
进程拥有共享的资源,如内存空间等,供其内部的线程贡献
-
进程间的通信比较负载
- 同一台计算机的进程通信是IPC
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,比如HTTP
-
线程通信比较简单,因为共享进程内的内存,一个例子是多个线程可以访问同一个共享变量(这个在《Java并发编程的艺术》中可以看到作者经常提及)
-
线程更轻量,线程上下文切换成本一般比进程上下文切换低。(上下文切换这个需要重点理解,后面会讲到)
1.2并行与并发
单核CPU下,线程实际上是串行运行的。操作系统中有一个组件叫做任务调度器,将CPU时间片(Windows下时间片最小约为15毫秒)分配给不同的线程使用,只是由于CPU在线程间(时间片很短)的切换非常快,所以我们会感觉到是同步运行的。
一般将线程轮流使用CPU的做法称为并发,concurrent。
多核CPU下,每个核都可以调度运行线程,这时候线程是并行的。
Rob Pike对并发和并行的描述是这样的:
并发(concurrent)是同一时间应对多件事情的能力
并行(parallel)是同一时间动手做多件事情的能力
2.Java线程
2.1创建和运行线程
方法一:直接使用thread
方法二:使用Runnable配合Thread
方法三:FutureTask配合Thread
以上3种方法在之前的Java基础学习中都详细讲到过,不了解请点我。
Thread和Runnable之间的关系
- Thread类是Runnable接口的子类
public class Thread implements Runnable
- 查找源码,看看run()方法会有如下收获:
public void run() {
if (target != null) {
target.run();
}
}
这个target是个啥呢?再来锁定一下,private Runnable target;
,这样一来就明白了:Thread实现Runnable接口,但是并没有完全实现run() 方法,此方法是Runnable子类完成的,所以如果继承Thread就必须覆写run方法。
- 方法一和方法二相比,以实现方法二为主。使用方法二的好处:
- 如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
- Runable 适合多个相同的程序代码的线程去处理同一个资源
- runnable 可以避免Java 中单继承的限制
- runnable 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
- PS:关于资源共享方面,我建议还是多看看dlao博客,我搜了几篇,对于这方面说法很多,还需要在实践下。
2.2查看线程
Windows
-
任务管理器可以查看进程和线程数,也可以杀死进程
-
tasklist查看进程
- 查看本机所有所有进程
tasklist
- 根据pid查询指定进程
tasklist |findstr pid
,这个命令出来的结果可能不止一个,可以理解为模糊查询吧
- 查看远程所有进程
Tasklist /s 218.22.123.26 /u admin /p 111111
- 查看远程所有进程的时候,需要远程机器的RPC服务的支持
- 查看本机所有所有进程
-
taskkill杀死进程
taskkill [/s system [/u username [/p [password]]]] { [/fi filter] [/pid processid | /im imagename] } [/t] [/f] 描述: 使用该工具按照进程 id (pid) 或映像名称终止任务。 参数列表: /s system 指定要连接的远程系统。 /u [domain\]user 指定应该在哪个用户上下文执行这个命令。 /p [password] 为提供的用户上下文指定密码。如果忽略,提示 输入。 /fi filter 应用筛选器以选择一组任务。 允许使用 "*"。例如,映像名称 eq acme* /pid processid 指定要终止的进程的 pid。 使用 tasklist 取得 pid。 /im imagename 指定要终止的进程的映像名称。通配符 '*'可用来 指定所有任务或映像名称。 /t 终止指定的进程和由它启用的子进程。 /f 指定强制终止进程。 /? 显示帮助消息。 筛选器: 筛选器名 有效运算符 有效值 ----------- --------------- ------------------------- status eq, ne running | not responding | unknown imagename eq, ne 映像名称 pid eq, ne, gt, lt, ge, le pid 值 session eq, ne, gt, lt, ge, le 会话编号。 cputime eq, ne, gt, lt, ge, le cpu 时间,格式为 hh:mm:ss。 hh - 时, mm - 分,ss - 秒 memusage eq, ne, gt, lt, ge, le 内存使用量,单位为 kb username eq, ne 用户名,格式为 [domain\]user modules eq, ne dll 名称 services eq, ne 服务名称 windowtitle eq, ne 窗口标题 说明 ---- 1) 只有在应用筛选器的情况下,/im 切换才能使用通配符 '*'。 2) 远程进程总是要强行 (/f) 终止。 3) 当指定远程机器时,不支持 "windowtitle" 和 "status" 筛选器。 例如: taskkill /im notepad.exe taskkill /pid 1230 /pid 1241 /pid 1253 /t taskkill /f /im cmd.exe /t taskkill /f /fi "pid ge 1000" /fi "windowtitle ne untitle*" taskkill /f /fi "username eq nt authority\system" /im notepad.exe taskkill /s system /u 域\用户名 /fi "用户名 ne nt*" /im * taskkill /s system /u username /p password /fi "imagename eq note*"
Linux
-
查找进程
- top命令
- ps命令
- lsop命令
-
结束进程
- kill命令
- killall命令
附上一篇博客,Linux查看进程和结束进程。
Java
jps
命令查看所有Java线程jstack <PID>
查看某个Java进程的所有线程状态jconsole
查看某个Java进程中线程的运行情况(图形界面)
2.3 线程运行的原理
栈与栈帧
Java Virtual Machine Stacks(Java虚拟机栈)
JVM由堆、栈、方法区所组成,其中每个线程启动之后,虚拟机就会为线程分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
举个例子,请看下面的代码,在IDEA编译器中将第4行打上断点,进入debug模式,然后观察左侧Frame框的变化。
public class Test05 {
public static void main(String[] args) {
method1(10);
}
private static void method1(int x){
int y=x+1;
Object m=method2();
System.out.println(m);
}
private static Object method2(){
Object n=new Object();
return n;
}
}
通过debug调试应该就可以明白什么叫做每个栈由多个栈帧组成,此外,在进行单点调试的过程当中,也能更好的理解它的确是后进先出的特点。
图解流程
1.类加载,将字节码加载到方法区,JVM启动一个main的主线程,并且给他分配一块内存。
2.CPU给主线程分配了时间片,主线程的代码就开始执行了。主线程里面的主方法就是一个栈帧,所以也会给它分配内存。
3.执行method1(),给method1()分配栈帧内存。
4.执行语句,当执行到Object m=method2()
时,又会创建一个栈帧,然后执行method2方法中的语句。
5.new一个对象,赋给n
6.return n。
- 执行完之后,就会逐渐释放栈帧内存(不放图了)。
多线程的情况下是什么样子呢,可以自行运行查看。代码如下,在执行method1()方法的地方都打上断点,右键断点在Suspend
处将All
改成Thread
。
public class Test05 {
public static void main(String[] args) {
Thread t1=new Thread(){
@Override
public void run(){
method1(20);
}
};
t1.setName("t1");
t1.start();
method1(10);
}
private static void method1(int x){
int y=x+1;
Object m=method2();
System.out.println(m);
}
private static Object method2(){
Object n=new Object();
return n;
}
}
线程上下文切换(Thread Context Switch)
因为下列原因会导致CPU停止执行当前的线程,转而去执行另外一个线程的代码。
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程本身调用了sleep、yield、wait、join、park、synchronized、lock等方法
当上下文切换发生的时候,需要有操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器,它的作用就是记住下一条JVM指令执行的地址,是线程私有的。
- 要记录的是什么状态?状态包括程序计数器、虚拟机栈中每个栈帧的信息,比如说局部变量、操作数栈、返回地址等。
- Context Switch频繁发生会影响性能(这一点很重要,不过暂时先不展开讲)
2.4 start方法和run方法
直接调用run()方法并不会启动一个线程,只有调用start()方法才会启动线程。直接调用run()方法,通过日志打印可以发现,该方法实际上是在主线程中调用的,而不是在新创建的Thread线程中。
start()方法只能够被调用1次,调用过了之后再调用就会报错。
2.5 yield与sleep
yield
- 调用yield会让当前线程从Running进入Runnable状态,案后调度执行其它同优先级的线程,如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果
- 具体的实现依赖于操作系统的任务调度器
sleep
- 调用sleep方法会让当前线程从Running进入Timed Waiting状态(因为使用sleep方法的时候传递了一个休眠多少毫秒的参数,所以是Timed Waiting)
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出
InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用TimeUnit(J.U.C包下)的sleep代替Thread的Sleep来获得更好的可读性
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但他仅仅是一个提示,调度器可以忽略它(如果你看过《并发编程的艺术》,也许会注意到有一个demo就忽略了人为设置的优先级)
- 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲的时候,优先级几乎没作用
应用:防止CPU占用100%
在单核的情况下,编译运行一个while(true)的代码,可以看到CPU占用率逼近100%,while(true)空转浪费CPU资源,那么怎么办呢?可以使用yield或者sleep方法让出CPU的使用权给其他程序,再次编译可以发现,CPU占用率会下降很多很多。
不过也可以使用其他方法,sleep方法适用于无锁同步的场景,也可以使用wait()或者条件变量来达到类似的效果,不过有的方法可能需要加锁并且需要相应的唤醒操作,一般适用于进行同步的场景。
2.7 join方法详解
为什么需要join()?
public class Test05 {
static int r=0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException{
System.out.println("开始");
Thread t1=new Thread(()->{
System.out.println("线程开始");
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束");
r=10;
},"t1");
t1.start();
System.out.println("结果r:"+r);
System.out.println("结果");
}
}
看看上面的程序,打印输出的结果r是10还是0呢?结果是0,因为主线程main和线程t1是并行执行的,t1线程需要休眠一段时间之后才能让r=10,而主线程一开始就打印r的结果所以只能打印出r-=0。
那么怎么解决呢?在t1.start()方法后调用join()方法。
视频上关于join()我觉得还是讲的不够的,此外,又看了2篇博客、join()方法的使用。
2.8 interrupt方法
interrupted()方法是判断当前线程是否被打断,会清除打断标记,而isInterrupted()判断是否被打断,不会清楚打断标记,interrupt()方法打断线程,如果被打断的线程正在sleep、wait或者join,会导致被打断的线程抛出异常,并清除打断标记,如果打断正在运行的线程,则会设置打断标记,park的线程被打断,也会设置打断标记。
打断sleep、wait、join的线程
使用interrupt方法后会清空打断状态。
打断正常运行的线程
public class Test05 {
public static void main(String[] args) throws InterruptedException{
Thread t1=new Thread(()->{
while(true){
boolean interrupted = Thread.currentThread().isInterrupted();;
if (interrupted){
System.out.println("线程t1被打断了,退出循环");
break;
}
}
});
t1.start();
Thread.sleep(1000);
System.out.println("interrupt");
t1.interrupt();
}
}
如果上面线程t1中,我们不加入判断的话,那么就算是调用了interrupt()方法,t1线程也不会被中断的。
两阶段终止模式
在一个线程T1中如何“优雅”终止线程T2?这里的优雅指的是给T2一个料理后事的机会。
几个错误思路:
- 使用线程对象的stop()方法停止线程
- 该方法会真正杀死线程,如果这时候线程锁住了共享资源,那么当它被杀死之后就再也没机会释放锁了,其他线程将会永远无法获取锁
- 使用
System.exit(int)
方法停止线程- 目的仅仅是停止一个线程,但是使用这个方法会让这个程序都停止
使用基于interrupt方法的两阶段终止模式是可行的,先看下流程图。
代码实现:
public class Test05 {
public static void main(String[] args)throws InterruptedException {
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()){
System.out.println("料理后事");
break;
}
try{
Thread.sleep(1000);//情况1
System.out.println("执行监控记录");//情况2
}catch (InterruptedException e){
e.printStackTrace();
//重新设置打断标记
current.interrupt();//不能省略!
}
}
});
monitor.start();
}
//停止监控线程
public void stop(){
monitor.interrupt();
}
}
运行结果:
执行监控记录
执行监控记录
执行监控记录
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at exam.offer.day05.TwoPhaseTermination.lambda$start$0(Test05.java:30)
at java.lang.Thread.run(Thread.java:748)
料理后事
打断park线程
这个park()方法或许你和我第一次听说,没事以后应该还会见。该方法是LockSupport类(J.U.C包)中的一个静态方法。
打断标记为真的时候,会让park方法失效。
public class Test05 {
public static void main(String[] args)throws InterruptedException {
Thread t1=new Thread(()->{
System.out.println("park");
LockSupport.park();
System.out.println("unpark");
System.out.println("打断标志:"+Thread.currentThread().isInterrupted());
//如果后面还想让他执行LockSupport.park()方法,就要让打断标志为false,
//可以将上面这行代码改为
//System.out.println("打断标志:"+Thread.interrupted());
//System.out.println("park");
//LockSupport.park();
//System.out.println("unpark");
});
t1.start();
sleep(1);
t1.interrupt();
}
}
2.9 不推荐的方法
已经过时了,容易破坏同步代码块,造成线程死锁。比如stop()方法【两阶段终止模式代替】、suspend()方法【wait()方法代替】和resume()方法【notify()方法代替】。
2.10 主线程与守护线程
默认情况下,Java进程需要等待所有线程都运行结束,才会结束,有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即时守护线程的代码没执行完,也会强制结束。
比如说下面这段代码,其中就将新创建的线程设置为守护线程。
public class Test05 {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
break;
}
}
System.out.println("thread结束");
});
//设为守护线程
thread.setDaemon(true);
thread.start();
Thread.sleep(1000);
System.out.println("主线程结束");
}
}
垃圾回收器线程就是一种守护线程;Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdown命令之后,不会等待它们处理完当前的请求。
2.11五种状态(从操作系统层面)
从操作系统层面描述线程可以分为5种。
- 初始状态:仅仅是在语言层面创建了线程对象,还没有和操作系统线程关联
- 可运行状态(就绪状态):该线程已经被创建(与操作系统线程关联),可以由CPU调度执行
- 运行状态
- 阻塞状态
- 如果调用了阻塞API,如BIO读写文件,这时候该线程实际并不会用到CPU,会导致线程上下文切换,进入阻塞状态
- 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换到可运行状态
- 与可运行状态的区别是,对阻塞状态的线程来说只要他们一直不唤醒,调度器就一直不会考虑调度他们
- 终止状态:表示线程执行完毕,生命周期已经结束,不会在转换为其它状态
2.12 六种状态(从Java API层面)
从Java API层面描述,根据Thread.State枚举,分为6种状态。