并发编程之美(基础篇)- 笔记

阅读 《Java并发编程之美》 – 翟陆续 (作者) 的笔记

第一章 并发编程线程基础

什么是线程

进程和线程的关系:

  • 线程是进程中的一个实体,线程本身是不会独立存在的
  • 进程是系统资源分配和调度的基本单位
  • 一个进程中至少有一个线程,进程中的多个线程共享进程的资源
  • CPU资源比较特殊,是分配到线程的

Java内存区域:一个进程中有多个线程,线程共享进程的堆和方法区,但线程有自己的程序计数器、虚拟机栈、本地方法栈

img

图片来源:Java内存区域(运行时数据区域)详解、JDK1.8与JDK1.7的区别 – 傑0327

线程的创建与运行

创建线程的三种方式:

  1. 继承 Thread 类
  2. 实现 Runnable 接口
  3. 使用 FutureTask 类

直接继承 Thread 并重写 run() 方法:

public class MyThread extends Thread{
    @Override
    public void run() {
        // code...
    }
}
new MyThread().start();

实现 Runnable 接口:

public class Task implements Runnable{
    @Override
    public void run() {
        // code...
    }
}
new Thread(new Task()).start();

实现 Callable 接口,可以有返回值:

public class Task implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return null;
    }
}
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
new Thread(futureTask).start();
futureTask.get();

总结:使用继承 Thread 的方式并没有将任务逻辑和线程机制分离,每次执行任务时都需要创建一个线程。而使用 Runnable 或 Callable 接口,可以使用一个线程执行多个任务。(最直接的方式就是将任务提交到线程池)

线程的等待与通知

// 阻塞线程的方法,直到:
// * 其他线程调用了 notify() 或 notifyAll()
// * 线程被中断,则抛出中断异常
// * 线程超时返回
// * 虚假唤醒
obj.wait();
obj.wait(5000);

obj.notify();
obj.notifyAll();

调用以上方法需要获取对象的监视器锁,有两种方式可以获得对象的监视器锁:

// 获取对象本身的监视器锁
public synchronized void method(){
    while (!condition)
    	wait();
}
// 获取 obj 对象的监视器锁
synchronized(obj){
    while (!condition)
        obj.wait();
}
  • 获取监视器锁后先检查条件是否满足,否则调用 wait() 将线程挂起并释放锁(使用 while循环 是为了避免虚假唤醒)
  • 当其他线程唤醒正在等待的线程时,被唤醒的线程会先竞争锁,得到锁的线程会从 wait() 方法返回并再次检查条件

线程中的方法

thread.join(); // 挂起调用线程,直到目标线程执行结束
Thread.sleep(1000); // 在指定时间内不参与CPU调度(静态方法)
Thread.yield(); // 提示CPU线程希望提前废弃CPU时间(静态方法)

线程中断:

thread.interrupt(); // 中断线程对象 
thread.isInterrupted(); // 判断线程对象是中断
Thread.interrupted(); // 判断调用线程是否被中断并清除中断标志(静态方法)

中断线程只是将标志置位,具体如何响应取决于线程自身(可能抛出异常或继续执行)

ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock(); // 等待线程获得锁再抛出中断异常
reentrantLock.lockInterruptibly(); // 立即抛出线程中断

线程上下文切换

系统的调度方式有两种:抢占式和非抢占式

在抢占式的系统中,线程通过时间片轮询的方式占用CPU,当时间片用完时让出CPU,执行线程上下文切换。

这种调度方式决定了当执行CPU密集型任务时,最多只能启动和CPU数同等的线程数;而执行IO密集型任务时,一般可以启动更多的线程。

线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的相互等待的现象,在无外力的作用下,这些线程会一直等待而无法继续运行下去。

产生死锁的四个必备条件:

  • 互斥条件:资源只能被一个线程持有(具有排他性)
  • 请求并持有:线程已持有资源,并请求其他被其他线程持有的资源
  • 不可剥夺条件:被持有的资源只有持有它的线程可以释放
  • 环路等待条件:对资源的请求形成一个环形链

解决死锁的方式:

  • 资源申请的有序性原则
  • (InnoDB)请求超时、图算法

守护线程与用户线程

