一、多线程的创建和运行
1、三种创建方式:
<1>继承Thread并且重写Thread的run方法
public static void method1(){
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.info("thread1");
}
};
t1.start();
}
<2>实现Runnable接口,重写run方法,并且将Runnable对象作为参数传入Thread中的。
public static void method2(){
Runnable runnable = new Runnable() {
@Override
public void run() {
log.info("thread2");
}
};
Thread t2 = new Thread(runnable,"t2");
t2.start();
}
<3>实现Callable接口,将Callable接口的对象作为参数传入FutureTask,FutureTask对象在传入Thread。Callable接口有返回值,并且可以通过FutureTask获取返回值。
public static void method3(){
Callable callable = new Callable() {
@Override
public Object call() throws Exception {
log.info("thread3");
return "hello";
}
};
FutureTask task = new FutureTask(callable);
Thread t3 = new Thread(task, "t3");
t3.start();
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
2、几种方式的联系。
最终运行的都是Thread中的Run方法,默认Run方法会调用Runnable对象的run方法。
public void run() {
if (target != null) {
target.run();
}
}
需要注意的是,FutureTask其实间接地实现了Runnable对象。start是开启一个线程,这个线程执行的内存就是run()方法。如果直接调用run方法,不会开启线程。
3、run()与start()的区别
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
4、sleep、yield、join、interrupt
<1>sleep:进入sleep的线程处于阻塞状态(Time_Waiting),阻塞期间内一定不会执行。需要在线程的run方法里面使用Thread.sleep(time)或者TimeUnit.SECONDS.sleep(1)来进行休眠。不需要注意的是,如果一个线程在sleep过程中被打断就会抛出异常。
<2>yield:让出CPU,由运行态(Running)转为就绪态(Runnable),这时线程还有可能被调度(执行)。
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(()->{
try {
log.info("t1线程运行");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} });
t1.start();
t1.yield();
System.out.println("执行完毕");
}
<3>join:使用了join方法的后面的代码会被阻塞直到目标线程执行完毕,效果好像是被join的线程加入了当前线程一样。如下,System.out.println("执行完毕")
只有在t1线程执行完毕后才会执行。这也是一种同步的方式。join可以设置等待时常,t1.join(1000);
在等待1秒后不论t1有没有执行完,后面的代码都会被执行。
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(()->{
try {
log.info("t1线程运行");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} });
t1.start();
t1.join();
System.out.println("执行完毕");
}
<4>interrupt:
打断sleep线程,sleep线程会结束阻塞状态而处于就绪状态,会清空打断标记(设计打断标记为false)并且会抛出异常。打断正在运行的线程,设置打断标记为true,不论打断多少次,都是true。
<5>两阶段终止模式:直接停止线程可能会造成已经申请的资源未被释放,而设置打断标记方便线程进行后续处理。如果在睡眠的时候被打断,需要重新打断。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
Thread cur = Thread.currentThread();
while(true){
if(cur.isInterrupted()){
System.out.println("后续处理");
break;
}
try {
System.out.println("休息一秒");
Thread.sleep(1000);
}catch (Exception e){
cur.interrupt();
}
}
}, "t");
t.start();
Thread.sleep(500);
t.interrupt();
}
5、守护线程:
在非守护线程执行完后,即使守护线程还没有执行完,守护线程也会停止。比如java的垃圾回收器,因为如果所有的线程都执行完了,那整个程序也就要停止了,也就没有必要再进行垃圾回收。
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(()->{
System.out.println("t1:" + Thread.currentThread().isInterrupted());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1:" + Thread.currentThread().isInterrupted());
});
//设置未守护线程
t1.setDaemon(true);
t1.start();
}
6、线程的五种状态与六种状态
<1>五种状态:从操作系统的角度而言,线程分为五种状态:新建,就绪,运行,阻塞,终止
<2>六种状态:java将线程分为了六种状态,将运行态(Running)、就绪态(Runnable)和部分阻塞状态合并为就绪态(Runnable),将操作系统中的阻塞状态细分为四部分:Time_Waiting、Waiting、Block、Runnable(除了前面三种情况其他的阻塞都被划分为Runnable)
调用了sleep方法后线程会进入Time_Waiting,调用了join后主线程会进入Waiting,如果使用了锁并且线程死锁后进入Block状态。
二、共享模型之管程
1、共享带来的问题:发生竞态条件
<1>临界资源:一次仅允许一个线程访问的资源
<2>临界区:访问临界资源的代码块
<3>竞态条件:多个线程在临界区执行,由于代码块执行顺序不同导致结果无法预测,称之为发生了竞态条件
2、互斥与同步
<1>互斥:同一时刻,只有一个线程执行临界区中的代码
<2>同步:多个线程安装特定的顺序执行代码片段
- 阻塞解决方案之synchronized:一个线程运行加了synchronized关键字的代码片段必须先获得锁。synchronized只能对对象加锁。
<1>作用在方法上
//同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
//等价于
public void test() {
synchronized(this) {}
}
//同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
//等价于
public static void test() {
synchronized(Test.class) {}
}
<2>作用在方法片段上
synchronized(obj) {}
3、synchronized的实现原理
<1>java对象结构:
java普通对象的结构
java数组对象的结构
32位Mark Word结构
64位Mark Word结构
<2>Monitor:java对象可以关联一个Monitor对象(由操作系统提供),synchronized有此来实现各种锁的机制。
<3>重量级锁:
加锁:在使用synchronized(obj)时,obj会关联一个Monitor,obj中的MarkWord存储Monitor的引用(Monitor也会把MarkWord中的内存存储起来)并且MarkWord的最后两位设置位00。如过Monitor中的Owner为空,则当前线程加锁成功并且将owner设置为当前线程;如果Owner不为空,则当前线程在EntryList中排队阻塞。
解锁:当前线程执行完毕后,设置Owner为null,Monitor会从EntryList中选一个线程作为新的Owner(这个过程不是公平的,也就是说来的早的线程不一定能够成为Owner)。
<4>轻量级锁:重量级锁的开销比较大,如果对线程对临界区的访问是错开的,那就没必要加重量级锁。
在使用synchronized(Object)时,会在线程的栈帧中创建一个锁记录的对象,锁记录对象存储了锁记录地址(lock record)和对象引用(Object reference)。
让对象引用指向Object,并且尝试使用cas交换锁记录地址和Object中的MarkWord。
交换成功,说明加轻量级锁成功。注意锁记录地址的最后两位是00,正好对应着轻量级锁。
如果cas失败则有两种情况:其他线程已经对Object加了轻量级锁,这时需要进行锁膨胀;本线程已经对Object加锁,这时需要进行锁重入。
<5>锁重入:当本线程已经对Object加锁之后,再加锁就会进行锁重入。会再线程的栈帧中再创建一个锁记录,锁记录对象引用指向Object,锁记录地址设置为null。
进行解锁时,如果锁记录地址为null,表示有锁重入,重置锁记录,锁记录地址减1。
进行解锁时,如果锁记录地址不为null,表示没有锁重入,使用cas交换锁记录中的的MarkWord和Object中的锁记录地址。交换成功说明解锁成功;交换失败说明轻量级锁已经进入了锁膨胀,升级为重量级锁,需要进入重量级锁解锁流程。
<6>锁膨胀:如果一个线程已经对Object加了轻量级锁,另一个对象再对它加轻量级锁就会失败,这时候需要进行锁膨胀,将轻量级锁升级为重量级锁。
加锁:为Object申请一个Monitor对象,将Object中锁记录地址更改为Monitor地址,Monitor中的Owner指向之前的线程,当前线程进入EntryList等待。
解锁:进行解锁时,尝试使用cas交换LockRecord中的MarkWord和Object中的Monitor地址,会失败,因为Monitor中的后两位是10已经不是轻量级锁的00。进入重量级锁的解锁流程,将Monitor中的Owner设置为null,并且从EntryList中的选一个线程作为新的Owner(猜测锁记录中的Object的MarkWord应该存入了Moitor)。
<7>自旋优化:在锁膨胀的过程,如果已经有线程加轻量级锁后,再有线程加轻量级锁就会将轻量级锁升级为重量级锁。在自旋优化后,如果之前有线程加了轻量级锁,并不会马上进行锁膨胀,而是多尝试几次加轻量级锁。
<8>偏向锁:偏向锁是对轻量级锁的优化。轻量级锁在没有竞争的时候(加锁的是自己),需要进行锁重入操作,锁重入的时候需要进行cas操作尝试交换锁记录中的锁记录地址和Object中的MarkWord,比较耗时。
加锁:而偏向锁再第一次进行加锁时,会用cas直接将线程ID放入Object中的MarkWord中(不会产生锁记录),后面再进行加锁的时候只需要判断MarkWord中的线程ID和自己的线程ID是否相同。线程ID会存放在MarkWord中的额前54位,biased_lock代表是不是使用偏向锁,1代表是,0代表不是。需要注意的是,如果启用了偏向锁biased_lock就会被设置位1,不论有没有对Object加锁,Object都处理使用偏向锁的状态,并且线程在执行完加锁片段后也不会主动撤销偏向锁。
禁用偏向锁:使用了hashCode()函数后就会禁用偏向锁,可以看到MarkWord不能同时放下hashCode和threadID并且偏向锁不会使用额外的空间来存放MarkWord。如果在添加偏向锁成功后调用了hashCode()则会直接升级为重量级锁。
撤销偏向锁:如果一个线程释放了偏向锁,并且这个释放锁的线程还在运行,另一个线程对Object加锁则会将偏向锁升级为轻量级锁;如果释放锁的线程停止运行,则不会升级为轻量级锁。
批量重偏向:(一个线程释同时对多个对象加锁,比如30个):如果一个线程释(Thread1)放了所有的偏向锁,并且这个释放锁的线程还在运行,另一个线程(Thread2)对这30个对象加锁,前20个对象会从偏向锁升级为轻量级锁而后面的对象都不会升级并且会重偏向于这Thread2。
批量撤销:当撤销偏向锁阈值超过 40 次(对同一个类的对象)后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
<9>锁消除:对于不会发生竞态条件的代码块,jvm会进行优化撤销添加的锁.
<10>锁粗化: 如果对一个代码片段频繁加锁(比如下面的操作),jvm会将多个加锁操作合并为一个加锁操作.
synchronized(this){
}
synchronized(this){
}
synchronized(this){
}
4、wait/notify:
当线程获得锁但加锁的代码块还不满足执行条件时,其他想要获得锁的线程就会被阻塞。使用wait可以使得不满足执行条件的线程进入WaitSet中等待,当使用notify唤醒这个线程时(使用notifyAll会唤醒所有的线程),这个线程会重新进入EntryList中阻塞。
<1>wait():是Object的方法,必须先获得这个Object的锁才可使用这个方法,调用wait()后,会将当前线程加入WaitSet中,并且会释放锁,直到有线程唤醒它。
<2>wait(long time):与wait()类似,但最多在WaitSet中等time毫秒。必须要获得Object的锁才能执行。
<3>notify():会唤醒在Object的WaitSet中等待的某一个线程。必须要获得Object的锁才能执行。
<4>notifyAll():会唤醒在Object的WaitSet中等待的所有线程。必须要获得Object的锁才能执行。
public class WaitNotify {
private static boolean hasCigarette = false;
public static void main(String[] args) {
Object room = new Object();
Thread t = new Thread(()->{
synchronized (room){
if(!hasCigarette){
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("没有烟,等一会");
}
if(hasCigarette){
System.out.println("有烟了,开始干活");
}
}
});
t.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
hasCigarette = true;
synchronized (room){
room.notify();
}
}
}
5、par/unpark:
执行park方法不需要加锁,并且不会释放锁。可以先执行unpark()方法,再执行park()方法时就不会暂停这个线程。
LockSupport.park(); //暂替执行当前线程
LockSupport.unpark(t); //唤醒指定线程
- 线程状态转换
6、活锁、死锁与饥饿
<1>活锁:任务没有被阻塞,由于条件没有被满足导致一直尝试并失败。
class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
Thread.sleep(200);
count--;
System.out.println("线程一count:" + count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
Thread.sleep(200);
count++;
System.out.println("线程二count:"+ count);
}
}, "t2").start();
}
}
<2>死锁:多个线程被阻塞,都在等待对方释放资源。
死锁产生的必要条件:
1.互斥:一个资源被一个线程占用时其他线程不能使用
2.不可剥夺:资源请求者不能强制从资源占有者手中夺取资源,只能等待对方释放资源
3.请求和保存:资源请求者在请求其他资源的时候同时保存对资源的占有
4.循环等待:存在一个循环对象,a请求b的资源,b请求a的资源
<3>饥饿:线程由于优先级太低始终得不到执行。
处理死锁的基本方法
- 预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件或其中的几个条件。预防死锁比较容易实现,但由于施加的条件过于严格可能会降低吞吐量。
<1>破坏请求和保持:在进程执行前必须一次性分配它所需要的资源,如果系统没有足够的资源分配给该进程就让该进程等待。
<2>破坏不可剥夺:一个进行申请的资源不被满足时,必须要释放它已经申请的资源。
<3>破坏循环等待:给资源的编号,并且按照编号排序,进程申请资源必须要按照编号从下到大顺序申请。 - 避免死锁:不去实现破坏死锁的四个条件,而是在资源分配的过程中用某种方法避免系统进入不安全状态(安全状态就是不会发生死锁的状态)。
1、银行家算法基本流程:
<1> T 0 T_0 T0时刻判断当前系统是否处于安全状态,如果处于安全状态就执行步骤<2>也就是能否找到一个安全序列使得当前进程安全执行,如果不处于安全状态就停止进程执行并且通过挂起某些进程来使得当系统处于安全状态,当系统处于安全状态时执行步骤<2>。
<2> T 1 T_1 T1时刻有进程对系统提出请求,判断请求的资源是否小于该进程需要的资源并且小于系统拥有的资源量,如果满足执行步骤<3>如果不满足则不分配。
<3>系统尝试将资源 P i P_i Pi分配给该进程并且通过安全性算法检测资源分配后系统是否处于安全状态,如果处于就分配如果不处于就不分配。
2、银行家算法的结构:
资源名 | 含义 |
---|---|
Available | 可利用资源向量,Available[i]表示可利用的第i类资源量 |
MAX | 最大需要矩阵,MAX[i][j]表示第j个线程需要多少第i类资源量 |
Allocation | 资源分配矩阵,Allocation[i][j]表示已经分配给第j个线程多少第i类资源 |
Request | 请求向量,表示第i个线程需要的各类资源的数目 |
Nee | 需求矩阵,Need[i][j]表示第j个线程需要的第i类资源的数目,Need[i][j] = MAX[i][j] - Request[i] |
算法
假设进程Pi提出资源请求Request[i] = k
(1)若Request[i]<= Need[i,j]便转向执行步骤 (2) ;否则认为出错。
(2)若Request[i]<= Available[i,j]便转 向执行步骤(3);否则表示系统尚无足够的资源分配,让Pi等待。
(3)系统假设将Pi所要求的资源分配给Pi,并对数据结构做如下修改:
Avaliable[j] =Available[j] - Request[j]
Allocation[i,j] = Allocation[i,j] - Request[j]
Need[i,j] = Need[i,j]- Request[j]
(4)系统执行安全性算法,检测此次资源分配后,系统是否处于安全状态。若安全才正式将资源分配给进程Pi,以完成本次分配;否则,本次试探分配废,恢复原来的资源分配状态,让Pi等待。
3、安全性算法
数据结构:
资源名 | 含义 |
---|---|
Work | Work[i]表示系统可以提供的第i类资源的数目,初始Work = Avaliable |
Finish | 结束向量,表示是否有足够的资源分配给向量,使之运行。初始Finsh[i]=false,有足够资源分配时Finish[i]=true |
算法:
<1>找到一个满足如下条件的线程,如果能找到执行步骤<2>,如果找不到执行步骤<3>
Finish[i] = false && Need[j][i] <= work[j]
<2>满足上面条件的进程获取资源后执行如下代码,然后转向步骤<1>。
Finish[i] = true;
work[j] = Allocate[i][j] + work[j]
<3>如果所有的Finsih[i]都为true,则处于安全状态,否则不处于安全状态。
总结一下:先将资源分配给一个能够满足其所有请求的线程,这个线程执行完后就释放所有资源,然后再找下一个能够执行完的线程,重复,如果所有线程都能执行完说明是安全的,否则不安全。
- 检测死锁:检测发生与死锁有关的进程和资源。
- 解除死锁:通过挂起或撤销进程来回收资源,然后将资源分配给一些阻塞的进程使之成为就绪态。
8、ReentrantLock:
可重入锁。可重入锁指的是一个线程可以对一个对象重复加锁。不可重入则指的是一个线程只能对一个对象加一次锁,再次加锁就会被锁住。
相比于synchronized,ReentrantLock具有以下特点:可打断(synchronized不可打断),可设置超时时间(不是锁的有效时间,而是尝试获取锁如果一段时间获取失败则放弃获取),可以设置为公平锁(先申请获得锁的对象会先获得锁),支持多个条件变量(也就是有多个WaitSet)。
1.可打断:lock.lockInterruptibly()设置可打断,它会先去尝试获得锁,如果获取锁成功则向下执行,如果获取锁失败则会阻塞。在阻塞过程中,如果有线程执行了interrupt()方法,那它就会停止阻塞并抛出异常。
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.info("启动...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.info("等锁的过程中被打断");
return;
}
try {
log.info("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.info("获得了锁");
t1.start();
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
log.info("执行打断");
} finally {
lock.unlock();
}
}
2.可设置超时时间:使用trylock()方法尝试获取锁,如果获取锁失败则直接执行后面的代码,也可以设置超时时间,如果超过一定时间还没有获取成功则直接执行后面的代码。
ReentrantLock lock = new ReentrantLock();
Thread t = new Thread(()->{
try {
if(lock.tryLock(1, TimeUnit.SECONDS)){
System.out.println("尝试加锁成功");
}else{
System.out.println("尝试加锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
lock.lock();
t.start();
3.设置为公平锁:ReentrantLock lock = new ReentrantLock(true);
4.支持多条件变量:类似与synchronized中的WaitSet,但ReentrantLock支持多个WaitSet,也必须要先加锁成功才能使用
ReentrantLock lock = new ReentrantLock();
Condition room1 = lock.newCondition();
Condition room2 = lock.newCondition();
Thread t1 = new Thread(()->{
try{
lock.lock();
System.out.println("休息一会");
room1.await();
System.out.println("继续执行");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
});
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
lock.lock();
room1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
三、共享模型之内存
1、JMM:
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念。JMM 体现在以下几个方面:
<1>原子性 - 保证指令不会受到线程上下文切换的影响
<2>可见性 - 保证指令不会受 cpu 缓存的影响
<3>有序性 - 保证指令不会受 cpu 指令并行优化的影响
2、可见性:
<1>产生的原因:对于主存中的共享变量,每个内存会在工程内存的高速缓存中保存一份拷贝,下次使用是直接去高速缓存中取。当有其他线程修改了主存中的共享变量时,当前线程使用的仍然是旧值。
<2>解决方案:使用volatile修饰共享变量或者使用synchronized关键字多需要读取的片段加锁,会强制线程去主存中取共享变量。
3、有序性:
<1>产生的原因:在不影响执行结果的情况下,jvm会对指令进行重排。对于线程2,ready=2在指令重排后可能先于num=2执行,在执行完read=true后执行线程1会得到num=0。
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
<2>指令重排:指令执行分为取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回,对不不同的命令来说上面的步骤可以并行执行,为了提高效率会对指令进行重排。在单线程下指令重排没有问题,但多线程下指令重排可能导致出错。
<3>内存屏障:
1.可见性:写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中;而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
2.有序性:写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后;读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
<4>volatile的原理:volatile在写操作的后面会添加写屏障,在读操作之前添加读屏障。
<5>解决方案:用volatile修饰变量
<6>MESI(缓存一致性协议):
(1):一个线程读主村中内容到CPU中,这是CPU中的数据状态为独享
(2):再有线程读,CPU中的数据状态变为共享状态
(3)当cpu2对数据进行修改时,cpu2中缓存变量的状态变为修改状态。
(4)修改完成后向总线发通知,cpu1嗅探到后,cpu1缓存中数据的状态变为无效,然后重新取数据。
缺陷:可能会导致总线风暴,如果有cpu不断地写,那其他cpu就需要不断地读,因此要控制volatile变量地个数。
4、happens-before:
一套规则,满足这套规则的中的一条可以使得线程的写操作对其他线程的读操可见。
<1>线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m) {
x = 10;
}
},"t1").start();
new Thread(()->{
synchronized(m) {
System.out.println(x);
}
},"t2").start();
<2>线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
new Thread(()->{
x = 10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
<3>线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
<4>线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
<5>线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
sleep(1);
x = 10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
<6>对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
<7>具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;
},"t1").start();
new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},"t2").start();
5、synchronized和volatile的区别
<1>synchronized可以保证原子性,可见性,有序性。
一个线程使用synchronized对一个变量加锁后,会对这个变量加总线锁,是的线程独占这个变量,并且会情况工作内存中内容,因此可以实现可见性。这里的有序性指定的是加锁代码块中的代码执行是有序的,因为加锁之后类似于单线程执行这个代码块。synchronized不能禁止指令重排。
<2>volatile可以保证可见性,有序性但不能保证原子性
四、共享模型之无锁
1、实现方式:cas+volatile
<1>基本步骤:在更改共享变量前先记录共享变量的值,进行共享变量的更改,在将更改后的值写回主内存时将更改前共享变量的值与现在的值比较,如果相同则将值写回主存,否则不写回。为了保证每次获取的都是最新值,需要保证可见性,所以需要加volatile。注意cas是个原子操作。
<2>java中实现cas操作:java中的unsafe对象提供了cas操作,但unsafe对象需要使用反射获取。
2、juc提供的无锁并发工具类:
<1>AtomicBoolean、AtomicInteger、AtomicLong。三者用法相似,下面以AtomicInteger进行说明。
private static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 100; i++){
new Thread(()->{
increment();
}).start();
}
for (int i = 0; i < 100; i++){
new Thread(()->{
decrement();
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num.get());
}
public static void increment(){
while(true){
int pre = num.get();
int next = pre + 1;
if(num.compareAndSet(pre, next)){
break;
}
}
}
public static void decrement(){
while(true){
int pre = num.get();
int next = pre - 1;
if(num.compareAndSet(pre, next)){
break;
}
}
}
<2>AtomicReference、AtomicMarkableReference、AtomicStampedReference,当变量类型是引用类型时,用来对引用变量进行多线程的安全操作。注意它不能保证引用指向的对象是线程安全的。
public class UnlockReference {
//将一个引用保护起来
AtomicReference<BigDecimal> balance;
public UnlockReference(BigDecimal balance){
this.balance = new AtomicReference<>(balance);
}
public void withdraw(BigDecimal amount){
BigDecimal pre = balance.get();
BigDecimal next = pre.subtract(amount);
balance.compareAndSet(pre, next);
}
}
<3>ABA问题:在进行cas操作时一个共享变量与之前的值比较没有改变可能有两种情况,1.这个变量真的没有变;2.变了之后又修改回来,比如A->B->A。
<4>ABA问题解决方法:对于引用类型的变量,Java提供了AtomicStampedReference和AtomicMarkableReference。
1.AtomicStampedReference:提供了版本号,通过判断版本号和之前的版本号是否相同即可。不仅能知道是否改变,还能得到改变的次数。
public class UnlockReference {
//将一个引用保护起来
AtomicStampedReference<BigDecimal> balance;
public UnlockReference(BigDecimal balance){
this.balance = new AtomicStampedReference<BigDecimal>(balance, 0);
}
public void withdraw(BigDecimal amount){
BigDecimal preRef = balance.getReference();
int preStamp = balance.getStamp();
BigDecimal nextRef = preRef.subtract(amount);
int nextStamp = preStamp + 1;
balance.compareAndSet(preRef, nextRef, preStamp, nextStamp);
}
}
2.AtomicMarkableReference,只判断是否改变,用了一个标记位,如果当前标记位与期望的标记位相同则认为没有改变。
public class UnlockReference {
//将一个引用保护起来
AtomicMarkableReference<BigDecimal> balance;
public UnlockReference(BigDecimal balance){
this.balance = new AtomicMarkableReference<BigDecimal>(balance, true);
}
public void withdraw(BigDecimal amount){
BigDecimal preRef = balance.getReference();
BigDecimal nextRef = preRef.subtract(amount);
Boolean preMark = balance.isMarked();
balance.compareAndSet(preRef, nextRef, true, false);
}
}
<5>AtomicReference实现多变量的CAS操作以及AtomicStampedReference的实现原理
1、AtomicReference实现多变量的cas操作
CAS一次只能更新一个变量,那如何保证多变量更新的原子性呢?简单,直接将多个变量放在一个对象中,然后通过更新这个对象来更新多个变量值。
// 声明一个AtomicReference,封装Demo对象的
private static AtomicReference<Demo> reference = new AtomicReference(new Demo());
// 将value1、value2、value3封装为Demo对象的属性
public static class Demo {
public int value1 = 0;
public int value2 = 0;
public int value3 = 0;
}
public void update(){
Demo expected;
Demo update;
do {
expected = reference.get();
update = new Demo();
update.value1 = expected.value1 + 1;
update.value2 = expected.value2 + 1;
update.value3 = expected.value2 + 1;
} while (!reference.compareAndSet(expected, update));
}
2、AtomicStampedReference实现:和上面介绍的实现多变量CAS的原理类似,AtomicStampedReference将对象引用和stamp组成了一个pair,在调用compareAndSet方法时,先判断数据是否被修改,如果被修改直接返回false,否则创建一个pair并且尝试修改。
public class AtomicStampedReference<V> {
//定义Pair
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
//将引用和Stamp封装成一个Pair
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
//先将之前的Pair值
Pair<V> current = pair;
return
//如果和期望的reference不同或者和期望的stamp不同说明Pair已经被修改直接返回false
expectedReference == current.reference &&
expectedStamp == current.stamp &&
//已经修改为期望值返回true,否则重新创建一个Pair然后修改引用
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
}
<6>AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray:保证整个数组的每个元素都是线程安全的。
//创建一个数组
AtomicIntegerArray array;
public UnlockReference(int len){
this.array = new AtomicIntegerArray(len);
}
public void withdraw(int amount){
//获取第0个元素
int pre = array.get(0);
//修改第0个元素
int next = pre - amount;
array.compareAndSet(0, pre, next);
}
<7>AtomicReferenceFieldUpdater 、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater:字段更新,保证对象中的某个字段在多线程的环境下是安全的(前提是这个对象的引用没有变),整个字段需要加volatile关键子。
public class UnlockReference {
volatile int num = 0;
public static void main(String[] args) {
AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(UnlockReference.class, "num");
UnlockReference obj = new UnlockReference();
int pre = obj.num;
int next = pre - 1;
updater.compareAndSet(obj, pre, next);
System.out.println(obj.num);
}
}
<8>LongAdder, DoubleAdder:对AtomicInteger、AtomicLong的改进,它创建了一个cell[]数组,每个线程在cell[]数组不同的位置上进行累加,最后再将累加结果汇总。性能得到了极大的提升
五、共享模型之不可变
1、DateTimeFormatter:
SimpleDateFormat是非线程安全的而DateTimeFormatter:SimpleDateFormat是一个不可变类,是安全的。
2、不可变的基本实现方式:
对变量加final防止修改,对类加final防止基础并重写父类方法,保护性拷贝。
3、无状态:
类似于controller,service等对象都是无状态的。
4、final的原理
<1>设置final变量:对final变量赋值比如final num = 3;时会在后面加上写屏障,防止出现指令重排和变量内容修改的不可见
七、共享模型之工具
1、线程池:
创建线程需要占用资源、花费时间,直接创建线程可能造成资源耗尽,并且也不便于管理线程。java线程池使用了生产者、消费者模式,只需要向线程池提供需要运行的任务,不需要手动地创建线程。
<1>线程池的继承关系:
<2>线程池的状态:
ThreadPoolExecutor用int类型的高3位表示状态后面的29位表示线程数
<3>ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize:核心线程数,maximumPoolSize:最大线程数,keepAliveTime:救急线程的生存时间,unit:时间的单位,workQueue:任务队列,threadFactory:线程工厂,handler:拒绝策略
<4>基本原理:
使用了生产者,消费者设计模式。
主线程向BlockingQueue中放入任务,ThreadPoolExecutor中的线程消费任务。当放入任务时,会首先创建核心线程(核心线程不会销毁,直到整个BlockingQueue终结,并且核心线程的创建是延迟的,创建ThreadPoll后不会创建核心线程,有任务时才会创建)。
如果BlockingQueue有界,并且在Blocking已经满了之后仍然有任务,就创建救急线程来处理任务,救急线程的数目不会超过maximumPoolSize - corePoolSize。如果救急线程空闲时间超过keepAliveTime,救急线程就会被销毁。
如果救急线程也处理不完任务,就会调用handler进行处理。
<5>拒绝策略
jdk提供的四种策略:
ThreadPoolExecutor.AbortPolicy:默认拒绝策略,直接抛出异常
ThreadPoolExecutor.CallerRunsPolicy: 让调用者运行任务
ThreadPoolExecutor.DiscardPolicy: 放弃本次任务
ThreadPoolExecutor.DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
Dubbo: 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方
便定位问题
Netty: 的实现,是创建一个新线程来执行任务
ActiveMQ: 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
PinPoint: 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
<6>Executors:jdk提供了Executors类,里面包含了几种静态方法用来创建ThreadPoolExecutor。
public class Executors {
//创建固定大小的ThreadPoll,没有救急线程,并且阻塞队列无解,可以放任意多的任务
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
//没有核心线程,全部都是救急线程(生存时间60秒),采用了SynchronousQueue(一个没有容量的对象),因此来了一个任务就要创建一个线程,线程数目最大位Integer.MAX_VALUE
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//只有一个核心线程,没有救急线程,队列无界,当核心线程因为异常等原因停止运行后会重新创建一个线程。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
}
为什么不建议使用Executors中的方法创建对象:对于newFixedThreadPool()方法来说,创建的线程都是核心线程,如果线程数不够,任务会得不到及时处理,如果核心线程数设置过大,可能会导致线程的浪费;对于newCachedThreadPool(),其中的最大线程数设为了Integer.MAX_VALUE,如果并发情况下会创建过多线程;对于newSingleThreadExecutor()方法,只创建一个线程,可能会出现线程不够用,任务得不到及时处理。
<7>ThreadPoolExecutor中的方法
/**执行任务的方法**/
// 执行任务,任务类型位Runnable
void execute(Runnable command);
// 执行任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
/ 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException
/**线程池状态控制**/
//线程池状态变为SHUTDOWN
void shutdown();
//线程池状态变为STOP,返回值位队列中的任务
List<Runnable> shutdownNow();
<8>ScheduledThreadPoolExecutor:任务调度线程池,控制线程的执行顺序。在ScheduledThreadPoolExecutor之前只能用Timer控制,但Timer是串行执行的。
public static void main(String[] args) throws Exception{
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.info("time task1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.info("time task2");
}
};
Timer timer = new Timer();
//延时一秒执行
timer.schedule(task1, 1000);
//延时两秒执行,但必须在task1执行完后才执行
timer.schedule(task2, 2000);
}
ScheduledThreadPoolExecutor中提供了定期执行,定时执行等方法
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
- ForkJoinPool:JDK1.7加入的性的线程池的实现,将一个任务拆分为多个任务,每个任务分配一个线程,体现了一种分治的思想。
ForkJoinPool需要和RecursiveTask配合使用,需要手动实现里面的compute方法并对任务进行拆分。
public class ThreadPool {
public static void main(String[] args) throws Exception{
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new Task(5)));
}
}
@Slf4j
class Task extends RecursiveTask<Integer>{
private int n;
public Task(int n){
this.n = n;
}
@Override
public String toString() {
return "{" + n + "}";
}
@Override
protected Integer compute() {
if(n == 1){
log.info("join() {}", n);
return n;
}
Task task = new Task(n - 1);
task.fork();
log.info("fork() {} + {}", n, task);
int result = n + task.join();
log.debug("join() {} + {} = {}", n, task, result);
return result;
}
2、J.U.C
<1>AQS:AbstractQueuedSynchronizer。阻塞式锁和相关同步器类的框架,特点:
1.用整型变量state表示资源的状态
2.提供了FIFO等待队列,类似于Monitor中的EntryList。
3.条件变量实现等待唤醒机制,支持多个条件变量,类似于Monitor中的WaitSet。
<2>用到AQS的并发工具类
<3>ReentrantLock实现原理:
1.尝试获得锁的原理:NonfairSync基础自AQS,ReentrantLock中维护了NonfairSync的一个实列,exclusiveOwnerHead指向获得锁的线程,没有获得锁的线程加加入到阻塞队列的尾部。阻塞队列的头节点是一个哑元节点,标志位为-1说明当前节点有义务唤醒后面的节点。哑元节点的后面一个节点会先被唤醒,也就是先进先出的序列唤醒节点。state = 1表示锁有线程占有,state = 0表示锁没有线程占有。
2.释放锁的原理:如果线程Thread_0执行完毕,exclusiveOwnerHead执行Thread-1,哑元节点被删除,哑元节点后面的一个节点的存储内存被设置为空。
3.可重入原理:当一个线程重复加锁时,state会不断加1,再解锁是state减1,如果state=0则代表释放锁。
4.公平锁与非公平锁
如果一个线程释放了锁,并且没有新的线程竞争锁,这时候无论公平还是非公平锁都会按照FIFO原则,选择哑元节点后面的一个节点的线程获取锁(也就是第一进入阻塞队列的线程)。
如果一个线程释放了锁,于此同时来了一个新的线程竞争锁,非公平锁会让新来的线程获得锁(避免线程上下文切换),而非公平锁则会让新来的线程进入阻塞队列排队等待并且让第一个进入阻塞队列的线程获得锁。
5.条件变量的实现:每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject。调用了wait后,线程就会进入ConditionObject阻塞队列,调用了signal后,线程从ConditionObject阻塞队列删除并且加入NonfairSync阻塞队列末尾。
<4>读写锁:ReentrantReadWriteLock,提供读锁ReentrantReadWriteLock.ReadLock和写锁ReentrantReadWriteLock.WriteLock w。加了读锁之后,所有线程都不能再加写锁(包括加锁的线程自己);加了写锁之后,其余线程都不可再加读锁(但自己可以加读锁)。读写锁都支持锁重入。
1.基本用法:
public static void main(String[] args) {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rw.readLock();
ReentrantReadWriteLock.WriteLock writeLock = rw.writeLock();
new Thread(()->{
writeLock.lock();
try {
System.out.println("线程1加读锁成功");
writeLock.lock();
try {
System.out.println("线程1加写锁成功");
}finally {
writeLock.unlock();
}
}finally {
writeLock.unlock();
}
}).start();
}
2.锁降级:前面已经说了,同一个线程获取写锁之后可以再获得读锁,如果先释放写锁那么就只剩下读锁,这也叫对应着锁降级。由于获取读锁之后不能再获取写锁,自然也就不存在锁升级。
public static void main(String[] args) {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rw.readLock();
ReentrantReadWriteLock.WriteLock writeLock = rw.writeLock();
new Thread(()->{
writeLock.lock();
try {
System.out.println("线程1加读锁成功");
readLock.lock();
try {
System.out.println("线程1加写锁成功");
}finally {
writeLock.unlock();
}
}finally {
readLock.unlock();
}
}).start();
}
3.基本原理:读锁和写锁使用了同一个NonfairSync对象,写锁占了state的低16位而读锁占用了state的高16位。加锁与解锁的流程都与ReentrantReadWriteLock类似。
<5>StampedLock,在读写锁的基础上实现乐观读。每次写操作都会更新时间戳,在进行乐观读的时候,比较读之间的时间戳和读之后的时间戳是否相同,相同说明在读的过程中数据没有变。
public class StampedLockTest {
public static StampedLock lock = new StampedLock();
public static int i = 0;
public static int read(){
//获取当前锁的时间戳,并且加乐观锁
long stamp = lock.tryOptimisticRead();
//时间戳没有改变,说明数据没有变
if(lock.validate(stamp)){
return i;
}
//否则升级锁为读锁
try{
//读锁
stamp = lock.readLock();
return i;
}finally {
//释放锁
lock.unlockRead(stamp);
}
}
public void write(){
//获取写锁,并且记录时时间戳
long stamp = lock.writeLock();
try{
i++;
}finally {
//释放锁并且更改版本号
lock.unlockWrite(stamp);
}
}
<6>Semaphore:信号量,用来控制访问共享资源(指的就是信号量)的线程数目。可以设置state的值,当有一个线程获得共享资源时state减1,如果state=0则将想要获得共享资源的线程阻塞。
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for(int i = 0; i < 5; i++){
int j = i;
new Thread(()->{
try {
semaphore.acquire();
log.info("thread " + j + " run");
}catch (Exception e){
e.printStackTrace();
}
try {
Thread.sleep(10);
log.info("thread " + j + " end");
}catch (Exception e){
e.printStackTrace();
}finally {
semaphore.release();
}
}).start();
}
}
<7>CountdownLatch:用来进行线程协作。由一个初始值,如果这个初始值被减为0则唤醒在等待的进程。
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for(int i = 0; i < 3; i++){
int j = i;
new Thread(()->{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("线程 " + j + " 执行完毕");
latch.countDown();
}).start();
}
latch.await();
log.info("主线程执行");
}
<8>CyclicBarrier:也维护了一个计数器,调用await()方法计数器会减1,并且线程会等待,当计数器为0时线程往下执行,并且将计数器重新设置为初始值。
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3);
ExecutorService service = Executors.newFixedThreadPool(3);
for(int i = 0; i < 3; i++){
int j = i;
service.submit(()->{
log.info("thread " + j + "开始");
try {
barrier.await();
log.info("thread " + j + "结束");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
}
<9>LinkedBlockingQueue与ArrayBlockingQueue。
1.LinkedBlockingQueue:本质上一个含有哑元节点的双向链表,它的put(),添加元素到队尾,和take(),取走队首元素,方法是线程安全的。在队列元素个数大于等于2时,在put()方法中对哑元节点加锁,在take()方法中在队尾元素加锁,由次来保证添加、删除元素是线程安全的,并且提高了效率。如果队列的元素等于1,take操作会被阻塞。
2.ArrayBlockingQueue
内部维护了一个ReentrantLock对象,put()和take()都使用的是这把锁。
3.ArrayBlockingQueue,与LinkedBlockingQueue基本操作类似,主要不同如下:
Linked 支持有界,Array 强制有界
Linked 实现是链表,Array 实现是数组
Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
Linked 两把锁,Array 一把锁
<10>ConcurrentLinkedQueue:与LinkedBlockingQueue基本操作类似,不同的是采用cas来保证线程安全,而非采用锁。
<11>CopyOnWriteArrayList与CopyOnWriteArraySet
1.CopyOnWriteArrayList:实现了读写分离,读操作直接在原始数组上进行,无需加锁,写操作时需要加锁,将元素数组复制一份,然后再复制的数组上进行写操作。
2.CopyOnWriteArraySet:持有一个CopyOnWriteArrayList对象,只不过它在添加元素时会进行去重操作。
3.弱一致性:CopyOnWriteArrayList进行读写分离时,由于读操作在原始数组上进行,写操作在复制的数组上进行,二者的数据可能不同步。
七、并发编程之模式
1、保护性暂停:
<1>单任务的保护性暂停:一个线程产生的结果传递到另一个线程,定一个中间对象GuardedSuspension,线程向其中写数据并且唤醒读线程;对于读线程,如果没有数据则等待。future的实现类就采用这种模式
public class GuardedSuspension {
private Object object;
public Object get(){
synchronized (this){
while (object == null){
try {
this.wait();
}catch (Exception e){
e.printStackTrace();
}
}
return object;
}
}
public void complete(Object object){
synchronized (this){
this.object = object;
this.notifyAll();
}
}
}
<2>多任务的保护性暂停,定义一个类包含多个GuardedSuspension对象,并且每个GuardedSuspension对象都有一个id,每个任务利用id来找到对应的GuardedSuspension对象。
public class GuardedSuspension {
private Object object;
private int id;
public GuardedSuspension(int id) {
this.id = id;
}
public int getId() {
return id;
}
public Object get(){
synchronized (this){
while (object == null){
try {
this.wait();
}catch (Exception e){
e.printStackTrace();
}
}
return object;
}
}
public void complete(Object object){
synchronized (this){
this.object = object;
this.notifyAll();
}
}
}
class Mailboxes {
private static Map<Integer, GuardedSuspension> boxes = new Hashtable<>();
private static int id = 1;
// 产生唯一 id
private static synchronized int generateId() {
return id++;
}
public static GuardedSuspension getGuardedObject(int id) {
return boxes.remove(id);
}
public static GuardedSuspension createGuardedObject() {
GuardedSuspension go = new GuardedSuspension(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
2、同步模式之Balking
Balking(犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。比如多线版本的单例模式:
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(INSTANCE != null)
return INSTANCE;
INSTANCE = new Singleton();
return INSTANCE;
}
}
3、同步模式之顺序控制:控制线程以规定的顺序运行
<1>.使用wait notify
public class Order {
public static boolean runnable;
public static void main(String[] args) {
new Thread(()->{
synchronized (Object.class){
while (!runnable){
try {
Object.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1运行");
}
}).start();
new Thread(()->{
synchronized (Object.class){
runnable = true;
System.out.println("线程1可以运行了");
Object.class.notifyAll();
}
}).start();
}
}
<2>.使用park unpark
public class Order {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
//后执行,需要线程2唤醒
LockSupport.park();
System.out.println("线程1运行");
});
Thread t2 = new Thread(() -> {
LockSupport.unpark(t1);
System.out.println("线程1可以运行了");
});
t1.start();
t2.start();
}
}
4、同步模式之循环:几个线程安装顺序交替执行
<1>使用wait notify
public class Cycle {
private int flag;
private int loopNum;
public Cycle(int flag, int loopNum) {
this.flag = flag;
this.loopNum = loopNum;
}
public void print(int waitFlag, int nextFlag, String str){
for(int i = 0; i < 5; i++){
synchronized (this){
while (flag != waitFlag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str);
flag = nextFlag;
this.notifyAll();
}
}
}
public static void main(String[] args) {
Cycle cycle = new Cycle(1, 5);
new Thread(()->{
cycle.print(1, 2, "a");
}).start();
new Thread(()->{
cycle.print(2, 3, "b");
}).start();
new Thread(()->{
cycle.print(3, 1, "c");
}).start();
}
}
<2>使用ReentrantLock 的多条件变量
public class Cycle {
private ReentrantLock lock;
public Cycle(ReentrantLock lock) { this.lock = lock; }
public void print(Condition cur, Condition next, String str){
for(int i = 0; i < 5; i++){
lock.lock();
try {
//当前线程阻塞
cur.await();
log.info(str);
//唤醒下一个线程
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Cycle cycle = new Cycle(lock);
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
new Thread(()->{
cycle.print(condition1, condition2, "a");
}).start();
new Thread(()->{
cycle.print(condition2, condition3, "b");
}).start();
new Thread(()->{
cycle.print(condition3, condition1, "c");
}).start();
lock.lock();
condition1.signal();
lock.unlock();
}
}
<3>使用par unpark
public class Cycle {
private int loopNumber;
private Thread[] threads;
public Cycle(int loopNumber) {
this.loopNumber = loopNumber;
}
public void setThreads(Thread... threads) {
this.threads = threads;
}
private Thread next() {
Thread current = Thread.currentThread();
int index = 0;
for (int i = 0; i < threads.length; i++) {
if(threads[i] == current) {
index = i;
break;
}
}
if(index < threads.length - 1) {
return threads[index+1];
} else {
return threads[0];
}
}
public void print(String str) {
for (int i = 0; i < 5; i++) {
//当前线程阻塞
LockSupport.park();
log.info(str);
//唤醒下一个线程
LockSupport.unpark(next());
}
}
public static void main(String[] args) {
Cycle cycle = new Cycle(3);
Thread t1 = new Thread(() -> {
cycle.print( "a");
});
Thread t2 = new Thread(() -> {
cycle.print("b");
});
Thread t3 = new Thread(() -> {
cycle.print("c");
});
cycle.setThreads(t1, t2, t3);
t1.start();t2.start();t3.start();LockSupport.unpark(t1);
}
}
5、异步模式之生产者消费者:
不同于保护性暂停,生产者消费者产生结果和消费结果不需要一一对应,生产者只需要将生产的东西放入队列,消费者只需要从队列中取。
public class MessageQueue {
private LinkedList<Object> queue;
private int capacity;
public MessageQueue(int capacity){
this.queue = new LinkedList<>();
this.capacity = capacity;
}
public void put(Object msg){
synchronized (queue){
while (capacity == queue.size()){
log.info("队列已满");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(msg);
queue.notifyAll();
}
}
public Object take(){
synchronized (queue){
while (queue.isEmpty()){
log.info("队列已空");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object node = queue.poll();
queue.notifyAll();
return node;
}
}
}
6、异步模式之工作线程:
为每一个类型的任务(比如说点餐和做饭)创建一个线程池,避免出现线程饥饿。
public class TestDeadLock {
static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
static Random RANDOM = new Random();
static String cooking() {
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) {
ExecutorService waiterPool = Executors.newFixedThreadPool(1);
ExecutorService cookPool = Executors.newFixedThreadPool(1);
waiterPool.execute(() -> {
log.debug("处理点餐...");
Future<String> f = cookPool.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
waiterPool.execute(() -> {
log.debug("处理点餐...");
Future<String> f = cookPool.submit(() -> {
log.debug("做菜");
return cooking();
});
try {
log.debug("上菜: {}", f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
}
7、终止模式之两阶段终止模式:
从外部给一个提示,提示当前线程可以停止了,当前线程收到提示后进行善后操作。
<1>利用interrupt标记
public static void main(String[] args) {
Thread t = new Thread(()->{
Thread cur = Thread.currentThread();
while (true){
if(cur.isInterrupted()){
log.info("善后操作");
break;
}
try {
Thread.sleep(1000);
}catch (Exception e){
cur.interrupt();
}
}
});
t.start();
t.interrupt();
}
<2>使用停止标记
public class InterruptedTest {
static boolean stop = false;
public static void main(String[] args) {
Thread t = new Thread(()->{
Thread cur = Thread.currentThread();
while (true){
if(stop ){
log.info("善后操作");
break;
}
try {
Thread.sleep(1000);
}catch (Exception e){
cur.interrupt();
}
}
});
t.start();
stop = true;
t.interrupt();
}
8、单例模式:
饿汉单例和懒汉单例
<1>饿汉单例:在类加载的时候就会将单例对象创建好。
//使用final防止基础该类
public final class Singleton1 implements Serializable {
//标志位,判断对象是否已经创建
private static boolean flag = false;
//不可用public,防止别人修改对象的内容,该语句要位于flag的下面,否则flag会重新被设置为false
private static final Singleton1 singleton = new Singleton1();
//防止使用反射破话单例
private Singleton1() {
synchronized (Singleton1.class) {
if (flag) {
throw new RuntimeException("不能重复创建对象");
}
flag = true;
}
}
public static Singleton1 getInstance() {
return singleton;
}
//如果不提供readResolve,在反序列化时会重新创建一个对象,提供了则使用这个方法提供的对象。
public Object readResolve() {
return singleton;
}
}
枚举也是单例的并且可以防止使用反射、序列号等破坏单例
enum Singleton {
INSTANCE;
}
<2>懒汉式:
1.直接在获取方法前加synchronized ,并发度低。如果已经创建对象,在获取时仍然要加锁
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析这里的线程安全, 并说明有什么缺点
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
2.双重检查,先检查对象是否创建,创建了则返回。否则加锁执行创建,在创建之前还要判断对象是否创建,因为在并发的情况下,有可能有多个线程都认为对象没有创建(注意第一个判断语句并没有加锁)。还要使用volatile 修饰被创建的对象,防止发生指令重排,先赋值,再创建对象的发生。
public class Singleton implements Serializable {
private static volatile Singleton singleton;
private static boolean flag = false;
private Singleton(){
if(flag) {
throw new RuntimeException("不能重复创建对象");
}
flag = true;
}
public static synchronized Singleton getSingleton(){
if(singleton == null)
singleton = new Singleton();
return singleton;
}
public static Singleton getInstance2(){
if(singleton != null)
return singleton;
synchronized (Singleton.class) {
if(singleton == null)
singleton = new Singleton();
return singleton;
}
}
public Object readResolve() {
return singleton;
}
}
3.使用内部静态类,只有在用到内部的静态内时才会加载这个类,这样也是懒汉式的。
public final class Singleton {
private Singleton() {
synchronized (Singleton.class) {
if(flag){
throw new RuntimeException("不能重复创建对象");
}
flag = true;
}
}
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
9、享元模式:
当需要重用数量有限的同一类对象时,会缓存这些对象。
<1>Long的valueOf方法,如果数值在 -128~127则会缓存,否则不会。还有String的串池,BigDecimal, BigInteger等。
<2>数据库的连接池:
class Pool {
// 1. 连接池大小
private final int poolSize;
// 2. 连接对象数组
private Connection[] connections;
// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;
// 4. 构造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i + 1));
}
}
// 5. 借连接
public Connection borrow() {
while (true) {
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if (states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 如果没有空闲连接,当前线程进入等待
synchronized (this) {
try {
log.debug("wait...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 6. 归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
synchronized (this) {
log.debug("free {}", conn);
this.notifyAll();
}
break;
}
}
}
}
class MockConnection implements Connection {
// 实现略
}
}
八、ThreadLocal
1、基本用法:
public static void main(String[] args) {
ThreadLocal local = new ThreadLocal();
Thread t1 = new Thread(()->{
local.set("线程1");
System.out.println("线程1:" + local.get());
});
Thread t2 = new Thread(()->{
local.set("线程2");
System.out.println("线程2:" + local.get());
});
t1.start(); t2.start();
}
2、ThreadLocal基本结构:
<1>在jdk1.8之前,ThreadLocal维护了一个map,map的键是线程的引用,map的值是需要存储的值
<2>在jdk1.8之后,map不再由ThreadLocal维护,而是由每个线程维护,map中的键指向ThreadLocal(这里的引用是弱引用),值就是要存储的值。
在调用ThreadLocal的set方法时,先获取当前线程,然后尝试从当前线程中获取map,如果没有则创建一个。然后向map中添加一个键值对,键就是ThreadLocal本身的引用,值就是要放的值。
public void set(T value) {
Thread t = Thread.currentThread();
//获取当前线程存放的map
ThreadLocalMap map = getMap(t);
if (map != null)
//调用map的set方法
map.set(this, value);
else
//创建map并且添加键值对
createMap(t, value);
}
在调用get方法时,先获取当前线程,在获取当前线程中的map,再从map中的去取对应的值。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
<3>hash碰撞解决方法:ThreadLocalMap使用开放寻址法而不是链地址法寻址,假设key进行散列后得到的地址是i,如果第i个位置已经被使用则寻址i+1,再被使用则i+2,直到找到一未被使用的位置。
<4>解决内存泄漏:内存泄漏指的是本不会再被使用的对象由于编码的问题而不会被垃圾回收。如下图,此时threadLocal对象已经被垃圾回收,Entry中的value也就不会再被使用(这时由于要使用threadLocal对象的get方法才能取出对象),但只要这个线程还存活,里面的map也就还在引用则里面Entry,导致对应的Entry不能被回收。
map中的Entry的键使用的弱引用而不是强引用,在threadLocal不再被强引用引用时,gc垃圾回收时就会将threadLocal对象回收,map中的ThreadLocalRef指向的就是null。在执行诸如get等方法时,在进行寻址的过程中(ThreadLocalMap使用的线性探索法找key对应的Entry),会将key位null的Enry进行回收,这样一定程度上解决了内存泄漏。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
<5>让线程维护threadLocalMap的优点:
1.线程中的ThreadLocal数目一般比线程数少,这样每个线程的ThreadLocalMap就比较小,减少了hash碰撞的概率
2.这样设计可以在一定程度上减少内存泄漏
3、ThreadLocal的用途:
在spring的声明式事务中,我们在dao中某个方法加了@Transactional注解之后,在dao中的方法之前需要获取数据库连接,而在dao的方法中也需要获取数据库连接。一般dao的方法中都没有设置参数传入数据库连接,但却能正常工作。这就用到了ThreadLocal,数据库的连接放在了ThreadLocal中,只需要去ThreadLocal中去取即可。