# 并发 - 理论基础


# 为什么需要多线程?

# CPU,内存,I/O 设备速度有差异,为了平衡三者速度差异。

  1. CPU 增加了缓存,以均衡与内存速度的差异。导致 “可见性 “ 问题
  2. 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异。导致 ” 原子性 “ 问题。
  3. 编译程序优化指令执行次序,使得缓存能够得到更加合理的利用。导致 ” 有序性 “ 问题。

# 线程不安全实例

多个线程对同一个共享数据进行访问而不采取同步操作的话,操作的结果是不一致的。

public class ThreadUnsafeExample {
    private int cnt = 0;
    public void add() {
        cnt++;
    }
    public int get() {
        return cnt;
    }
}
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}
857	// 结果总是小于1000

# 并发问题的根源

  1. 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。

    //线程1执行的代码
    int i = 0;
    i = 10;
     
    //线程2执行的代码
    j = i;
    假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
    
  2. 原子性:即一个操作或者多个操作要么全部执行并且执行过程不会被任何因素打断,要么就都不执行。

    int i = 1;
    
    // 线程1执行
    i += 1;
    
    // 线程2执行
    i += 1;
    由于CPU分时复用(线程切换)的存在,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,将造成最后写到内存中的i值是2而不是3。# 有序性: 重排序引起
    
  3. 有序性:即程序执行的顺序按照代码的先后顺序执行。


# JAVA 是怎么解决并发问题的:JMM(JAVA 内存模型)

# 第一 维度

JMM 本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法,具体包括:

​ 1. volatile、synchronized 和 final 三个关键字

​ 2. Happens-Before 规则

# 第二维度

1. 原子性:Java内存模型中,只保证基本的读取和赋值是原子性操作,要实现更大范围的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,从而保证了原子性。
1. 可见性:Java提供了一个volatile关键字来保证可见性。当一个共享变量被volatile关键字修饰时,它会保证修改的值立即被更新到主存中,当其他线程需要读取时,它会去内存中读取新值。另外,通过synchronized和Lock也能保证可见性,在释放锁之前会将对变量的修改刷新到主存当中,因此也可以保证可见性。
1. 有序性:在Java中,可以通过volatile关键字保证有序性,另外可以通过synchronized和Lock来保证有序性。当然JMM是通过 ”Happens-Before“ 规则来保证有序性的。
# Happens-Before 原则
1. 单一线程原则:在一个线程内,在程序前面的操作先行发生于后面的操作。
1. 管程锁定规则:一个unlock操作先行发生与后面对同一个锁的lock操作。
1. volatile变量规则:对一个volatile变量的写操作,先行发生与后面对这个变量的读操作。
1. 线程启动规则:Thread对象的start()方法调用先行发生于此线程的每一个动作。
1. 线程加入规则:Thread对象的结束先行发生于join()方法返回。
1. 线程中断规则:对线程interrupt()方法的调用先行发生与被中断线程的代码检测到到中断事件的发生,可以通过interrupted()方法检测到是否又中断发生。
1. 对象终结规则:一个对象的初始化完成先行发生与它的finalize()方法的开始。
1. 传递性:如果操作A先行发生与操作B,操作B先行发生与操作C,那么操作A先行发生与操作C。

# 线程安全?

将共享数据按照安全程度的强弱顺序分为一下五类:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。

# 线程安全的实现方法

  1. 互斥同步:synchronized 和 ReentrantLock

    ​ 互斥同步最主要的问题就是线程的阻塞和唤醒所带来的性能问题,因此这种同步也叫阻塞同步。

    ​ 互斥同步属于一种悲观的并发策略,无论共享数据是否真的会出现竞争,它都要进行加锁。

  2. 非阻塞同步:基于冲突检测的乐观并发策略,先进行操作,如果没有竞争,那就操作成功,否则才去补偿措施(不断重试,直到成功为止),这种策略不需要阻塞线程,因此这种同步操作也被称为非阻塞同步。

    ​ 乐观锁需要操作和冲突检测这两个步骤具备原子性,这里不能使用互斥同步来保证了,而是靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要又三个操作数,分别是内存地址 V,旧的预期值 A 和新值 B,当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

    ​ JUC 包中的整数原子类 AtomicInteger,其中的 compareAndSet () 和 getAndIncrement () 等方法都使用了 Unsafe 类的 CAS 操作。

    ABA 问题:如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

    ​ J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

  3. 无同步方案

    ​ 要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,它自然无须任何同步措施去保证正确性。

    # 栈封闭

    ​ 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的

    # 线程本地存储 (Thread Local Storage)
    # 可重入代码 (Reentrant Code)

    ​ 这种代码也叫纯代码 (Pure Code), 可以在代码执行的任何时刻中断它,转而去执行另一段代码,而在控制权返回后,原来的程序不会出现任何错误。

    特点:

    ​ 不依赖存储在堆上的数据和公共的系统资源。

    ​ 用到的状态量都由参数中传入。

    ​ 不调用非可重入方法等。

