并发编程JUC

1. 进程与线程

1.1 进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备,进程就是用来加载指令、管理内存、管理IO的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程,也有的程序只能启动一个实例进程

线程

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

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为IPC
    • 不同计算机之间的进程通信,需要通过网络,并遵循共同的协议,例如http
  • 线程通信相对简单,因为他们共享进程内的空间,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

1.2 并行与并发

并发:一般将 线程轮流使用CPU的做法称为并发,concurrent

并行:多核cpu下,每个核都可以调度运行线程,这时候线程是可以并行的,parallel

1.3 应用

从方法调用的角度来讲:

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

2. java线程

2.1 创建和运行线程

方法一. 直接使用Thread

// 创建线程对象
Thread t = new Thread("t") {
    public void run() {
        // 要执行的任务
    }
};
// 启动线程
t.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> task = new FutureTask<>(()->{
    log.debug("hello");
    return 100;
});
// 参数1是任务对象,参数2是线程名字
new Thread(task,"t").start();
// 主线程阻塞,同步等待task执行完毕的结果
Integer result = task.get();
log.debug("结果是:{}",result);

2.2 查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

linux

  • ps -ef 查看所有进程
  • ps -ft -p 查看某个进程的所有线程
  • kill 杀死进程
  • top 按大写的 H 是否显示进程
  • top -H -p 查看某个进程的所有线程

Java

  • jps 查看所有 Java 进程
  • jstack 查看某个 Java 进程的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况 (图形界面)

2.3 线程运行原理

栈与栈帧

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

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

线程上下文切换

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

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

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

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

2.4 sleep 与 yield

sleep

  • 调用 sleep 会让当前线程从 Running 到 Timed Waiting 状态
  • 其他线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行
  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  • 调用 yield 会让当前线程从 Running 进入 Runnable 状态,然后调度执行其它同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果
  • 具体的实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示 (hint) 调度器优先调读该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用

2.5 主线程与守护线程

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

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                if(Thread.currentThread().isInterrupted()){
                    break;
                }
            }
            log.debug("结束");
        },"t1");
        t1.setDaemon(true);   // 设置为守护线程
        t1.start();

        Thread.sleep(1000);
        log.debug("结束");
    }
  • 垃圾回收器就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所有 Tomcat 接收到 Shutdown 命令后,不会等待它们处理完当前请求

2.6 五种状态

这是从操作系统层面来描述的

  • 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联

  • 可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由CPU调度

  • 运行状态:指获取了CPU时间片运行中的状态

    • 当CPU时间片用完,会从运行状态转换为可运行状态,会导致线程的上下文切换
  • 阻塞状态:

    • 如果调入了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态
    • 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态


2.7 六种状态

这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态

  • NEW:线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE:当调用了 start() 方法后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统的可运行状态、运行状态和阻塞状态。(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED、WAITING、TIMED_WAITING:都是 Java API 层面对阻塞状态的细分
  • TERMINATED:当线程代码运行结束

3.共享模型之管程 (有锁 悲观锁 阻塞)

3.1 共享带来的问题

java代码体现
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做500次,最终结果是0吗?

    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count--;
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        log.debug("" + count);     // 结果并不是0
    }

临界区

  • 一个程序运行多个线程本身是没有问题的

  • 问题出在多个线程访问共享资源

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


3.2 synchronized 解决方案

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

  • 非阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

synchronized 俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能够持有对象锁,其它线程再想获取这个对象锁时就会发生阻塞。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

    static int count = 0;
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock) {
                    count++;
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock) {
                    count--;
                }
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        log.debug("" + count);
    }

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


3.3 方法上的 synchronized

加在成员方法上,锁住的是this对象
加上静态方法上,锁住的是类对象


3.4 变量的线程安全分析

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

  • 如果他们没有共享,则线程安全

  • 如果他们被共享了,根据他们的状态能否被改变,又分两种情况

    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的

  • 但局部变量引用的对象则未必

    • 如果该对象没有逃离方法的作用范围,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

3.5 常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • LocalDateTime (jdb8新加入的日期安全类)
  • java.util.concurrent 包下的类

这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的。也可以理解为:

  • 它们的每个方法是原子的
  • 但它们多个方法的组合不是原子的

不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此他们的方法都是线程安全的


4. synchronized 原理

