乐观锁 CAS(Compare And Swap)

乐观锁是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改操作(所以没有上锁)。写数据时,判断当前与期望值是否相同,如果相同则进行更新(更新期间加锁,保证原子性)。
Java中的乐观锁:CAS,比较并替换,比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作。

悲观锁 synchronized、vector、hashtable

悲观锁是一种悲观思想,即认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为其他线程会发生修改操作,所以每次读写数据时都会上锁。其他线程如果想要读写这个数据时,会被这个线程block,直到这个线程释放锁然后其他线程才能获取到锁。
Java中的悲观锁:synchronized修饰的方法和方法块,ReentrantLock。

自旋锁 CAS

自旋锁是一种技术:为了让线程等待,我们只需让线程执行一个忙循环(自旋)。
优点:避免了线程切换的开销。挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给JVM的并发性能带来了很大的压力;
缺点:占用处理器的时间,如果占用时间过长,会白白消耗处理器资源,而不会做任何有价值的工作,带来性能浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过限定次数仍然没有成功获取锁,就应当使用传统的方式挂起线程。
自旋次数默认值:10次,使用参数-XX:PreBlockSpin来修改;
自适应自旋:自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,VM对程序锁的状态会越来越精准。
Java中的自旋锁:CAS操作中的比较操作失败后的自旋等待。

可重入锁 synchronized、Reentrantlock、Lock 递归锁

可重入锁是一种技术:任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞;
原理:通过组合自定义同步器来实现锁的获取与释放。
再次获取锁:识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。获取锁后,进行计数自增,释放锁时,进行计数自减。
可重入锁的作用:避免死锁。
Java中的可重入锁:ReentrantLock、synchronized中修饰的方法或代码段;
image.png

读写锁 ReentrantReadWriteLock、CopyOnWriteArrayList、CopyOnWriteArraySet

读写锁是一种技术,通过ReentrantReadWriteLock类来实现。为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁与写锁,多个读锁不互斥,读锁与写锁互斥,由JVM控制。
读锁:允许多个线程获取读锁,同事访问同一个资源;
写锁:只允许一个线程获取写锁,不允许同时访问同一个资源。
Java中的读写锁:ReentrantReadWriteLock

公平锁 Reentrantlock(true)

公平锁是一种思想:多个线程按照申请锁的顺序来获取锁。在并发环境中,每个线程会先查看此锁维护的等待队列,如果当前等待队列为空,则占有锁,如果等待队列不为空,则加入到等待队列的末尾,按照FIFO的原则从队列中拿到线程,然后占有锁。

非公平锁 synchronized、Reentrantlock(false)

非公平锁是一种思想:线程尝试获取锁,如果获取不到,则再采用公平锁的方式。多个线程获取锁的顺序,不是按照先到先得的顺序,有可能后申请锁的线程比先申请的线程优先获取锁。
优点:非公平锁的性能高于公平锁;
缺点:有可能造成线程饥饿(某个线程很长一段时间获取不到锁);
Java中的非公平锁:synchronized是非公平锁,ReentrantLock通过构造函数指定锁是公平锁还是非公平锁,默认是非公平锁。

共享锁 ReentrantReadWriteLock中读锁

共享锁是一种思想:可以有多个线程获取读锁,以共享的方式持有锁。和乐观锁、读写锁同义。
Java中的共享锁:ReentrantReadWriteLock。

独占锁 synchronized、vector、hashtable、ReentrantReadWriteLock中写锁

独占锁是一种思想:只有一个线程获取锁,以独占的方式持有锁。和悲观锁、互斥锁同义。
Java中的独占锁:synchronized、ReentrantLock。

重量级锁 synchronized

重量级锁是一种称谓,synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统(Operation System OS)的Mutex Lock来实现。OS实现线程的切换需要从用户态切换到核心态,成本非常高。这种依赖于OS Mutex Lock 来实现的锁称为重量级锁。为了优化synchronized,引入了轻量级锁,偏向锁。
Java中的重量级锁:synchronized

轻量级锁 锁优化技术

轻量级锁是JDK6时加入的一种锁优化机制:轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是你想对于使用OS互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用OS互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况下,那轻量级锁将不会有效,必须膨胀为重量级锁。
优点:如果没有竞争,通过CAS操作成功避免了使用互斥量的开销;
缺点:如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。

