浅谈java中的锁

1.synchronized

参考:
synchronized详解
synchronized详解-附带编译

1.1.synchronized介绍

synchronized中文意思是同步,也称之为”同步锁“。
synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。

1.1.1.synchronized的主要作用

**原子性:**确保线程互斥地访问同步代码;
**可见性:**保证共享变量的修改能够及时可见,其实是通过Java内存模型中的“对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
**有序性:**有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;

1.1.2.synchronized的两条规定

1.线程解锁前,必须把共享变量的最新值刷新到主内存中
2.线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值(注意:加锁与解锁需要是同一把锁)

1.2.synchronized的使用

1.2.1.使用方式

  • 修饰实例方法:作用于当前实例加锁
  • 修饰静态方法:作用于当前类对象加锁
  • 修饰代码块:指定加锁对象,对给定对象加锁

image.png

1.2.2.使用总结

  • 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  • 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

1.3.synchronized是如何从jdk1.5的重量级锁优化的

参考:
聊聊Synchronized的优化

2.ReentrantLock

2.1.什么是重入锁

参考:
ReentrantLock 锁详解
到底什么是重入锁 ReentrantLock
java.util.concurrent.locks.ReentrantLock
这个是 JDK @since 1.5 添加的一种颗粒度更小的锁,它完全可以替代 synchronized 关键字来实现它的所有功能,而且 ReentrantLock 锁的灵活度要远远大于 synchronized 关键字。

从类结构图看出,ReentrantLock 实现了 Lock 接口,ReentrantLock 只是 Lock 接口的一个实现而已。
它们都是 java.util.concurrent 包里面的内容(俗称 JUC、并发包),也都是 JDK 1.5 开始加入的。

2.1.1.为什么叫重入锁呢?

ReentrantLock,我们把它拆开来看就明了了。
Re-Entrant-Lock:即表示可重新反复进入的锁,但仅限于当前线程;

public void m() {
    lock.lock();
    lock.lock();
    try {
        // ... method body
    } finally {
        lock.unlock()
        lock.unlock()
    }
}

如示例代码所示,当前线程可以反复加锁,但也需要释放同样加锁次数的锁,即重入了多少次,就要释放多少次,不然也会导入锁不被释放。
试想一下,如果不设计成可重入锁,那自己如果反复给自己加锁,不是会把自己加死锁了吗?所以,到现在,重入锁的概念大概应该清楚了吧?

2.1.2.重入锁最重要的几个方法

这几个方法都是 Lock 接口中定义的:
image.png

1).lock()

获取锁,有以下三种情况:
**锁空闲:**直接获取锁并返回,同时设置锁持有者数量为:1;
**当前线程持有锁:**直接获取锁并返回,同时锁持有者数量递增1;
**其他线程持有锁:**当前线程会休眠等待,直至获取锁为止;

2).lockInterruptibly()

获取锁,逻辑和 lock() 方法一样,但这个方法在获取锁过程中能响应中断。

3).tryLock()

从关键字字面理解,这是在尝试获取锁,获取成功返回:true,获取失败返回:false, 这个方法不会等待,有以下三种情况:
**锁空闲:**直接获取锁并返回:true,同时设置锁持有者数量为:1;
**当前线程持有锁:**直接获取锁并返回:true,同时锁持有者数量递增1;
**其他线程持有锁:**获取锁失败,返回:false;

4).tryLock(long timeout, TimeUnit unit)

逻辑和 tryLock() 差不多,只是这个方法是带时间的。

5).unlock()

释放锁,每次锁持有者数量递减 1,直到 0 为止。
所以,现在知道为什么 lock 多少次,就要对应 unlock 多少次了吧。

6).newCondition

返回一个这个锁的 Condition 实例,可以实现 synchronized 关键字类似 wait/ notify 实现多线程通信的功能,不过这个比 wait/ notify 要更灵活,更强大!

2.2.ReentrantLock中Sync、NonfairSync与FairSync

2.2.1.什么是AQS

参考:AQS原理简介
原名 AbstractQueuedSynchronizer 即队列同步器 是构建锁和其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch)。
AQS提供了大量用于自定义同步器实现的 Protected方法。自定义同步器实现的相关方法也只是为了通过修改 State字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock需要实现的方法如下,并不是全部):

方法名描述
protected boolean isHeldExclusively()该线程是否正在独占资源。只有用到Condition才需要去实现它。
protected boolean tryAcquire(int arg)独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。
protected boolean tryRelease(int arg)独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。
protected int tryAcquireShared(int arg)共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int arg)共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。

一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease。
image.png

2.2.2.什么是Sync

