Java并发学习笔记 —— 浅析Java中的锁

引言

在多线程环境下,为了保证共享变量的原子性操作,我们需要锁来保证资源的独占;在数据库连接等资源不足的情况下,我们需要控制获取连接的资源数以防出现异常;还有一些情况下,我们需要多个线程任务完成的条件满足后再继续程序……在以上的种种情况,我们都需要使用锁,让我们的程序按照我们的预期执行。

本篇文章主要分为三个部分,第一个部分简单介绍目前Java中锁的区别,第二部分会介绍最简单的锁,也是Java的保留字——Synchronized。第三部分则会介绍基于同步器的锁,也就是java.util.concurrent包含的锁。

Java中锁的分类

独占锁与共享锁

独占的意思是只是否只能有一个线程拥有锁,当锁被线程独占了之后,其它再想要获取锁的线程就只能等待。等待的机制有自旋等待以及阻塞等待。自旋等待的时候,线程会不断循环查询是否满足获取锁的条件,这样消耗CPU但是却节省了线程上下文切换所需的时间,一般在同步块执行速度较短时选择使用。而阻塞等待则会导致线程从运行态转变成堵塞态。

共享锁则允许多个线程同时获取锁,一般在并发读的时候采用共享锁。当然也可以使用共享锁来控制当前并发线程的数量,如引言中提到的在数据库连接有限的情况下,就可以控制并发线程数与数据库连接数一致。

可重入锁和不可重入锁

根据是否可以多次获得同一个锁,又有可重入锁与不可重入锁的区分。

Synchronized —— 不可重入的独占锁

用法及简介

Synchronized做为Java的保留字,有以下两种用法。

  • 在方法上使用Synchronized

    public synchronized void sync() {
        int a = 1;
    }
    
  • 在特定的代码区域中使用Synchronized

    public void syncInBlock() {
        synchronized(this) {
            int a = 1;
        }
    }
    

但无论哪个地方使用Synchronized,其实都是在一个特定的对象上加锁。如果是在静态方法上加锁,则是对Class对象进行了加锁,而在方法上或者是方法肿的同步块上加锁,则是对相应的实例加锁。

加锁原理

我们将上述代码的Java文件编译后再使用javap -verbose *>*.txt命令进行反编译,可以得到以下的字节码命令。通过字节码命令,我们能够更加清晰地看到synchronized的加锁原理。

public class com.java.SampleSynchronized
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // com/java/SampleSynchronized
   #2 = Utf8               com/java/SampleSynchronized
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/java/SampleSynchronized;
  #14 = Utf8               sync
  #15 = Utf8               a
  #16 = Utf8               I
  #17 = Utf8               syncInBlock
  #18 = Utf8               SourceFile
  #19 = Utf8               SampleSynchronized.java
{
  public com.java.SampleSynchronized();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/java/SampleSynchronized;

  public synchronized void sync();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED //因为Synchronized在方法上使用,因此方法的ACC_SYNCHRONIZED标志位被设置成1
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: return
      LineNumberTable:
        line 5: 0
        line 6: 2
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lcom/java/SampleSynchronized;
            2       1     1     a   I

  public void syncInBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter //加锁,然后进入同步区
         4: iconst_1
         5: istore_2
         6: aload_1
         7: monitorexit //正常执行完毕,解锁
         8: goto          14 //程序返回
        11: aload_1
        12: monitorexit //异常代码处理,线程抛出异常的时候,释放锁
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     8    11   any
            11    13    11   any
      LineNumberTable:
        line 9: 0
        line 10: 4
        line 9: 6
        line 12: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/java/SampleSynchronized;
}

从上面我们可以看出,对于使用在方法上的sychronized,在方法的flags上会多出一个ACC_SYNCHRONIZED的flag,以标志这个方法是必须要同步访问的。而在方法内声明的同步块,则会有monitorentermonitorexit来完成加锁和解锁。

实际上,monitor也就是加在对象上的锁,如果执行monitorenter的时候发现无法获取对象上的锁,线程就会对锁进行等待。而具体的加锁/等待方式又会因为锁的类型不同而有所不同,包括有偏向锁,轻量级锁和重量级锁。对于这些锁的实现在这里就不展开描述了。