偏向锁 锁优化技术

偏向锁是JDK6时加入的一种锁优化机制:在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏是指偏心,他的意思是这个锁会偏向于第一个获取它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对mark Word的更新操作等)。
优点:把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。
缺点:如果程序中大多数的锁都总数被多个不同的线程访问,那偏向锁就是多余的。

分段锁 ConcurrentHashMap

分段锁是一种机制:最好的例子来说明分段锁是ConcurrentHashMap。
ConcurrentHashMap原理:它内部细分为若干个小的HashMap,称之为段(segment)。默认情况下一个ConcurrentHashMap被进一步细化分为16个段,即这就是锁的并发度。如果需要在ConcurrentHashMap添加一项key-value,并不是将整个HashMap加锁,而是首先根据hashcode得到该key-value应该存在哪一段,然后对该段加锁,并完成put操作。在多线程环境下,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做好真正的并行。
线程安全:ConcurrentHashMap是一个Segment数组,Segment通过集成ReentrantLock来进行加锁,所以每次需要加锁的操作锁住的是一个segment,这样只需保证每个Segment是线程安全的,也就是实现了全局的线程安全。

互斥锁 synchronized

互斥锁与悲观锁、独占锁同义,表示某个资源只能被一个线程访问,其他线程不能访问。
读-读互斥
读-写互斥
写-写互斥
写-读互斥
Java中的同步锁:synchronized

同步锁 synchronized

同步锁与互斥锁同义,表示并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。
Java中的同步锁:synchronized

死锁 互相请求对方的资源

