image.png

1.锁特性

公平锁、非公平锁

公平锁指多个线程按照申请锁的顺序来获取锁。 等待锁的线程不会饿死。等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。 image.png
非公平锁就是没有顺序完全随机,所以能会造成优先级反转或者饥饿现象。非公平锁的吞吐量性能比公平锁好。
image.png
synchronized 就是非公平锁,ReentrantLock(使用 CAS 和 AQS 实现) 通过构造参数可以决定是非公平锁还是公平锁,默认构造是非公平锁。

乐观锁、悲观锁

悲观锁认为对于同一个数据的并发操作一定是会发生修改的,因此悲观锁采取加锁的形式,悲观锁认为不加锁的操作一定有问题。
image.png
乐观锁则认为对于同一个数据的并发操作是不会发生修改的,在更新数据的时候会采用不断的尝试更新。
image.png
悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景 。synchronized、ReentrantLock是悲观锁,CAS 是乐观锁。

独享锁和共享锁

独享锁是指该锁一次只能被一个线程持有。
image.png
共享锁指该锁可以被多个线程持有。
image.png
synchronized 和 ReentrantLock 都是独享锁,ReadWriteLock 的读锁是共享锁、写锁是独占锁。ReentrantLock 的独享锁和共享锁都是通过 AQS 来实现的。

可重入锁、不可重入锁

在同一个线程在外层方法获取锁的时候在进入内层方法会自动获取锁,synchronized 和 ReentrantLock 都是可重入锁,可重入锁可以在一定程度避免死锁。
不可重入锁是不可递归调用,递归调用就发生死锁。

互斥锁、读写锁

互斥锁是在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。加锁后任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
读写锁既是互斥锁,又是共享锁。read模式是共享,write是互斥。 ReadWriteLock是读写锁

可中断锁

可中断建立在阻塞等待中断,运行中是无法中断的。 synchronized 是不可中断的,Lock 是可中断的。

偏向锁、轻量级锁、自旋锁、重量级锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
自旋锁是当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

分段锁

实质是一种锁的设计策略,不是具体的锁。 分段锁的设计是为了细化锁的粒度。 对于ConcurrentHashMap 而言其并发的实现就是通过分段锁的形式来实现高效并发操作。当要 put 元素时并不是对整个 hashmap 加锁,而是先通过 hashcode 知道它要放在哪个分段,然后对分段进行加锁,所以多线程 put 元素时只要放在的不是同一个分段就做到了真正的并行插入,但是统计 size 时就需要获取所有的分段锁才能统计。

2.功能

隐式锁

synchronized这个特性是隐式锁,隐式的加锁与释放锁。

显式锁

image.png

1.死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

发生必要条件

  • 资源互斥:一个资源只能被一个进程使用
  • 请求与保持:当一个进程因请求资源而阻塞的时候,保持已获得资源不放
  • 不剥夺:进程已获得资源,在未使用完成之前,不能被其他进程强行剥夺
  • 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系
  1. public static void main(String[] args) {
  2. final Object a = new Object();
  3. final Object b = new Object();
  4. Thread threadA = new Thread(new Runnable() {
  5. public void run() {
  6. synchronized (a) {
  7. try {
  8. System.out.println("now i in threadA-locka");
  9. Thread.sleep(1000l);
  10. synchronized (b) {
  11. System.out.println("now i in threadA-lockb");
  12. }
  13. } catch (Exception e) {
  14. // ignore
  15. }
  16. }
  17. }
  18. });
  19. Thread threadB = new Thread(new Runnable() {
  20. public void run() {
  21. synchronized (b) {
  22. try {
  23. System.out.println("now i in threadB-lockb");
  24. Thread.sleep(1000l);
  25. synchronized (a) {
  26. System.out.println("now i in threadB-locka");
  27. }
  28. } catch (Exception e) {
  29. // ignore
  30. }
  31. }
  32. }
  33. });
  34. threadA.start();
  35. threadB.start();
  36. }

预防死锁

1.避免一个线程同时获取多个锁。
2.避免在一个资源内占用多个 资源,尽量保证每个锁只占用一个资源。
3.尝试使用定时锁,使用tryLock(timeout)来代替使用内部锁机制。
4.对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
5.避免同步嵌套的发生

死锁检测

Jstack命令

jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。
Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

jps -l:查看当前线程信息
jstack xxxx:查看进程的堆栈信息

Jconsole工具

Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。

以确定的顺序获得锁

针对两个特定的锁,开发者可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,那么死锁也不会发生。

超时放弃

当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。