目录
一、了解和JUC相关的概念
1.1 什么是JUC?
JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题!
1.2 什么是进程?
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还要用到磁盘、网络等设备。进程就是用来加载指令,管理内存管理IO的。
- 当一个进程被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为一个实例。大部分程序可以同时运行多个实例(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士)。
1.3 什么是线程?
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
- Java中,线程作为最小的调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。
进程与线程的对比:
-
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
-
进程拥有共享的资源,如内存空间等,供其内部的线程共享
-
进程间通信较为复杂 。
同一台计算机的进程通信称为 IPC
(
Inter-process communication
)。不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP。 -
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
-
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
1.4 并发与并行
cpu
下,线程实际还是
串行执行
的。操作系统中有一个组件叫做任务调度器,将
cpu
的时间片(
windows下时间片最小约为 15
毫秒)分给不同的程序使用,只是由于
cpu
在线程间(时间片很短)的切换非常快,人类感觉是同时运行的
。总结为一句话就是:
微观串行,宏观并行
, 一般会将这种线程轮流使用
CPU
的做法称为并发(
concurrent)。
多核 cpu
下,每个
核(
core
)
都可以调度运行线程,这时候线程可以是并行(Parallel)的。简单来说:
-
并发(
concurrent
)是同一时间段分别应对(
dealing with
)多件事情的能力 -
并行(parallel
)是同一时间段同时动手做(
doing
)多件事情的能力
1.5 同步和异步
从方法调用的角度来讲,如果:
- 需要等待结果返回,才能继续运行的就是同步
- 不需要等待结果返回,就能继续运行的就是异步
注意:同步在多线程中还有另外一层意思,是让多个线程步调一致
二、Java线程
2.1 创建线程的三种方法
方法一:直接使用Thread
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
方法二:使用Runnable配合Thread
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
Java 8以后可以使用lambda精简代码:
// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
- 方法一 是把线程和任务合并在了一起,方法二 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
方法三:FutureTask 配合 Thread
// 创建任务对象
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);
2.2 查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
- ps –fe 查看所有进程
- ps –fT –p <PID> 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top –H –p <PID> 查看某个进程(PID)的所有线程
Java
- jps 命令查看所有 Java 进程
- jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
2.3 线程有关的常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() |
启动一个新线程,在新的线程运行 run
方法中的代码 |
start
方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU
的时间片还没分给它)。每个线程对象的 start方法只能调用一次,
如果调用了多次会出现 IllegalThreadStateException
|
|
run()
|
新线程启动后会
调用的方法
|
如果在构造
Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为 |
|
join()
|
等待线程运行结束
|
||
join(long n)
|
等待线程运行结
束(
最多等待 n 毫秒) |
如果线程结束了,就继续向下执行,不会一直等待到最大时间 | |
getId()
|
获取线程长整型的 id
|
id
唯一 |
|
getName()
|
获取线程名
|
||
setName(String)
|
修改线程名
|
||
getPriority()
|
获取线程优先级
|
||
setPriority(int)
|
修改线程优先级
|
java
中规定线程优先级是 1~10 的整数,较大的优先级
能提高该线程被
CPU 调度的机率 |
|
getState()
|
获取线程状态
|
Java
中线程状态是用 6 个 enum 表示,分别为:
NEW, RUNNABLE, BLOCKED, WAITING,
TIMED_WAITING, TERMINATED
|
|
interrupted()
|
判断是否被打断
|
不会清除
打断标记 |
|
isAlive()
|
线程是否存活(还没有运行完毕)
|
||
interrupt()
|
打断线程
|
如果被打断线程正在
sleep , wait , join 会导致被打断
的线程抛出
InterruptedException ,并清除 打断标
记
;如果打断的正在运行的线程,则会设置 打断标
记
; park 的线程被打断,也会设置 打断标记 |
|
interrupted()
|
static
|
判断当前线程是
否被打断
|
会清除
打断标记 |
currentThread()
|
static
|
获取当前正在执行的线程
|
|
sleep(long n)
|
static
|
让当前执行的线
程休眠
n 毫秒,
休眠时让出
cpu
的时间片给其它
线程
|
|
yield()
|
static
|
提示线程调度器
让出当前线程对
CPU
的使用 |
主要是为了测试和调试
|
【start与run方法】
我们通过代码示例可以看出start和run的区别:使用 t1.run() 时:
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things ...");
}
输出:
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...
main
线程运行,
FileReader.read() 方法调用还是同步的,如果将上述代码的
t1.run()
改为 t1.start(),则输出为:
19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms
t1
线程运行,
FileReader.read()
方法调用是异步的。
小结:
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
【sleep与yield方法】
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 阻塞状态
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态 ,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(可能出现没 “让” 出去地的现象)
【jion方法】
- 等待线程运行结束
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
Thread.sleep(1000);
log.debug("结束");
r = 10;
});
t1.start();
t1.jion();// 在start后调用join
log.debug("结果为:{}", r);
log.debug("结束");
}
不加 t1.join 输出结果r为 0 ,加了 t1.join() 后输出结果r为 10;原因是加了 t1.join() ,主线程运行到此行后会等待 t1 运行结束后再继续向下运行,即让 main 线程同步等待 t1 线程。
【interrupt方法】
sleep
,wait,join 的线程(会让线程进入阻塞状态的线程)时:会清空打断状态,即打断标志置为false;同时,在catch中抛出异常
sleep
为例
private static void test1() throws InterruptedException {
Thread t1 = new Thread(()->{
log.debug("sleep...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace;
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug(" 打断标记: {}", t1.isInterrupted());
}
2、打断正常运行的线程时:不会清空打断状态,即打断标志置为true;同时,interrupt打断正常运行的线程时,不会让线程停下来,线程会继续执行。若想让线程停下来,需要根据对打断标志为 true 的判断从而手动让线程停下来。
private static void test2() throws InterruptedException {
Thread t1 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
if(current.isInterrupted()) {
log.debug("被打断了,退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
下面来一道关于interrupt的常见面试题:在一个线程T1中如何优雅地终止线程T2?这里的优雅是指给T2一个料理后事的机会。 1. 如果使用线程的 stop() 方法停止线程:stop() 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当他被杀死后就再也没有机会释放锁,其他线程将永远无法获取该锁。(容易破坏代码块,造成死锁的方法还有suspend():挂起/暂停线程运行、resume():恢复线程运行,这些方法已经过时) 2. 若使用 System.exit(int) 方法停止线程:该方法是让整个进程都停止,而我们只想要一个线程通知,这种做法明显不划算。 此时,使用 interrupt() 方法的两阶段终止模式为最优解:
代码实现如下:
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:中断发生在线程睡眠时,会在catch中抛出异常,中断标志为false
log.debug("执行监控记录");// 情况2:中断发生在正常运行线程时,中断标志置为true
} catch (InterruptedException e) {
e.printStackTrace();
// 重新设置打断标志
current.interrupt();
}
}
});
monitor.start();// 启动线程
}
// 停止监控线程
public void stop() {
monitor.interrupt();
}
}
【守护线程】
Java
进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
线程对象名.setDaemon(true);// 设置守护线程
守护线程示例:
- 垃圾回收器线程就是一种和守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 tomcat 接收到 shutdown 命令后,不会等待他们处理完当前请求
2.4 线程的五种状态
-
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
-
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调
度执行 -
【运行状态】指获取了 CPU
时间片运行中的状态 。-
当 CPU
时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
-
-
【阻塞状态】
-
如果调用了阻塞 API
,如
BIO
读写文件,这时该线程实际不会用到
CPU
,会导致线程上下文切换,进入【阻塞状态】 -
等 BIO
操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 -
与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
-
-
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
2.5 线程的六种状态
-
NEW:
线程刚被创建,但是还没有调用
start()
方法 -
RUNNABLE:当调用了
start()
方法之后。注意,
Java API
层面的
RUNNABLE
状态涵盖了
操作系统
层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO
导致的线程阻塞,在
Java
里无法区分,仍然认为是可运行) -
BLOCKED(无锁) ,
WAITING(join)
,
TIMED_WAITING(sleep):
都是
Java API
层面对【阻塞状态】的细分,后面会在状态转换一节详述 -
TERMINATED:当线程代码运行结束
1:
NEW —> RUNNABLE
- 当调用 t.start() 方法时,由 NEW —> RUNNABLE
2:
RUNNABLE <—> WAITING
- t 线程用 synchronized(obj) 获取了对象锁后
-
调用
obj.wait()
方法时,
t
线程
从
RUNNABLE
—
> WAITING -
调用 obj.notify()
,
obj.notifyAll()
,
t.interrupt() 时:
竞争锁成功,
t
线程
从
WAITING
—
> RUNNABLE;
竞争锁失败,
t
线程
从
WAITING
—
> BLOCKE
-
3:
RUNNABLE <—> WAITING
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE —> WAITING。注意是当前线程在t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING —> RUNNABLE
4:
RUNNABLE <
—
> WAITING
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE —> WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING —> RUNNABLE
5:
RUNNABLE <
—
> TIMED_WAITING
- t 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE —> TIMED_WAITING
- t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时:竞争锁成功,t 线程从 TIMED_WAITING —> RUNNABLE;竞争锁失败,t 线程从 TIMED_WAITING —> BLOCKED
6:
RUNNABLE <
—
> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE —> TIMED_WAITING。注意是当前线程在t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING —> RUNNABLE
7:
RUNNABLE <
—
> TIMED_WAITING
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE —> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
8:
RUNNABLE <
—
> TIMED_WAITING
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE —> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING—> RUNNABLE
9:
RUNNABLE <
—
> BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE —> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED —> RUNNABLE ,其它失败的线程仍然 BLOCKED
10
RUNNABLE <
—
> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
三、线程共享模型
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/2149.html