4.1 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的 (也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized


4.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其他线程为此对象加上了清轻量级锁 (有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁


4.3 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功 (即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。


4.4 偏向锁

轻量级锁在没有竞争时 (就自己这个线程),每次重入仍然性需要执行 CAS 操作
Java6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头。之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。


5. wait / notify

  • Owner 线程发生条件不满足时,调用 wait 方法,即可进入 WaitSet 变为 Waiting 状态
  • Blocked 和 Waiting 的线程都处于阻塞状态,不占用 CPU 的时间
  • Blocked 线程会在 Owner 线程释放锁时唤醒
  • Waiting 线程会在 Owner 线程调用 notify 和 notify all 时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入 EntryList 阻塞对列重新竞争

5.1 API

  • obj.wait():让进入 obj 的监视器的线程到 waitSet 等待
  • obj.notify():让 obj 上正在 waitSet 等待的线程挑一个唤醒
  • obj.notifyAll():让obj 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 obj 对象的方法。必须获得此对象的锁,才能调用这几个方法


5.2 wait / notify 的使用

sleep(long n) 和 wait(long n) 的区别:

  • sleep 是 Thread 的方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁

正确使用

synchronized(lock) {
	while(条件不成立){  // 解决虚假唤醒的问题
		lock.wait();
	}
	// 干活
}

// 另一个线程负责唤醒
synchronized(lock) {
	lock.notifyAll();
}

5.2 Park & Unpark

它们是 LockSupport 类中的方法

// 暂停当前线程
LockSupport.park();

// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象);

案例:

thread t1 = new Thread(()-> {
	log.debug("start...");
	sleep(1);
	log.debug("park...");
	LockSupport.park();
	log.debug("resume...");
},"t1").start();

sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);

特点:
与 Object 的 wait & niotify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 unpark 不用
  • park & unpark 是以线程为单位来阻塞唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyall 是唤醒所有的线程,就不那么精确
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

6. ReentrantLock

相对于 synchronized 它具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与 Synchronized 一样,都支持可重入

基本语法

// 获取锁
reentrantLock.lock();
try {
	// 临界区
} finally {
	// 释放锁
	reentrantLock.unlock();
}

6.1 可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的持有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被挡住

static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
	method1();
}

public static void method1() {
	lock.lock();
	try {
		log.debug("execute method1");
		method2();
	}finally{
		lock.unlock();
	}
}

public static void method2() {
	lock.lock();
	try {
		log.debug("execute method2");
	}finally{
		lock.unlock();
	}
}

6.2 可打断

	private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                // 如果没有竞争,那么此方法就会获取 Lock 对象锁
                // 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 方法打断
                log.debug("尝试获取锁");
                lock.lockInterruptibly();        // 可打断的锁
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("没有获取到锁,返回");
                return;
            }
            try {
                log.debug("获取到锁");
            }finally {
                lock.unlock();
            }
        },"t1");

        lock.lock();
        t1.start();

        Thread.sleep(1000);
        log.debug("打断t1");
        t1.interrupt();
    }

6.3 锁超时

立刻失败

	ReentrantLock lock = new ReentrantLock();
	Thread t1 = new Thread(() -> {
		log.debug("启动...");
		if(!lock.tryLock()) {
			log.debug("获取立刻失败,返回");
			return;
		}
		try {
			log.debug("获得了锁");
		}finally {
			lock.unlock();
		}
	},"t1");

	lock.lock();
	log.debug("获得了锁");
	t1.start();
	try {
		sleep(2);
	}finally{
		lock.unlock();
	}

6.4 公平锁

ReentrantLock 默认也是不公平的
可以通过构造方法设置成公平锁

ReentrantLock lock = new ReentrantLock(true);

6.5 条件变量

synchronized 中也有条件变量,就是 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比:

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室

使用流程

  • await 前先要获得锁
  • await 执行后会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒去重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
    public static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        
        // 创建一个新的条件变量
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        
        lock.lock();
        // 进入休息室等待
        condition1.await();
        
        condition1.signal();
        condition1.signalAll();
    }

7. Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念。底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等
JMM 体现在以下几个方面:

  • 原子性:保证指令不会受到线程上下文切换的影响
  • 可见性:保证指令不会受到 CPU 缓存的影响
  • 有序性:保证指令不会受到 CPU 指令并行优化的影响

7.1 可见性

