Java Concurrency

331 阅读23分钟

前言

本文全面覆盖并发相关知识点,建立完整的知识体系。

基础

本章节包含并发概念,缓存,线程,Java内存模型

并发概念

并发指多个线程同时执行程序的能力。在单核处理器中,多个线程交替占用内存空间实现并发。在多核处理器中,每个线程可以分配到独立的处理核,达到真正的同时运行。需要注意的是在并发中,多个线程是同时存在的,每个线程都处于执行的某个状态。

并发与高并发区别:

并发主要专注于保证线程安全与每个线程对资源的合理使用。高并发专注于解决程序或服务器同时处理多种请求的能力,主要目的在于提高程序的性能。

CPU 多级缓存

图中所示为高速缓存。高速通道(bus)连接了高速缓存与CPU核心来达到高速效果。随着系统的复杂化,高速缓存与主内存(main memory)的速度差异越来越大,促使了不同级别缓存的出现。比如:L1,L2,L3 caches

缓存有不命中的风险,它的存在意义来自于数据的时间和空间局部性:当某个数据被访问时,不久的将来可能被再次访问。当某个数据被访问时,它的相邻数据也可能被访问。

CPU 多级缓存一致性 (MESI)

MESI协议保证多个CPU缓存之间缓存数据的一致性

NESI是四种状态的缩写:
M: 被当前cpu修改过,与主内存不一致
E: 未被当前cpu修改过,与主内存一致
S: 共享状态,多个cpu可以同时访问
I: 无效状态:Invalid

CPU缓存的四种操作:
local read: 读取本地缓存数据
local write: 写入本地缓存
remote read: 讲内存中数据读取出来
remote write: 写回主内存

CPU 多级缓存 - 乱序执行优化

处理器为提高运算速度会作出违背代码原有顺序的优化。
如果我们不做任何防护措施,并发会有严重后果。

Java内存模型

为了屏蔽不同硬件与操作系统内存访问的差异来让Java程序在不同的场景实现相同的并发效果,JVM虚拟机定义了Jave Memory Model的规范。JMM规范了虚拟机与计算机内存的协同工作,比如规定了一个线程在何时和何种方式可以共享其他线程上的值。

Heap: Java堆是运行时的数据区。堆的大小是程序运行中动态分配的,最后由gc来统一回收。因为其动态性,堆的存储速度很慢。
Stack: 栈的存储速度比堆要快不少,仅次于计算机里的寄存器(register)。栈中的数据是可以共享的,但是栈中的数据大小与生存期必须提前确定。

Java内存模型要求类的方法与本地变量存储在栈上面,对象存储在堆上面。当两个本地变量同时访问堆中的静态变量的方法时候,每个线程获得一份成员方法的拷贝。

JMM 寄存器 (register)

CPU在寄存器(register)上的执行速度远大于在主内存(main memory)上的执行速度。尽管CPU访问register的速度很快,但是还是比访问计数器要慢一些。

Program Counter Register (计数器) PC计数器为线程私有,指向下一条命令的地址,也就是下一条即将执行的指令代码,由执行引擎来读取。

线程与主内存的抽象关系

JVM中的Thread StackHeap都储存在主内存中,部分可能储存在寄存器register或者cache中。线程的共享变量也储存在主内存中。每个线程都有一个私有的本地内存,涵盖了缓存,写缓存区,寄存器,已经其他硬件或者编译器优化。

线程私有的本地内存并不真实存在,只是对主内存物理内存的划分和定义

Java 内存模型 - 同步操作

同步操作:

  • Lock: 作用于主内存,上锁。
  • Unlock: 作用于内存,解锁。
  • Read: 将主内存读入工作内存
  • Load:作用于把主内存值放入工作副本中
  • Use:把工作内存中变量传递给执行引擎
  • Assign:把执行引擎的值传给工作内存
  • Store:把工作内存传入主内存
  • Write:写入主内存

