Java多线程技术

2019/03/02

Java多线程技术

Java多线程技术主要从以下3个方面展开讲述:

  1. 单线程:线程的实现方式、线程提供的基本方法;多线程:线程池实现;
  2. 线程同步:同步锁Synchronized、ReentrantLock,无锁实现CAS;
  3. 线程通信:Object的wait/notify方法、Lock.condition变量,以及ThreadLocal、Volatile变量;

Java多线程技术还有几个概念:

  1. Java内存模型
  2. 锁优化
  3. 原子性

单线程/多线程

Java的线程实现方式有两种,通过继承Thread类或则实现Runnable接口。其中Thread类也是实现了Runnable接口。Thread构造函数接受一个实现了Runnable接口的Runnable对象,通过调用Thread的start方法启动线程,执行run方法。

Thread类常见方法:

  1. yield 当前线程让出CPU执行时间片,主动挂起等待。
  2. sleep 当前线程休眠指定时间,但是并不会让出CPU执行时间片。
  3. start 启动线程,创建一个新的线程,将线程状态更改为Runnable可执行状态,如果获取到了执行权则直接执行。
  4. run 执行run方法,不会创建新的线程,使用当前线程执行。
  5. interrupt 配合sleep等操作执行,将处于休眠或则等待、挂起的线程自动退出,并释放锁。
  6. join 主线程等待子线程执行完。
  7. setDaemon 将线程设置为后台线程,不会随着主线程退出而退出

线程同步

多线程环境下对共享资源操作会引入线程安全问题,对于共享资源需要做好线程同步操作。线程安全主要通过同步来实现,Java中线程同步的实现方式主要有两种,一种是基于锁实现的同步,比如Synchronized、ReentrantLock,另一种是基于无锁的CAS算法实现。

Synchronized

Synchronized变量可以作用于方法名、代码块、类名、静态方法等,通过获取对象锁或则类锁来实现线程间同步的。

修饰方法名

public void synchronized methodA() {}; public void synchronized methodB() {};

多线程访问同一对象实例的methodA方法只能同步执行,synchronized作用于方法名获取的是对象锁。一个对象只要一个对象锁,所以当一个线程执行methodA方法获取到了当前对象的对象锁后,其他线程执行methodB也要进行等等,直到methodA方法执行完,对象锁被释放。

修饰代码块

synchronized (object) {}

synchronzied可以直接作用于一段代码块上,通过获取指定对象的对象锁。这个对象可以是当前对象也可以是其他对象。

这里要注意,不同对象的对象锁是不会互相影响的,所以一个类的多个方法,获取的是不同对象锁的话,这些方法是可以同时执行的。

修饰静态方法或则静态类

synchronized (ObjectA.class) {};

一个类只有一个Class对象,所以获取类的锁和静态方法锁是同一个锁,这两个地方的代码是同步的,无法同时执行。

Synchronized是可重入锁 当一个线程获取到了当前对象的对象锁后,可以再次获取该对象的对象锁。

public void synchronized methodA() { methodB() }; public void synchronized methodB() {};

Synchronized子类可以获取到父类的锁。

ReentrantLock

JDK1.5引入了ReentrantLock类,可重入锁,实现了Lock接口,需要手动加解锁,特别是在代码异常情况下一定要释放锁。

ReentrantLock常见方法:

  1. lock 加锁,获取对象锁
  2. tryLock 尝试加锁
  3. unlock 解锁
  4. newCondition 作用于线程间通信,类似于wait/notify。可以创建多个condition(即对象监视器)实例。注意调用condition对象方法也需要先获取对象锁,否者会抛异常。

ReentrantLock

  1. 方法
    • lock方法 - Synchronized关键字作用
    • condition对象 - wait/notify方法作用
  2. 实现
    • 等待/通知实现
    • 生产/消费实现
    • 公平锁,先到先得,FIFO顺序。
    • 非公平锁,抢占式,随机获取锁。

ReentrantReadWriteLock

读写锁,锁分离技术,实现读写分离。读操作为共享锁,读写、写写操作为排它锁。readLock允许多个线程同时执行,writeLock只允许一个线程执行。

无锁实现的CAS

上面介绍的基于锁实现的Synchronized和ReentrantLock是Java语言层面提供的解决方案,CAS是硬件层面提供的技术方案,JVM层可以直接调用CAS指令。

线程通信

wait/notify

wait/notify是Object类提供的方法,wait使当前线程挂起进入等待队列并释放锁,notify从等待队列唤醒一个线程进入就绪队列获取锁。notify方法执行完并不会立即释放锁,notify可以调用多次,每次都会唤醒线程,也可以直接调用notifyAll方法唤醒等待队列里的所有线程进入就绪队列。

