→ 死锁
什么是死锁 、 死锁如何解决
“死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。”
当然死锁的产生是必须要满足一些特定条件的:
1.互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
2.请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3.不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4.循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
死锁的解除办法:
1、抢占资源。从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以解除死锁状态。
2、终止(撤销)进程:将一个或多个思索进程终止(撤销),直至打破循环环路,使系统从死锁状态解脱。
→ synchronized
synchronized 是如何实现的?
同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。
同步代码块
同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor ,重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。
值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
Java虚拟机对synchronized的优化: 锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到 高升级,不会出现锁的降级 。
【参考链接】https://blog.csdn.net/mingwulipo/article/details/87955187
synchronized 和 lock 之间关系、不使用 synchronized 如何实现一个线程安全的单例
C类应聘者可以想到饿汉单例模式
public class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
部分人还可以想到饿汉的变种:使用static定义静态成员变量或者静态代码,借助Class类的类加载机制实现线程安全单例。
public class Singleton{
private static instance = null;
static{
instance = new Singleton();
}
private Singleton(){}
public static getInstance(){
return this.instance;
}
}
B类应聘者:使用静态内部类
public class Singleton{
public static class SingletonInner{
private static final Singleton INSTANCE =new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonInner.INSTANCE;
}
}
这种方式相对前边有所优化,因为使用的是lazy-loading,Singleton类被加载了,但是INSTANCE并没有立即初始化,因为内部类SingletonInner没有被主动调用。只有调用getInstance方法时,才会加载内部类SingletonInner,从而实现实例化。
A类应聘者:使用枚举的方式。这种方式是Effictive Java的作者提倡的方式, 不仅能避免线程安全问题, 还能防止反序列化重新生成对象,作用非常强大。
public enum Singleton{
INSTANCE;
public void method(){
}
}
所谓ClassLoader的线程安全机制,就是ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字,所以,这个方法一般情况下都是线程同步的,除非被重写。
所以以上各种方法,虽然没有显示的使用synchronized, 但是在其底层还是使用了synchronized。
A+类应聘者:使用CAS乐观锁技术, 多个线程尝试使用CAS修改同一个变量的时候,只有其中一个能更新变量的值,其他线程全部失败,失败的线程并不会被挂起,而是被通知这次竞争失败,并且可以再次尝试。
public class Singleton{
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton(){}
public static Singleton getInstance(){
for(;;){
Singleton singleton = INSTANCE.get();
if(null != singleton){
return singleton;
}
singleton = new Singleton();
if(INSTANCE.compareAndSet(null ,singleton)){
return singleton();
}
}
}
}
用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。
另外,如果N个线程同时执行到singleton = new Singleton();的时候,会有大量对象创建,很可能导致内存溢出。
【参考链接】https://blog.csdn.net/singwhatiwanna/article/details/104568025
synchronized 和原子性、可见性和有序性之间的关系
synchronized与原子性
线程是CPU调度的基本单位。CPU有时间片的概念,根据不同的调度算法进行线程调度,当一个线程获取时间片后开始执行,时间片消耗完时失去CPU的使用权。所以在多线程情况下,时间片在线程之前轮换,就会发生原子性问题。
在Java中,为了保证原子性,提供了两个高级的字节码指令,monitorenter和monitorexit。这两个字节码指令,在Java中对应的关键字就是synchronized。
通过monitorenter和monitorexit指令可以保证被synchronnized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到,因此在Java中可以使用synchronized保证方法和代码块内的操作是原子性的。
synchronized与可见性
可见性是指,当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。
Java内存模型规定了所有的变量存在于主内存中,每个线程还是有自己的工作内存,线程的工作内存保存了该线程中使用到的变量的主存副本拷贝,线程对变量的所有操作必须在工作内存中进行,而不能直接读取主内存。不同线程之间也无法访问对方工作内的工作内存,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步,所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
被synchronized锁住的代码,在解锁之前必须把修改的变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。所以,使用synchronized关键字修锁住的对象,其值是具有可见性的。
synchronized与有序性
有序性即执行程序的顺序按照代码的先后顺序执行。
除了引入时间片概念以外,由于处理器优化和指令重排,CPU可以对输入的代码进行乱序执行,比如load->add->save有可能被优化成load-save-add。这可能就存在有序性问题。需要注意的是,synchronized并不能禁止指令重排和处理器优化的。所以为什么说synchronized也保证了有序性呢?
现在需要把有序性的概念扩展一下:Java中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程观察另外一个线程,所有操作都是无序的。这其实和as-if-serial
语义有关。as-if-serial
语义的意思是:不管怎么重排序,单线程程序的执行结果都不能被改变,编译器和处理器无论怎么优化,都必须遵守as-if-serial
语义。简单说就是,as-if-serial
语义保证了单线程中,指令重排是有一定限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。
所以呢,由于synchronized修饰的代码,同一时间只能被一个线程访问,那么也就是单线程执行的,所以,可以保证其有序性。
【参考链接】https://blog.csdn.net/qq_33173608/article/details/88202474
→ volatile
happens-before、内存屏障、编译器指令重排和 CPU 指令重排
happens-before:在JMM中,如果一个操作的执行结果需要对另一个程序可见,那么两个操作之间必然存在happens-before关系,这个的两个操作既可以在一个线程,也可以在两个不同的线程。
其规则如下:
- 程序次序规则:一个线程内,按照代码顺序,书写在前边的操作先行发生于写在后边的操作。
- 监视器锁规则:一个unlock操作先行发生于后边对同一个锁的unlock操作。
- volatile域规则:对一个变量的写操作先行发生于后边对这个变量的读操作,如果一个线程先去写一个变量,然后一个线程再去读取,那么写入操作必定先于读取操作。
- 传递规则:如果A happens-before B, 然后 B happens-before C,那么可以得出A happens-before C。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程结束规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 终结器规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
内存屏障(Memory Barrier):是让一个CPU处理单元中的内存状态对其他处理单元可见的一种。其实就是强制刷新出各种CPU cache。
读屏障:在读指令前插入读屏障,保证从主内存同步最新的数据。
写屏障:在写指令后插入写屏障,保证写入的数据立马对其他线程可见。
全能屏障:具备读写屏障的能力。
void cpu_01(){
value = 10;
// 在更新数据之前必须将所有存储缓存中的指令执行完毕
storeMemoryBarrier();
finished = true;
}
void cpu_02(){
while(!finished);
// 在读取之前将所有失效队列中关于该数据的指令执行完毕
loadMemoryBarrier();
assert value = 10;
}
Java内存模型volatile是基于Memory Barrier实现的。
如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,就可以保证:
一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。
指令重排:执行代码时jvm会进行指令重排序,处理器为了提高效率,可以对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是可以使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
常见的重排序有三个层面:
- 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
【参考链接】https://blog.csdn.net/weixin_44046437/article/details/99093145
https://www.jianshu.com/p/bf08e4546bfa
volatile 的实现原理
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,volatile关键字比synchronized的使用和执行成本更低,因为他不会引起上下文的切换和调度。
那么,volatile的实现原理是什么呢?也就是说volatile如何实现的可见性。在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU会做什么事情。http://ifeve.com/volatile/
Java代码 | instance = new Singleton();//instance是volatile变量 |
---|---|
汇编代码 | 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp); |
有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。
- 将当前处理器缓存行的数据会写回到系统内存。
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
总结下来就是:1、Lock前缀指令会引起处理器缓存回写到内存。2、一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
volatile 和原子性、可见性和有序性之间的关系
原子性:原子性就是指原子性的操作是不可中断的,要么全部执行,要么全部不执行。
volatile的两大特性是:1、保证变量在线程之间的可见性。可见性的保证是基于CPU的内存屏障指令,被JSR-133抽象为happens-before原则。2、阻止编译时和运行时的指令重排。编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。可见volatile肯定实现了可见性和有序性。
但是volatile没有实现原子性。volatile只对单个变量的读/写操作具有原子性,对于volatile++这种复杂操作不具有原子性,会因多个线程轮流执行产生线程安全问题,因为volatile并没有synchronized的字节码指令。
Synchronized和Volatile的比较 1)Synchronized保证内存可见性和操作的原子性 2)Volatile只能保证内存可见性 3)Volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。) 4)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化). 5)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。 volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。
有了 synchronized 为什么还需要 volatile
我们都知道synchronized其实是一种加锁机制,那么既然是锁,天然就具备:1 有性能损耗 2 产生阻塞 这些缺陷。
synchronize实现的锁本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。
而volatile是Java虚拟机提供的一种轻量级同步机制,他是基于内存屏障实现的。说到底,他并不是锁,所以他不会有synchronized带来的阻塞和性能损耗的问题。
所以从两个方面论述volatile的不可替代性:
① 一方面是因为synchronized是一种锁机制,存在阻塞问题和性能问题,而volatile并不是锁,所以不存在阻塞和性能问题。
② 另外一方面,因为volatile借助了内存屏障来帮助其解决可见性和有序性问题,而内存屏障的使用还为其带来了一个禁止指令重排的附件功能,所以在有些场景中是可以避免发生指令重排的问题的。
【参考链接】https://www.cnblogs.com/hollischuang/p/11386988.html
→ sleep 和 wait
sleep()和wait()的区别
- 属于不同的两个类,sleep()方法是线程类(Thread)的静态方法,wait()方法是Object类里的方法。
- sleep()方法不会释放锁,wait()方法释放对象锁,使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁)。
- wait(),notify()和notifyAll()只能在同步控制方法或者同步控制块里面使用,而sleep()可以在任何地方使用(使用范围)
- sleep()必须捕获异常,而wait(),notify()和notifyAll()不需要捕获异常
- sleep()使线程进入阻塞状态(线程睡眠),wait()方法使线程进入等待队列(线程挂起),也就是阻塞类别不同。
→ wait 和 notify
wait()和notify()的区别
wait()、notify()方法属于Object中的方法;对于Object中的方法,每个对象都拥有。
wait()方法:该方法用来使得当前线程进入等待状态,直到接到通知或者被中断打断为止。在调用wait()方法之前,线程必须要获得该对象的对象级锁;换句话说就是该方法只能在同步方法或者同步块中调用,如果没有持有合适的锁的话,线程将会抛出异常IllegalArgumentException。调用wait()方法之后,当前线程则释放锁。
notify()方法:该方法用来唤醒处于等待状态获取对象锁的其他线程。如果有多个线程,则线程规划器任意选出一个线程进行唤醒,使其去竞争获取对象锁,但线程并不会马上就释放该对象锁,wait()所在的线程也不能马上获取该对象锁,要程序退出同步块或者同步方法之后,当前线程才会释放锁,wait()所在的线程才可以获取该对象锁。
wait()方法是释放锁的;notify()方法不释放锁,必须等到所在线程把代码执行完。→ notify 和 notifyAll
Java提供了两个方法notify和notifyAll来唤醒在某些条件下等待的线程,你可以使用它们中的任何一个,但是Java中的notify和notifyAll之间存在细微差别,这使得它成为Java中流行的多线程面试问题之一。当你调用notify时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。虽然如果你调用notifyAll方法,那么等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁定,这就是为什么在循环上调用wait,因为如果多个线程被唤醒,那么线程是将获得锁定将首先执行,它可能会重置等待条件,这将迫使后续线程等待。因此,notify和notifyAll之间的关键区别在于notify()只会唤醒一个线程,而notifyAll方法将唤醒所有线程。
其实,一句话解释就是之所以我们应该尽量使用notifyAll()的原因就是,notify()非常容易导致死锁。当然notifyAll并不一定都是优点,毕竟一次性将Wait Set中的线程都唤醒是一笔不菲的开销,如果你能handle你的线程调度,那么使用notify()也是有好处的。
【参考链接】https://blog.csdn.net/u014658905/article/details/81035870
https://www.jianshu.com/p/25e243850bd2?appinstall=0