部分规范

  • Read Load 必须同时执行
  • Store Write 必须同时执行
  • Assign使用后必须同步给主内存
  • 不允许线程在没有Assign的情况下同步主内存
  • 一个新的变量只能从主内存中诞生
  • 如果同线程Lock多次,需相同数量Unlock来解锁
  • 执行Lock操作会清空工作内存中此变量的值
  • Unlock不能解锁没上锁变量,或者非本线程锁
  • 在Unlock前必须同步数据回主内存

部分并发测试工具

Apache Bench
Apache JMeter
Postman

Java 并发模拟

@NotThreadSafe
public class ConcurrencyTest {
    public static int count = 0;
    public static int clientTotal = 5000;
    public static int threadTotal = 200;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i=0; i<clientTotal; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    System.out.println(e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println(count);
    }

    public static void add() {
        count++;
    }
}

CountDownLatch 源码中解释如下:

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

Semaphore 源码中解释如下:

Semaphores are often used to restrict the number of threads than can access some (physical or logical) resource

代码中,countDownLatch设置了初始值为5000,也就是总共需要执行的add()次数。每次执行调用countDown()直到为0,关闭线程池并打印最终结果。每次执行过程中使用Semaphore.acquire()Semaphore.release()来加锁解锁。

线程安全性

此章节讲解线程安全的三大特性

安全性概念

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程如何进行交替执行,并且在主调代码中不需要任何和歪的同步或者协调,这个类都能表现出正确的行为,这个类就是线程安全的。

  • 原子性:提供了互斥访问,同一时刻只能有一个线程对它操作。
  • 可见性:一个线程对祝内存的修改可以及时被其他线程观察到。
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

原子性 - Atomic包

java.util.concurrent.atomic 中提供了很多atomic类,都是通过CAS来完成原子性的。

AtomicInteger

首先来看下AtmoicXXX 系列用法:

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
//count.getAndIncrement();

再来看下incrementAndGet的方法:

public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}
//private static final Unsafe U = Unsafe.getUnsafe()

我们可以发现此方法调用了U也就是unsafe这个类
继续来看下这个类中的getAndAddInt的用法:

@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

这个方法里面又调用了weakCompareAndSetInt,返回

return compareAndSetInt();

最后来看下核心函数:compareAndSetInt

public final native boolean compareAndSetInt();

native是Java底层实现的,总而言之CAS在收到修改请求后,会在修改前查看请求的数据有没有被别的线程修改。每次while loop 中getLongVolatile拿到的值都会在修改操作之前再次确认一下来保证其数据的原子性

LongAdder

JVM允许将64位的数据拆成两个32位的数据进行计算。Java Atomic 包底下的 LongAdder 可以存储一个 Int 类型数组,所有线程对数组进行操作最后将数组合并得到最终结果。通过不同 cell的分散热点,此类在AtomicLong的基础上增加了并行效率。
缺陷:如果在同行时候有并发更新,可能有数据误差。

AtomicReference

看一下案例代码:

public class AtomicExample {
    private static AtomicReference<Integer> count =
            new AtomicReference<>(0);
    public static void main(String[] args) {
        count.compareAndSet(0,2); //2
        count.compareAndSet(0,1); //skip
        count.compareAndSet(1,3); //skip
        count.compareAndSet(2,4); //4
        count.compareAndSet(3,5); //skip
        System.out.println(count.get());
    }
}

这个类可以调用compareAndSet来设定自定义的CAS规则

AtomicXXXFieldUpdater

看一下案例代码:

public class AtomicExample2 {
    @Getter
    public volatile int count = 100;
    private static AtomicExample2 example2 = new AtomicExample2();
    private static AtomicIntegerFieldUpdater<AtomicExample2> updater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicExample2.class, "count");

    public static void main(String[] args) {
        if (updater.compareAndSet(example2, 100, 120)) {
            System.out.println("update succeed");
        }
    }
}

被更改的值必须使用volatile关键字,之后会详细说明。

AtomicStampReference

此类主要为了解决 CAS 中的 ABA 问题。

