Java 多线程并发编程(二) 线程安全

1.Java 内存模型

说到 Java 的内存模型,可能大部分人想到的是什么新生代,老年代,Eden 区,Survivor 区,但是这些只是 JVM 运行时数据区的 堆内存 部分的分布而已,主要是为了 GC 的分代回收。Java 内存模型就是描述程序的可能行为。Java 编程语言内存模型通过检查执行跟踪中的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作,只要程序的所有执行产生的结果都可以由内存模型预测,具体的实现可以由实现者任意实现,包括操作的重排序和删除不必要的同步。内存模型决定了在程序的每个点上可以读取什么值。

对于同步的规则定义:

  • 对于监视器 m 的解锁与所有后续操作对于 m 的加锁同步
  • 对 volatile 变量 b的写入与所有线程后续对 v 的读同步
  • 启动线程的操作与线程中的第一个操作同步
  • 对每个属性写入默认值与每个线程对其进行的操作同步
  • 线程 T1 的最后操作与线程 T2 发现线程 T1 已经结束同步(isAlive,join 可以判断线程是否终结)
  • 如果线程 T1 中断了线程 T2,那么线程 T1 的终端操作与其他所有线程发现 T2 被中断了同步,通过抛出 InterruptedException 异常或者调用 Thread.interrupted 或 Thread.isInterrupted

Java 内存模型中指出了如下的一种规则:Happens-before 先行发生原则,它主要用于强调两个有冲突的动作之间的顺序,以及定义数据争用的发生时机。如下是它的一些规则:

  • 某个线程中的每个动作都 happens-before 该线程中该动作后面的动作
  • 某个 monitor 上的 unlock 动作 happens-before 同一个 monitor 上后续的 lock 动作
  • 对某个 volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作
  • 在某个线程对象上调用 start() 方法 happens-before 该启动了的线程中的任意动作
  • 某个线程中的所有动作 happens-before 任意其他线程成功从该线程对象上的 join() 中返回
  • 如果某个动作 a happens-before 动作 b,且 b happens-before 动作 c,那么 a happens-before 动作 c

final 在 JMM 中的处理:

  • final字段在该对象的构造函数中被设置值,当线程看到该对象时,将始终看到该对象的 final 字段的正确构造版本
  • 如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则将看到它的默认值
  • 读取该共享对象的 final 成员变量之前,先要读取共享对象
  • 通常 static final 修饰的字段是不可以修改的,然而 System.in,System.out 和 System.err 是 static final 字段,遗留原因,允许通过 set 方法改变,我们将这些字段成为写保护,以区别于普通 final 字段

Word Tearing 字节处理:

一个字段或元素的更新不得与任何其他字段或元素的读取或更新交互

特别是,分别更新字节数组的相邻元素的两个线程不得干涉或交互,也不需要同步以确保顺序一致性

有些处理器没有提供写单个字节的功能。

在这样的处理器上更新 byte 数组,若只是简单的读取整个内容,更新对应的字节,然后将整个内容再写回到内存,将是不合法的。

这个问题有时候被称为 “字分裂”,在单独更新某个字节有难度的处理器上,就需要寻求其他方式了。

double 和 long 特殊处理

虚拟机规范中,写 64 为的 double 和 long 分成了两次 32 位值的操作

由于不是原子操作,可能导致读取到某次写操作中的前 32 位,以及另外一个写操作中的后 32 位

读写 volatile 的 long 和 double 总是原子的,读写引用也总是原子的。

商业 JVM 不会存在这个问题,虽然规范没要求实现原子性,但是考虑到实际应用,大部分都实现了原子性。

2.线程安全相关概念

  1. 竞态条件与临界区:多个线程访问了相同的资源,对这些资源做了写操作时,对执行顺序有要求。竞态条件表示可能会发生在临界区域内的特殊条件。临界区表示关键部分代码的多线程并发执行,会对执行结果产生影响。
  2. 共享资源:如果一段代码是线程安全的,则它不包含竞态条件,只有当多个线程更新共享资源时,才会发生竞态条件。
  3. 不可变对象:创建不可变对象来保证对象在线程间共享时不会被修改,从而实现线程安全。

3.线程安全之可见性

