Java和锁的爱恨情仇

文章目录

      • 乐观锁
      • 悲观锁
      • 自旋锁
      • Synchronized同步锁
      • ReentrantLock
        • 非公平锁
        • 公平锁
        • ReentrantLock与Synchronized
        • ReentrantLock实现
        • Condition类和Object类锁方法区别:
        • tryLock和lock和locklnterruptibly的区别
      • Semaphore信号量
      • 可重入锁(递归锁)
      • 公平锁与非公平锁
      • ReadWriteLock读写锁
        • 读锁
        • 写锁
      • 共享锁和独占锁
        • 独占锁
        • 共享锁
      • 重量级锁(Mutex Lock)
      • 轻量级锁
        • 锁升级
      • 偏向锁
      • 锁优化
        • 减少锁持有时间
        • 减少锁粒度
        • 锁分离
        • 锁粗化
        • 锁消除

乐观锁

  • 核心思想:
    • 读多写少,遇到并发写的可能性低,去拿数据的时候不会上锁
  • 并发处理:
    • 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采用在写时先读出当前的版本号,然后加锁操作,如果失败则要重复读-比较-写的操作
  • 实现:
    • java中的乐观锁基本都是通过CAS操作来实现了。
      • CAS是一种更新的原子操作,比较当前值根传入值是否一样,一样则更新,否则失败

悲观锁

  • 核心思想:
    • 认为写多读少,遇到并发写的可能性高,每次在读写数据的时候会上锁
  • 并发处理:
    • 别人想要读写这个数据就会block直到拿到锁
  • 实现:
    • Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到才会转化为悲观锁如RetreenLock

自旋锁

  • 原理:
    • 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,只需要等一等(自旋),可以有效的避免用户线程和内核线程的切换的消耗。
    • 但是自旋消耗CPU,所以需要设定一个自旋等待的最大时间
    • 如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态
  • 自旋锁时间阈值:
    • jdk1.5:写死
    • jdk1.6:引入适应性自旋锁:由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定的
  • 自旋锁的开启
    • jdk1.6中-XX:+UseSpinning开启
    • JKD1.7去掉此参数由jvm开启

Synchronized同步锁

synchronized它可以把任意一个非NULL的对象作为锁,他属于独占式的悲观锁,同时属于可重入锁

  • 作用范围:
    • 作用域方法时:锁住的是对象的实例(this)
    • 当作用域静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8是metaspace),永久带是全局共享的,一次静态方法锁相当于一个类的一个全局锁,会锁所有调用该方法的线程
    • 作用域一个对象实例时,锁住的是所有以该对象为锁的代码块,它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中
  • 核心组件
    • Wait Set:那些调用wait方法被阻塞的线程放在此处
    • Contention List:竞争队列,所有请求所的线程首先被放在这个竞争队列中
    • Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中
    • OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为OnDeck
    • Owner:当前已经获取到所资源的线程被称为Owner
    • !Owner:当前释放锁的线程
  • 实现
    • JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,
      ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将
      一部分线程移动到 EntryList 中作为候选竞争线程。
    • Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定
      EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
    • Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,
      OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在
      JVM 中,也把这种选择行为称之为“竞争切换”。
    • OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList
      中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify
      或者 notifyAll 唤醒,会重新进去 EntryList 中。
    • 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统
      来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
    • Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先
      尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是
      不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁
      资源。
    • 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加
      上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
    • synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线
      程加锁消耗的时间比有用操作消耗的时间更多。
    • Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向
      锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做
      了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
    • 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
    • JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。

ReentrantLock

