线程相关知识总结如下:
- 线程的所有切换的状态(基本功)
- 线程安全必备知识Monitor(基本功)
- 线程内存模型
- 线程安全各个实现原理
- 线程间通信
一、线程间所有状态
补充一张图,看一下线程的各个状态
各种状态一目了然,值得一提的是"blocked"这个状态:线程在Running的过程中可能会遇到阻塞(Blocked)情况
- 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
- 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool
),释放同步锁使线程回到可运行状(Runnable)- 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。
- 此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。
二、Monitor机制
结合上图来分析Object的Monitor机制。
概念:Monitor可以类比为一个特殊的房间,这个房间中有一些被保护的数据,Monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有Monitor,退出房间即为释放Monitor。
当一个线程需要访问受保护的数据(即需要获取对象的Monitor)时,它会首先在entry-set入口队列中排队(这里并不是真正的按照排队顺序),如果没有其他线程正在持有对象的Monitor,那么它会和entry-set队列和wait-set队列中的被唤醒的其他线程进行竞争(即通过CPU调度),选出一个线程来获取对象的Monitor,执行受保护的代码段,执行完毕后释放Monitor,如果已经有线程持有对象的Monitor,那么需要等待其释放Monitor后再进行竞争。
再说一下wait-set队列。当一个线程拥有Monitor后,经过某些条件的判断(比如用户取钱发现账户没钱),这个时候需要调用Object的wait方法,线程就释放了Monitor,进入wait-set队列,等待Object的notify方法(比如用户向账户里面存钱)。当该对象调用了notify方法或者notifyAll方法后,wait-set中的线程就会被唤醒,然后在wait-set队列中被唤醒的线程和entry-set队列中的线程一起通过CPU调度来竞争对象的Monitor,最终只有一个线程能获取对象的Monitor。
需要注意的是:
- 当一个线程在wait-set中被唤醒后,并不一定会立刻获取Monitor,它需要和其他线程去竞争
- 如果一个线程是从wait-set队列中唤醒后,获取到的Monitor,它会去读取它自己保存的PC计数器中的地址,从它调用wait方法的地方开始执行。
三、线程的内存模型介绍:
为什么要这样分内存模型?
屏蔽掉各种硬件和操作系统的内存访问差异,让java程序在各种平台上都能提高运算速度。所以设计线程工作内存。
线程的工作的内存: 方便理解就可以看成CPU上的寄存器或者高速缓存,所以线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在同步回主内存。
总结:让CPU执行效率远远大于,内存执行效率,最终达成高效效果
读取顺序优先级 :寄存器-高速缓存-内存
但是有了这种设计以后,弊端就会造成数据同步及时,造成线程安全问题。继续看下面:
四、线程安全介绍、和解决方案
结合上面我画了一张图来解释线程安全:
下面代码是一个例子:
public class ThreadUnSecurity {
static int tickets = 10;
class SellTickets implements Runnable{
@Override
public void run() {
// 未加同步时产生脏数据
while(tickets > 0) {
System.out.println(Thread.currentThread().getName()+"--->售出第: "+tickets+" 票");
tickets--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (tickets <= 0) {
System.out.println(Thread.currentThread().getName()+"--->售票结束!");
}
}
}
public static void main(String[] args) {
SellTickets sell = new ThreadUnSecurity().new SellTickets();
Thread thread1 = new Thread(sell, "1号窗口");
Thread thread2 = new Thread(sell, "2号窗口");
Thread thread3 = new Thread(sell, "3号窗口");
Thread thread4 = new Thread(sell, "4号窗口");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
打印结果:
1号窗口--->售出第: 10 票
3号窗口--->售出第: 10 票
2号窗口--->售出第: 10 票
4号窗口--->售出第: 10 票
2号窗口--->售出第: 6 票
1号窗口--->售出第: 5 票
3号窗口--->售出第: 4 票
4号窗口--->售出第: 3 票
2号窗口--->售出第: 2 票
4号窗口--->售出第: 1 票
1号窗口--->售出第: 1 票
3号窗口--->售票结束!
2号窗口--->售票结束!
1号窗口--->售票结束!
4号窗口--->售票结束!
注:由于线程调度的不确定性,测试时可能跟我打印结果不一致
线程安全解决方案一 synchronized:
@Override
public void run() {
// 同步代码块
while(tickets > 0) {
synchronized (this) {
if (tickets <= 0) {
return;
}
System.out.println(Thread.currentThread().getName()+"--->售出第: "+tickets+" 票");
tickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (tickets <= 0) {
System.out.println(Thread.currentThread().getName()+"--->售票结束!");
}
}
上面是synchronized代码块,synchronized方法我就不在多余解释了
解决方案二:(Lock)
Lock锁机制, 通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块
class SellTickets implements Runnable{
Lock lock = new ReentrantLock();
@Override
public void run() {
// Lock锁机制
while(tickets > 0) {
try {
lock.lock();
if (tickets <= 0) {
return;
}
System.out.println(Thread.currentThread().getName()+"--->售出第: "+tickets+" 票");
tickets--;
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}finally {
lock.unlock();
}
}
if (tickets <= 0) {
System.out.println(Thread.currentThread().getName()+"--->售票结束!");
}
}
}
我就不一一举例了,接下来详细介绍一个每种方式的不同:
五、线程安全实现原理
线程安全的三个特性:
1、原子性: java中,对基本数据类型的读取和赋值操作,所谓原子性操作就是指这些操作是不可中断的,要做就一定做完,要么就没有执行。
i = 2; //读取操作,必定是原子性操作
j = i; //你以为是原子性操作,其实吧,分为两步,一是读取i的值,然后再赋值给j,这就是2步操作了,称不上原子操作
i++;
i = i + 1; // i++和i = i + 1其实是等效的,读取i的值,加1,再写回主存,那就是3步操作了
2、可见性: java就是利用volatitle来提供可见性。当一个变量呗volatitle修饰时,那么它的修改会立即刷新到主线程,当其他线程需要读取该变量时,会在内存中读取新值。普通变量则不能保证这一点。
其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。
**3、有序性:**代码有序执行
synchronized:
- 分为类锁,对象锁
- synchronized发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生
- 非公平锁,每次都是相互争抢资源。
- synchronized依赖JVM实现锁,该关键字作用对象的作用范围内同一时刻只能有一个线程进行操作
- 原理: synchronized通过对象的对象头(markword)来实现锁机制,java每个对象都有对象头,都可以为synchronized实现提供基础,都可以作为锁对象,在字节码层面synchronized块是通过插入monitorenter
monitorexit完成同步的。持有monitor对象,通过进入、退出这个Monitor对象来实现锁机制。
Lock
- JDK提供的代码层面的锁,依赖CPU指令,代表性是ReentrantLock。
- lock可以让等待锁的线程响应中断。在发生异常时,如果没有主动通过unLock()去释放锁,则可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
ThreadLocal
- 使用局部变量ThreadLocal实现线程同步,每个线程都会保存一份该变量的副本,副本之间相互独立,这样每个线程都可以随意修改自己的副本,而不影响其他线程
- 常用方法ThreadLocal()创建一个线程本地变量;get()返回此线程局部的当前线程副本变量;initialValue()返回此线程局部变量的当前线程的初始值;set(T
value)将此线程变量的当前线程副本中的值设置为value
Volatile
- 这要解释一下,它没有原子性,不能保证线程安全。但是有可见性。
- Java模型下,线程会把数据拷贝到本地内存(比如寄存器),而不是直接在主内存中读写,会造成A线程改了值,B线程还在用他拷贝的值。造成数据的不一致。
而Volatile修饰的会直接强制修改主内存。效率高,这样一个线程改了,其他线程立马可见。- 写操作的时候会多一条汇编代码,lock addl $0x0,lock前缀的指令在多核处理器下会将当前处理器缓存行的数据会写回到系统内存。这会导致cpu缓存无效,这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。同时lock前缀也相当于一个内存屏障,对内存操作顺序进行了限制。
总结:
写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量
synchronized与Lock的区别
五、线程间通信:
介绍线程间通信之前,在补充一点知识,如果停止线程?
- 定义变量 return
- thread.stop()
- thread.interrupt()
定义变量我就直接略过了,直接介绍stop
总结:会立即中断线程,很暴力中断,api已经废弃。不介意使用
interrupt中断
总结:
- 调用interrupt(),方法以后,不会马上中断,只是给线程设置中断状态,在run方法中,自行判断
- 判断方式有两种,第一种isInterrupt(),单纯判断状态,对线程没影响,第二种Thread.isInterrupt(),判断完状态以后,会把状态改为false。
- 解释一下我sleep(2000),如果在我代码执行到try那一行的时候,外部调用了interrupt,此时sleep会睡2s吗?答案是不是的。会被里面唤醒,执行return。
- 注意::::interrupt 也会唤醒wait(),下面我会详细解释到
线程通信案例:
总结:
- wait等待释放锁,回来之后是在wait,之后接着执行
- 为什么wait的时候用while,不用if,因为我上面解释过了,interrupt()这个方法也会把wait唤醒,所以要用while处理
- 调用wait 会立即不执行吗?答案是肯定的? notifyAll,也是释放锁有什么区别,它会立即不中断不执行吗?答案:不是的,调用notifyAll或者notify之后,会执行所同步的代码块,才会执行
画个图吧(解释一下获取锁的流程) 排队的A C,会听从CPU调度,并不是直接让C持有Moniter
按照A B C D线程顺序看就可以了。
join 插队
总结:可以让其他线程插队到自己前面执行
yeild介绍
让出自己的位置,让队列左边的先执行,至于是排在左边线程后面,还是排在队列最后面,跟CPU调度有关系