我们先从一段代码开始,在下面的代码中,主线程中创建了一个子线程 thread1,thread1 的目的就是进入一个 while 循环,通过判断 demo1.flag 的值来决定是否继续执行 i++ 的操作。当 thread1 启动后,使主线程休息 2 秒从而让 thread1 获得 CPU 执行,两秒后将 demo1.flag 设置为 false 企图结束 thread1 的执行。如下所示:

public class VisibilityDemo {
    private boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        VisibilityDemo demo1 = new VisibilityDemo();
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                
                while (demo1.flag) { 
                    i++;
                }
                System.out.println(i);
            }
        });
        thread1.start();

        TimeUnit.SECONDS.sleep(2);
        // 设置is为false,使上面的线程结束while循环
        demo1.flag = false;
        System.out.println("被置为false了.");
    }
}

那么结果真的如我们所料吗?其实并不是如此,子线程可能会一直不跳出 while 循环而导致主线程也无法结束执行。主线程对 demo1.flag 的修改,子线程 thread1 并没有获取到。那么问题的原因是什么呢?可能是 CPU 的高速缓存导致 thread1 没有读取到最新的值?但是即使是 CPU 的高速缓存也不会一直不去更新主内存的值。可能是 JIT 编译代码时会有指令重排序的动作?嗯,确实有可能如此,JIT 编译器在编译代码的时候也伴随着通过对代码的掌握从而对代码进行一定的优化,在这个代码实例中,thread1 在经过多次的 while 循环判断 demo1.flag 的值之后,JIT 可能在下一次循环的时候不再读取 demo1.flag 的值而使用原先获取到的值,导致主内存中的 demo1.flag 已经更新但是子线程中读取到的 demo1.flag 还是 true。

以上就是线程可见性的问题了,那么面对这样的情况我们应该怎么做呢?Java 给我们提供了一个关键字:volatile,那么这个关键字的作用是什么呢?volatile 就是为了解决线程的可见性问题,那么 volatile 又是怎么做到这点的呢?在 JDK 的官方文档 Java Language and Virtual Machine Specifications 的 《The Java Virtual Machile Specification,Java SE 8 Edition》中就有如下这么一段描述:

Java 多线程并发编程(二) 线程安全

 

这个里面就提到了,如果字段被声明为 volatile 类型的话,在代码编译成字节码文件的时候就会将此字段设置成 ACC_VOLATILE,而这个标志的作用就是后面那句 cannot be cached,也就是说,如果使用了 volatile 来修饰变量或者字段的话话,那么将会在变量的读取时插入一个读内存屏障,强制从主存中读取最新的值。从下面的代码中我们可以看到,除了可以通过将变量声明为 volatile 类型之外,还可以通过 JVM 参数 -Djava.compiler=NONE 来关闭 JIT 优化,但 JIT 优化不建议被关闭。也可以设置 JVM 参数打印出 JIT编译的内容,并通过 jitwatch 工具来查看 JIT 编译之后的汇编代码:

// 1、 jre/bin/server  放置hsdis动态链接库
//  测试代码 将运行模式设置为-server, 变成死循环   。 没加默认就是client模式,就是正常(可见性问题)
// 2、 通过设置JVM的参数,打印出jit编译的内容 (这里说的编译非class文件),通过可视化工具jitwatch进行查看
// -server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=jit.log
//  关闭jit优化-Djava.compiler=NONE
public class VisibilityDemo {
    private volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        VisibilityDemo demo1 = new VisibilityDemo();
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                // class ->  运行时jit编译  -> 汇编指令 -> 重排序
                while (demo1.flag) { // 指令重排序
                    i++;
                }
                System.out.println(i);
            }
        });
        thread1.start();

        TimeUnit.SECONDS.sleep(2);
        // 设置is为false,使上面的线程结束while循环
        demo1.flag = false;
        System.out.println("被置为false了.");
    }
}

 

4.线程安全之原子性

所谓的原子性就是指一个或多个操作步骤,其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。将整体操作视为一个整体,资源在该次操作中保持一致。

接下来还是以一段代码来开始,以下代码预想是启动两个线程,每个线程执行 i++ 操作 10000 次,企图得到 i 最后的结果是 20000,但是真的能如愿以偿吗?