ABA: 当某个线程把值A改成B再改回A的操作。此时其他线程调用CAS算法不会发现当前线程已经修改过了内存中的值。

解决方案:给每一次修改操作增加版本号,来看一下AtomicStampReference:

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

由此可知,expectedStamp 就是版本控制变量。

AtomicLongArray

此类维护的是一个数组,数组可以进行选择性的更新。对应某个index的修改也具备原子性。

AtomicBoolean

private static AtomicBoolean status;
status.compareAndSet(false,true);

原子性 - 锁

除了Atomic包之类,锁也可以保证程序的原子性。

  • synchronized: 依赖JVM的锁
  • Lock: 代码层面锁,依赖CPU命令 (ReetrantLock)

synchronized

修饰代码块:大括号内,作用于调用的对象
修饰方法:整个方法,作用于调用的对象
修饰静态方法:作用于所有对象
修饰类:括号里,作用于所有对象

// 修饰一个代码块
public void test1(int j) {
    synchronized (this) {
        for (int i = 0; i < 10; i++) {
            log.info("test1 {} - {}", j, i);
        }
    }
}

// 修饰一个方法
public synchronized void test2(int j) {
    for (int i = 0; i < 10; i++) {
        log.info("test2 {} - {}", j, i);
    }
}

下面看一下模拟多线程的结果:

public static void main(String[] args) {
        SynchronizedExample1 example1 = new SynchronizedExample1();
        SynchronizedExample1 example2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test2(1);
        });
        executorService.execute(() -> {
            example2.test2(2);
        });
    }

我们会发现,修饰同步代码块和直接修饰整个方法的结果相同。两个test方法都只保证了某个线程执行循环是有序的,不保证线程间的执行顺序。当类为静态类的时候,这时候一个线程执行完了才会另一个线程执行

注意:当子类继承带有synchronized关键字的父类时,子类中调用这些方法不具备锁的能力。(关键字不属于方法声明的一部分)

synchronized vs Lock

synchronized 为不可中断锁,适合竞争不激烈,可读性好。Lock 为可中断锁,多样化同步,竞争激烈时能维持常态。

可见性

一个线程对主内存的修改可以及时被其他线程接收。

导致共享变量线程间不可见原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 更新后共享变量没有及时更新

synchronized

JMM 对 synchronized的两条规定:

  • 线程解锁前,必须将共享变量的最新值刷新到主内存
  • 线程加锁时,将清空工作内存中的共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

volatile

实际在代码中,直接将变量加上volatile并不能保证线程安全。下面代码中,count++在底层实际执行了三步骤:

volatile count = 0;
count++;
// 1. read count
// 2. +1
// 3. store count

假设两个线程同时执行操作,volatile保证两个线程能读到内存中的值,但是两个线程的写入操作可能同时执行,导致其中一个线程的执行无效。

volatile 使用

volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while (!inited) {
    sleep();
}
doSomethingWithConfig(context);

volatile特别适合作为状态标记量

有序性

Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

happens-before 原则

不需要通过任何手段就可以得到保证的有序性。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 锁定规则: 一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,操作A先行于操作C。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程中断贵的:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件发生。
  • 线程中断规则:线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束,Thread.isAlive()的返回值检测是否终结。
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

安全发布对象

发布对象:使一个对象能够被当前范围之外的代码所使用
对象溢出:错误的发布,对象构造完成前被其他线程看见

安全发布的四种方法

  • 静态初始化函数中初始化对象引用
  • 初始化对象保存到volatile或者AtomicReference
  • 将对象的引用保存到某个正确构造对象的final中
  • 将对象引用保存到有锁的保护域中

单例写法 Singleton

先来看一个@NotThreadSafe的懒汉模式

@NotThreadSafe
public class SingletonExample1 {

    // 私有构造函数
    private SingletonExample1() {}

    // 单例对象
    private static SingletonExample1 instance = null;

    // 静态的工厂方法
    public static SingletonExample1 getInstance() {
        if (instance == null) {
            instance = new SingletonExample1();
        }
        return instance;
    }
}

