Loading

JUC 并发编程 Part 1

😉 本文共8790字,阅读时间约15min

目录

CPU硬件原理

指令级并行原理 - 流水线

CPU时钟周期

例如,运行一条加法指令一般需要一个时钟周期时间。有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数。

程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time

指令五阶段

每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回这 5 个阶段

image-20230319124500109

注意:CPU是可以同时执行五个阶段的,相当于可以五个指令不同阶段并行,效率较高

指令重排序与组合 - 指令级并行

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行

// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2

支持流水线的处理器

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。

这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。

提示:奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃

image-20230319124950357

CPU缓存结构

CPU缓存结构

image-20230319125123162

  • 速度比较,大约4倍的增长
从CPU到 大约需要的时钟周期
寄存器 1 cycle
L1 Cache 3~4 cycle
L2 10~20 cycle
L3 40~45 cycle
内存 120~240 cycle

内存屏障

Memory Barrier(Memory Fence)

  • 可见性
    • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
    • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
  • 有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

image-20230319130248536

进程与线程

操作系统层面的进程与线程

  1. 进程:程序由指令和数据组成。

    1. 但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程
    2. 在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
  2. 线程:

    1. 一个进程之内可以分为一到多个线程。
    2. 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
    3. Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
  3. 对比:

    1. 进程基本上相互独立的,而线程存在于进程内,内部的线程共享进程的资源
    2. 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
    3. 进程间通信较为复杂
      1. 同一台计算机的进程通信称为 IPC(Inter-process communication)
      2. 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
    4. 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量

创建和运行线程

直接使用Thread

Thread t1 = new Thread("t1") {
    public void run() {
    	log.debug("hello");
    }
};
t1.start();

使用Runnable配合Thread

// Thread 代表线程
// Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
    public void run(){
    	// 要执行的任务
    }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
    log.debug("hello");
    return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

线程池方式创建Thread

查看进程线程的方法

# linux
ps -ef # 查看所有进程
top # 按大写 H 切换是否显示线程
top -H -p <PID> # 查看某个进程(PID)的所有线程
# Java
jps # 命令查看所有 Java 进程
jstack <PID> # 查看某个 Java 进程(PID)的所有线程状态
jconsole # 来查看某个 Java 进程中线程的运行情况(图形界面)

Thread类常见方法

start和run

  1. start():启动一个新线程,在新的线程运行 run 方法中的代码。start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。
  2. run():运行任务,不start绑定操作系统线程的话,实际上还是同步调用。直接调用 run 是在主线程中执行了 run,没有启动新的线程。

sleep与yield

  1. sleep()
    1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
    2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
    3. 睡眠结束后的线程未必会立刻得到执行
  2. yield()
    1. 用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
    2. 具体的实现依赖于操作系统的任务调度器

wait、join、park

interrupt

  1. 打断 sleep,wait,join 的线程:这几个方法都会让线程进入阻塞状态
    1. 打断 sleep 的线程, 会清空打断状态,以 sleep 为例
  2. 打断正常运行的线程, 不会清空打断状态
  3. 打断 park 线程, 不会清空打断状态

不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

  1. stop():停止线程运行
  2. suspend():挂起(暂停)线程运行
  3. resume():恢复线程运行

主线程和守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

t1.setDaemon(true);

垃圾回收器线程就是一种守护线程

Java线程六态

image-20230320163745520

  1. NEW 线程刚被创建,但是还没有调用 start() 方法
  2. RUNNABLE 当调用了start()方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  3. BLOCKEDWAITING TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
  4. TERMINATED 当线程代码运行结束

假设有线程 Thread t

情况 1 NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <--> WAITING

  • t 线程用 synchronized(obj) 获取了对象锁后
    • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
    • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
      • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
      • 竞争锁失败,t 线程从 WAITING --> BLOCKED

情况3 RUNNABLE <--> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
    • 注意是当前线程在t 线程对象的监视器上等待
    • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况4 RUNNABLE <--> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况5 RUNNABLE <--> TIMED_WAITING

  • wait(long n)、join(long n)、sleep(long n)

  • LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis)