wait/notify方法执行必须要先获取到当前对象的对象锁,否者会抛异常。

condition

Java内存模型

内存模型

volatile

volatitle变量用于解决多个线程之间的变量可见性问题。

synchronized除了解决同步问题,同时也解决了可见性问题。

ThreadLocal

线程内的局部变量,内部通过一个map集合实现。

原子类型

Atomic基于CAS实现。

锁优化

对象锁(对象监视器)即monitor监视器锁,底层是依赖操作系统的Mutex Lock来实现的。Mutex Lock称为“重量级锁”,效率低。所以JDK对Synchronized进行优化,引入了“轻量级锁”和“偏向锁”。

获取锁JVM层面是通过CAS操作实现的。

JDK锁优化策略: 1)锁粗化,同一系列操作会反复对同一把锁进行上锁和解锁操作,编译器会扩大锁的边界,从而只使用一次上锁和解锁。 2)锁清除,代码块中没有涉及共享数据,编译器会清除锁。 3)自旋锁,锁竞争失败线程会被挂起、阻塞都要被转入内核态,进行上下文切换。自旋线程竞争失败时并不会立即进去阻塞状态,而会继续持有CPU执行一段时间,效率高,但会造成CPU资源浪费。 4)降低获取锁和释放锁带来的性能开销,引入了“偏向锁”和“轻量级锁”。

偏向锁

锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,状态会升级,但是是单向升级,只能低到高,不能降级。JDK1.6默认开启偏向锁和轻量级锁。-XX:-UserBiasedLocking 关闭偏向锁。锁对象保存在对象的头文件中。

锁升级膨胀方向为 无锁(偏向锁)=》 轻量级锁 =》 重量级锁,这个过程为单向,对象头中的markword记录了当前锁的状态信息。

偏向锁

在无多线程竞争的情况下,减少不必要的轻量级锁执行路径,。用于只有一个线程执行的同步块,轻量级锁获取释放需要依赖多次CAS原子指令,偏向锁只依赖一次CAS。偏向锁为无锁的一种。

加锁:线程第一次进入同步块,CAS更新对象的Mark Word(偏向锁标志位为“1”,记录线程的ID) 释放:当有另一个线程来竞争锁时,就不再使用偏向锁,升级为轻量级锁。拥有偏向锁的线程不会主动释放锁,每次执行同步代码的时候直接判断偏向线程ID是否等于自己ID,等于就执行代码;不等于就说明有竞争了。

1)每次退出同步块释放锁,偏向锁只有在发生竞争时才释放锁。 2)每次进入和退出都需要CAS更新对象头。 3)竞争锁失败时,适应性自旋尝试抢占锁。

轻量级锁

在无多线程竞争的情况下,减少传统重量级锁产生的性能消耗。适用线程交替执行同步块的情况,同步块执行速度非常快。

Java代码层面锁优化的建议:

  • 减少锁的持有时间
  • 减小锁的粒度 - 例如ConcurrentHashMap的实现
  • 使用读写锁替代独占锁 - 例如ReentrantReadWriteLock的实现
  • 锁分离技术 - 例如LinkedBlockingQueue的实现

Java中的锁实现

AQS

AQS是JDK中为“线程同步”提供的一套基础工具类,标准同步器类的实现方式,基于AQS可以实现Java中的非常多“锁”。

java.util.concurrent并发包中,有非常多的同步类都是基于AQS实现的,比如

  • ReentrantLock
  • ReentrantReadWriteLock
  • Semaphore
  • CountDownLatch
  • FutureTask

AQS功能说明:

  1. AQS内部是通过state整数状态值来维护锁是否可用,默认0表示锁可用,不等于0表示锁被占用;
  2. AQS封装了基本的操作,包括获取操作和释放操作;获取操作就是一个获取状态的操作,通过状态判断锁是否可用;
  3. 通过getState、setState以及compareAndSetState方法来操作状态值;状态值可以用来表示任意的状态;AQS不同的实现者,可以自定义状态值的作用;
  4. 获取操作,一般的步骤是先获取状态值,判断是否允许获取;如果允许,就尝试更新状态值;

    根据AQS实现者的不同,实现的方法不同: 如果是独占锁,则要实现AQS的tryAcquire、tryRelease等方法; 如果是共享锁,则需要实现AQS的tryAcquireShared、tryReleaseShared方法。 AQS最终是调用子类实现的这些try方法来判断是否可执行。

  5. AQS内置提供了一个ConditionObject类,实现了AQS的基本方法,可以作为一个基本的同步器类来使用。

ReentrantLock里的AQS实现