JVM 进程在用户线程都结束后退出,而不管是否在有守护线程在执行。

Thread thread = new Thread();
thread.setDaemon(true);
thread.start();

ThreadLocal

下面代码每个线程都会拥有自己的SimpleDateFormat对象:

// 从SimpleDateFormat 的注释可以得知,SimpleDateFormat 不是线程安全的
private ThreadLocal<SimpleDateFormat> DateFormatContext = ThreadLocal.withInitial(()->{
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});

ThreadLocal 实现原理:

img

图片来源:ThreadLocal explained – DuyHai

Thread.set() 方法:

// 获取当前线程的 threadLocals 变量并将键值对(theadLocal, value)放到里面
// 并且 threadLocals 是懒加载的
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

线程本地的变量:TheadLocalMap 是一个线程本地的散列表

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

懒加载的 threadLocals:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
 * The initial capacity -- MUST be a power of two.
 */
private static final int INITIAL_CAPACITY = 16;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

ThreadLocal.get() 方法:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 以 TheadLocal 对象为键,从线程本地的散列表取值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

如果 Map 或对应的键还未初始化,则会返回初始值:

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

初始值默认是 null:上面的示例则将初始值设置为 SimpleDateFormat 对象

protected T initialValue() {
    return null;
}
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
	
    // 所以 supplier 本质是一个工厂,而 ThreadLocalMap 就是一个线程本地的容器
    // supplier.get() 会返回新创建的对象
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

ThreadLocal.remove():当本地对象不使用时要将其移除,防止内存溢出

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

ThreadLocal 总结:

  • ThreadLocal 其实就是一个工具壳,它会操作线程本地的散列表 threadLocals,散列表以 ThreadLocal 对象为键
  • 线程本地的 threadLocals 是懒加载的,初始容量是 16
  • 在不使用对象后应调用 remove() 避免内存溢出

InheritableThreadLocal

使用 TheadLocal,子线程访问不了父线程的本地变量,InheritableThreadLocal 解决了该问题。

源码分析:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
	// 根据父线程本地变量的值计算子线程本地变量的值(这里是直接返回原值)
    protected T childValue(T parentValue) {
        return parentValue;
    }

    // 重写父类 ThreadLocal 的方法,将 threadLocals 替换成 inheritableThreadLocals
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

