JUC锁相关

本文详细探讨了Java中的对象锁、重量级锁、乐观锁与悲观锁,以及ReentrantLock、限时抢锁、可中断锁等高级特性,包括自旋锁、公平锁、非公平锁、队列机制和AQS抽象同步器的应用。此外,还比较了CAS和LockSupport的优劣以及它们在并发控制中的作用。
摘要由CSDN通过智能技术生成

显示锁

  • 对象锁

    • 存在竞争,会升级为重量级锁,性能低
    • 功能相对单一,不具备一些比较高级的锁功能
  • Java显式锁:为了解决这些Java对象锁的功能问题、性能问题而生的

    • 1)限时抢锁:在抢锁时设置超时时长,如果超时还未获得锁就放弃,不至于无限等下去。
    • 2)可中断抢锁:在抢锁时,外部线程给抢锁线程发出一个中断信号,就能唤起等待锁的线程,并终止抢占过程。
    • 3)多个等待队列:为锁维持多个等待队列,以便提高锁的效率。比如在生产者-消费者模式
      实现中,生产者和消费者共用一把锁,该锁上维持两个等待队列,即一个生产者队列和一个消费者队列。

从同一个线程是否可以重复占有同一个锁对象的角度来分:

  • 可重入锁:递归锁,指的是一个线程可以多次抢占同一个锁,连续上锁
  • 不可重入锁:一个线程只能抢占一次同一个锁。ReentrantLock

从线程进入临界区前是否锁住同步资源的角度来分:

  • 悲观锁:每次去入临界区操作数据的时候都认为别的线程会修改,每次在读写数据时都会上锁,适用于写多读少。Synchronized重量级锁
  • 乐观锁:每次去拿数据的时候都认为别的线程不会修改,所以不会上锁。CAS、Synchronized轻量级锁、基于抽象队列同步器(AQS)实现的显式锁(如ReentrantLock)都是乐观锁

CAS 实现乐观锁

1)检测位置V的值是否为A。
2)如果是,将位置V更新为B值;否则不要更改该位置。

不可重入的自旋锁

自旋锁(SpinLock):

当一个线程在获取锁的时候,如果锁已经被其他线程获取,调用者就一直在那里循环检查该锁是否已经被释放,一直到获取到锁才会退出循环。

CAS自旋锁的实现原理:

抢锁线程不断进行CAS自旋操作去更新锁的owner(拥有者),如果更新成功,表明已经抢锁成功,退出抢锁方法。如果锁已经被其他线程获取(也就是owner为其他线程),调用者就一直在那里循环进行owner的CAS更新操作,一直到成功才会退出循环。

可重入的自旋锁

自旋锁的特点:线程获取锁的时候,如果锁被其他线程持有,当前线程将循环等待,直到获取到锁。线程抢锁期间状态不会改变,一直是运行状态(RUNNABLE),在操作系统层面线程处于用户态。

自旋锁的问题:在争用激烈的场景下,如果某个线程持有锁的时间太长,就会导致其他空自旋的线程耗尽CPU资源。另外,如果大量的线程进行空自旋,还可能导致硬件层面的“总线风暴”。

CAS 可能导致“总线风暴”

CPU会通过MESI协议保障变量的缓存一致性。为了保障“缓存一致性”,不同的内核需要通过总线来回通信,因而产生的流量一般称为“缓存一致性流量”。因为总线被设计为固定的“通信能力”,如果缓存一致性流量过大,总线将成为瓶颈,这就是所谓的“总线风暴”。

解决方案::使用队列对抢锁线性进行排队,最大程度上减少了CAS操作数量。

CLH 自旋锁:基于队列(具体为单向链表)排队的自旋锁

原理:

抢锁线程在队列尾部加入一个节点,然后仅在前驱节点上做普通自旋,它不断轮询前一个节点状态,如果发现前一个节点释放锁,当前节点抢锁成功
由于CLH锁只有在节点入队时进行一下CAS的操作,在节点在加入队列之后,抢锁线程不需要进行CAS自旋,只需普通自旋即可。因此,在争用激烈的场景下,CLH锁能大大减少的CAS操作的数量,以避免CPU的总线风暴。

CLH锁是一种队列锁,其优点是空间复杂度低。

从线程公平性来分:

  • 公平锁:不同的线程抢占锁的机会是公平的、平等的,按照顺序来的,先到的线程先获取锁。
  • 非公平锁:指不同的线程抢占锁的机会是非公平的、不平等的。ReentrantLock
    • 优点:吞吐量大
    • 缺点:有可能会导致线程优先级反转或者线程饥饿现象