两个线程可能同时判断对象没有被初始化,导致最后返回的构造器的对象错误。(虽然在这个例子中,没啥影响)

再来看一下ThreadSafe的饿汉模式

@ThreadSafe
public class SingletonExample2 {

    // 私有构造函数
    private SingletonExample2() {}

    // 单例对象
    private static SingletonExample2 instance = new SingletonExample2();

    // 静态的工厂方法
    public static SingletonExample2 getInstance() {
        return instance;
    }
}

虽然线程安全,饿汉模式保证了类的加载。如果类的加载损耗很大但是却没有被实力使用,会造成资源浪费。

所以我们来看一下@ThreadSafe的懒汉模式

@ThreadSafe
@NotRecommend
public class SingletonExample3 {

    // 私有构造函数
    private SingletonExample3() { }

    // 单例对象
    private static SingletonExample3 instance = null;

    // 静态的工厂方法
    public static synchronized SingletonExample3 getInstance() {
        if (instance == null) {
            instance = new SingletonExample3();
        }
        return instance;
    }
}

使用synchronized保证只有一个线程可以使用getInstance方法,但是性能开销会很大。

再来看一下NotThreadSafe的双重同步锁懒汉模式

@NotThreadSafe
public class SingletonExample4 {

    // 私有构造函数
    private SingletonExample4() { }

    // 1、memory = allocate() 分配对象的内存空间
    // 2、ctorInstance() 初始化对象
    // 3、instance = memory 设置instance指向刚分配的内存

    // JVM和cpu优化,发生了指令重排

    // 1、memory = allocate() 分配对象的内存空间
    // 3、instance = memory 设置instance指向刚分配的内存
    // 2、ctorInstance() 初始化对象

    // 单例对象
    private static SingletonExample4 instance = null;

    // 静态的工厂方法
    public static SingletonExample4 getInstance() {
        if (instance == null) { // 双重检测机制        // B
            synchronized (SingletonExample4.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample4(); // A - 3
                }
            }
        }
        return instance;
    }
}

双重检测机制并不能保证线程安全,因为此方法可能会返回一个还没有被构造完的实例对象。具体来说JVM的重排序可能导致instance=memory优先执行给对象制定了内存空间,让instance != null 但是对象的构造ctorInstance还来不及调用。

线程安全策略

本章节包含不可变对象,线程封闭,线程不安全的类与写法,以及同步并发容器的介绍。

不可变对象

不可变对象满足了发布安全策略因为只要发布他们就一定是安全的。不可变对象需要满足以下条件:

  • 对象创建以后其状态不能修改
  • 对象所有的field都是final类型
  • 对象是正确创建的

final关键字

final可以修饰类,方法,和变量

  • 修饰类:类不能被继承
  • 修饰方法:1.方法锁定 2.效率(早起会优化)
  • 修饰变量:基础数据变量,引用变量

集合框架转换不可变对象

  • Collections.unmodifiableXXX
  • Guava: ImmutableXXX 这两个方法提供了针对集合框架实现类中:Collection, List, Set, Map, 等等的不可变对象方法。

下面来看一下UnmodifiableMap的实现

private final Map<? extends K, ? extends V> m;
public boolean containsKey(Object key)   {
    return m.containsKey(key);
}
public V remove(Object key) {
    throw new UnsupportedOperationException();
}

其实底层就是存储了一个HashMap的实例来调用了常规的MapAPI,当调用修改函数时候,手动抛出异常

线程封闭

  • Ad-hoc 线程封闭:程序控制实现,糟糕,忽略
  • 堆栈封闭:局部变量,无并发问题。
  • ThreadLocal 线程封闭:特别好的封闭

ThreadLocal

在httpRequest中,假设用户传递进入一个值,不使用ThreadLocal的话需要把传入对象一层层的传递到数据库层。代码复杂。使用此类可以在接口操作前取出信息,又能保证线程安全(通过从类中拿值)