public class LockDemo {
    volatile int i = 0;
    

    public void add() {
        // TODO xx00
        i++;// 三个步骤
        
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo ld = new LockDemo();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.i);
    }
}

我们都知道,i++ 的操作具体是由三个步骤实现的,第一步获取 i 的值,第二步 将获取到的值加 1,第三步将计算得到的值回写到 i 中,这三个步骤不是一个原子操作,因此在上面代码的多线程情况下,就会出现线程安全问题,即线程 1 读取到 i=100,然后准备给读取到的 100 加 1 变成 101,此时线程 2 也读取到 i=100 ,也准备给读取到的 100 加 1 变成 101,这个时候线程1 将结果回写到 i,线程 2 也将结果回写到 i,那么这两个线程的 i++ 操作只让 i 变成 i+1,并没有如我们想象的变成 i+2。那么如何解决这样的原子性问题呢?

下面介绍一种黑科技:使用 sun.misc.Unsafe 类提供的直接操作内存的方法(类似的概念)来操作 i++,通过 Unsafe 实例提供的 CAS(CompareAndSwap)底层同步原语+while 循环来形成一个自旋锁的效果,从而实现 i++ 的同步操作。

public class LockDemo1 {
    volatile int value = 0;

    static Unsafe unsafe; // 直接操作内存,修改对象,数组内存....强大的API
    private static long valueOffset;

    static {
        try {
            // 反射技术获取unsafe值
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            // 获取到 value 属性偏移量(用于定于value属性在内存中的具体地址)
            valueOffset = unsafe.objectFieldOffset(LockDemo1.class
                    .getDeclaredField("value"));

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public void add() {
        // TODO xx00
        // i++;// JAVA 层面三个步骤
        // CAS + 循环 重试
        int current;
        do {
            // 操作耗时的话, 那么 线程就会占用大量的CPU执行时间
            current = unsafe.getIntVolatile(this, valueOffset);
        } while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
        // 可能会失败
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo1 ld = new LockDemo1();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.value);
    }
}

当然 JDK 已经为我们封装好了相应的原子操作类,我们也没有必要去获取 Unsafe 对象(毕竟是 Unsafe 不安全的,因此平时还是不要用到这个类的实例对象),例如位于 rt.jar 核心 jar 包中的 java.util.concurrent.atomic 包下面的各种原子操作类:

Java 多线程并发编程(二) 线程安全

通过使用对应的原子操作类,我们可以轻松的实现 i++ 的过程:

public class LockDemo {
    // volatile int i = 0;
    AtomicInteger i = new AtomicInteger(0);


    public void add() {
        // TODO xx00
        // i++;// 三个步骤
        i.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo ld = new LockDemo();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.i);
    }
}

原子操作类内部其实也是通过 CAS 的操作实现对变量的同步修改的,它的简单易用的优点我们都可以看到,但是在涉及到多个变量、多个步骤的情况下, 它就显得无力了。

 

5.Java 锁

这个时候自然而然的就想到 java 中提供的锁的概念,Java 中的锁分成如下的几种:

  • 自旋锁:为了不放弃 CPU 执行事件,循环的使用 CAS 技术对数据尝试进行更新,直至成功
  • 悲观锁:假定会发生并发冲突,同步所有对数据的操作,从读数据就开始上锁
  • 乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,进行修改
  • 独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁
  • 共享锁(读):给资源加上读锁只能读不能改,其他线程也只能加读锁,不能加写锁
  • 可重入锁、不可重入锁:线程拿到一把锁之后,可以*进入同一把锁所同步的其他代码
  • 公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。

(1)synchronized 关键字

同步关键字属于最基本的线程通信机制,是基于对象监视器 monitor 实现的。

Java 中的每个对象都有一个监视器相关联,一个线程可以锁定或者解锁

一次只有一个线程可以锁定监视器

视图锁定监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。

特性:可重入、独享、悲观锁

锁的范围:类锁、对象锁、锁消除、锁粗化

可以使用同步关键字 synchronized 来修饰方法、代码块使得多个线程同步访问方法、代码块等。

public class LockDemo2 {
    int i = 0;

    public void add() {
        synchronized (this) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo2 ld = new LockDemo2();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.i);
    }
}