在创建线程时,会调用 init() 方法:将父线程的本地变量浅拷贝到子线程

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc) {

    //...
    
    // 如果父线程的 inheritableThreadLocals 不为 null,则执行以下代码
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
	
    //...
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    // 变量父线程散列表键值对
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            // 如果键不为 null
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                // 获取键对应的值
                Object value = key.childValue(e.value);
                // 创建新的键值对
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                // 使用线性探测法解决散列冲突
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

InheritableThreadLocal 总结:InheritableThreadLocal 在创建线程时通过将父线程的本地变量复制到子线程以实现子线程可以访问父线程本地变量的目的。

第二章 并发编程的其他基础知识

什么是多线程并发编程

并发与并行的概念:

  • 并发是指同一时间段内多个任务同时在执行
  • 并行是指单位时间内多个任务同时在执行

时间段由多个单位时间组成

任务类型:

  • IO 密集型任务:对于IO密集型任务,我们应该尽量减少线程阻塞时对CPU的占用,减少CPU空闲时间
  • CPU密集型任务:对于CPU密集型任务,我们应该尽量减少线程上下文切换的开销

Java 中的线程安全问题

共享资源:可以被多个线程读写的资源

当多个线程同时对共享资源进行读写时,就必须进行同步。

Java 内存模型规定,将所有变量都存放在主内存中,当线程使用变量时,会把内存里面的变量复制到自己的工作内存,线程读写变量时操作的都是自己工作内存中的变量。

这时,就需要通过同步机制来保证对共享资源操作的原子性

image-20201102152438158

Java 中共享变量的内存可见性

Java 的内存模型是一个抽象的概念,工作内存对应到硬件架构就是CPU内的存储器、一级缓存、二级缓存。

image-20201102152855951

缓存导致的内存可见性问题:

  1. 线程A对共享变量1进行读写,并将结果同步到缓存和主内存
  2. 线程B对共享变量1进行读写(从二级缓存读取),并将结果同步到缓存和内存
  3. 此时,线程A在此对共享变量A进行读写,就会从一级缓存读到脏数据

Java 中的 synchronized 关键字

synchronized 是对象内部的一个监视器锁,它是一个排他锁。

synchronized 的内存语义:

  • 进入 synchronized 块的内存语义是把 synchronized 块内使用到的变量从线程的工作内存中清除,这样在 synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取;
  • 退出 synchronized 的语义是把 synchronized 块内对共享变量的修改刷新到主内存。

synchronized 可以解决共享变量内存的可见性问题,也可以用来实现原子性操作

Java 中的 volatile 关键字

当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或其他地方,而是会把值刷新会主内存;当其他线程读取该共享变量时,会从主内存重写获取最新值,而不是使用当前线程工作内存中的值。

volatile 可以解决共享变量内存的可见性问题和指令重排序问题,但不能保证操作的原子性

Java 中的 CAS 操作

CAS 通过硬件保证操作的原子性。

如果被操作的值存在环形转换,使用CAS算法就可能会出现ABA问题。解决的方式是增加一个递增的版本号或时间戳。

Unsafe 类

Unsafe 类中的方法:

// 获取变量的偏移值
public native long objectFieldOffset(Field f);
// 获取数组第一元素的地址
public native int arrayBaseOffset(Class<?> arrayClass);
// 获取数组一个元素的占用的字节
public native int arrayIndexScale(Class<?> arrayClass);

// 原子性地更新
public final native boolean compareAndSwapLong(Object o, long offset,
                                               long expected,
                                               long x);

// 获取 Long 类型的值(具有 volatile 语义)
public native long getLongVolatile(Object o, long offset);
// 设置 Long 类型的值(具有 volatile 语义)
public native void putLongVolatile(Object o, long offset, long x);
// 设置 Long 类型的值(不具有 volatile 语义)
public native void putOrderedLong(Object o, long offset, long x);


// 阻塞当前线程
public native void park(boolean isAbsolute, long time);
// 唤醒指定线程
public native void unpark(Object thread);
// 封装 CAS 算法的方法
public final long getAndSetLong(Object o, long offset, long newValue) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!compareAndSwapLong(o, offset, v, newValue));
    return v;
}

public final long getAndAddLong(Object o, long offset, long delta) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!compareAndSwapLong(o, offset, v, v + delta));
    return v;
}

获取 Unsafe 对象:

public final class Unsafe {

    private static native void registerNatives();
    static {
        registerNatives();
        sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
    }

    private Unsafe() {}

    private static final Unsafe theUnsafe = new Unsafe();
    
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
}	

由于 Unsafe 类做了限制,这里需要使用反射来获取 Unsafe 对象:

public class UnsafeTest {
    volatile long value;
    static long valueOffset;
    static Unsafe unsafe;

    static {
        try {
            Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            unsafe = (Unsafe) unsafeField.get(null);
            valueOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        UnsafeTest test = new UnsafeTest();
        boolean success = UnsafeTest.unsafe.compareAndSwapLong(test, UnsafeTest.valueOffset, 0, 1);
        System.out.println(success);
        System.out.println(test.value);
    }
}

指令重排序

Java 内存模型允许编译器和处理器对指令重排序以提高运行性能。

在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。

volatile 可以解决指令重排序的导致的问题。

伪共享

伪共享出现的原因是因为缓存和主内存进行数据交换的单位是缓存行,多线程去修改同一缓存行的不同变量时,只有一个线程可以去修改缓存行的变量,因为缓存一致性协议会使其他线程的同一缓存行失效,使线程只能重新从二级缓存或主内存读取,从而造成性能下降。

在单线程下,缓存行可以充分利用程序运行的局部性原理,从而提高程序性能。

多线程解决缓存行的方法:

  1. 字节填充
  2. @sun.misc.Contended 注解

使用 @Contended 注解使需要使用参数:-XX:-RestrictContended

默认宽度是 128 字节,可以使用 -XX:ContendedPaddingWidth 自定义宽度;

锁的概述

  • 悲观锁与乐观锁
  • 独占锁与共享锁
  • 公平锁与非公平锁
  • 可重入锁
  • 自旋锁 (默认次数是10次,-XX:PreBlockSpinsh