Contents

Java并发编程——锁分类

按上锁方式划分

隐式锁:synchronized

  • Java的关键字,是Java提供的同步机制
  • 不需要显式的执行加锁和解锁过程

显式锁:JUC包中提供的锁

  • JUC中提供的锁,加锁和解锁的方法都是显式的

按特性划分

乐观锁/悲观锁

  • 按照线程在使用共享资源时,要不要锁住同步资源
  • 悲观锁
    • 在获取数据的时候会先加锁
    • 实现:JUC的锁synchronized
  • 乐观锁
    • 在获取数据的时候不会加锁
    • 在更新数据的时候会判断数据是否被其他线程修改过
    • 实现:CAS算法、JPA提供的@Version注解、关系型数据库的版本号机制

可重入锁/不可重入锁

  • 按照同一个线程是否可以重复获取同一把锁
  • 可重入锁
    • 同一个线程可以重复获取同一把锁
    • 实现:synchronizedReentrantLock
  • 不可重入锁
    • 重复获取会发生死锁
    • 实现:ReentrantReadWriteLock.WriteLock
    • 一般来说,应该尽量避免使用不可重入锁,除非确实有必要使用它来解决某些特殊问题
      • 简化实现:在某些场景下,使用不可重入锁可以简化代码实现,因为不需要考虑同一线程多次获取锁的问题
      • 限制资源访问:不可重入锁可以限制某些共享资源的访问,从而保证资源的独占性和线程安全性
      • 防止死锁:不可重入锁可以防止同一线程出现死锁的情况
      • 提高公平性:不可重入锁可以提高锁的公平性,因为同一线程无法多次获取锁,其他线程可以更容易地获得锁

公平锁/非公平锁

  • 按照多个线程竞争同一锁时需不需要排队,能不能插队划分
  • 公平锁
    • 多个线程按照申请锁的顺序来获得锁
    • 实现:new ReentrantLock(true)
  • 非公平锁
    • 不是按照申请锁的顺序,允许“插队”
    • 实现:new ReentrantLock(false)synchronized
    • 非公平锁的性能一般会比公平锁的性能好,因为非公平锁减少了线程切换的次数
    • 非公平锁的优势在于吞吐量比公平锁大

独享锁/共享锁

  • 按照多个线程能不能同时共享同一个锁
  • 独享锁(排他锁)(写锁)
    • 一次只能有一个线程获取到锁
    • 实现:synchronizedReentrantLock
  • 共享锁(读锁)
    • 一次可以有多个线程获取到锁
    • 实现:ReentrantReadWriteLock.ReadLock
    • 读锁可保证在读多写少的场景中,提高并发读的性能,增加程序的吞吐量

其他常见的锁

自旋锁

  • 获取锁失败时,线程不会阻塞而是循环尝试获得锁,直至获得锁成功。
  • 实现:CAS,举例:AtomicInteger#getAndIncrement()

分段锁

  • 实现:ConcurrentHashMap
  • 可以理解为对独占锁的性能优化

无锁/偏向锁/轻量级锁/重量级锁

  • 是synchronized独有的四种状态
  • 不可降级,只能升级

Java已经提供了synchronized,为什么还要使用JUC的锁呢?

  • synchronized同步锁的线程阻塞,存在有两个致命的缺陷:无法控制阻塞时长;阻塞不可中 断。
    • 假如占有锁的线程被长时间阻塞(IO阻塞,sleep方法,join方法 等),由于线程在阻塞时没有释放锁,如果其他线程尝试获取锁,就会被阻塞只能一直等待下去, 甚至会发生死锁,这样就会造成大量线程的堆积,严重的影响服务器的性能。
    • JUC的锁可以解决这两个缺陷:
      • tryLock(long time, TimeUnit unit)
      • lockInterruptibly()
  • 读多写少的场景,需要共享锁提高性能
    • JUC的ReentrantReadWriteLock.ReadLock