文章目录

  • 独享锁(互斥锁)/共享锁(读写锁)
  • 独享锁
  • 共享锁
  • 可重入锁(递归锁)
  • 公平锁/非公平锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁
  • 适应性自旋锁
  • 锁的优化
  • 应用优化
  • JVM优化


独享锁(互斥锁)/共享锁(读写锁)

独享锁

  • 定义:独享锁是指该锁一次只能被一个线程所持有;
  • 特点:独占锁是一种悲观保守的加锁策略,他避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
  • 引用:ReentrantLock就是以独占方式实现的互斥锁。

共享锁

  • 定义:共享锁是指锁可同时被多个线程所持有,并发访问,共享资源;
  • 特点:共享锁是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
  • 应用:
    (1)AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,它们分别标识AQS队列中等待线程的锁获取模式。
    (2)java的并发包中提供了ReedWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。

可重入锁(递归锁)

  • 定义:可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。
  • 应用:在JAVA环境下ReentrantLock和synchronized都是可重入锁。

公平锁/非公平锁

  • 公平锁
    加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。
  • 非公平锁
    加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动队尾等待。
    TIP:
    (1)非公平锁性能比公平锁高5-10倍,因为公平锁需要在多喝的情况下维护一个队列;
    (2)Java中的synchronized是非公平锁,ReentrantLock默认的lock方法采用的是非公平锁。

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

  • 偏向锁
    指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
  • 轻量级锁
    指当锁是偏向锁时,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
  • 重量级锁
    指当锁是轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当咨询按一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

  • 特点
    (1)自旋锁尽可能的减少线程的阻塞;
    (2)减少线程上下文切换的消耗,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升;
    (3)**如果锁的竞争激烈,或者占用锁时间长的代码块,不适合使用自旋锁。**同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其他需要COU的线程又不能获取到CPU,造成CPU的浪费。所以这种情况下要关闭自旋锁。

适应性自旋锁

在JKK5及之前自旋锁的时间是固定的,从JDK6开始,引入了适应性自旋锁。

  • 特点
    (1)自旋时间由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定;
    (2)基本认为一个线程上下文切换的时间是最佳的一个时间。
  • 优化
    JVM还针对当前CPU的负荷情况做了较多的优化
    (1)如果平均负载小于CPUs则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞;
    (2)如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞;
    (3)如果CPU处于节点模式则停止自旋
    (4)自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据的直接时间差),自旋时会适当放弃线程优先级之间的差异。

锁的优化

在Java中,需要谨慎使用锁,如无必要,不用最好;必须要用的时候,也需要尽可能优化锁的使用,以此来提高程序的吞吐量,关于锁的优化,主要分为应用方面的优化和JVM方面的优化。

应用优化

  • 减少锁持有时间:只有在有线程安全要求的程序上加锁;
  • 减小锁粒度:将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最典型的减小锁粒度的案例就是ConcurrentHashMap;
  • 锁分离:最常见的锁分离就是读-写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程安全,又提高了性能,读写分离思想可以延申,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue从头部取出,从尾部放数据。

JVM优化

  • 锁粗化:通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁,但是,凡事有个度,如果对同一个锁不停的进行请求,同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
  • 锁消除:锁消除实在编译器级别的事情,在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起的。