RequestHolder 存储该线程的值

public class RequestHolder {

    private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>();

    public static void add(Long id) {
        requestHolder.set(id);
    }

    public static Long getId() {
        return requestHolder.get();
    }

    public static void remove() {
        requestHolder.remove();
    }
}

HttpFilter过滤http请求

@Slf4j
public class HttpFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        log.info("do filter, {}, {}", Thread.currentThread().getId(), request.getServletPath());
        RequestHolder.add(Thread.currentThread().getId());
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

HttpInterceptor

@Slf4j
public class HttpInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle");
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        RequestHolder.remove();
        log.info("afterCompletion");
        return;
    }
}

afterCompletion表示httpRequest被执行完毕,我们需要手动删除ThreadLocal避免内存泄漏。
ThreadLocal 与内存泄漏

线程不安全类与写法

  • StringBuilder -> StringBuffer
  • SimpleDataFormat -> JodaTime.DataTimeFormatter
  • ArrayList, HashSet, HashMap 等集合
  • if(condition(a)) {handle(a);}

同步容器

开发中任何线程不安全的类与写法都需要程序员今天同步处理。Java中提供了同步容器:

  • List -> Vector, Stack
  • Map -> HashTable (key,value != null)
  • Collections.synchronizedXXX

需要注意的是,同步容器不能保证线程安全:

while (true) {
    for (int i = 0; i < 10; i++) {
        vector.add(i);
    }

    Thread thread1 = new Thread() {
         public void run() {
             for (int i = 0; i < vector.size(); i++) {
                 vector.remove(i);
             }
         }
    };

    Thread thread2 = new Thread() {
        public void run() {
            for (int i = 0; i < vector.size(); i++) {
                vector.get(i);
            }
        }
    };
    thread1.start();
    thread2.start();
}

上面代码虽然getremove都保证了原子性,但是两个线程的交替执行可能导致get(i)与remove(i)同时执行,导致数组越界或者空指针异常。

同步容器的遍历器有时候也不能保证线程安全:

// java.util.ConcurrentModificationException
private static void test1(Vector<Integer> v1) { // foreach
    for(Integer i : v1) {
        if (i.equals(3)) {
            v1.remove(i);
        }
    }
}

// java.util.ConcurrentModificationException
private static void test2(Vector<Integer> v1) { // iterator
    Iterator<Integer> iterator = v1.iterator();
    while (iterator.hasNext()) {
        Integer i = iterator.next();
        if (i.equals(3)) {
            v1.remove(i);
        }
    }
}

// success
private static void test3(Vector<Integer> v1) { // for
    for (int i = 0; i < v1.size(); i++) {
        if (v1.get(i).equals(3)) {
            v1.remove(i);
        }
    }
}

建议不要在遍历的过程中进行更新操作。

并发容器

Java.Util.Concurrent 实现了并发容器

  • ArrayList -> CopyOnWriteArrayList
  • HashSet -> CopyOnWriteArraySet
  • TreeSet -> ConcurrentSkipListSet
  • HashMap -> ConcurrentHashMap
  • TreeMap -> ConcurrentSkipListMap

CopyOnWriteArrayList

  • 写操作时复制旧数组,完成写操作然后读入。
  • 在锁的保护下达到线程安全。
  • 因为要频繁开辟新内存,导致young/full gc
  • 保证数据一致性,不能保证实时性。
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

需要注意的是,JDK中锁不再使用ReentrantLock

/**
* The lock protecting all mutators.  
* (We have a mild preference
* for builtin monitors over ReentrantLock
* when either will do.)
*/
final transient Object lock = new Object();

CopyOnWriteArraySet

  • 底层实现为CopyOnWriteArrayList
  • 开销较大
  • 迭代器不支持可变的remove操作

ConcurrentSkipListSet

  • JDK6新增的类,支持自然排序
  • 基于Map集合,多线程都是线程安全的
  • addAll等不能保证整个操作的原子性