案例一:不会停下来的线程

    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (run) {
                log.debug("run...");
            }
        },"t1").start();

        Thread.sleep(1000);
        log.debug("停下来");
        run = false;      // 现象是t1线程并不会停下来
    }

这是由于 t1 线程要频繁的从主内存中读取 run 的值,JIT 即时编译器会将 run 的值缓存至自己的工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
即时 main 线程修改了 run 的值并同步到了主存中,而 t1 线程是从自己的工作内存中的高速缓存中读取的这个变量的值,结果就永远是 true

解决方案:加上 volatile 关键字 (volatile 关键字适用于一个线程写,多个线程读的场景,不能保证原子性)

    // 易变
   volatile static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (run) {
                log.debug("run...");
            }
        },"t1").start();

        Thread.sleep(1000);
        log.debug("停下来");  
        run = false;          
    }

volatile (易变关键字)

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

volatile 的原理:

volatile 的底层原理是内存屏障:

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

注意:

synchronized 语句块既可以保证代码快的原子性,又可以保证块内代码的可见性和有序性。但缺点是 synchronizd
是属于重量级操作,性能相对更低。

7.2 有序性

JVM 在不影响正确性的前提下,可以调整语句的执行顺序。这种特性称之为指令重排


7.3 volatile 原理

7.3.1 如何保证可见性

  • 写屏障保证在该屏障之前,对共享变量的改动,都同步到主存当中
	public static opreate(I_Result r){
		num = 2;
		ready = true;    // ready 是 volatile 赋值带写屏障
		// 写屏障    =>  在这之前的改动都会同步到主存中去
	}
  • 而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中的最新数据
	public static opreate(I_Result r) {
		// 读屏障   =>  保证之后的读取都是在主存中取值
		// ready 是 volatile 读取值带屏障
		if(ready) {
			num = num + num;
		}else {
			r = 1;
		}
	}

7.3.2 如何保证有序性

  • 写屏障会确保指令重排序时,不会屏障之前的代码排在屏障之后
	public static opreate(I_Result r){
		num = 2;
		ready = true;    // ready 是 volatile 赋值带写屏障
		// 写屏障    
	}
  • 读屏障会确保指令重排时,不会将读屏障之后的代码排在读屏障之前
	public static opreate(I_Result r) {
		// ready 是 volatile 读取值带屏障
		if(ready) {
			num = num + num;
		}else {
			r = 1;
		}
	}

线程安全单例习题:

单例模式有很多实现,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象时的线程安全

  • 饿汉式:类加载就会导致该实例对象被创建
  • 懒汉式:类加载不会导致该单例对象被创建,而是首次使用该对象时才会被创建

实现1:

// 问题1:为什么加final:防止它有子类,子类中有一些不恰当的方法破坏它的单例
// 问题2:如果实现了序列化接口,还要做什么来防止反序列化破坏单例:提供一个readResovle方法
public final class Singleton implements Serializable {
	// 问题3:为什么设置为私有?(防止其他会创建,破坏单例对象)  是否能防止反射创建新的实例?(不能)
	private Singleton(){}
	// 问题4:这样初始化是否能保证单例对象创建时的线程安全? 是
	private static final Singleton INSTANCE = new Singleton();
	public static Singleton getInstance() {
		return INSTANCE;
	}
	
	// 防止反序列化破坏单例
	public Object readResovle() {
		return INSTANCE;
	}
}

8. 共享模型之无锁 (无锁 乐观锁 非阻塞)

8.1 CAS 与 Volatile

CAS:比较并交换,在每次更新时,都会先将自己刚开始获得的最新值和现在的最新值进行比较,相同才会执行更新操作,不同则循环。

CAS 和 Volatile
获取共享变量时,为了保证该变量的可见性,需要使用 Volatile 修饰

它可以用来修饰成员变量和静态成员变量。它可以避免线程从自己的工作缓存中查找变量的值,必须到主存获取它的值,线程操作 Volatile 变量都是直接操作主存,即一个线程对 Volatile 变量的修改,对另一个线程可见。

注意:Volatile 仅仅保证了共享变量的可见性,让其他线程能够看到最新值,但不能解决指令交错问题 (不能保证原子性)

CAS 必须结束 Volatile 才能读取到共享变量的最新值来实现 比较并交换 的效果


8.2 为什么无锁效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得所的情况下,发生上下文切换,进入阻塞。