情况6 RUNNABLE <--> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

临界区

  1. 多个线程访问共享资源
    1. 多个线程读共享资源其实也没有问题
    2. 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  2. 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

阻塞式的解决方案:synchronized,Lock。

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

非阻塞式的解决方案:原子变量

线程的变量安全分析

看是否能逃逸出方法作用范围

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

定位死锁

  • 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁

避免死锁要注意加锁顺序,顺序加锁法,一次封锁法

另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后用 jstack 排查

JVM层面线程模型

JVM层面线程运行原理

栈与栈帧

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

线程上下文切换

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器,它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • 线程频繁上下文切换会影响性能

Java内存模型

  • JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
  • JMM 体现在以下几个方面
    • 原子性 - 保证指令不会受到线程上下文切换的影响
    • 可见性 - 保证指令不会受 cpu 缓存的影响
    • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

image-20230320171945194

image-20230320164518852

并发编程之模式

同步模式之保护性暂停

  • 保护性暂停,即Guarded Suspension ,用在一个线程等待另一个线程的执行结果。

  • 要点:

    • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject。
    • 如果有结果不断地从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
    • JDK 中,join 的实现、Future 的实现,采用的就是此模式

image-20230327144157300

实现1 wait-notify

class GuardedObject {
    private Object response;
    private final Object lock = new Object();

    public Object get() {
        synchronized (lock) {
            // 条件不满足则等待
            while (response == null) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足,通知等待线程
            this.response = response;
            lock.notifyAll();
        }
    }

    // 应用
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(() -> {
            try {
                // 子线程执行下载
                List<String> response = download();
                guardedObject.complete(response);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
        
        // 主线程阻塞等待
        Object response = guardedObject.get();
    }
}

实现2 带超时版本

class GuardedObjectV2 {
    private Object response;
    private final Object lock = new Object();

    public Object get(long millis) {
        synchronized (lock) {
            // 1) 记录最初时间
            long begin = System.currentTimeMillis();
            // 2) 已经经历的时间
            long timePassed = 0;
            while (response == null) {
                // 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等
                long waitTime = millis - timePassed;
                if (waitTime <= 0) {
                    break;
                }
                try {
                    lock.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 3) 如果提前被唤醒,这时已经经历的时间假设为 400
                timePassed = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    public void complete(Object response) {
        synchronized (lock) {
            // 条件满足,通知等待线程
            this.response = response;
            lock.notifyAll();
        }
    }
}

同步模式之Balking

  • Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。
  • 单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用getInstance)时的线程安全,并思考注释中的问题
    • 饿汉式:类加载就会导致该单实例对象被创建
    • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

饿汉单例

// 问题1:为什么加 final
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
public final class Singleton implements Serializable {
	// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
    private Singleton() {}
    
    // 问题4:这样初始化是否能保证单例对象创建时的线程安全?
    private static final Singleton INSTANCE = new Singleton();
    
    // 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
    public static Singleton getInstance() {
    	return INSTANCE;
    }
    public Object readResolve() {
   		return INSTANCE;
    }
}

枚举单例

// 问题1:枚举单例是如何限制实例个数的
// 问题2:枚举单例在创建时是否有并发问题
// 问题3:枚举单例能否被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {
	INSTANCE;
}

懒汉单例

public final class Singleton {
	private Singleton() { }
    private static Singleton INSTANCE = null;
    
    // 分析这里的线程安全, 并说明有什么缺点
    public static synchronized Singleton getInstance() {
        if( INSTANCE != null ){
        	return INSTANCE;
        }
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}

DCL懒汉单例

public final class Singleton {
    private Singleton() {
    }

    // 问题1:解释为什么要加 volatile ?
    private static volatile Singleton INSTANCE = null;

    // 问题2:对比实现3, 说出这样做的意义
    public static Singleton getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        synchronized (Singleton.class) {
            // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
            if (INSTANCE != null) { // t2
                return INSTANCE;
            }
            INSTANCE = new Singleton();
            return INSTANCE;
        }
    }
}

静态内部类懒汉单例

public final class Singleton {
    private Singleton() { }
    // 问题1:属于懒汉式还是饿汉式
    private static class LazyHolder {
    	static final Singleton INSTANCE = new Singleton();
    }
    // 问题2:在创建时是否有并发问题
    public static Singleton getInstance() {
    	return LazyHolder.INSTANCE;
    }
}

同步模式之顺序控制

固定运行顺序

wait-notify
class Order {
    // 用来同步的对象
    static Object obj = new Object();
    // t2 运行标记, 代表 t2 是否执行过
    static boolean t2runed = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                // 如果 t2 没有执行过
                while (!t2runed) {
                    try {
                        // t1 先等一会
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(1);
            }
        });


        Thread t2 = new Thread(() -> {
            System.out.println(2);
            synchronized (obj) {
                // 修改运行标记
                t2runed = true;
                // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
                obj.notifyAll();
            }
        });
        t1.start();
        t2.start();
    }
}
park-unpark

可以看到,实现上很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:

可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目,park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』。

class Order {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            // 当没有『许可』时,当前线程暂停运行;
            // 有『许可』时,用掉这个『许可』,当前线程恢复运行
            LockSupport.park();
            System.out.println("1");
        });
        
        Thread t2 = new Thread(() -> {
            System.out.println("2");
            // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
            LockSupport.unpark(t1);
        });
        t1.start();
        t2.start();
    }
}