可重入性:使用 synchronized 关键字修饰的代码块是可重入的,如下代码所示,在 test1 方法上加上同步关键字,在方法的内部再次调用 test1 方法,从而再次进入并获取到对象锁。

public class ObjectSyncDemo2 {

    public synchronized void test1(Object arg) {
        System.out.println(Thread.currentThread() + " 我开始执行 " + arg);
        if (arg == null) {
            test1(new Object());
        }
        System.out.println(Thread.currentThread() + " 我执行结束" + arg);
    }

    public static void main(String[] args) throws InterruptedException {
        new ObjectSyncDemo2().test1(null);
    }
}

锁粗化:如下 test1 方法中有两段同步代码,都是对 i 进行 i++ 的操作,此时 JIT 在编译的时候就会进行优化, 将原本 test1 中的两段同步代码合并成一段,避免多次获取释放锁带来的性能损失。

// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看
public class ObjectSyncDemo3 {
    int i;

    public void test1(Object arg) {
        synchronized (this) {
            i++;
        }
        synchronized (this) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000000; i++) {
            new ObjectSyncDemo3().test1("a");
        }
    }
}

锁消除:我们都知道 StringBuffer 是线程安全的,因为它的每个方法都有 synchronized 关键字修饰,在如下的 test1 方法中,虽然使用到了 StringBuffer,但是由于 StringBuffer 不是一个共享变量,因此对于 StringBuffer 的操作不会出现线程安全的问题,因此 JIT 在编译执行的时候就将 StringBuffer 中的 append 方法以及其他方法的同步关键字 synchronized 的描述去掉,避免获取和释放锁的过程。

// 锁消除(jit)
public class ObjectSyncDemo4 {
    
    public void test1(Object arg) {
        // jit 优化, 消除了锁
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("a");
        stringBuffer.append(arg);
        stringBuffer.append("c");
        // System.out.println(stringBuffer.toString());
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000000; i++) {
            new ObjectSyncDemo4().test1("123");
        }
    }
}

 

同步关键字的加锁原理

HotSpot 中,每个对象的头部信息如下所示:

Java 多线程并发编程(二) 线程安全

其中对象头信息的前 32 位(32 位的JVM是 32 bits,64 位的 JVM 是 64 bits)被称为 Mark Word。

Java 多线程并发编程(二) 线程安全

 

以下是 Java 锁的升级过程,从无锁状态到偏向锁,再到轻量级锁,最后到重量级锁,注意其中的几个转折点,当有线程 A 来尝试获取对象的锁的时候,如果对象处于无锁状态(锁标志位 01)并且偏向锁标记为0则直接通过 CAS 将自己的 ThreadID 赋值给对象头的前25位,如果偏向锁标记为 1则判断当前对象头信息中的前 25 位表示的 ThreadID 是否是自己,如果不是就尝试使用 CAS 将其赋值为自己的 ThreadID,这一段是从无锁到偏向锁的升级(锁标志位 01);如果线程 A 在尝试使用 CAS 赋值头信息中的ThreadID 失败了,那么此偏向锁就会升级为轻量级锁(锁标志位 00),原持有偏向锁的线程会继续执行代码,而线程 A 则通过循环+CAS 的方式不断的尝试获取轻量级锁,如果这样的自旋操作达到一定次数之后,原先的轻量级锁将会升级为重量级锁(锁标志位 10),此时对象头信息中的前 30 位的值是指向对象监视器 monitor 的地址。

Java 多线程并发编程(二) 线程安全

 

 附上一张简化版的锁升级的过程图:

 

Java 多线程并发编程(二) 线程安全

 

以上说到当锁由轻量级锁升级成重量级锁之后,对象头信息中存放的是 monitor 的地址,而 monitor 又是什么呢?monitor 是每个对象都有的一个监视器对象,监视器对象主要由三部分组成:第一部分是 owner,表示持有这个监视器锁的线程;第二部分是一个 entrySet,表示的是等待获取这个监视器锁的线程集合;第三部分是一个 waitSet,表示的是曾经持有这个监视器锁的线程调用 wait() 方法后进入阻塞等待唤醒的线程集合。

Java 多线程并发编程(二) 线程安全

 

