并发编程

超线程:一个ALU对应多个PC

并发程序的特点:

线程间通信

06_等待唤醒案例分析

sequenceDiagram
    生产者 ->> 同步对象: wait
    消费者 ->> 同步对象: notify
    同步对象 -->> 生产者: 继续执行

要注意,wait() notify() notifyAll()都需要在synchronized中

wait() 会释放锁,sleep() 不会

Object object = new Object();

new Thread(){
    @Override
    public void run() {
        synchronized (object){
            System.out.println("要5个包子");
            // 进入等待,这时候锁会被释放
            try {
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("得到了5个包子");
        }
    }
}.start();

new Thread(){
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (object){
            System.out.println("包子生产完毕,告诉顾客");
            // 通知等待线程中的任意一个
            object.notify();
        }
    }
}.start();

对象的共享

如果this在构造器完成构造之前逸出,还没被构造完成的对象被别人使用,会发生什么问题?

线程封闭

某个对象只能在某个线程之内使用

Ad-hoc线程封闭

栈封闭

public void process() {
    Object obj = new Object();
    // 对对象做些计算
    int result = obj.process();
    return result;
}

ThreadLocal

private static ThreadLocal<Connection> holder = ThreadLocal.withInitial(() -> getConnection());

不变性

不可变对象一定是线程安全的

安全发布

在多线程环境下使用可变的对象,需要通过安全发布的方式并且需要通过锁来保护

对象的组合

依赖状态的操作:某个操作包含有基于状态的先验操作

if (a== 1){
    a++;
}

在并发编程中,由于其他线程也会修改状态,所以需要一些JUC中的基础类库来帮助我们在并发环境下执行基于依赖的操作

在提供多线程API时,将是否线程安全文档化

实例封闭

将线程不安全的对象封装在某个进行良好并发控制的对象内

private Object obj = new Object();
...
synchronized(obj){
    obj.xxx();
}

线程安全委托

线程不安全的对象将线程安全的职责委托给线程安全的对象

// 线程安全的类
private AtomicInteger coutner = new AtomicInteger();
...
void increase() {
    counter.increase();
}

这种方式要求委托方对被委托方的API调用不能出现复合操作,否则委托方仍需要采用一定的线程安全机制

private AtomicInteger coutner1 = new AtomicInteger();
private AtomicInteger coutner2 = new AtomicInteger();
...
// × 不安全
void increase() {
    coutner1.increase();
    coutner2.increase();
}

取消与关闭

一个可取消的任务必须拥有取消策略

while(runnable) {
    // do something
}

使用中断来取消是最合理的方式,线程中断是线程之间协作的一种手段,中断是取消的一种语义实现,所以这要求你自己的线程必须决定如何响应中断

除非知道某个线程的中断策略,否则不要中断该线程

// thread1
while(!isInterrupted()){
    System.out.println("running");
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        break;
    }
}
System.out.println("my thread done");

// main thread
thread1.interrupt();

JVM 在线程阻塞状态时若发生中断,会抛出一个中断异常,在非阻塞情况下,就需要检查中断状态来判断是否发生中断

使用Future取消

Future<Double> future = service.submit(() -> {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return Math.random();
});
try {
    Double ret = future.get(3, TimeUnit.SECONDS);
    System.out.println("result"+ret);
} catch (ExecutionException | TimeoutException e) {
    e.printStackTrace();
}finally {
    future.cancel(true);
    System.out.println("task cancel");
}

处理不可中断的阻塞

由于如IO等的资源一旦阻塞就无法进行中断,所以可对其做关闭处理来模拟中断

停止基于线程的服务

基于生产者消费者的队列模式,要求消费者等待生产者完全关闭后,才能安全结束

处理非正常的线程终止

thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(t + "something happen" + e);
    }
});

new Thread(){
    @Override
    public void run() {
        throw new RuntimeException("aaaa");
    }
}.start();

JVM关闭钩子

Runtime.getRuntime().addShutdownHook(new Thread(){
    @Override
    public void run() {
        System.out.println("jvm shutdown");
    }
});

性能与伸缩性

引入线程的开销

如何减少锁的竞争

锁的请求频率越快 持有锁时间越长 竞争越激烈

并发程序测试

正确性测试

传统的单元测试只能在线程串行的情况运行

阻塞行为的测试:一个阻塞方法调用后线程应该等待到直至该线程被中断,抛出InterruptExpcetion

安全性测试:检查在并发情况下,极易发生错误的一些属性

资源管理测试:如测试对资源的限制是否真正起作用了

使用回调帮助测试:对于一些并发类库,会在某些节点回调客户端代码,可以利用这些回调来验证后验条件

加大线程切换以暴露错误:通过Thread.yield() 让步,产生更多的上下文切换,可能会更早暴露出错误

性能测试

使用场景选择 -> 多次执行场景 -> 衡量执行效率

性能测试陷阱

JVM 的某些行为会导致性能测试测量不准

锁优化

自旋锁与自适应自旋

是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态

自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,如果等待时间比较短,自旋还是很划算的

自旋超过一定的阈值就不会再继续重试,自适应自旋则代表这个阈值不是固定的,会根据性能监控情况动态调整

锁消除

对于被检测出不可能存在竞争的共享数据的锁进行消除

锁细化

经历缩小锁的作用范围

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗

如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部

synchronized(obj){
    //...
}
synchronized(obj){
    //...
}
synchronized(obj){
    //...
}
synchronized(obj){
    //...
    //..
    //...
}

轻量级锁

轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销

偏向锁

偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要

同步工具类设计

状态依赖性管理

// 可阻塞的状态依赖操作结构
获取锁
while(前置条件不满足) {
    释放锁
    等待至前置条件满足
    如果超时或者被interrupt则失败
}
执行动作
释放锁

调用者处理失败

简单地将失败传递给调用者:只是将处理失败的职责从服务代码转移到客户代码

synchroized void put() {
    if (full) throw Excetion
    putVal()
}

自旋阻塞

这种方式的问题在于如果线程一进入休眠,条件马上变为真,此时会浪费大量的时间在休眠上

void put(){
    while(true) {
        synchroized(this) {
            if (full) {
                Thread.sleep(1999);
                continue;
            }
            putVal()
            return;
        }
    }
}

条件队列

synchroized void put() {
    // wait会释放锁
    // 当从wait中恢复,也就是被唤醒了,此时又获得了这把锁
    // 等待的这个条件必须在变真时,以某种形式发出通知 否则死锁
    while(full) wait(); // 即使被唤醒了 也不代表前置条件为真了 所以wait必须在一个循环中
    doPut();
    notifyAll();
}

显式Condtion

这种方式相较于条件队列拥有更多的功能:可中断不可中断等待、基于时限的等待、公平等待

notFull = lock.newCondtion();
notEmpty = lock.newCondtion();
...
void put(){
    lock.lock();
    while(full) notFull.await();
    putVal();
    notEmpty.singnal();
    lock.unlock(); // 应该使用finally释放
}

并发编程良好实践