ConcurrentHashMap

  • 针对读操作做了大量的优化
  • 高并发有特别好的表现

ConcurrentSkipListMap

  • 使用SkipList跳表结构实现
  • key是有序的
  • 它支持更高的并发,存储实际和线程无关
  • 在非多线程情况下使用ConcurrentHashMap

什么是SkipList?

SkipList in Data Structure

J.U.C

AQS - J.U.C 核心

全称 AbstractQueuedSynchronizer

  • 使用Node实现FIFO队列,可以用于构造锁或者其他同步装置的基础框架。
  • 利用init类表示状态
  • 使用方法是继承AQS
  • 子类需要通过实现acquirerelease方法来操纵状态
  • 可以同时实现排它锁和共享锁模式(E,M)

AQS 实现思路

  1. AQS 内部维持了一个CRS队列来管理锁
  2. 线程会尝试获得锁。如果失败,线程会包装成Node节点加入到同步队列。然后不断的循环尝试获取锁。只有Head的直接后径会尝试获得锁。
  3. 我们所用的同步组件,都是基于AQS的实现完成的。

AQS 同步组件

  • CountDownLatch
  • Semaphore
  • CyclicBarrier
  • ReentrantLock
  • Condition
  • FutureTask
  • ......

CountDownLatch

此类为同步辅助类,拥有阻塞当前线程的功能。该计数器的操作为原子操作。当计数为0,等待线程会被释放。需要注意的是只有一个线程会被堵塞,也就是一个线程会被释放,继续执行。

在指定时间完成CountDownLatch:

countDownLatch.await
(10, TimeUnit.MILLISECONDS);
for (int i = 0; i < threadCount; i++) {
    final int threadNum = i;
    Thread.sleep(10); //false!!!
    exec.execute(() -> {
        try {
        test(threadNum);
    } catch (Exception e) {
        log.error("exception", e);
    } finally {
        countDownLatch.countDown();
        }
    });
}
countDownLatch.await(10, TimeUnit.MILLISECONDS);
exec.shutdown();

需要注意的是,时间限制比如sleep必须在并发方法中调用。时间限制记录的不是整个循环语句的时间,而是方法调用时间。shutdown()操作不会立刻关系所有线程,而是让没有执行完的线程继续执行,所以await(10)还是会输出部分结果。

Semaphore

public static void main(String[] args) throws Exception {
    ExecutorService exec = Executors.newCachedThreadPool();
    final Semaphore semaphore = new Semaphore(3);
    for (int i = 0; i < threadCount; i++) {
        final int threadNum = i;
        exec.execute(() -> {
            try {
                semaphore.acquire(); // 获取一个许可
                test(threadNum);
                semaphore.release(); // 释放一个许可
            } catch (Exception e) {
                log.error("exception", e);
            }
        });
    }
    exec.shutdown();
}

非常简单,只需要使用acquirerelease来加锁解锁。下面来看一下获取多个锁的操作:

try {
    if (semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)) { // 尝试获取一个许可
    test(threadNum);
    semaphore.release(); // 释放一个许可
    }
} catch (Exception e) {
    log.error("exception", e);
}

尝试拿到多个锁,如果没有拿到,等到一定时间再丢弃。如果不加时间限制,那么没拿到锁就会直接丢弃当前线程。

CyclicBarrier

此类也是个同步辅助类,可以使多个线程互相等待,才能各自执行下面的操作。注意此类是从0开始向上累加。由于此类在释放等待线程后可以重用,所以是循环屏障

private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
    log.info("callback is running");
}); 

可以设置此类让它获得线程时优先执行callback中的指令,再继续方法执行。

private static void race(int threadNum) throws Exception {
    Thread.sleep(1000);
    log.info("{} is ready", threadNum);
    try {
        barrier.await(2000, TimeUnit.MILLISECONDS);
    } catch (Exception e) {
        log.warn("BarrierException", e);
    }
    log.info("{} continue", threadNum);
}

