什么是线程死锁?死锁产生的条件是什么?如何避免死锁?
- 什么是线程死锁?
- 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
- 死锁产生的条件是什么?
- 互斥条件 :该资源任意一个时刻只由一个线程占有
- 请求与保持条件 : 一个线程/进程因请求资源时对已经获得的资源保持不放
- 不剥夺条件 : 线程/进程已经获得的资源在未使用之前不能被其他线程/进程强行剥夺 (只有自己使用完毕主动释放)
- 循环等待条件 : 若干线程/进程之间形成一种头尾相接的循环等待资源关系
- 如何避免死锁?
- 破坏互斥条件:无法破坏,因为使用锁的本意就是想让它们互斥的(临界资源需要互斥访问)
- 破坏请求与保持条件:一次性申请所有的资源
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
- 破坏循环等待条件:按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件(最常用)
synchronized 和 java.util.concurrent.locks.Lock 的异同
- 相同点 : Lock 能完成 synchronized 所实现的所有功能
- 不同点 : Lock 有比 synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized 会自动释放锁,而 Lock 则要求手工释放。具体:
- 含义层面:Synchronized 是关键字,属于 JVM 层面,底层是通过 monitorenter 和 monitorexit 完成,依赖于 monitor 对象来完成;Lock 是 java.util.concurrent.locks.lock 包下的,是 JDK1.5 以后引入的新 API 层面的锁;
- 使用方法层面 :Synchronized 不需要用户手动释放锁,代码完成之后系统自动让线程释放锁;ReentrantLock 需要用户手动释放锁,没有手动释放可能导致死锁 (Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方))
- 等待是否可以中断 :Synchronized 不可中断,除非抛出异常或者正常运行完成;ReentrantLock 可以中断。一种是通过 tryLock (long timeout, TimeUnit unit),另一种是 lockInterruptibly () 放代码块中,调用 interrupt () 方法进行中断
- 加锁是否公平 :Synchronized 是非公平锁;ReentrantLock 默认非公平锁,可以在构造方法传入 boolean 值,true 代表公平锁,false 代表非公平锁
浅谈AtomicInteger类
- 介绍: 内部使用CAS原语处理加减等操作,CAS :Compare And Swap(比较与交换)。通过判断内存某个位置的值是否与预期值相等,如果相等则进行值更新。(CAS 是内部是通过 Unsafe 类实现,而 Unsafe 类的方法都是 native 的,在 JNI 里是借助于一个 CPU 指令完成的,属于原子操作)
- 缺点:
- 循环开销大,如果CAS失败会一直尝试,如果长时间CAS不成功会对CPU带来很大的开销。
- 保证单个共享变量的原子操作,对于多个共享变量,CAS是无法保证的。
- 存在ABA问题 ( ABA 问题一般通过版本号的机制来解决。每次变量更新的时候,版本号加 1,这样只要变量被某一个线程修改过,该版本号就会发生递增操作。 JDK 从 1.5 开始提供了 AtomicStampedReference 类来解决 ABA 问题,具体操作封装在 compareAndSet () 中。compareAndSet () 首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值)
sleep() 方法和 wait() 方法区别和共同点?
- sleep()方法没有释放锁,而 wait()方法释放了锁
- 两者都可以暂停线程的执行
- wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行
- wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒
JDK1.6 之后的synchronized 关键字底层做了哪些优化
-
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
1. 偏向锁:无竞争条件下,消除整个同步互斥,连CAS都不操作。消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。即在无竞争的情况下,把 整个同步都消除掉。这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁 没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。
2.轻量级锁:无竞争条件下,通过CAS消除同步互斥,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
3.自旋锁:为了减少线程状态改变带来的消耗,不停地执行当前线程;
4.自适应自旋锁:自旋的时间不固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 如果一个锁对象,自旋等待刚刚成功获得锁,并且持有锁的线程正在运行,那么虚拟机认 为这次自旋仍然可能成功,进而运行自旋等待更长的时间。 如果对于某个锁,自旋很少成功,那在以后要获取这个锁,可能省略掉自旋过程,以免浪费处理器资源。 有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会 越来越准确,虚拟机也会越来越聪明。
5.锁消除:不可能存在共享数据竞争的锁进行消除;
6.锁粗化:将连续的加锁,精简到只加一次锁。原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和 解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。 锁粗化就是增大锁的作用域; -
其中,锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
自旋锁解决的问题,原因及缺点 ?
- 解决的问题: 互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来很大压力。同时很多应用共享数据的锁定状态,只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得,从而引入自旋锁
- 原理: 如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。为了让线程等待,我们只需让线程执行一个忙循环(自旋)
- 缺点: 自旋等待本身虽然避免了线程切换的开销,但它要占用处理器时间。所以如果锁被占用的时间很短,自旋等待的效果就非常好;如果时间很长,那么自旋的线程只会白白消耗处理器的资 源。所以自旋等待的时间要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁, 那就应该使用传统的方式挂起线程了
为什么基于volatile变量的运算在并发下不一定是安全的 ?
- volatile变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中volatile变 量,每次使用前都要刷新到主内存)。但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。
synchronized关键字的作用?
- synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
- 在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为**监视器锁(monitor)**是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销
synchronized关键字如何使用?
- 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
- 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到普通方法上是给对象实例上锁。(尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能)
synchronized 关键字的底层原理是什么?
synchronized 关键字底层原理属于 JVM 层面
- synchronized 同步语句块的情况。 synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
- synchronized 修饰方法的的情况。 synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用
synchronized 关键字和 volatile 关键字的区别?
-
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量,而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升
-
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
-
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证
-
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性