8.3 CAS 的特点

结合 CAS 和Volatile 可以实现无锁并发,试用于线程数少、多核 CPU 的场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,比较失败重试就行了

  • synchronized 是基于悲观锁的思想:最悲观的估计,需要预防别的线程来修改共享变量,在我操作的时候,其它线程都得等待。

  • CAS 体现的是无锁并发、无阻塞并发

    • 因为没有使用 synchronized,所以线程不会阻塞,这是提升效率的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受到影响

8.4 Unsafe

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

8.4.1 Unsafe CAS 操作

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        System.out.println(unsafe);

        // 1.获取域的偏移地址
        long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
        long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));

        Teacher t = new Teacher();
        // 2.执行 cas 操作
        unsafe.compareAndSwapInt(t,idOffset,0,10);
        unsafe.compareAndSwapObject(t,nameOffset,null,"lzf");

        // 3.验证结果
        System.out.println(t);
    }
}
@Data
class Teacher {
    volatile int id;
    volatile String name;
}

9. 共享模型之不可变

9.1 线程安全的日期类 DateTimeFormatter

    public static void main(String[] args) {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");  // 线程安全

        for (int i = 0;i < 10; i++) {
            new Thread(() -> {
                TemporalAccessor parse = dtf.parse("2022-07-15");
                log.debug("{}",parse);
            }).start();
        }
    }

9.2 不可变设计

9.2.1 final 的使用

  • 属性用 final 修饰保证了该属性是只读的,不能修改
  • 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

10. 并发工具

10.1 线程池

自定义线程池

public class Test14 {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(2,
                1000, TimeUnit.MILLISECONDS, 10,(queue,task)->{
            // 死等
            queue.put(task);
        });
        for (int i = 0; i < 5; i++) {
            int j = i;
            threadPool.execute(() -> {
                log.debug("{}",j);        
            });
        }
    }
}
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
    void reject(BlockingQueue<T> queue,T task);
}

@Slf4j(topic = "c.ThreadPool")
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);
                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 queueCapacity,RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.taskQueue = new BlockingQueue<>(queueCapacity);
        this.rejectPolicy = rejectPolicy;
    }

    class Worker extends Thread{
        private Runnable task;

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

        @Override
        public void run() {
            // 执行任务
            // 当 task 不为空,执行任务
            // 当 task 执行完毕,再接着从任务队列获取任务并执行
            //while (task != null || (task = taskQueue.take()) != null) {
            while (task != null || (task = taskQueue.poll(timeout,timeUnit)) != null) {
                try {
                    task.run();
                }catch (Exception e) {

                }finally {
                    task = null;
                }
            }
            synchronized (workers) {
                workers.remove(this);
            }
        }
    }
}

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 capacity;

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    // 带超时的阻塞获取
    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();      // ReentrantLock 必须要手动释放
        }
    }

    // 阻塞获取
    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();      // ReentrantLock 必须要手动释放
        }
    }

    // 阻塞添加
    public void put(T task) {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                try {
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            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() == capacity) {
                try {
                    if (nanos <= 0) {
                        return false;
                    }
                    nanos = fullWaitSet.awaitNanos(nanos);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            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() == capacity) {
                rejectPolicy.reject(this,task);
            }else {      // 队列还有空闲
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        }finally {
            lock.unlock();
        }
    }
}

10.2 ThreadPoolExecutor

10.2.1 线程池状态

ThreadPoolExecutor 使用 int 的高3位来表示线程池状态,低29位表示线程数量。目的是将线程池状态和线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值
在这里插入图片描述

10.2.2 构造方法

