Java并发编程(三)-锁
参考文献
- 锁的简单应用
- 图解Java中那18 把锁
- <<Java并发编程艺术>>
- Synchronization
锁
锁的分类
- 分类标准把锁分为以下 7 大类别
- 偏向锁/轻量级锁/重量级锁;
- 可重入锁/非可重入锁;
- 共享锁/独占锁;
- 公平锁/非公平锁;
- 悲观锁/乐观锁;
- 自旋锁/非自旋锁;
- 可中断锁/不可中断锁.
使用锁的目的
- 锁是用来控制多个线程访问共享资源的方式.一个锁能够防止多个线程同时访问共享资源.
死锁
产生死锁的原因
- 当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,同时不放弃自己拥有的资源;
如何预防死锁
- 只有以下这四个条件都发生时才会出现死锁:
- 互斥:共享资源X和Y只能被一个线程占用;
- 占有且等待: 线程T1已取得共享资源X,在等待共享资源Y的时候,不释放共享资源X;
- 不可抢占: 其他线程不能强行抢占线程T1占有的资源;
- 循环等待: 线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待.
- 即破坏其中一个,就可以成功避免死锁的发生
- 对于"占用且等待"这个条件,可以一次性申请所有资源,这样就不存在等待了.
- 对于"不可抢占"这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破了.
- 对于"循环等待"这个条件,可以按序申请资源来预防.所谓按序申请是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的.
- 当多个线程持有不同的锁并试图获取对方持有的锁时,就会发生死锁问题.以下是一些避免死锁问题的方法:
- 避免多个锁的嵌套使用.如果必须使用多个锁,则应该确定所有线程获取锁的顺序,并确保所有线程以相同的顺序获取锁.
- 使用可重入锁.可重入锁可以允许同一个线程多次获取同一个锁,因此不会因为自身持有锁而导致死锁.
- 使用定时锁.在使用锁的时候,可以设置一个超时时间,如果在规定时间内无法获取锁,则放弃锁的获取,避免因等待过长时间而引起死锁.
- 使用并发集合类.Java中的
ConcurrentHashMap
和ConcurrentSkipListMap
等并发集合类可以避免锁的嵌套使用,从而减少死锁的可能性. - 尽量避免在持有锁的情况下调用外部方法.这可能会导致锁的嵌套,从而增加死锁的风险.
- 避免在线程持有锁的情况下阻塞.如果一个线程持有锁并在等待某个资源时阻塞了,那么其他线程也就不能进入临界区,这可能会导致死锁.
定位死锁
- 检测死锁可以使用
jconsole
工具或者jps
定位进程id,再用jstack
定位死锁.- 使用
jps
查看运行的Java
进程:jps -l
- 使用
jstack
查看线程堆栈信息:jstack -l 进程id
- 使用
jstack
命令:jstack
是 Java 虚拟机提供的命令行工具,它可以用于打印出虚拟机中所有线程的堆栈信息,如果某些线程处于BLOCKED
(阻塞)状态,并且相互之间形成了闭环,就表示发生了死锁.通过jstack
命令输出的信息可以帮助我们定位死锁问题.jconsole
工具:jconsole
是 Java 虚拟机提供的一款监控工具,它可以用于检查 Java 程序的运行状态,包括线程状态.在jconsole
中,我们可以查看所有线程的状态,如果发现有多个线程处于BLOCKED
状态,并且相互等待对方释放锁,就很可能发生了死锁.
活锁
- 出现在两个线程互相改变对象的结束条件,最终导致谁也无法结束.
- 活锁往往出现在多个线程之间协调处理共同任务时,由于过度的互动而发生.比较常见的例子是在“礼让”同步算法中出现活锁的问题.在这种算法中,一个线程让另一个线程先执行,但当两个线程都使用相同策略(如同时放弃执行并重新尝试)时,就会导致活锁的出现.
- 为避免活锁的出现,可以使用以下几种方法:
- 引入随机因素:在线程的等待时间或其他操作中加入一些随机因素,以避免多个线程以相同的方式相互作用.
- 使用时间戳:每个线程使用不同的时间戳来控制自己的行为,可以避免多个线程同时执行相同的操作.
- 调整算法:尝试调整算法,减少线程的相互依赖性,避免线程之间的反复交互.
- 降低负载:在高负载的情况下,线程相互作用的频率较高,更容易导致活锁的出现.因此,可以通过减少负载来降低线程之间的互动次数,从而减少活锁发生的可能性.
饥饿
-
一个线程由于优先级太低,始终得不到CPU调用执行,也不能够结束.
-
解决办法: 顺序加锁
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 HoleLin's Blog!