​ ReentrantLock继承接口Lock并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法

  • Lock接口的主要方法
    • void lock():执行此方法时,如果锁处于空闲状态,当前线程将会去到锁,相反,如果锁以及被其他线程持有,将禁用当前线程,直到当前线程获取到锁
    • boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和
      lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,
      当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一
      直等待, 在未获得锁之前,当前线程并不继续向下执行.
    • void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程
      并不持有锁, 却执行该方法, 可能导致异常的发生.
    • Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,
      当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
    • getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次
      数。
    • getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个
      线程获得锁,此时返回的是 9
    • getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线
      程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了
      condition 对象的 await 方法,那么此时执行此方法返回 10
    • hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件
      (condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
    • hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
    • hasQueuedThreads():是否有线程等待此锁
    • isFair():该锁是否公平锁
    • isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分
      别是 false 和 true
    • isLock():此锁是否有任意线程占用
    • lockInterruptibly():如果当前线程未被中断,获取锁
    • tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
    • tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,
      则获取该锁。

非公平锁

​ JVM按随机、就近原则分配锁的机制称为不公平锁,ReentrantLock在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率远远超过公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制

公平锁

​ 锁的分配时公平的,通常先对锁提出获取请求的线程会先分配到锁,ReentrantLock在构造函数中提供了是否公平锁的初始化方式来定义公平锁

ReentrantLock与Synchronized

  • 相同点
    • 都是用来协调多线程对共享对象、变量的访问
    • 都是可重入锁,同一线程可以多次获得同一锁
    • 都保证了可见性和互斥性
  • 不同点
    • ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
    • ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
    • ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
    • ReentrantLock 可以实现公平锁
    • ReentrantLock 通过 Condition 可以绑定多个条件
    • 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略
    • Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。
    • synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
    • Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
    • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
    • Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。

ReentrantLock实现

public class MyService {
    private Lock lock = new ReentrantLock();
    //Lock lock=new ReentrantLock(true);//公平锁
    //Lock lock=new ReentrantLock(false);//非公平锁
    private Condition condition=lock.newCondition();//创建 Condition
    public void testMethod() {
        try {
            lock.lock();//lock 加锁
            //1:wait 方法等待:
            //System.out.println("开始 wait");
            condition.await();
            //通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
            //:2:signal 方法唤醒
            condition.signal();//condition 对象的 signal 方法可以唤醒 wait 线程
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadName=" + Thread.currentThread().getName()+ (" " + (i + 1)));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally
        {
            lock.unlock();
        }
    }
}

Condition类和Object类锁方法区别:

  • Condition类的awiat方法和Object类的wait方法等效
  • Condition类的signal方法和Object类的notify方法等效
  • Condition类的signalAll方法和Object类的notifyAll方法等效
  • ReentrantLock类可以唤醒指定条件的线程,而Object的唤醒时随机的

tryLock和lock和locklnterruptibly的区别

  • tryLock能获得锁就返回true,不能就立即返回false,trylock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,就返回false
  • lock能获得锁就返回true,不能的话一直等待获得锁
  • lock和lockInterruptibly,如果两个线程分别执行了这两个方法,但此时中断这两个线程,lock不会抛出异常,而locklnterruptibly会抛出异常

Semaphore信号量

​ semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,昨晚自己的生气后返回,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构件一些对象池,资源池之类的比如数据库连接池

  • 实现互斥锁(计数器为1)

    • 我们可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态
  • 代码实现

    // 创建一个计数阈值为 5 的信号量对象
        // 只能 5 个线程同时访问
        Semaphore semp = new Semaphore(5);
        try {  // 申请许可
            semp.acquire();
            try {
                // 业务逻辑
            } catch (Exception e) {
                } finally {
                    // 释放许可
                    semp.release();
                }
            } catch (InterruptedException e) {
                
                }
    
  • Semaphore与ReentrantLock

​ Semaphore基本能完成ReentrantLock的所有工作,使用方法也类似,通过acquire()与release()方法来获得和释放临界资源。

​ Semaphore.acquire()方法默认为可响应中断锁,与ReentrantLock.locklnterruptibly()方法作用一致,即在等待临界资源的过程中可以被Thread.interrupt()方法中断

​ 此外Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire与tryLock不同,其使用的方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定

​ Semaphore的锁释放操作也由手动进行操作,因此与ReentrantLock一样,为了避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成

可重入锁(递归锁)

​ 在同一线程外层函数获得锁以后,内层递归函数仍然有获取该锁的代码,但不受响应,java中的ReentrantLock和Synchronized都是可重入锁

公平锁与非公平锁

  • 公平锁
    • 加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
  • 非公平锁
    • 加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
      • 非公平锁比公平锁性能高,公平锁需要在多核的情况下维护一个队列
      • java中的Synchronized是非公平锁,ReentrantLock默认的lock()方法采用的是非公平锁

ReadWriteLock读写锁

​ 为了提高性感,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制

读锁

​ 如果只读取数据,可以很多人同时读但是不能同时写就使用读锁,多个读锁不互斥

写锁

​ 如果需要修改数据,只能有一个人写,且不能同时读取,那就上写锁。读锁和写锁互斥

共享锁和独占锁

独占锁

  • 每次只能有一个线程持有锁,ReentrantLock就是以独占方式实现的互斥锁。
  • 是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待。
  • 限制了不必要的并发性,因为读写操作并不会影响数据的一致性

共享锁

  • 运行多个线程同时获取锁,并发访问共享资源:ReadWriteLock
  • 是一种乐观锁,它放宽了加锁策略,运行多个执行读操作的线程同时访问共享资源
    • AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,他们分别识别AQS队列中等待线程的锁获取模式
    • java的并发包中提供了ReadWriteLock,读-写锁,它运行一个资源可以被多个读操作访问,或者被一个写操作访问,但是两者不能同时进行

重量级锁(Mutex Lock)

  • 依赖于操作系统Mutex Lock所实现的锁我们成为“重量级锁”

    Synchronized 效率低的原因:是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间。

轻量级锁

无状态锁、偏向锁、轻量级锁、重量级锁

锁升级

​ 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(单向的)

轻量级锁不是代替重量级锁的,他是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。

轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时刻访问同一锁的情况,会导致轻量级锁膨胀为重量级锁

偏向锁

  • 目的:
    • 在某个线程获得锁了以后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。
    • 在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子命令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子命令
  • 轻量级锁是为了在线程交替执行同步块的时候提高性能
  • 偏向锁时在只有一个线程执行同步块的时候进一步提高性能

锁优化

减少锁持有时间

  • 只用在有线程安全要求的程序上加锁

减少锁粒度

  • 将大对象,拆为小对象,大大增加并行读,降低锁竞争,降低了锁的竞争,偏向锁,轻量级锁成功率才会提高
  • 案例:ConcurrentHashMap

锁分离

  • 读写锁:根据功能进行分离成读锁和写锁

锁粗化

锁消除

你可能感兴趣的:(java,java,多线程,并发编程)