Java并发包中Lock的实现原理

本文深入解析Java并发包中Lock接口的实现原理,包括其基本思想、使用方法、公平锁与非公平锁的区别,以及Condition接口的基本思想。同时,对比Lock与synchronized关键字的不同之处。

                                   Java并发包中Lock的实现原理

  1. Lock 的简介及使用

         Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\locks中),它包含以下方法

//尝试获取锁,获取成功则返回,否则阻塞当前线程void lock(); 

//尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 void lockInterruptibly() throws InterruptedException; 

//尝试获取锁,获取锁成功则返回true,否则返回false boolean tryLock(); 

//尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 boolean tryLock(long time, TimeUnit unit) 
                                   throws InterruptedException; 

//释放锁void unlock(); 

//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量Condition newCondition();


        Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。

          使用方法:多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。

Lock l = ...; //根据不同的实现Lock接口类的构造函数得到一个锁对象 l.lock(); //获取锁位于try块的外面 try { 
      // access the resource protected by this lock } finally { 
     l.unlock(); 
}


         注意,加锁位于对资源访问的try块的外部,特别是使用lockInterruptibly方法加锁时就必须要这样做,这为了防止线程在获取锁时被中断,这时就不必(也不能)释放锁。

try {
     l.lockInterruptibly();//获取锁失败时不会执行finally块中的unlock语句
      try{          // access the resource protected by this lock
     }finally{
          l.unlock();
     }
} catch (InterruptedException e) {     // TODO Auto-generated catch block
     e.printStackTrace();
}


2. 实现Lock接口的基本思想

          需要实现锁的功能,两个必备元素,一个是表示(锁)状态的变量(我们假设0表示没有线程获取锁,1表示已有线程占有锁),另一个是队列,队列中的节点表示因未能获取锁而阻塞的线程。为了解决多核处理器下多线程缓存不一致的问题,表示状态的变量必须声明为voaltile类型,并且对表示状态的变量和队列的某些操作要保证原子性和可见性。原子性和可见性的操作主要通过Atomic包中的方法实现。

 

      线程获取锁的大致过程(这里没有考虑可重入和获取锁过程被中断或超时的情况)

          1. 读取表示锁状态的变量

         2. 如果表示状态的变量的值为0,那么当前线程尝试将变量值设置为1(通过CAS操作完成),当多个线程同时将表示状态的变量值由0设置成1时,仅一个线程能成功,其

            它线程都会失败

            2.1 若成功,表示获取了锁,

                  2.1.1 如果该线程(或者说节点)已位于在队列中,则将其出列(并将下一个节点则变成了队列的头节点)

                  2.1.2 如果该线程未入列,则不用对队列进行维护

                  然后当前线程从lock方法中返回,对共享资源进行访问。            

             2.2 若失败,则当前线程将自身放入等待(锁的)队列中并阻塞自身,此时线程一直被阻塞在lock方法中,没有从该方法中返回(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)

        3. 如果表示状态的变量的值为1,那么将当前线程放入等待队列中,然后将自身阻塞(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)

          注意: 唤醒并不表示线程能立刻运行,而是表示线程处于就绪状态,仅仅是可以运行而已

 

      线程释放锁的大致过程

        1. 释放锁的线程将状态变量的值从1设置为0,并唤醒等待(锁)队列中的队首节点,释放锁的线程从就从unlock方法中返回,继续执行线程后面的代码

        2. 被唤醒的线程(队列中的队首节点)和可能和未进入队列并且准备获取的线程竞争获取锁,重复获取锁的过程

        注意:可能有多个线程同时竞争去获取锁,但是一次只能有一个线程去释放锁,队列中的节点都需要它的前一个节点将其唤醒,例如有队列A<-B-<C ,即由A释放锁时唤醒B,B释放锁时唤醒C

 

3. 公平锁和非公平锁

         锁可以分为公平锁和不公平锁,重入锁和非重入锁(关于重入锁的介绍会在ReentrantLock源代码分析中介绍),以上过程实际上是非公平锁的获取和释放过程。

公平锁严格按照先来后到的顺去获取锁,而非公平锁允许插队获取锁。

          公平锁获取锁的过程上有些不同,在使用公平锁时,某线程想要获取锁,不仅需要判断当前表示状态的变量的值是否为0,还要判断队列里是否还有其他线程,若队列中还有线程则说明当前线程需要排队,进行入列操作,并将自身阻塞;若队列为空,才能尝试去获取锁。而对于非公平锁,当表示状态的变量的值是为0,就可以尝试获取锁,不必理会队列是否为空,这样就实现了插队获取锁的特点。通常来说非公平锁的吞吐率比公平锁要高,我们一般常用非公平锁。

           这里需要解释一点,什么情况下才会出现,表示锁的状态的变量的值是为0而且队列中仍有其它线程等待获取锁的情况。

           假设有三个线程A、B、C。A线程为正在运行的线程并持有锁,队列中有一个C线程,位于队首。现在A线程要释放锁,具体执行的过程操作可分为两步:

            1. 将表示锁状态的变量值由1变为0,

            2. C线程被唤醒,这里要明确两点:

              (1)C线程被唤醒并不代表C线程开始执行,C线程此时是处于就绪状态,要等待操作系统的调度

              (2)C线程目前还并未出列,C线程要进入运行状态,并且通过竞争获取到锁以后才会出列。

            如果C线程此时还没有进入运行态,同时未在队列中的B线程进行获取锁的操作,B就会发现虽然当前没有线程持有锁,但是队列不为空(C线程仍然位于队列中),要满足先来后到的特点(B在C之后执行获取锁的操作),B线程就不能去尝试获取锁,而是进行入列操作。

 