Sync类继承了AQS框架,自身为非公平锁

 abstract static class Sync extends AbstractQueuedSynchronizer {
     // 序列号
     private static final long serialVersionUID = -5179523762034025860L;
     
     // 获取锁
     abstract void lock();
     
     // 非公平方式获取
     final boolean nonfairTryAcquire(int acquires) {
         // 当前线程
         final Thread current = Thread.currentThread();
         // 获取状态
         int c = getState();
         if (c == 0) { // 表示没有线程正在竞争该锁
             if (compareAndSetState(0, acquires)) { // 比较并设置状态成功,状态0表示锁没有被占用
                 // 设置当前线程独占
                 setExclusiveOwnerThread(current); 
                 return true; // 成功
             }
         }
         else if (current == getExclusiveOwnerThread()) { // 当前线程拥有该锁
             int nextc = c + acquires; // 增加重入次数
             if (nextc < 0) // overflow
                 throw new Error("Maximum lock count exceeded");
             // 设置状态
             setState(nextc); 
             // 成功
             return true; 
         }
         // 失败
         return false;
     }
     
     // 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它
     protected final boolean tryRelease(int releases) {
         int c = getState() - releases;
         if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程不为独占线程
             throw new IllegalMonitorStateException(); // 抛出异常
         // 释放标识
         boolean free = false; 
         if (c == 0) {
             free = true;
             // 已经释放,清空独占
             setExclusiveOwnerThread(null); 
         }
         // 设置标识
         setState(c); 
         return free; 
     }
     
     // 判断资源是否被当前线程占有
     protected final boolean isHeldExclusively() {
         return getExclusiveOwnerThread() == Thread.currentThread();
     }
 
     // 新生一个条件
     final ConditionObject newCondition() {
         return new ConditionObject();
     }
 
     // 返回资源的占用线程
     final Thread getOwner() {        
         return getState() == 0 ? null : getExclusiveOwnerThread();
     }
     // 返回状态
     final int getHoldCount() {            
         return isHeldExclusively() ? getState() : 0;
     }
 
     // 资源是否被占用
     final boolean isLocked() {        
         return getState() != 0;
     }
 
     // 自定义反序列化逻辑
     private void readObject(java.io.ObjectInputStream s)
         throws java.io.IOException, ClassNotFoundException {
         s.defaultReadObject();
         setState(0); // reset to unlocked state
     }
 }

2.2.3.什么是NonfairSync

NonfairSync 类继承了 Sync类,表示采用非公平策略获取锁,其实现了 Sync类中抽象的 lock方法,源码如下:从 lock方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。Acquire方法是 FairSync和 UnfairSync的父类 AQS中的核心方法。

 // 非公平锁
 static final class NonfairSync extends Sync {
     // 版本号
     private static final long serialVersionUID = 7316153563782823691L;
 
     // 获得锁
     final void lock() {
         /**
          * 若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。
          * 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入Acquire方法进行后续处理。
          */
         if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用
             // 把当前线程设置独占了锁
             setExclusiveOwnerThread(Thread.currentThread());
         else // 锁已经被占用,或者set失败
             // 以独占模式获取对象,忽略中断
             acquire(1); //Acquire方法是FairSync和UnfairSync的父类AQS中的核心方法。
     }
 
     protected final boolean tryAcquire(int acquires) {
         return nonfairTryAcquire(acquires);
     }
 }

2.2.4.什么是FairSync

FairSync 类也继承了 Sync类,表示采用公平策略获取锁,其实现了 Sync类中的抽象 lock方法,源码如下:

 // 公平锁
 static final class FairSync extends Sync {
     // 版本序列化
     private static final long serialVersionUID = -3000897897090466540L;
 
     final void lock() {
         // 以独占模式获取对象,忽略中断
         acquire(1);
     }
 
     // 尝试公平获取锁
     protected final boolean tryAcquire(int acquires) {
         // 获取当前线程
         final Thread current = Thread.currentThread();
         // 获取状态
         int c = getState();
         if (c == 0) { // 状态为0
             if (!hasQueuedPredecessors() &&
                 compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功
                 // 设置当前线程独占
                 setExclusiveOwnerThread(current);
                 return true;
             }
         }
         else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据
             // 下一个状态
             int nextc = c + acquires;
             if (nextc < 0) // 超过了int的表示范围
                 throw new Error("Maximum lock count exceeded");
             // 设置状态
             setState(nextc);
             return true;
         }
         return false;
     }
 }

跟踪 lock方法的源码可知,当资源空闲时,它总是会先判断 sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync 类的 lock的方法调用如下,只给出了主要的方法。
image.png

2.2.5.NonfairSync与FairSync区别

FairSync只要资源被其他线程占用,该线程就会添加到 sync queue中的尾部,而不会先尝试获取资源。
NonfairSync每一次都会先尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公
平的现象,当获取不成功,再加入队列尾部。

总结:公平锁就是按照每个线程进队列的时间先后顺序来让每个线程来占有锁,非公平锁则是先去尝试占有锁,占有不到才回去排队。

2.2.6.NonfairSync加锁与解锁逻辑

image.png

加锁:

  • 通过ReentrantLock的加锁方法Lock进行加锁操作。
  • 会调用到内部类Sync的Lock方法,由于Sync#lock是抽象方法,根据ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的Lock方法,本质上都会执行AQS的Acquire方法。
  • AQS的Acquire方法会执行tryAcquire方法,但是由于tryAcquire需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法,因此会根据锁类型不同,执行不同的tryAcquire。
  • tryAcquire是获取锁逻辑,获取失败后,会执行框架 AQS的后续逻辑,跟ReentrantLock自定义同步器无关。

解锁:

  • 通过 ReentrantLock的解锁方法 Unlock进行解锁。
  • Unlock会调用内部类 Sync的 Release方法,该方法继承于AQS。
  • Release中会调用 tryRelease方法,tryRelease需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。
  • 释放成功后,所有处理由AQS框架完成,与自定义同步器无关。
  • 通过上面的描述,大概可以总结出 ReentrantLock加锁解锁时 API层核心方法的映射关系。

通过上面的描述,大概可以总结出 ReentrantLock加锁解锁时 API层核心方法的映射关系。
image.png

3.二者区别是什么

image.png

4.什么是死锁

还没写完…

5.可重入锁、双重效验锁、自旋锁和互斥锁

还没写完…

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值