ReentrantLock——使用同步器的锁

除了Synchronized之外,JDK还提供了可重入的独占锁——ReentrantLock以及可重入的共享锁——ReentrantReadWriteLock。因为这两个锁的实现都是基于同步器的,因此这里以ReentrantLock为例,介绍基于同步器的锁。

基本用法

ReentrantLock实现了java.util.concurrent.locks.Lock接口。其主要方法如下所示:

public void lock(){} //获取锁,若不成功线程会被阻塞

public boolean tryLock() {} //尝试获取锁,获取成功返回true,获取失败返回false,不阻塞线程

public void unlock() {} //释放锁

public final boolean isFair() {} //公平锁还是非公平锁,此处暂时略过

对于锁的使用者而言,我们只要了解了这些接口,就能够使用锁了。最简单的用法如下所示:

ReentrantLock lock = new ReentrantLock();

//线程阻塞式获取锁  
try{
    lock.lock();
    ...sync code...
} finally {
    lock.unlock();
}

//线程非阻塞式获取锁
try{
    while(lock.tryLock()){}
} finally {
    lock.unlock();
}

加锁原理

通过查看ReentrantLock的源码,我们能够发现,在ReentrantLock中,还存在着一个内部抽象类Sync,而其具体实现则有FairSyncNonfairSyncSync继承了AbstractQueuedSynchronizer。而AbstractQueuedSynchronizer就是我们常说的同步器。

当我们查看加锁以及释放锁的源码的时候,不难发现,所有的加锁/释放锁的操作,最后都会委托给同步器。也就是说,同步器才是真正加锁的实现者。=

具体调用过程(加锁)如下图所示:

Created with Raphaël 2.1.0 lock.lock sync.lock sync.acquire(1) sync.tryAcquire(1) get the lock addToWaitingQueue to be waked up interrupt thread yes no yes no

而观察AbstractQueuedSynchronizer我们可以发现,在加锁过程中,除了tryAcquire方法之外,其它方法都被final修饰,或者为私有方法。

事实上,抽象同步器已经实现了同步器大部分的功能,包括线程的等待与唤醒操作,同步状态原子更新等。而我们需要做的,就是在获取锁以及释放锁的时候,完成对同步状态的管理

同步状态是如何管理的呢?是通过同步器中的一个volatile变量,state来管理的。要修改/获取state的值,有三个非常重要的方法。

protected final int getState()//获取state的值

protected final void setState() //非原子性地修改state的值

protected final boolean compareAndSetState() //原子性修改state的值,适用于多线程竞争时

state可以表明当前有多少个线程拥有锁,也可以表明当前还有多少个线程可以申请锁,这一切,都取决于我们对同步器中两个关键方法,tryAcqure以及tryRelease的实现方式。

然后,我们以ReentrantLock为例,研究其中重写的tryAcquire方法。

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {//c为0,说明当前没有线程获得锁
            if (!hasQueuedPredecessors()  //确保没有线程在排队
                && compareAndSetState(0, acquires)) { //原子性修改同步器状态
                setExclusiveOwnerThread(current); //设置当前线程为独占线程
                return true; //加锁成功
            }
        }
        else if (current == getExclusiveOwnerThread()) { //若拥有锁的线程为当前线程,则修改同步器状态,并返回加锁成功
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc); //因为是当前线程拥有锁,所以可以直接修改同步器状态(其它线程此时已经不可能修改同步器状态,不会出现冲突)
            return true;
        }
        return false; //加锁失败
    }

在上面的代码中我们能够知道,state为0则表示当前没有线程获取锁,而state大于0的时候,则代表有线程持有锁,因为ReentrantLock是可重入的独占锁,因此当持锁线程再申请锁时,state也会进行加1。

以上,就是同步器加锁的具体原理。使用同步器实现具体功能的还有CountDownLatchCyclicBarrierSemaphore

小结

本篇文章简单介绍了Java中的锁,包括锁的不同类别,以及基于关键字Synchronized的锁和基于同步器的锁,并且分析了他们的实现原理。

参考文献

《并发编程的艺术》—— 方腾飞

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值