死锁问题
*
- 多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题,
- 即死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将
- 无法向前推进。
- 特殊的概念
- 饥饿指的线程无法访问到它需要的资源而不能继续执行时,引发饥饿最常见资源就是CPU时钟周期。虽然在
- Thread API中由指定线程优先级的机制,但是只能作为操作系统进行线程调度的一个参考,换句话说就是
- 操作系统在进行线程调度是平台无关的,会尽可能提供公平的、活跃性良好的调度,那么即使在程序中指定
- 了线程的优先级,也有可能在操作系统进行调度的时候映射到了同一个优先级。通常情况下,不要去修改线
- 程的优先级,一旦修改程序的行为就会与平台相关,并且会导致饥饿问题的产生。在程序中使用的
- Thread.yield或者Thread.sleep表明该程序试图客服优先级调整问题,让优先级更低的线程拥有被CPU调
- 度的机会。
- 活锁指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。尽管这个问题不会阻塞线程,但
- 是程序也无法继续执行。活锁通常发生在处理事务消息的应用程序中,如果不能成功处理这个事务那么事务
- 将回滚整个操作。解决活锁的办法是在每次重复执行的时候引入随机机制,这样由于出现的可能性不同使得
- 程序可以继续执行其他的任务。
- Java中活锁和死锁有什么区别?
- 活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥
- 饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让
- 的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改
- 变但是却不能继续执行。
- 怎么检测一个线程是否拥有锁?
- 在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象
- 的锁。
- 重点:### 死锁产生的必要条件
- 产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
- 1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程
- 所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进
- 程自己来释放(只能是主动释放)。
- 3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有
- ,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所
- 请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0,
- 1, ..., n-1),Pn等待的资源被P0占有。
- 如何避免死锁
- 加锁顺序(线程按照一定的顺序加锁,避免嵌套封锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- Java中如何获取到线程dump文件
- 死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线
- 程堆栈,获取到线程堆栈有两步:
- 1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
- 2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
- Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是
- 和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈
- 死锁检测
Jconsole
Jconsole是JDK自带的图形化界面工具,使用JDK的工具JConsole,可以通过打开cmd然后输入jconsole打开
jconsole就会给我们检测出该线程中造成死锁的线程,点击选中即可查看详情:
线程相关的模型
- 1)启动一个os的用户线程,然后实际的任何操作都直接对应该用户线程,这就是1:1,这样做之后,调度就由
- os负责,jvm就不管了,hotspot等主流jvm基本上都是这种做法
- 2)启动一个虚拟线程,然后执行的时候,交给os上的一个用户线程去执行,这样做的好处就是,jvm可以自己实
- 现调度,而且可以控制虚拟线程的大小,这就是n:m或者1:m,看具体的实现,而如果将线程虚拟化之后,调度就
- 可以由jvm来实现了,做成1:m还是n:m完全看jvm调度的实现,这就是协程,go其实是这种做法,早期的solaris
- 上的hotspot也是这种做法,后来改了,改成选择1的做法,这样在不同os上hotspot的实现就统一了
- volatile类型修饰符
-
- 工具需要自行下载安装
- 通过 hsdis 可以查看 Java 编译后的机器指令
HSDIS是一个Sun官方推荐的HotSpot虚拟机编译代码的反汇编插件,包含在HotSpot虚拟机的源码之中,但没
- 有提供编译后的程序。在Project Kenai的网站也可以下载到单独的源码
- 使用 jitwatch 工具,可以帮助分析该日志
HotSpot JIT编译器的日志分析器和可视化工具
- volatile用于修改属性,保证多线程访问属性时的可见性、顺序性,但是并不保证原子性
-
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立
- 即可见的。(实现可见性)--原理
-
- 禁止进行指令重排序。(实现有序性)
- volatile 只能保证对单次读/写的原子性。i++这种操作不能保证原子性
- volatile 变量的内存可见性是基于内存屏障Memory Barrier实现。内存屏障,又称内存栅栏,是一个CPU指令。
- 在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上
- 有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏
- 障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
- volatile的应用场景
- 只有在状态真正独立于程序内其他内容时才能使用 volatile
- 典型案例:双重检查
- 忽略volatile关键字程序也可以很好的运行,只不过代码的稳定性不是100%,说不定在未来的某个时刻,
- 隐藏的 bug 就出来了
两个重要的实现模型 AQS和CAS
AQS模型
AbstractQueuedSynchronizer抽象队列同步器:AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁
和相关的同步器的一个同步框架。AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待
队列(多线程争用资源被阻塞时会进入此队列)
抽象队列同步器中包括两个部分:临时资源和一个FIFO的CLH阻塞队列.
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资
源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,
这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
内部使用AQS的例子:以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用
tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到
state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁
的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能
回到零态的。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync; 锁的具体实现,依赖于同步器实现锁机制
内部类:
abstract static class Sync extends AbstractQueuedSynchronizer 同步器实际上就是AQS的一个子实现
static final class NonfairSync extends Sync非公平同步器的实现类
static final class FairSync extends Sync 公平同步器的实现类