Java 中的并发锁大致分为隐式锁和显式锁两种。

隐式锁就是我们最常使用的 synchronized 关键字
显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和ReentrantReadWriteLock,这两个类都是基于 AQS(AbstractQueuedSynchronizer) 实现的。还有的地方将 CAS 也称为一种锁,在包括 AQS 在内的很多并发相关类中,CAS 都扮演了很重要的角色。

概念辨析

1. 悲观锁和乐观锁

在Java和数据库中都有此概念对应的实际应用。

悲观锁(独占锁) ,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。这么做的好处是简单安全,但是挂起线程和恢复线程都需要转入内核态进行,这样做会带来很大的性能开销。
悲观锁的代表: synchronized关键字和Lock的实现类。
然而在真实环境中,大部分时候都不会产生冲突。悲观锁会造成很大的浪费。
使用场景: 适合写操作多的场景

乐观锁 ,它假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试)。这种锁不会阻塞其他的线程,也不涉及上下文切换,性能开销小。

乐观锁会乐观的认为每次查询都不会造成更新丢失,利用版本字段控制
最常采用的是CAS算法
java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的
使用场景: 适用于多读的应用类型,这样可以提高吞吐量

**

2. 公平锁和非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁是指线程加锁前不考虑排队问题,直接尝试获取锁,获取不到再去队尾排队。
性能略高于公平锁
值得注意的是,在 AQS 的实现中,一旦线程进入排队队列,即使是非公平锁,线程也得乖乖排队。
⽐如synchronized、ReentrantLock()无参构造

3. 可重入锁和不可重入锁

如果某个线程试图获取一个已经由他自己持有的锁,这个请求可以成功,那么此时的锁就是可重入锁,重入锁的这种机制也说明了它是以“线程”为粒度获取锁,而不是以“调用”为粒度。

又叫递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。

从名字上理解,ReentrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数 器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

4. 独享锁 和 共享锁

独享锁(互斥锁,排它锁,独占锁):该锁每一次只能被一个线程所持有。 ReeReentrantLock/synchronized

共享锁:该锁可被多个线程共有 ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占

5. 读写锁

相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。

6. 自旋锁 和 自适应自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

在 JDK1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。
不会发⽣线程状态的切换,⼀直处于⽤户态,减少了线程上下⽂切换的消耗,缺点是循环会消耗CPU
常⻅的⾃旋锁:TicketLock,CLHLock,MSCLock

image.png
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

死锁

当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试
获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻
塞现象,我们称为死锁。

  1. public class DeadLock {
  2. public static String obj1 = "obj1";
  3. public static String obj2 = "obj2";
  4. public static void main(String[] args){
  5. Thread a = new Thread(new Lock1());
  6. //Thread b = new Thread(new Lock2());
  7. a.start();
  8. //b.start();
  9. }
  10. }
  11. class Lock1 implements Runnable{
  12. @Override
  13. public void run(){
  14. try{
  15. System.out.println("Lock1 running");
  16. while(true){
  17. synchronized(DeadLock.obj1){
  18. System.out.println("Lock1 lock obj1");
  19. Thread.sleep(3000);
  20. synchronized(DeadLock.obj2){
  21. System.out.println("Lock1 lock obj2");
  22. }
  23. }
  24. }
  25. }catch(Exception e){
  26. e.printStackTrace();
  27. }
  28. }
  29. }
  30. class Lock2 implements Runnable{
  31. @Override
  32. public void run(){
  33. try{
  34. System.out.println("Lock2 running");
  35. while(true){
  36. synchronized(DeadLock.obj2){
  37. System.out.println("Lock2 lock obj2");
  38. Thread.sleep(3000);
  39. synchronized(DeadLock.obj1){
  40. System.out.println("Lock2 lock obj1");
  41. }
  42. }
  43. }
  44. }catch(Exception e){
  45. e.printStackTrace();
  46. }
  47. }
  48. }