在抢锁过程是否可中断角度来分:

  • 可中断锁:线程A正占有锁在执行临界区代码,另一线程B正在阻塞式抢占锁,由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己的阻塞等待。ReentrantLock
  • 不可中断锁:一旦这个锁被其他线程占有,只能选择等待或者阻塞,直到别的线程释放这个锁。synchronized

独占锁和共享锁

  • 独占锁:每次只有一个线程能持有的锁。ReentrantLock
  • 共享锁:允许多个线程同时获取锁,容许线程并发进入临界区。ReentrantReadWriteLock(读写锁)、Semaphore(信号量)、ReadLock(读写锁)中的读锁、CountDownLatch

Semaphore 共享锁

用来控制在同一时刻访问共享资源的线程数量,通过协调各个线程以保证共享资源的合理使用。
Semaphore维护了一组虚拟许可,其数量可以通过构造器的参数指定。线程在访问共享资源前必须使用Semaphore的acquire()方法获得许可,如果许可数量为0,该线程就一直阻塞。
线程访问完成资源后,必须使用Semaphore的release()方法释放许可。更形象的说法是:Semaphore是一个是许可管理器。

CountDownLatch 共享锁

指定一个计数值,在并发环境下由线程进行减1操作,当计数值变为0之后,被await方法阻塞的线程将会唤醒。通过CountDownLatch可以实现线程间的计数同步。

ReadWriteLock 读写锁

  • 读操作、读操作能共存,是相容的。
  • 读操作、写操作不能共存,是互斥的。
  • 写操作、写操作不能共存,是互斥的。

ReadWriteLock接口实现类为ReentrantReadWriteLock,更适合于读多写少的场景,可以提高并发读的效率;而ReentrantLock更适合于读写比例相差不大或写比读多的场景。

StampedLock是对ReentrantReadWriteLock读写锁的一种改进

主要改进:在没有写只有读的场景下,StampedLock支持不用加读锁而是直接进行读操作,最大程度提升读的效率,只有在发生过写操作之后,再加读锁才能进行读操作。

LOCK,位于java.util.concurrent.locks包中,是JUC显式锁的一个抽象

void lock() 抢锁。成功则向下运行,若失败则阻塞当前抢锁线程
void lockInterruptibly() throws InterruptedException 可中断抢锁,当前线程在抢锁的过程中可以响应中断信号
boolean tryLock() 尝试抢锁,线程为非阻塞模式,在调用 tryLock 方法后立即返回。若抢锁成功则返回 true,若抢锁失败则返回 false 
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 限时抢锁,到达超时时间返回 false。并且此限时抢锁方法也可以响应中断信号
void unlock(); 释放锁
Condition newCondition();获取与显式锁绑定的 Condition 对象,用于“等待-通知”方式的线程间通信

优势:

  • (1)可中断获取锁
    使用synchronized关键字获取锁的时候,如果线程没有获取到被阻塞,阻塞期间该线程是不响
    应中断信号(interrupt)的;而使用Lock.lockInterruptibly()方法获取锁时,如果线程被中断,线程将抛出中断异常。
  • (2)可非阻塞获取锁
    使用synchronized关键字获取锁时,如果没有成功获取,线程只有被阻塞;而使用Lock.tryLock()
    方法获取锁时,如果没有获取成功,线程也不会被阻塞,而是直接返回false。
  • (3)可限时抢锁
    使用Lock.tryLock(long time, TimeUnit unit)方法,显式锁可以设置限定抢占锁的超时时间。而
    在使用synchronized关键字获取锁时,如果不能抢到锁,线程只能无限制阻塞。

可重入的独占锁 ReentrantLock

基于内置的抽象队列同步器(Abstract Queued Synchronized,AQS)实现

  • 可重入的含义:表示该锁能够支持一个线程对资源的重复加锁,也就是说,一个线程可以多次进入同一个锁所同步的临界区代码块。

  • 独占的含义:在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能等待,只有拥有锁的线程释放了锁后,其他的线程才能够获取锁。

  • 公平锁

  • 非公平锁

Condition接口,“等待-通知”方式的线程间通信

public interface Condition{
	//方法1:等待。此方法在功能上与 Object.wait()语义等效
	//使当前线程加入 await() 等待队列中,并释放当前锁
	//当其他线程调用signal()时,等待队列中的某个线程会被唤醒,重新去抢锁
	void await() throws InterruptedException;
	
	//方法2:通知。此方法在功能上与Object.notify()语义等效
	// 唤醒一个在await()等待队列中的线程
	void signal();
	