ReentrantLock是独占锁(独占锁是悲观锁),所以要实现AQS的tryAcquire和tryRelease方法;

  1. ReentrantLock中的维护的状态信息:包括锁被获取到次数,以及通过owner变量保存持有锁的当前线程信息;
  2. tryAcquire获取操作逻辑:首先获取state状态值,判断锁是否可用;如果可用,通过CAS更新状态值,并持有当前线程,返回成功;如果锁不可用,通过owner变量判断锁是否被自己占用;如果是,则更新状态值,记录锁获取次数,返回成功;如果不是,说明锁被其他线程占用,返回失败。
final boolean nonfairTryAcquire(int acquires) { 
    final Thread current = Thread.currentThread();
    // 获取锁状态,0 标识锁可用
    int c = getState();
    // c == 0 说明锁可以用
    if (c == 0) {
        // 通过CAS更新状态,表明锁被占用
        if (compareAndSetState(0, acquires)) {
            // 保存持有锁的当前线程状态信息
            owner = current;
            return true;
        }
    }
    // 如果 c != 0,说明锁已被占用
    // 通过owner判断,是否是被自己占用了锁
    else if (current == owner) {
        // 如果是,则说明这次是一次重入操作,状态++
        setState(c+acquires);
        return true;
    }
    // 如果owner不等于自己,说明锁被其他线程占用,return false 获取锁失败。
    return false;
}
  1. tryRelease释放操作逻辑:获取state状态值,判断是否是持有锁的线程在执行这个方法,如果不是,直接抛异常;更新锁状态,返回成功;
protected final boolean tryRelease(int releases) {
    // 获取锁状态
    int c = getState() - releases;
    // 必须是持有锁的线程才能执行release操作
    if (Thread.currentThread() != owner)
        throw new IllegalMonitorStateException();
    boolean free = false;
    // c == 0 说明锁可以被释放,如果是非0,说明这个锁是共享锁,要等其他持有锁的线程都释放,每次释放state-1。
    if (c == 0) {
        free = true;
        owner = null;
    }
    // 更新锁状态
    setState(c);
    return free;
}
  1. newCondition方法:使用AQS提供的ConditionObject类来实现wait/notify方法功能。
final ConditionObject newCondition() {
    // 直接使用AQS提供的ConditionObject类
    return new ConditionObject();
}

Semaphore和CountDownLatch

Semaphore和CountDownLatch都是非独占锁,它们将state状态值用于保存当前可用许可的数量。

FutureTask

FutureTask不是一个同步器类,但是它的作用就是一个线程一直等待某件事件的发生(即任务的结束),这里就涉及到任务状态的维护。所以FutureTask利用AQS的state来维护任务的状态,还额外保存任务结束的结果信息。

/** State value representing that task is running */
private static final int RUNNING   = 1;
/** State value representing that task ran */
private static final int RAN       = 2;
/** State value representing that task was cancelled */
private static final int CANCELLED = 4;
/** The result to return from get() */
private V result;

ReentrantReadWriteLock

ReentrantReadWriteLock内部有两个锁,读锁和写锁,但都是基于一个AQS子类实现的。读锁用于共享操作,写锁用于独占操作。

无锁实现

CAS

实现“线程同步”有两种方式:第一种是基于“锁”方式实现的,第二种是基于“无锁”方法实现的。

基于“锁”的常见实现方案有:Synchronized和ReentrantLock等;

基于“无锁”实现的方案为:CAS算法。

CAS(Compare and Swap)无锁并发控制的一种技术,是一种乐观锁。CAS的优势:

  1. 无锁操作,节省锁竞争带来的开销;
  2. 没有线程间频繁调度的开销。

CAS的工作原理:

  1. CAS是基于硬件层面实现的技术,是一条CAS指令;
  2. CAS包括三个值,V 内存中的值(当前正确的值),A 比较的值(期望内存中当前的值),B 新值;CAS首先判断V是否等于A,如果等于则更新V为B;如果不等于,说明值已被更改过,直接返回失败;
  3. 线程执行CAS操作并不会阻塞,CAS更新失败并不会挂起线程,调用者可以自行决定后续的操作,可以继续尝试更新(获取锁操作失败,线程会被挂起,CAS并不会)。

CAS存在问题:

ABA问题:即V值先由A变为了B,后又由B变为了A,对于V来说值还是A,无法知道是否有更新过。 解决方案:引入版本号,更新值的同时一起更新版本号,这样即使V值相同,版本号记录了更新记录。

JDK中基于CAS的实现

Atomic原子类 JDK1.5开始支持CAS操作,Atomic原子类基于volatile变量和CAS操作来实现。

Post Directory