4. 实现Condition接口的基本思想

         Condition 本质是一个接口,它包含如下方法

// 让线程进入等通知待状态 
void await() throws InterruptedException; 
void awaitUninterruptibly();
 
//让线程进入等待通知状态,超时结束等待状态,并抛出异常  long awaitNanos(long nanosTimeout) throws InterruptedException; 
boolean await(long time, TimeUnit unit) throws InterruptedException; 
boolean awaitUntil(Date deadline) throws InterruptedException; 

//将条件队列中的一个线程,从等待通知状态转换为等待锁状态 void signal(); 

//将条件队列中的所有线程,从等待通知阻塞状态转换为等待锁阻塞状态void signalAll();


           一个Condition实例的内部实际上维护了一个队列,队列中的节点表示由于(某些条件不满足而)线程自身调用await方法阻塞的线程。Condition接口中有两个重要的方法,即 await方法和 signal方法。线程调用这个方法之前该线程必须已经获取了Condition实例所依附的锁。这样的原因有两个,(1)对于await方法,它内部会执行释放锁的操作,所以使用前必须获取锁。(2)对于signal方法,是为了避免多个线程同时调用同一个Condition实例的singal方法时引起的(队列)出列竞争。下面是这两个方法的执行流程。

          await方法:

                            1. 入列到条件队列(注意这里不是等待锁的队列

                            2. 释放锁

                            3. 阻塞自身线程

                             ------------被唤醒后执行-------------

                            4. 尝试去获取锁(执行到这里时线程已不在条件队列中,而是位于等待(锁的)队列中,参见signal方法)

                                4.1 成功,从await方法中返回,执行线程后面的代码

                                4.2 失败,阻塞自己(等待前一个节点释放锁时将它唤醒)

         注意:await方法时自身线程调用的,线程在await方法中阻塞,并没有从await方法中返回,当唤醒后继续执行await方法中后面的代码(也就是获取锁的代码)。可以看出await方法释放了锁,又尝试获得锁。当获取锁不成功的时候当前线程仍然会阻塞到await方法中,等待前一个节点释放锁后再将其唤醒。

 

         signal方法:

                           1. 将条件队列的队首节点取出,放入等待锁队列的队尾

                           2. 唤醒该节点对应的线程

         注意:signal是由其它线程调用

Lock和Condition的使用例程

           下面这个例子,就是利用lock和condition实现B线程先打印一句信息后,然后A线程打印两句信息(不能中断),交替十次后结束


5. Lock与synchronized的区别

          1. Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的

          2. 当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的     条件下提供一种退出的机制。

          3. 一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以    及设置等待时限等方式退出条件队列。

         4. synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式

原文地址:http://www.cnblogs.com/nullzx/p/4968674.html


<think>我们正在讨论Java并发包中的并发容器,特别是它们的数据结构和实现原理。根据之前的讨论和引用内容,我们可以将并发容器分为List、Set、Map和Queue等几类,并分别探讨其内部实现机制。 注意:由于用户要求详细解析数据结构和实现原理,我们将重点放在每个容器的内部实现上,包括使用的数据结构、同步机制(如CAS、锁、写时复制等)以及关键操作的流程。 我们将按照以下结构进行: 1. 线程安全List:CopyOnWriteArrayList 2. 线程安全Set:CopyOnWriteArraySet, ConcurrentSkipListSet 3. 线程安全Map:ConcurrentHashMap, ConcurrentSkipListMap 4. 线程安全队列:由于之前已经讨论过,这里简要提及,重点在非阻塞队列ConcurrentLinkedQueue和阻塞队列的原理 注意:引用内容中提到了LongAdder(空间换时间,多段锁理念)和Semaphore(基于AQS)等,但这些不属于容器,因此不在本问题讨论范围内。 下面开始详细解析:</think>### Java 并发容器:数据结构与实现原理详解 Java 并发包(`java.util.concurrent`)通过创新设计平衡线程安全与性能,核心容器采用独特数据结构与同步机制。以下是关键容器的实现原理分析: --- #### 一、`CopyOnWriteArrayList`(写时复制列表) **数据结构**: - 底层使用 `volatile Object[] array` 存储数据 - 写操作时复制新数组($O(n)$),更新引用指向新数组 **实现原理**: ```java // JDK 源码核心逻辑 public boolean add(E e) { synchronized(lock) { Object[] es = getArray(); int len = es.length; Object[] newElements = Arrays.copyOf(es, len + 1); // 复制新数组 newElements[len] = e; setArray(newElements); // volatile 写保证可见性 return true; } } ``` **同步机制**: - 写操作:互斥锁(JDK15+ 使用 `synchronized`,早期用 `ReentrantLock`) - 读操作:无锁直接访问数组(依赖 `volatile` 保证可见性) - **适用场景**:监听器列表、配置管理等**读多写少**场景(写频率 < 1%)[^1] --- #### 二、`ConcurrentHashMap`(高并发哈希表) **数据结构演进**: | JDK版本 | 数据结构 | 锁机制 | |---------|-------------------|-----------------------| | JDK7 | 分段数组 + 链表 | 分段锁(`ReentrantLock`)| | JDK8+ | 数组 + 链表/红黑树 | CAS + `synchronized` | **JDK8 实现原理**: 1. **初始化**:延迟加载,首次插入时初始化数组 2. **插入操作**: ```java final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); // 懒初始化 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) break; // CAS 插入空桶 } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 协助扩容 else { synchronized (f) { // 锁住桶头节点 if (tabAt(tab, i) == f) { if (fh >= 0) { // 链表插入 } else if (f instanceof TreeBin) { // 红黑树插入 } } } } } } ``` 3. **扩容机制**: - 多线程协同扩容(通过 `ForwardingNode` 标记迁移状态) - 扩容期间查询可访问新旧两表 **关键优化**: - **链表转红黑树**:桶节点数 ≥ 8 时转换(退化阈值=6) - **计数器分离**:采用 `LongAdder` 思想实现 `CounterCell[]` 分散计数[^3] --- #### 三、`ConcurrentSkipListMap`(跳表映射) **数据结构**: - 多层跳表结构(空间复杂度 $O(n)$,查询复杂度 $O(\log n)$) - 节点结构: ```java static final class Node<K,V> { final K key; volatile Object value; volatile Node<K,V> next; volatile Index<K,V> down; // 下层指针 } ``` **实现原理**: 1. **查询**:从顶层开始向右/向下搜索(无锁) 2. **插入**: - CAS 创建新节点 - 随机生成层级($P(level=k) = \frac{1}{2^k}$) - 自顶向下构建索引链(CAS 更新指针) 3. **删除**:标记节点逻辑删除 + CAS 更新指针 **优势**: - 相比平衡树:无全局锁,范围查询高效 - 天然有序(支持 `ConcurrentNavigableMap`) --- #### 四、非阻塞队列 **`ConcurrentLinkedQueue` 实现原理**: - **数据结构**:单向链表 + CAS ```java private transient volatile Node<E> head; private transient volatile Node<E> tail; ``` - **入队操作**: 1. CAS 修改 `tail.next` 2. CAS 更新 `tail` 指针(可能延迟更新) - **出队操作**: 1. CAS 设置头节点 `item=null`(逻辑删除) 2. CAS 更新 `head` 指针 **关键特性**: - 无界队列,依赖 CAS 保证线程安全 - `size()` 非精确(遍历计数) --- ### 设计思想对比 | 容器类型 | 核心思想 | 数据结构 | 适用场景 | |------------------------|------------------|-------------------|-----------------------| | `CopyOnWriteArrayList` | 读写分离 | 动态数组 | 读多写少(配置管理) | | `ConcurrentHashMap` | 锁粒度最小化 | 哈希表+红黑树 | 高频K-V访问(缓存) | | `ConcurrentSkipListMap`| 概率平衡 | 多层跳表 | 有序并发访问(排行榜)| | `ConcurrentLinkedQueue`| 无锁算法 | 单向链表 | 高吞吐生产消费 | --- ### 性能优化实践 1. **避免复合操作**: ```java // 错误:非原子操作 if (!map.containsKey(k)) map.put(k, v); // 正确:使用原子方法 map.computeIfAbsent(k, key -> new AtomicLong()); ``` 2. **控制写时复制频率**: ```java // 批量写入减少复制次数 list.addAll(Arrays.asList(data1, data2, data3)); ``` 3. **利用有序容器特性** ```java // 高效范围查询(跳表) map.subMap("2023-01", "2023-12").forEach(...); ``` --- ### 总结 Java 并发容器的实现核心: 1. **降低冲突**:通过分段(JDK7 HashMap)、分散计数(`CounterCell`)、跳表层级等减少竞争 2. **无锁化**:CAS 实现非阻塞操作(如 `ConcurrentLinkedQueue`) 3. **读写分离**:写时复制牺牲写性能换取读并发(`CopyOnWriteArrayList`) 4. **结构优化**:链表转红黑树(哈希冲突)、跳表替代平衡树(范围查询) > **设计启示**: > - 读多写少场景优先写时复制 > - 高频更新场景选择锁粒度更小的 `ConcurrentHashMap` > - 有序需求使用跳表结构 > - 生产消费模型匹配队列特性(阻塞/非阻塞)[^2] ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值