部分来自黑马和javaguide
线程与进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
线程生命周期
6种状态:
• NEW:初始状态,线程被构建,但是还没有调用start()方法
• RUNNABLE:运行状态,java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
• BLOCKED:阻塞状态,表示线程阻塞于锁
• WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
• TIME_WAITING:超时等待状态,不同于WAITING,该状态可以在指定时间自行返回
• TERMINATED:终止状态,表示当前线程已经执行完毕
上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
死锁
• 互斥条件:该资源任意一个时刻只由一个线程占用。
• 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
• 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
• 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
sleep、wait、yield
-
sleep是Thread的静态方法,wait是Object的方法
-
sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
-
两者都可以暂停线程的执行。
-
wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。
-
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。
-
yield只是让出cpu的使用权,会使当前线程从Running进入Runnable就绪状态,然后调度执行其他线程
-
sleep、wait、join在被打断后都会清空interrupt标记
public void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
log.debug("t1的打断状态:{}", t1.isInterrupted());
}
此时调用t1.isInterrupted()会输出false。
join
join表示等待某线程结束后,本线程再继续运行。join使当前线程变为RUNNABLE状态
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
s = 20;
});
t1.start();
t2.start();
final long start = System.currentTimeMillis();
log.debug("join begin");
t2.join();
log.debug("t2 join end");
t1.join();
log.debug("t1 join end");
final long end = System.currentTimeMillis();
log.debug("r:{}, s:{}, cost: {}", r, s, end - start);
}
以上代码表示在t1和t2线程运行结束后,在去读取r和s的值
join的原理:保护性暂停
class GuardedTimeoutObject{
//结果
private Object response;
//获取结果
public Object get(long timeout){
synchronized (this){
long begin = System.currentTimeMillis();
long passedTime = 0;
while(response == null){
long waitTime = timeout - passedTime;
if(waitTime <=0){
break;
}
try {
//防止虚假唤醒
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - begin;
}
return response;
}
}
public void complete(Object response){
synchronized (this){
this.response = response;
this.notifyAll();
}
}
}
park、unpark
它们是LockSupport类中的方法
// 暂停当前线程
LockSupport.park();
//恢复某个线程的运行
LockSupport.unpark(暂停线程对象);
可以看到park & unpark是以线程为单位来【阻塞】和【唤醒】的,而notify只能随机唤醒,notifyAll是唤醒所有。
park与wait有很大的一个不同:
- notify()如果先于wait()调用是无效的
- 但是unpark()可以先于park()调用,先调用unpark(t1),t1再调用park()时会被瞬间唤醒
原理
每个线程都有自己的一个Parker对象,mutex为互斥锁,cond为条件变量,counter为计数器。每次调用unpark就会将counter+1,只要counter > 0,线程就可以消耗一份counter继续运行。
start和run
new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
两阶段终止
在一个线程T1中如何优雅地终止另一个线程T2?这里的【优雅】指的是可以给T2一个料理后事的机会
错误思路:
- 使用stop()方法停止线程:这样会真正而且马上地杀死线程,如果这个线程锁住了共享资源,那么共享资源就永远无法被释放了。
正确思路:两阶段终止
class TwoPhaseTermination {
private Thread monitor;
//气动监控线程
public void start(){
monitor = new Thread(() -> {
while(true){
Thread currentThread = Thread.currentThread();
if(currentThread.isInterrupted()){
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);//可能在睡眠时被打断了
log.debug("执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置打断标记
currentThread.interrupt();
}
}
});
monitor.start();
}
//停止监控线程
public void stop(){
monitor.interrupt();
}
}
也可以使用volatile关键字
@Slf4j
class TwoPhaseTermination {
private Thread monitor;
volatile private boolean stop;
//气动监控线程
public void start(){
monitor = new Thread(() -> {
while(true){
Thread currentThread = Thread.currentThread();
if(stop){
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);//可能在睡眠时被打断了
log.debug("执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
monitor.start();
}
//停止监控线程
public void stop(){
stop = true;
}
}
CAS
CAS本身通过底层的处理器代码实现原子性。
为什么无锁效率高?
- 无锁情况下,即使重试失败,线程始终在高速运行,而synchronized会让线程在没有获得锁的时候进入阻塞状态,这时候发生了上下文切换。
- 无锁状态下,因为线程要保持运行,需要额外CPU的支持。如果没有额外CPU的支持,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换
java.util.concurrent.atomic包下的原子操作类都是基于CAS实现的
ABA 问题
描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。
解决:采用版本号