public ThreadPoolExecutor(int corePoolSize
						int maximumPoolSize
						long keepAliveTime
						TimeUnit unit
						BlockingQueue<Runnable> workQueue
						ThreadFactory threadFactory
						RejectedExecutionHandler handler)
  • corePoolSize:核心线程数目 (最多保留的线程数)
  • maximumPoolSize:最大线程数目 (核心线程数 + 最大线程数)
  • keepAliveTime:生存时间 - 针对救急线程
  • unit:时间单位 - 针对救急线程
  • workQueue:阻塞队列
  • threadFactory:线程工厂 - 可以为线程创建时取个好名字
  • handler:拒绝策略

  • 线程池中刚开始没有进程,当一个任务提交给线程池之后,线程池会创建一个新线程来执行任务

  • 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入 workQueue 对列排队,直到有空闲的线程

  • 如果队列选择了有界队列,那么任务超过了队列大小,会创建 maximumPoolSize - corePoolSize 数目的救急线程来救急

  • 如果线程到达 maximumPoolSize 仍有新任务这时会执行拒绝策略,拒绝策略 JDK 体统四种实现,其它著名框架也提供了实现。

    • AbortPolicy:让调用者抛出 RejectedExecutionException 异常,这是默认策略
    • CallerRunsPolicy:让调用者运行任务
    • DiscardPolicy:放弃本次任务
    • DiscardOldestPolicy:放弃队列中最早的任务,本任务取而代之
    • Dubbo 的实现:在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
    • Netty 的实现:是创建一个新线程来执行任务
    • ActiveMQ 的实现:带超时等待 (60s) 尝试放入队列,会逐一尝试策略链中每种拒绝策略
    • PinPoint 的实现:它使用一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
  • 当高峰过去后,超过 corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个事件由 keepAliveTime 和 unit 来控制


10.2.3 newFixedThreadPool

特点:

  • 最大线程数 = 核心线程数 (没有救急线程被创建),因此也不需要超时时间
  • 阻塞队列是无界的,可以放任意数量的任务
    适用于任务量已知,相对耗时的任务
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
            private AtomicInteger t = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r,"mypool_t" + t.getAndIncrement());
            }
        });

        pool.execute(() -> {
            log.debug("1");
        });

        pool.execute(() -> {
            log.debug("2");
        });

        pool.execute(() -> {
            log.debug("3");
        });
    }

10.2.4 newCachedThreadPool

特点:

  • 核心线程数是0,最大线程数是 Intger.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着

    • 全部都是救急线程 (60s后可以回收)
    • 救急线程可以无限创建
  • 对列采用了 SynchroouosQueue 实现特点是,它没有容量,没有线程来取是放不进去的 (一手交钱,一手交货)
    整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲一分钟后释放线程 适合任务数比较密集,但每个任务执行时间较短的情况


10.2.5 newSingleThreadExecutor

使用场景:
希望多个任务排队执行。线程数固定为1,任务数多于1时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会释放。

区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会创建一个线程,保证池的正常工作
    public static void main(String[] args) {
        test2();
    }

    public static void test2() {
        ExecutorService pool = Executors.newSingleThreadExecutor();

        pool.execute(() -> {
            log.debug("1");
            int i = 1 / 0;
        });

        pool.execute(() -> {
            log.debug("2");
        });

        pool.execute(() -> {
            log.debug("3");
        });
    }

10.2.6 提交任务

// 执行任务
void execute(Runnable command);

// 提交任务 task,用返回值 future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);

// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
			throws EnterruptedException;

// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout,TimeUnit unit)
			throws EnterruptedException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T> tasks>)
			throws InterruptedException,ExecutionExecption;

// 提交 tesks 中所有任务,哪个任务先成功执行完毕,返回此任务结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T> tasks>,long timeout,TimeUnit unit)
			throws InterruptedException,ExecutionExecption,TimeoutExecption;

10.2.7 关闭线程池

shutdown:不会阻塞 (调用线程)

/*
	线程池状态变为 shutdown
	- 不会接收新任务
	- 但已提交任务会执行完
	- 此方法不会阻塞调用线程的执行
*/
void shutdown();

shutdownNow

/*
	线程池状态变为 stop
	- 不会接收新任务
	- 会将队列中的任务返回
	- 并用 interrupt 的方式中断正在执行的任务
*/
List<Runnable> shutdownNow();

10.3 JUC

10.3.1 AQS 原理

全称是 AbstractQueueSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:

  • 用 state 属性来表示资源的状态 (分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁

    • getState:获取 state 状态
    • setState:设置 state 状态
    • compareAndSetState:乐观锁机制设置 state 状态
    • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
  • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList

  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

获取锁:

// 如果获取锁失败
if(!tryAcquire(arg)){
	// 入队,可以选择阻塞当前线程 park unpark 机制
}

释放锁:

// 如果释放锁成功
if(tryRelease(arg)){
	// 让阻塞线程恢复运行
}

10.3.2 线程安全集合类

10.3.2.1 ConcurrentHashMap

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值