死锁是一种现象:如线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己只有的资源,则两个线程都获取不到对方的资源,就会造成死锁。
Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应。所以一定要注意程序的并发场景,避免造成死锁。

  1. p
  2. ackage com.myself.learn.lock;
  3. import java.util.concurrent.TimeUnit;
  4. /**
  5. * @author zhangxiaofan
  6. * @date 2021/12/18 11:29
  7. */
  8. public class DeathLock {
  9. public static void main(String[] args) {
  10. String lockA = "lockA";
  11. String lockB = "lockB";
  12. new Thread( new MyThread( lockA , lockB ) , "T1" ).start();
  13. new Thread( new MyThread( lockB , lockA ) , "T2" ).start();
  14. }
  15. }
  16. class MyThread implements Runnable{
  17. private String lockA;
  18. private String lockB;
  19. public MyThread( String lockA , String lockB ) {
  20. this.lockA = lockA;
  21. this.lockB = lockB;
  22. }
  23. @Override
  24. public void run() {
  25. synchronized ( lockA ){
  26. System.out.println( Thread.currentThread().getName() + "lock:" + lockA + ",want to get " + lockB );
  27. try {
  28. TimeUnit.SECONDS.sleep( 3 );
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. synchronized ( lockB ){
  33. System.out.println( Thread.currentThread().getName() + "lock:" + lockB + ",want to get " + lockA );
  34. }
  35. }
  36. }
  37. }

死锁问题排查

查日志

使用jps -l 查看定位进场号,jstack + 进程号 排查 tips:服务在运行中使用

jps -l信息

image.png

  1. 10112 com.myself.learn.lock.DeathLock
  2. 1936 sun.tools.jps.Jps
  3. 3040 org.jetbrains.idea.maven.server.RemoteMavenServer36
  4. 12828
  5. 9868 org.jetbrains.jps.cmdline.Launcher

jstack信息

image.png

  1. 2021-12-18 11:36:18
  2. Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.261-b12 mixed mode):
  3. "DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000015383bc9800 nid=0x5f0 waiting on condition [0x0000000000000000]
  4. java.lang.Thread.State: RUNNABLE
  5. "T2" #13 prio=5 os_prio=0 tid=0x00000153a0aa8000 nid=0x1da0 waiting for monitor entry [0x00000051453ff000]
  6. java.lang.Thread.State: BLOCKED (on object monitor)
  7. at com.myself.learn.lock.MyThread.run(DeathLock.java:40)
  8. - waiting to lock <0x000000076c3e1610> (a java.lang.String)
  9. - locked <0x000000076c3e1648> (a java.lang.String)
  10. at java.lang.Thread.run(Thread.java:748)
  11. "T1" #12 prio=5 os_prio=0 tid=0x00000153a0aa7800 nid=0x990 waiting for monitor entry [0x00000051452fe000]
  12. java.lang.Thread.State: BLOCKED (on object monitor)
  13. at com.myself.learn.lock.MyThread.run(DeathLock.java:40)
  14. - waiting to lock <0x000000076c3e1648> (a java.lang.String)
  15. - locked <0x000000076c3e1610> (a java.lang.String)
  16. at java.lang.Thread.run(Thread.java:748)
  17. "Service Thread" #11 daemon prio=9 os_prio=0 tid=0x00000153a08a9800 nid=0xccc runnable [0x0000000000000000]
  18. java.lang.Thread.State: RUNNABLE
  19. "C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x00000153a08a1000 nid=0x3010 waiting on condition [0x0000000000000000]
  20. java.lang.Thread.State: RUNNABLE
  21. "C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x00000153a089e000 nid=0x1bec waiting on condition [0x0000000000000000]
  22. java.lang.Thread.State: RUNNABLE
  23. "C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x00000153a089b800 nid=0x1ec4 waiting on condition [0x0000000000000000]
  24. java.lang.Thread.State: RUNNABLE
  25. "C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x00000153a089a800 nid=0x333c waiting on condition [0x0000000000000000]
  26. java.lang.Thread.State: RUNNABLE
  27. "Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x00000153a0899000 nid=0x34b0 runnable [0x0000005144bfe000]
  28. java.lang.Thread.State: RUNNABLE
  29. at java.net.SocketInputStream.socketRead0(Native Method)
  30. at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
  31. at java.net.SocketInputStream.read(SocketInputStream.java:171)
  32. at java.net.SocketInputStream.read(SocketInputStream.java:141)
  33. at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
  34. at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
  35. at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
  36. - locked <0x000000076c2cfc28> (a java.io.InputStreamReader)
  37. at java.io.InputStreamReader.read(InputStreamReader.java:184)
  38. at java.io.BufferedReader.fill(BufferedReader.java:161)
  39. at java.io.BufferedReader.readLine(BufferedReader.java:324)
  40. - locked <0x000000076c2cfc28> (a java.io.InputStreamReader)
  41. at java.io.BufferedReader.readLine(BufferedReader.java:389)
  42. at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:48)
  43. "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000001539e8d3000 nid=0x3154 waiting on condition [0x0000000000000000]
  44. java.lang.Thread.State: RUNNABLE
  45. "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000001539e8d2000 nid=0x3ac runnable [0x0000000000000000]
  46. java.lang.Thread.State: RUNNABLE
  47. "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000001539e84b000 nid=0xd7c in Object.wait() [0x00000051448ff000]
  48. java.lang.Thread.State: WAITING (on object monitor)
  49. at java.lang.Object.wait(Native Method)
  50. - waiting on <0x000000076c008ee0> (a java.lang.ref.ReferenceQueue$Lock)
  51. at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
  52. - locked <0x000000076c008ee0> (a java.lang.ref.ReferenceQueue$Lock)
  53. at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
  54. at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
  55. "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000001539e83a000 nid=0x35f0 in Object.wait() [0x00000051447ff000]
  56. java.lang.Thread.State: WAITING (on object monitor)
  57. at java.lang.Object.wait(Native Method)
  58. - waiting on <0x000000076c006c00> (a java.lang.ref.Reference$Lock)
  59. at java.lang.Object.wait(Object.java:502)
  60. at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
  61. - locked <0x000000076c006c00> (a java.lang.ref.Reference$Lock)
  62. at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
  63. "VM Thread" os_prio=2 tid=0x000001539e812800 nid=0x6f8 runnable
  64. "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000015383be0800 nid=0x2158 runnable
  65. "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000015383be1800 nid=0x688 runnable
  66. "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000015383be3000 nid=0x230c runnable
  67. "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000015383be4800 nid=0x191c runnable
  68. "GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000015383be6800 nid=0x3620 runnable
  69. "GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000015383be7800 nid=0x2098 runnable
  70. "GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000015383bea800 nid=0x2720 runnable
  71. "GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000015383beb800 nid=0x1c48 runnable
  72. "VM Periodic Task Thread" os_prio=2 tid=0x00000153a0a49000 nid=0x3024 waiting on condition
  73. JNI global references: 12
  74. Found one Java-level deadlock:
  75. =============================
  76. "T2":
  77. waiting to lock monitor 0x000001539e84a6f8 (object 0x000000076c3e1610, a java.lang.String),
  78. which is held by "T1"
  79. "T1":
  80. waiting to lock monitor 0x000001539e847db8 (object 0x000000076c3e1648, a java.lang.String),
  81. which is held by "T2"
  82. Java stack information for the threads listed above:
  83. ===================================================
  84. "T2":
  85. at com.myself.learn.lock.MyThread.run(DeathLock.java:40)
  86. - waiting to lock <0x000000076c3e1610> (a java.lang.String)
  87. - locked <0x000000076c3e1648> (a java.lang.String)
  88. at java.lang.Thread.run(Thread.java:748)
  89. "T1":
  90. at com.myself.learn.lock.MyThread.run(DeathLock.java:40)
  91. - waiting to lock <0x000000076c3e1648> (a java.lang.String)
  92. - locked <0x000000076c3e1610> (a java.lang.String)
  93. at java.lang.Thread.run(Thread.java:748)
  94. Found 1 deadlock.