交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现?

wait-notify
class SyncWaitNotify {
    private int flag;
    private int loopNumber;

    public SyncWaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }

    public void print(int waitFlag, int nextFlag, String str) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (this.flag != waitFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
        new Thread(() -> {
            syncWaitNotify.print(1, 2, "a");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(2, 3, "b");
        }).start();
        new Thread(() -> {
            syncWaitNotify.print(3, 1, "c");
        }).start();
    }
}
Lock 条件变量版
class AwaitSignal extends ReentrantLock {
    // 循环次数
    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void start(Condition first) {
        this.lock();
        try {
            log.debug("start");
            first.signal();
        } finally {
            this.unlock();
        }
    }

    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                current.await();
                log.debug(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();
            }
        }
    }

    public static void main(String[] args) {
        AwaitSignal as = new AwaitSignal(5);
        Condition aWaitSet = as.newCondition();
        Condition bWaitSet = as.newCondition();
        Condition cWaitSet = as.newCondition();
        new Thread(() -> {
            as.print("a", aWaitSet, bWaitSet);
        }).start();
        new Thread(() -> {
            as.print("b", bWaitSet, cWaitSet);
        }).start();
        new Thread(() -> {
            as.print("c", cWaitSet, aWaitSet);
        }).start();
        as.start(aWaitSet);
    }
}

注意:该实现没有考虑 a,b,c 线程都就绪再开始

park-unpark
class SyncPark {
    private int loopNumber;
    private Thread[] threads;

    public SyncPark(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void setThreads(Thread... threads) {
        this.threads = threads;
    }

    public void print(String str) {
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park();
            System.out.print(str);
            LockSupport.unpark(nextThread());
        }
    }

    private Thread nextThread() {
        Thread current = Thread.currentThread();
        int index = 0;
        for (int i = 0; i < threads.length; i++) {
            if (threads[i] == current) {
                index = i;
                break;
            }
        }
        if (index < threads.length - 1) {
            return threads[index + 1];
        } else {
            return threads[0];
        }
    }

    public void start() {
        for (Thread thread : threads) {
            thread.start();
        }
        LockSupport.unpark(threads[0]);
    }