最后,如果需要给当前barrier设置最高等待时间,一定要抓住所有可能的异常抛出来保证后续程序能够完整执行。

ReentrantLock 与锁

ReentrantLock和synchronized区别:

  • 可重入性相差不大,都是通过计数器来获得锁,计数器为0的时候才能释放锁。
  • 锁的实现:synchronized依赖JVM,ReentrantLock是依赖JDK的。
  • 性能的区别:在synchronized引入了轻量锁,偏向锁后,两者性能差不多。
  • 功能的区别: synchronized简单一些,ReentrantLock灵活性更好。

ReentrantLock 独有的功能

  • 可指定是公平锁还是非公平锁
  • 提供了Condition类,实现分组唤醒。(synchronized要不随机唤醒,要不全部唤醒)
  • 提供能够中断等待锁的线程机制:lock.lockInterruptibly()
  • 公平锁:先等待的线程先获得锁

ReentrantLock 部分方法:

ReentrantReadWriteLock

看一下代码实例,有点长:

public class LockExample3 {

    private final Map<String, Data> map = new TreeMap<>();

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private final Lock readLock = lock.readLock();

    private final Lock writeLock = lock.writeLock();

    public Data get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Set<String> getAllKeys() {
        readLock.lock();
        try {
            return map.keySet();
        } finally {
            readLock.unlock();
        }
    }

    public Data put(String key, Data value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            readLock.unlock();
        }
    }

    class Data {

    }
}

此类实现的是悲观锁,当writeLock.lock()执行的时候,不允许其他线程还有readLock.lock()在执行。所以当读取很多写入很少的时候,此类容易让线程饥饿。(写操作一直等待读取操作完成,但是一直等不到)

StampedLock

此类有三种操作:写,读,乐观读
此锁的状态是由版本和模式两个部分组成。
锁的获取方法返回一个数字作为票据。

乐观读: 乐观的认为读取和写入的同时发生的情况很少。不用悲观的使用读取锁定

下面看一下乐观锁的源码案例:

//下面看看乐观读锁案例
double distanceFromOrigin() { // A read-only method
    long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
    double currentX = x, currentY = y;  //将两个字段读入本地局部变量
    if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
        stamp = sl.readLock();  //如果没有,我们再次获得一个读悲观锁
        try {
            currentX = x; // 将两个字段读入本地局部变量
            currentY = y; // 将两个字段读入本地局部变量
        } finally {
            sl.unlockRead(stamp);
        }
    }
    return Math.sqrt(currentX * currentX + currentY * currentY);
}

下面看一下悲观锁的源码案例:

//下面是悲观读锁案例
void moveIfAtOrigin(double newX, double newY) { // upgrade
    // Could instead start with optimistic, not read mode
    long stamp = sl.readLock();
    try {
        while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
            long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
            if (ws != 0L) { //这是确认转为写锁是否成功
                stamp = ws; //如果成功 替换票据
                x = newX; //进行状态改变
                y = newY;  //进行状态改变
                break;
            } else { //如果不能成功转换为写锁
                sl.unlockRead(stamp);  //我们显式释放读锁
                stamp = sl.writeLock();  //显式直接进行写锁 然后再通过循环再试
            }
        }
    } finally {
        sl.unlock(stamp); //释放读锁或写锁
    }
}

ReentrantLock + Condition

public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock();
    Condition condition = reentrantLock.newCondition();
    new Thread(() -> {
        try {
            reentrantLock.lock();
            log.info("wait signal"); // 1
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("get signal"); // 4
        reentrantLock.unlock();
    }).start();
    new Thread(() -> {
        reentrantLock.lock();
        log.info("get lock"); // 2
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        condition.signalAll();
        log.info("send signal ~ "); // 3
        reentrantLock.unlock();
    }).start();
}

下图是之前讲到的AQS队列,使用condition().await()会使当前线程加入到Condition queue里面。当condition().signalAll();执行时候,所有Condition里面的线程回到Sync queue里面来。当前线程unlock后,按照顺序唤醒其他线程。