因此 synchronized 实现锁的机制就是将对象的监视器中的 owner 设置成当前执行的线程,并且将对象的头信息中的锁标志位更新成 10,头信息的前 30 位改成 monitor 的地址。

 

(2)java.util.concurrent.Lock 

Java 的核心类库jar包 rt.jar 里面给我们提供了这样的一个并发编程包:java.util.concurrent,里面包含了大量的并发编程的工具实现,其中有一个锁的最*接口 Lock 接口,其他的锁都是 Lock 接口的实现。根据上面第(1)部分的分析以及结合当前 Lock 接口的定义,我们可以自己实现一个锁 MyLock(有一个原子引用对象 owner 表示占有当前锁的线程,另外定义了一个等待队列 waiters):

public class MyLock implements Lock {
    // 当前锁的拥有者
    volatile  AtomicReference<Thread> owner = new AtomicReference<>();
    // java q 线程安全
    volatile LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();

    @Override
    public boolean tryLock() {
        return owner.compareAndSet(null, Thread.currentThread());
    }

    @Override
    public void lock() {
        boolean addQ = true;
        while (!tryLock()) {
            if (addQ) {
                // 塞到等待锁的集合中
                waiters.offer(Thread.currentThread());
                addQ = false;
            } else {
                // 挂起这个线程
                LockSupport.park();
                // 后续,等待其他线程释放锁,收到通知之后继续循环
            }
        }
        waiters.remove(Thread.currentThread());
    }

    @Override
    public void unlock() {
        // cas 修改 owner 拥有者
        if (owner.compareAndSet(Thread.currentThread(), null)) {
            Iterator<Thread> iterator = waiters.iterator();
            while (iterator.hasNext()) {
                Thread waiter = iterator.next();
                LockSupport.unpark(waiter); // 唤醒线程继续 抢锁
            }

        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }


    @Override
    public Condition newCondition() {
        return null;
    }
}

使用我们自定义的锁来实现本文中一开始出现的问题也是可以解决的:

public class LockDemo3 {
    volatile int i = 0;

    //Lock lock = new ReentrantLock();
    Lock lock = new MyLock();

    public void add() {
        lock.lock();
        try {
            // TODO  很多业务操作
            i++;
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo3 ld = new LockDemo3();

        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.i);
    }
}

通过自定义的锁我们了解了锁的机制之后,接下来再看 java.util.concurrent 里面给我们提供的 ReentrantLock 的原理吧!

整个 ReentrantLock 的源码只有不到 1000行,而其中大部分的是注释,真正代码部分可能只有一两百行,那么 ReentrantLock 是怎么做到使用这么点代码就可以完成一个并发编程下面的锁的呢?下图是 ReentrantLock 的部分源码,里面涉及了一个 Sync 类的对象 sync,以及两个继承自 Sync 类的子类 FairSync 和 NonfairSync(分别表示公平和非公平,其实公平和非公平的差别就是在公平的锁在获取锁资源的时候,会先判断是否有等待该锁的队列,如果有的话,直接将自己加入到等待队列的末尾,而非公平的则是直接进行争抢而不去判断是否有等待队列,这点可以在源码中看到),在构造函数中默认创建了一个 NonfairSync 的对象并且赋值给了 sync 对象字段: 

Java 多线程并发编程(二) 线程安全

 

我们再看下 ReentrantLock 的 lock 是如何实现的:

Java 多线程并发编程(二) 线程安全

What?仅仅是调用了 sync 的 lock 方法?就这么简单?我们再去看下这个 sync.lock() 又是什么名堂。解读下面的源码,我们可以看到 lock 方法的实现过程是:尝试用 CAS 给 State 设置,将其从 0 变成1,如果成功的话,就将 exclusiveOwnerThread 属性设置为当前线程,由 synchronized 的锁机制我们可以知道设置的那个属性值 exclusiveOwnerThread 很有可能就是类似于监视器 monitor 里面的 owner 对象。

Java 多线程并发编程(二) 线程安全

如果第一步 if 判断不成功的话,那么就调用 acquire(1) 来尝试获取锁,以下是 acquire 的代码:

Java 多线程并发编程(二) 线程安全

从这段可以看到 acquire 方法会调用 tryAcquire 方法,如果这个方法返回 false 就接着调用后面的 acquireQueued(addWaiter(Node.EXECLUSIVE),arg) 方法,按照我们对 synchronized 获取锁的机制的了解,当线程尝试获取锁失败后,线程将进入一个等待集合,直到原持有monitor 锁的线程释放并通知等待集合中的线程,因此 addWaiter 方法应该是将当前线程加入到等待集合中,我们来看代码验证一下: 

Java 多线程并发编程(二) 线程安全

可以看到首先利用当前线程创建了一个 node 节点,然后通过调用 enq(node) 方法将 node 节点插入到队列结尾,如下所示:

Java 多线程并发编程(二) 线程安全