	//方法3:通知全部。唤醒await()等待队列中所有的线程
	//此方法与object.notifyAll()语义上等效
	void signalAll();
	
	//方法4:限时等待。此方法与await()语义等效
	//不同点在于,在指定时间time等待超时后,如果没有被唤醒,线程将中止等待
	//线程等待超时返回false,其他情况返回true
	boolean await(long time, TimeUnit unit) throws InterruptedException;
}

LockSupport 线程阻塞与唤醒的工具类

// 无限期阻塞当前线程
public static void park();
// 唤醒某个被阻塞的线程
public static void unpark(Thread thread);
// 阻塞当前线程,有超时时间的限制
public static void parkNanos(long nanos);
// 阻塞当前线程,直到某个时间
public static void parkUntil(long deadline);
// 无限期阻塞当前线程,带blocker对象,用于给诊断工具确定线程受阻塞的原因
public static void park(Object blocker);
// 限时阻塞当前线程,带blocker对象
public static void parkNanos(Object blocker, long nanos);
// 获取被阻塞线程的blocker对象,用于分析阻塞的原因
public static Object getBlocker(Thread t);

LockSupport.park()和 Thread.sleep()的区别

  • 1)sleep() 自己醒过来;而park()方法调用LockSupport.unpark()唤醒。
  • 2)sleep()方法声明了InterruptedException中断异常,这是一个受检异常,调用者需要捕获这个异常或者再抛出;而使用park()方法时不需要捕获中断异常。
  • 3)被LockSupport.park()方法、Thread.sleep()方法所阻塞的线程有一个特点,当被阻塞线程的
    Thread.interrupt()方法调用时,被阻塞线程都会响应线程的中断信号,唤醒线程的执行。二者对中断信号的响应方式不同:park()仅仅设置了线程的中断标志;而sleep()方法还会抛出InterruptedException异常。
  • 4)与sleep()相比,调用park()能更精准、更加灵活地阻塞、唤醒指定线程。
  • 5)sleep()本身就是一个原生(native)方法;park()并不是一个原生方法,只是调用了一个Unsafe类的原生方法(名字也叫park)去实现。
  • 6)LockSupport.park()方法还允许设置一个Blocker对象,主要用来供监视工具或诊断工具确定
    线程受阻塞的原因。

LockSupport.park()与 Object.wait()的区别

  • 1)Object.wait()方法需要在synchronized块中执行,而LockSupport.park()可以在任意地方执行。
  • 2)当被阻塞线程中断时,Object.wait()方法抛出了中断异常,调用者需要捕获或者再抛出;LockSupport.park()不会抛出异常,调用时不需要处理中断异常。

AQS

JUC包内的许多类都是基于AQS构建,如:ReentrantLock、线程同步工具Semaphore、CountDownLatch、ReentrantReadWriteLock、异步回调工具FutureTask等

CAS自旋实现的轻量级锁有两个大的问题:

1)CAS恶性空自旋会浪费大量的CPU资源。
2)在SMP架构的CPU上会导致“总线风暴”。

解决CAS恶性空自旋的有效方式之一是以空间换时间,较为常见的方案有两种:

  • 分散操作热点
  • 使用队列削峰

JUC并发包使用的是队列削峰的方案解决CAS的性能问题,并提供了一个基于双向队列的削峰基类——抽象基础类AbstractQueuedSynchronizer(抽象同步器类,简称为AQS)。

  1. CLH 锁的内部队列:

    • FIFO的单向队列。在独占锁中,竞争资源在一个时间点只能被一个线程锁访问;队列的
      队首节点(队列的头部)表示占有锁的节点,新加入的抢锁线程则需要等待,会插入到队列的尾部。
      在这里插入图片描述
  2. 分布式锁的内部队列:

    • 基于队列的方式进行不同节点中“等锁线程”的统一调度和管理。以基于ZooKeeper的分布式锁为例
      在这里插入图片描述
  3. AQS 的内部队列

    • AQS队列内部维护的是一个FIFO的双向链表
    • 当线程通过AQS获取锁失败时,线程将被封装成一个Node节点,通过CAS原子操作插入队列尾部;
    • 当有线程释放锁时,AQS会尝试让队首的后驱节点占用锁;
      在这里插入图片描述

状态标志位

AQS中维持了一个单一的volatile修饰的状态信息state,AQS使用int类型的state标示锁的状态,可以理解为锁的同步状态,使用CAS来保证状态的原子性。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程执行该锁的lock()操作时,会调用tryAcquire()独占该锁并将state加1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值