锁的小结

当只有少量线程时,synchronized很适合。如果线程不少但是增加可预估,ReentrantLock是个非常通用的锁。其他不可测情况用StampedLock

FutureTask

J.U.C 里面非 AQS 的子类。传统的Runnable实现线程的缺陷:执行完任务之后无法获取执行结果。从JDK5开始通过FutureTask可以收到任务执行的结果。

Callable 与 Runnable 接口对比:

  • Runnable 简单粗暴,实现run()
  • Callable 是个范型接口,可以有返回值

Future

  • 也是一个接口
  • 它可以取消任务,查询任务状态,获取结果,等等。可以监视目标线程调用call的情况。也就是可以得到其他线程的返回值。

FutureTask

  • 父类为RunnableFuture
  • 父类继承了RunnableFuture
  • 最终执行Callable类型动作

下面是Future的演示

public class FutureExample {
    static class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            log.info("do something in callable");
            Thread.sleep(5000);
            return "Done";
        }
    }
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<String> future = executorService.submit(new MyCallable());
        log.info("do something in main");
        Thread.sleep(1000);
        String result = future.get();
        log.info("result:{}", result);
    }
}

submit提交Callable任务得到返回值
当调用future.get()发现返回值还没有计算结束时候,线程会堵塞直到收到返回值。

再来看一下FutureTask

public class FutureTaskExample {
    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                log.info("do something in callable");
                Thread.sleep(5000);
                return "Done";
            }
        });
        new Thread(futureTask).start();
        log.info("do something in main");
        Thread.sleep(1000);
        String result = futureTask.get();
        log.info("result:{}", result);
    }
}

futureTask传入Thread构造器可以直接调用start()执行当前线程,也可以使用futureTask来得到执行结果。就很nice。

Fork/Join 框架

JDK7提供的用于并行执行任务的框架。用于拆分大任务成小任务执行,最后汇总的框架。

工作窃取算法: 如果能将任务分成互不影响的小任务,那么假设一个线程已经执行完了它的全部任务,它可以从底部开始偷走别的线程的任务来提高程序效率。

缺点:当线程中只有一个任务,竞争依然会出现。创建多个线程进行小任务拆分也会有内存开销。

Fork/Join局限性

  • 只能使用ForkJoin进行同步机制,比如sleep
  • 不能进行I/O操作
  • 不能抛弃异常,必须代码处理异常

最后来看下代码:

public class ForkJoinTaskExample extends RecursiveTask<Integer> {

    public static final int threshold = 2;
    private int start;
    private int end;

    public ForkJoinTaskExample(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;

        //如果任务足够小就计算任务
        boolean canCompute = (end - start) <= threshold;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 如果任务大于阈值,就分裂成两个子任务计算
            int middle = (start + end) / 2;
            ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
            ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);

            // 执行子任务
            leftTask.fork();
            rightTask.fork();

            // 等待任务执行结束合并其结果
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();

            // 合并子任务
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkjoinPool = new ForkJoinPool();

        //生成一个计算任务,计算1+2+3+4
        ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);

        //执行一个任务
        Future<Integer> result = forkjoinPool.submit(task);

        try {
            log.info("result:{}", result.get());
        } catch (Exception e) {
            log.error("exception", e);
        }
    }
}

BlockingQueue

队列堵塞两种情况

  • 队列满时入队列
  • 队列空时出队列

堵塞队列是线程安全的,主要用于消费者与生产者环境

ArrayBlockingQueue

  • 容量有限,FIFO方式插入写入数据 DelayQueue
  • 必须实现delete接口
  • 定时关闭连接,缓存对象,超时处理都可以使用
  • 底层是PriorityQueue + Lock LinkedBlockingQueue
  • 可以指定容量
  • 内部是链表。 PriorityBlockingQueue
  • 带优先级的阻塞队列,没有边际
  • 有排序规则,允许插入NULL
  • 插入对象必须实现Comparable接口 SynchronousQueue
  • 只能存一个元素,同步队列