 接着将创建的 Node 对象返回给 acquireQueued 方法,然后使用 shouldParkAfterFailedAcquire 来判断是否需要阻塞当前线程

Java 多线程并发编程(二) 线程安全

如果  shouldParkAfterFailedAcquire 返回 true 则调用 parkAndCheckInterrupt 来实现当前线程的阻塞(调用的是 LockSupport的park 方法)

Java 多线程并发编程(二) 线程安全

以上就是 ReentrantLock 的实现过程,在阅读源码的过程中,我们会关注到一个类 AbstractQueuedSynchronizer 抽象队列同步器,简称为 AQS。ReentrantLock 的实现能如此简单,离不开 AQS 的使用,在 AQS 里面定义了如下几个重要的概念:head & tail,用来形成一个等待链表,存放处于等待中的线程;state 表示资源的数量;以及位于父类中的 exclusiveOwnerThread 用来表示持有当前锁对象的线程。

Java 多线程并发编程(二) 线程安全

Java 多线程并发编程(二) 线程安全

AQS 里面封装好了大量的共有逻辑,包括构建一个等待链表,持有锁的线程对象以及控制锁的获取的资源数量,还有就是线程的出入队列,线程的阻塞,在尝试获取锁成功和不成功时的逻辑等等。而使用者只需要在 AQS 的子类中定义好真正的获取和释放逻辑即可,例如 ReentrantLock 中的 FailSync 和 NonfairSync,只是实现了 tryAcquire 的逻辑,它们的父类 Sync 则实现了tryRelease 的逻辑 ,后面我们遇到的其他锁中也会有基于 AQS 的实现。

Java 多线程并发编程(二) 线程安全

Java 多线程并发编程(二) 线程安全

以上是 ReentrantLock 的实现代码,我们再看下 ReentrantReadWriteLock 的部分代码:可以看到 ReentrantReadWriteLock 中也定义了实现 AQS 类的子类 Sync 及它的实例对象 sync;NonfairSync 和 FairSync 用来实现公平和非公平锁的两种情况;

Java 多线程并发编程(二) 线程安全

另外还包括自定义的实现了 Lock接口的 ReadLock 类以及 WriteLock 类,用来提供读锁和写锁的功能,ReadLock 读锁是共享锁,因此它的 lock 方法是判断 tryAccquire 方法返回的值是否大于0 ,如果大于 0 表示还可以继续加共享锁;而 WriteLock 写锁是独占锁,因此它的 lock 方法是判断 tryAccquire 方法是否返回 true,如果返回 true 表示可以加独占锁,以下是分析的 ReadLock 的 lock 方法的实现(WriteLock 的实现和 ReentrantLock 的实现方式类似):

Java 多线程并发编程(二) 线程安全

这里可以看到,如果 tryAccquireShared 的方法返回小于0,则将当前线程加入到等待队列并且阻塞(加入到等待队列并阻塞式是AQS 实现的通用代码,而 tryAccquireShared 方法是由 AQS 的实现者定义的) 

Java 多线程并发编程(二) 线程安全

 这段注释告诉我们:如果当前的写锁被另外一个线程持有,那么无法加读锁;否则如果当前存在获取读锁的等待队列,那么就跳到 fullTryAcquireShared 方法(This code is in part redundant with that in  tryAcquireShared but is simpler overall by not complicating tryAcquireShared with interactions betweenretries and lazily reading hold counts.)执行;否则就用 CAS 的方式更新 state 变量的值,以此来表示当前持有读锁的数量。

Java 多线程并发编程(二) 线程安全

以上如有不足和理解错误之处,还请各位大神指教,谢谢。