# 并发 - 线程基础

# 线程状态转换

image

  1. 新建(NEW):创建后尚未启动。
  2. 可运行(Runnable):可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态的 Running 和 Ready。
  3. 阻塞(Blocking):等待获取一个排它锁,如果其它线程释放了锁就会结束此状态。
  4. 无限期等待(Waiting):等待其他线程显示地唤醒,否则不会被分配 CPU 时间片。
进入方法 退出方法
没有设置 Timeout 参数的 Object.wait () 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join () 方法 被调用的线程执行完毕
LockSupport.park () 方法
  1. 限期等待(Timed Waiting):无需等待其他线程显式的唤醒,在一定时间之后会被系统自动唤醒。

    ​ 调用 Thread.sleep () 方法使线程进入限期等待状态时,常常用 “使一个线程睡眠” 进行描述。

    ​ 调用 Object.wait () 方法使线程进入限期等待或者无限期等待时,常常用 “挂起一个线程” 进行描述。

    ​ 睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

    ​ 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep () 和 Object.wait () 等方法进入。

    进入方法 退出方法
    Thread.sleep () 方法 时间结束
    设置了 Timeout 参数的 Object.wait () 方法 时间结束 / Object.notify () / Object.notifyAll ()
    设置了 Timeout 参数的 Thread.join () 方法 时间结束 / 被调用的线程执行完毕
    LockSupport.parkNanos () 方法
    LockSupport.parkUntil () 方法
  2. 死亡(Terminated)

    可以是线程结束任务之后自己结束,或者产生了异常而结束。

# 线程的使用方式

1. 实现Runnable接口。
1. 实现Callable接口。
1. 继承Thread类。

# 实现接口 VS 继承 Thread

实现接口会更好一些,因为:

  • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

# 基础线程机制

# Executor

​ Executor 管理多个异步任务的执行,无须显式的管理线程生命周期。

主要又三种 Executor:

  • CachedThreadPool:一个任务创建一个线程。
  • FixedThreadPool:所有任务只能使用固定大小的线程。
  • SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。

# Daemon

守护线程是程序运行时在后台提供服务的线程,当所有非守护线程结束时,程序也就终止,同时杀死所有守护线程。

main () 属于非守护线程。

使用 setEaemon () 方法将一个线程设置为守护线程。

# sleep()

休眠当前线程,单位为毫秒。

sleep () 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main () 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

# yield()

对静态方法 Thread.yield () 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其他线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

# 线程中断

一个线程执行完毕后会自动结束,如果在运行过程中发生异常也会提前结束。

# InterruptedException

​ 通过调用一个线程的 interrupt () 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

# interrupted()

如果一个线程的 run () 方法执行一个无限循环,并且没有执行 sleep () 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt () 方法就无法使线程提前结束。

但是调用 interrupt () 方法会设置线程的中断标记,此时调用 interrupted () 方法会返回 true。因此可以在循环体中使用 interrupted () 方法来判断线程是否处于中断状态,从而提前结束线程。

# Executor 的中断操作

​ 调用 Executor 的 shutdown () 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow () 方法,则相当于调用每个线程的 interrupt () 方法。

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("Thread run");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    executorService.shutdownNow();
    System.out.println("Main run");
}

​ 如果只想中断 Executor 中的一个线程,可以通过使用 submit () 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel (true) 方法就可以中断线程。

Future<?> future = executorService.submit(() -> {
    // ..
});
future.cancel(true);

# 线程互斥同步

​ Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

# 比较

1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

3. 等待可中断

当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。

ReentrantLock 可中断,而 synchronized 不行。

4. 公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

5. 锁绑定多个条件

一个 ReentrantLock 可以同时绑定多个 Condition 对象