    public static void main(String[] args) {
        SyncPark syncPark = new SyncPark(5);
        Thread t1 = new Thread(() -> {
            syncPark.print("a");
        });
        Thread t2 = new Thread(() -> {
            syncPark.print("b");
        });
        Thread t3 = new Thread(() -> {
            syncPark.print("c\n");
        });
        syncPark.setThreads(t1, t2, t3);
        syncPark.start();
    }
}

异步模式之生产者/消费者

  • 要点
    • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
      消费队列可以用来平衡生产和消费的线程资源
    • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
      消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
    • JDK 中各种阻塞队列,采用的就是这种模式

image-20230327153714903

实现

class MessageQueue {
    private LinkedList<Message> queue;
    private int capacity;

    public MessageQueue(int capacity) {
        this.capacity = capacity;
        queue = new LinkedList<>();
    }

    public Message take() {
        synchronized (queue) {
            while (queue.isEmpty()) {
                log.debug("没货了, wait");
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Message message = queue.removeFirst();
            queue.notifyAll();
            return message;
        }
    }

    public void put(Message message) {
        synchronized (queue) {
            while (queue.size() == capacity) {
                log.debug("库存已达上限, wait");
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            queue.addLast(message);
            queue.notifyAll();
        }
    }
    
    public static void main(String[] args) {
        MessageQueue messageQueue = new MessageQueue(2);
        // 4 个生产者线程, 下载任务
        for (int i = 0; i < 4; i++) {
            int id = i;
            new Thread(() -> {
                try {
                    log.debug("download...");
                    List<String> response = Downloader.download();
                    log.debug("try put message({})", id);
                    messageQueue.put(new Message(id, response));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }, "生产者" + i).start();
        }
        // 1 个消费者线程, 处理结果
        new Thread(() -> {
            while (true) {
                Message message = messageQueue.take();
                List<String> response = (List<String>) message.getMessage();
                log.debug("take message({}): [{}] lines", message.getId(), response.size());
            }
        }, "消费者").start();
    }
}

异步模式之享元模式

  • 让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。

  • 不同工作类型使用不同的线程池

包装类

在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象:

public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
    	return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

注意:

  • Byte, Short, Long 缓存的范围都是 -128~127
  • Character 缓存的范围是 0~127
  • Boolean 缓存了 TRUE 和 FALSE
  • Integer的默认范围是 -128~127
    • 最小值不能变
    • 但最大值可以通过调整虚拟机参数 -Djava.lang.Integer.IntegerCache.high 来改变

String串池

饥饿

  • 固定大小线程池会有饥饿现象

    • 两个工人是同一个线程池中的两个线程。他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作。

      客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待。后厨做菜:没啥说的,做就是了。

      比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好、但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿。

  • 解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,不同的任务类型,采用不同的线程池。

创建多少线程合适

跟任务与硬件资源都有关

  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存
CPU密集型
  • 通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费
IO密集型
  • CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。
  • 经验公式如下
    • 线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间
    • 例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式4 * 100% * 100% / 10% = 40

自定义工作线程池

image-20230327162958972

步骤一:自定义拒绝策略接口
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
	void reject(BlockingQueue<T> queue, T task);
}
步骤2:自定义任务队列
class BlockingQueue<T> {
    // 1. 任务队列
    private Deque<T> queue = new ArrayDeque<>();
    // 2. 锁
    private ReentrantLock lock = new ReentrantLock();
    // 3. 生产者条件变量
    private Condition fullWaitSet = lock.newCondition();
    // 4. 消费者条件变量
    private Condition emptyWaitSet = lock.newCondition();
    // 5. 容量
    private int capcity;

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }// 带超时阻塞获取

    public T poll(long timeout, TimeUnit unit) {
        lock.lock();
        try {
            // 将 timeout 统一转换为 纳秒
            long nanos = unit.toNanos(timeout);
            while (queue.isEmpty()) {
                try {
                    // 返回值是剩余时间
                    if (nanos <= 0) {
                        return null;
                    }
                    nanos = emptyWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 阻塞获取
    public T take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                try {
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        } finally {
            lock.unlock();
        }
    }

    // 阻塞添加
    public void put(T task) {
        lock.lock();
        try {
            while (queue.size() == capcity) {
                try {
                    log.debug("等待加入任务队列 {} ...", task);
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
        } finally {
            lock.unlock();
        }
    }

    // 带超时时间阻塞添加
    public boolean offer(T task, long timeout, TimeUnit timeUnit) {
        lock.lock();
        try {
            long nanos = timeUnit.toNanos(timeout);
            while (queue.size() == capcity) {
                try {
                    if (nanos <= 0) {
                        return false;
                    }
                    log.debug("等待加入任务队列 {} ...", task);
                    nanos = fullWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            log.debug("加入任务队列 {}", task);
            queue.addLast(task);
            emptyWaitSet.signal();
            return true;
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return queue.size();
        } finally {
            lock.unlock();
        }
    }

    public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
        lock.lock();
        try {
            // 判断队列是否满
            if (queue.size() == capcity) {
                rejectPolicy.reject(this, task);
            } else { // 有空闲
                log.debug("加入任务队列 {}", task);
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        } finally {
            lock.unlock();
        }
    }
}
步骤3:自定义线程池
class ThreadPool {
    // 任务队列
    private BlockingQueue<Runnable> taskQueue;
    // 线程集合
    private HashSet<Worker> workers = new HashSet<>();
    // 核心线程数
    private int coreSize;
    // 获取任务时的超时时间
    private long timeout;
    private TimeUnit timeUnit;
    private RejectPolicy<Runnable> rejectPolicy;

    // 执行任务
    public void execute(Runnable task) {
        // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
        // 如果任务数超过 coreSize 时,加入任务队列暂存
        synchronized (workers) {
            if (workers.size() < coreSize) {
                Worker worker = new Worker(task);
                log.debug("新增 worker{}, {}", worker, task);
                workers.add(worker);
                worker.start();
            } else {
                // taskQueue.put(task);
                // 1) 死等
                // 2) 带超时等待
                // 3) 让调用者放弃任务执行
                // 4) 让调用者抛出异常
                // 5) 让调用者自己执行任务
                taskQueue.tryPut(rejectPolicy, task);
            }
        }
    }

    public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,
                      RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapcity);
        this.rejectPolicy = rejectPolicy;
    }

    class Worker extends Thread {
        private Runnable task;

        public Worker(Runnable task) {
            this.task = task;
        }

        @Override
        public void run() {
            // 执行任务
            // 1) 当 task 不为空,执行任务
            // 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
            // while(task != null || (task = taskQueue.take()) != null) {
            while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                try {
                    log.debug("正在执行...{}", task);
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            synchronized (workers) {
                log.debug("worker 被移除{}", this);
                workers.remove(this);
            }
        }
    }
}

连接线程池

例如:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。

import java.util.concurrent.atomic.AtomicIntegerArray;

class Pool {
    // 1. 连接池大小
    private final int poolSize;
    // 2. 连接对象数组
    private Connection[] connections;
    // 3. 连接状态数组 0 表示空闲, 1 表示繁忙
    private AtomicIntegerArray states;

    // 4. 构造方法初始化
    public Pool(int poolSize) {
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.states = new AtomicIntegerArray(new int[poolSize]);
        for (int i = 0; i < poolSize; i++) {
            connections[i] = new MockConnection("连接" + (i + 1));
        }
    }

    // 5. 借连接
    public Connection borrow() {
        while (true) {
            for (int i = 0; i < poolSize; i++) {
                // 获取空闲连接
                if (states.get(i) == 0) {
                    if (states.compareAndSet(i, 0, 1)) {
                        log.debug("borrow {}", connections[i]);
                        return connections[i];
                    }
                }
            }
            // 如果没有空闲连接,当前线程进入等待
            synchronized (this) {
                try {
                    log.debug("wait...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 6. 归还连接
    public void free(Connection conn) {
        for (int i = 0; i < poolSize; i++) {
            if (connections[i] == conn) {
                states.set(i, 0);
                synchronized (this) {
                    log.debug("free {}", conn);
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

class MockConnection implements Connection {
    // 实现略
}
  • 以上实现没有考虑:
    • 连接的动态增长与收缩
    • 连接保活(可用性检测)
    • 等待超时处理
    • 分布式 hash
  • 对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid等 对于更通用的对象池,可以考虑使用apache
  • commons pool,例如redis连接池可以参考jedis中关于连接池的实现

终止模式之两阶段终止模式

Two Phase Termination:在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。

  • 错误思路:
    • 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
    • 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止。

实现

image-20230327163719391

利用isInterrupted

interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行

class TPTInterrupt {
    private Thread thread;

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                    current.interrupt();
                }
                // 执行监控操作
            }
        }, "监控线程");
        thread.start();
    }

    public void stop() {
        thread.interrupt();
    }
}
利用停止标记
// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性
// 我们的例子中,即主线程把它修改为 true 对 t1 线程可见
class TPTVolatile {
    private Thread thread;
    private volatile boolean stop = false;

    public void start() {
        thread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                }
                // 执行监控操作
            }
        }, "监控线程");
        thread.start();
    }

    public void stop() {
        stop = true;
        thread.interrupt();
    }
}

多线程应用

多线程提高运算效率

单核与多核,注意单核多线程下频繁的上下文切换的影响

限制对资源的使用

CPU

  • 在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yieldsleep 来让出 cpu 的使用权给其他程序
  • 可以用 waitcondition条件变量达到类似的效果,不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
  • sleep 适用于无需锁同步的场景

共享资源

  • semaphore信号量

    • 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,对比 Tomcat LimitLatch 实现)
    • 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
  • 单位时间内限流:guava

互斥

悲观锁

synchronized

乐观锁

CAS

同步和异步

需要等待结果

这时既可以使用同步处理,也可以使用异步来处理

join同步
  1. 需要外部共享变量,不符合面向对象封装的思想
  2. 必须等待线程结束,不能配合线程池使用
static int result = 0;
private static void test1() throws InterruptedException {
    log.debug("开始");
    Thread t1 = new Thread(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        result = 10;
    }, "t1");
    t1.start();
    t1.join();
    log.debug("结果为:{}", result);
}
Future同步
  1. 规避了使用 join 之前的缺点
  2. 可以方便配合线程池使用
  3. 仍然是 main 线程接收结果
  4. get 方法是让调用线程同步等待
private static void test2() throws InterruptedException, ExecutionException {
    log.debug("开始");
    FutureTask<Integer> result = new FutureTask<>(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        return 10;
    });
    new Thread(result, "t1").start();
    log.debug("结果为:{}", result.get());
}
private static void test3() throws InterruptedException, ExecutionException {
    ExecutorService service = Executors.newFixedThreadPool(1);
    log.debug("开始");
    Future<Integer> result = service.submit(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        return 10;
    });
    log.debug("结果为:{}, result 的类型:{}", result.get(), result.getClass());
    service.shutdown();
}
自定义同步实现(保护性暂停)

CountDownLatch

CompletableFuture 实现(异步)
  1. 可以让调用线程异步处理结果,实际是其他线程去同步等待
  2. 可以方便地分离不同职责的线程池
  3. 以任务为中心,而不是以线程为中心
private static void test4() {
    // 进行计算的线程池
    ExecutorService computeService = Executors.newFixedThreadPool(1);
    // 接收结果的线程池
    ExecutorService resultService = Executors.newFixedThreadPool(1);
    log.debug("开始");
    
    CompletableFuture.supplyAsync(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        return 10;
    }, computeService).thenAcceptAsync((result) -> {
    	log.debug("结果为:{}", result);
    }, resultService);
}
BlockingQueue 实现(异步)
private static void test6() {
    ExecutorService consumer = Executors.newFixedThreadPool(1);
    ExecutorService producer = Executors.newFixedThreadPool(1);
    BlockingQueue<Integer> queue = new SynchronousQueue<>();
    
    log.debug("开始");
    producer.submit(() -> {
        log.debug("开始");
        sleep(1);
        log.debug("结束");
        try {
        	queue.put(10);
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }
    });
    
    consumer.submit(() -> {
        try {
            Integer result = queue.take();
            log.debug("结果为:{}", result);
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }
    });
}

不需等待结果

这时最好是使用异步来处理

普通线程实现
private static void test1() {
    new Thread(() -> FileReader.read(Constants.MP4_FULL_PATH)).start();
    log.debug("do other things ...");
}

@Slf4j(topic = "c.FileReader")
public class FileReader {
    public static void read(String filename) {
        int idx = filename.lastIndexOf(File.separator);
        String shortName = filename.substring(idx + 1);
        
        try (FileInputStream in = new FileInputStream(filename)) {
            log.debug("read [{}] start ...", shortName);
            byte[] buf = new byte[1024];
            int n = -1;
            do {
            n = in.read(buf);
            } while (n != -1);
  
        } catch (IOException e) {
        	e.printStackTrace();
        }
    }
}

输出

18:41:53 [main] c.Async - do other things ...
18:41:53 [Thread-0] c.FileReader - read [1.mp4] start ...
18:41:57 [Thread-0] c.FileReader - read [1.mp4] end ... cost: 4197 ms
线程池实现
private static void test2() {
	ExecutorService service = Executors.newFixedThreadPool(1);
    service.execute(() -> FileReader.read(Constants.MP4_FULL_PATH));
    log.debug("do other things ...");
    service.shutdown();
}
CompletableFuture 实现
private static void test3() throws IOException {
    CompletableFuture.runAsync(() -> FileReader.read(Constants.MP4_FULL_PATH));
    log.debug("do other things ...");
    System.in.read();
}

输出

11:09:38.145 c.TestAsyc [main] - do other things ...
11:09:38.145 c.FileReader [ForkJoinPool.commonPool-worker-1] - read [1.mp4] start ...
11:09:40.514 c.FileReader [ForkJoinPool.commonPool-worker-1] - read [1.mp4] end ... cost: 2369 ms

缓存

缓存更新策略

  • 更新时,是先清缓存还是先更新数据库?

    • 先清缓存

      image-20230322105923116

    • 先更新数据库

      image-20230322110004116

补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询

image-20230322110048170

这种情况的出现几率非常小,见 facebook 论文

读写锁实现一致性缓存

使用读写锁实现一个简单的按需加载缓存

  • 读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
    • 适合读多写少,如果写操作比较频繁,以上实现性能低
    • 没有考虑缓存容量
    • 没有考虑缓存过期
    • 只适合单机
    • 并发性还是低,目前只会用一把锁
    • 更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)
  • 乐观锁实现:用 CAS 去更新

分治

典型的Fork-Join

统筹

顺序性问题,满足性问题,保护性暂停,wait-notify

定时

如何让每周四 18:00:00 定时执行任务?

// 获得当前时间
LocalDateTime now = LocalDateTime.now();
// 获取本周四 18:00:00.000
LocalDateTime thursday =
now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0);
// 如果当前时间已经超过 本周四 18:00:00.000, 那么找下周四 18:00:00.000
if(now.compareTo(thursday) >= 0) {
	thursday = thursday.plusWeeks(1);
}

// 计算时间差,即延时执行时间
long initialDelay = Duration.between(now, thursday).toMillis();
// 计算间隔时间,即 1 周的毫秒值
long oneWeek = 7 * 24 * 3600 * 1000;

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
    System.out.println("开始时间:" + new Date());
    executor.scheduleAtFixedRate(() -> {
    System.out.println("执行时间:" + new Date());
}, initialDelay, oneWeek, TimeUnit.MILLISECONDS);
posted @ 2023-04-09 19:13  iterationjia  阅读(74)  评论(0编辑  收藏  举报