锁粗化 锁优化技术

锁粗化是一种优化技术:如果一系列的连续操作都对同一个对象进行反复加锁和解锁,甚至加锁操作都是出现在循环体中,就算真的没有现成竞争,频繁的进行互斥同步操作将会导致不必要的性能消耗,所以就采取了一种方案,把加锁的范围扩展(粗化)到整个操作序列的外部,这样加锁解锁的频率就会大大降低,从而减少性能消耗。

锁消除 锁优化技术

锁消除是一种优化技术:就是把锁干掉。当JVM运行时发现有些共享数据不会被线程竞争是就进行锁消除。
如何判断共享数据不会被线程竞争?
利用逃逸分析技术:分析对象的作用于,如果对象在A方法中定义后,被作为参数传到B方法中,则称为方法逃逸;如果被其他线程访问,则称为线程逃逸。
在堆中的某个数据不会逃逸出去被其他线程访问到,就可以把它当做栈上数据对的,认为它是线程私有的,同步加锁就不需要了。

synchronized

synchronized是Java中的关键字:用来修饰方法,对象实例。属于独占锁、悲观锁、可重入锁、非公平锁。

  1. 作用于实例方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的Class类,相当于类的一个全局锁,会锁所有调用该方法的线程;
  3. synchronized作用于一个非NULL的对象实例时,锁住的是所有以该对象为锁的代码块。它由多个队列,当多个线程一起访问某个对象监视器时,对象监视器会将这些线程存储在不同的容器中。

每个对象都有个monitor对象,加锁就是在竞争monitor对象,代码块加锁是在代码块前后分别加上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记为来判断的 。

Lock与synchronized的区别

Lock:Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁

  1. Lock需要手动获取锁和释放锁;
  2. Lock是一个接口,synchronized是Java中的关键字,synchronized是内置的语言实现;
  3. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  4. Lock可以让等待锁的线程响应中断,而synchronized不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  5. 通过Lock可以知道有没有成功获取锁,而synchronized不行;
  6. Lock可以通过实现读写锁提高多个线程进行读操作的效率。

synchronized的优势:足够清晰简单,只需要基础的同步功能时,使用synchronized。
Lock必须在finally块中释放锁。如果使用synchronized,JVM确保即使出现异常,锁也能被自动释放,使用Lock时,JVM很难得到哪些锁对象由哪些特定线程持有。

ReentrantLock和synchronized的区别

ReentrantLock是Java类:继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁;
相同点:

  1. 解决共享变量如何安全访问的问题;
  2. 都是可重入锁,也叫作递归锁,同一个线程可以多次获得同一个锁;
  3. 保证线程安全的两大特性:可见性、原子性。

不同点:

  1. ReentrantLock需要手动调用lock()和unlock()方法,synchronized隐式获得释放锁;
  2. ReentrantLock可响应中断,synchronized不可响应中断,ReentrantLock为处理锁的不可用性提供了更高的灵活性;
  3. ReentrantLock是API级别的,synchronized是JVM级别的;
  4. ReentrantLock可以实现公平锁、非公平锁、默认是非公平锁,synchronized是非公平锁,且不可更改;
  5. ReentrantLock通过Condition可以绑定多个条件。