关于ReentrantLock锁机制的那些事
- 背景引出
背景:关于并发编程,多线程的业务,之前很想写一篇文章来的,因很多时候,忙于工作,而忽视了这些基础的知识点,项目中用的这些知识点也是很少。
今天在在代码中,突然看到了一个前同事写的一个CopyToWriteArraySet这个变量,很好奇,为啥使用这个变量,而不去使用我们经常使用的HashSet相关集合呢,于是,我跟踪源码似乎明白了一些道理了。具体情况
先了解下这个CopyToWriteArrayList源码
可以看到其类中使用了一个ReentrantLock变量,和一个存储数据的数组(使用了volatile修饰,transient修饰表示这个类在序列化时,当前这个变量不会被序列化)
而从源码中,我们可以很容易知道CopyToWriteArraySet内部采用的是CopyToWriteArrayList,只不过,CopyToWriteArraySet在进行调用CopyToWriteArrayList中的add操作前,检查了一下是否有重复的元素,看如下源码:
再来看看这个add操作源码
很明显,使用ReentrantLock锁机制,同时,使用Arrays.copyof进行创建副本操作,然后加setArray到数组中。
通过上面讲解,得出关于CopyToWriteArrayList的结论:
CopyToWriteArraySet内部采用的是CopyToWriteArrayList,存储的数据是一个数组,这个数组用volatile修饰的(保证变量可见性,修改后的值,直接存储到主内存中,达到一种共享变量),而这个CopyToWriteArrayList中在进行add时,进行了加锁操作,保证线程的安全。但不影响其他线程对其读操作(为什么呢,因为CopyToWriteArrayList对数据进行add/update/remove时,都是通过一个副本来操作,操作完成后,在赋值给这个被volatile修饰的数组)
所以,如果我们想使用线程安全的集合,可以使用CopyToWriteArrayList来代替ArrayList集合来操作。
- 继续研究ReentrantLock机制
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
Lock 接口的主要方法(选择了几个重要的方法)
1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
2. boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
3. void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
4. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
5. isLock():此锁是否有任意线程占用
6. lockInterruptibly():如果当前线程未被中断,获取锁
7. tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
8. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。
非公平锁 JVM 按随机、就近原则分配锁的机制则称为不公平锁,ReentrantLock 在构造函数中提供了 是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非 程序有特殊需要,否则最常用非公平锁的分配机制。
公平锁 公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁, ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
ReentrantLock 与 synchronized
1. ReentrantLock通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized会被JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出 现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操 作。
2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要 使用 ReentrantLock。
- 写作最后:
采用synchronized 实现ReentrantLock
package com.quanroon.ysq; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; /** * @author quanroong.ysq * @version 1.0.0 * @description 采用synchronized 实现ReentrantLock * 分析思路参考此地址:https://www.cnblogs.com/aspirant/p/8601937.html 分析的不错。从两者使用的不同点出发,然后,利用其相同点来进行构思 * @createtime 2020/8/15 14:31 */ public class SynchronizedToReentrantLock { private static final long NONE=-1; private long owner =NONE; public synchronized void lock(){ long currentThreadId=Thread.currentThread().getId(); if(owner==currentThreadId){ throw new IllegalStateException("lock has been acquired by current thread"); } while(this.owner!=NONE){ System.out.println(String.format("thread %s is waiting lock", currentThreadId)); try { wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } this.owner=currentThreadId; System.out.println(String.format("lock is acquired by thread %s", currentThreadId)); } public synchronized void unlock(){ long currentThreadId=Thread.currentThread().getId(); if(this.owner!=currentThreadId){ throw new IllegalStateException("Only lock owner can unlock the lock"); } System.out.println(String.format("thread %s is unlocking", owner)); owner=NONE; notify(); } public static void main(String[] args) { // TODO Auto-generated method stub 给定锁 final SynchronizedToReentrantLock lock=new SynchronizedToReentrantLock(); //产生20个线程 ExecutorService executor= Executors.newFixedThreadPool(20, new ThreadFactory(){ private ThreadGroup group =new ThreadGroup("test thread group"); { group.setDaemon(true); } @Override public Thread newThread(Runnable r) { // TODO Auto-generated method stub return new Thread(group,r); }}); for(int i=0;i<20;i++){ executor.submit(new Runnable(){ @Override public void run() { // TODO Auto-generated method stub lock.lock(); System.out.println(String.format("thread %s is running...", Thread.currentThread().getId())); try { //执行业务逻辑 for (int i1 = 0; i1 < 10; i1++) { System.out.println("===> i1=" + i1); Thread.sleep(1000); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } lock.unlock(); }}); } } }