多线程
线程和进程区别
进程
定义:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。当一个程序被运行,就开启了一个进程,比如 Word、Excel。
线程
定义: 线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
并行与并发区别
并发:单核 CPU 运行多线程时,时间片进行很快的切换。线程轮流执行 CPU
并行:多核 CPU 运行 多线程时,真正的在同一时刻运行
使用多线程好处
让程序运行速度更快界面更加流畅
充分使用 CPU 资源,发挥多核 CPU 强大的能力
定义、创建和运行线程
定义线程
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口
这三种定义线程优缺点
1. 第一种方式通过继承 Thread 类可以直接访问线程的属性比如线程名称 getName()
不需要使用 Thread.currentThread()
来获取当前线程对象但是因为 Java 是单继承的所以一旦继承 Thread 这个类将无法再继承其他类。创建多线程时,每个任务有成员变量时不共享,必须加 static 才能做到共享
2. 第二种方式和第三种方式通过实现接口的方式完美的解决了第一种方式继承的局限性
3. 第三种方式是解决前面两种都不能抛出异常和返回值的局限性
具体代码如下
java 线程定义
@Log4j2
class T extends Thread {
@Override
public void run() {
log.info("我是继承 Thread 实现的多线程");
}
}
@Log4j2
class R implements Runnable {
@Override
public void run() {
log.info("我是实现 Runnable 接口多线程");
}
}
@Log4j2
class C implements Callable<String> {
@Override
public String call() throws Exception {
log.info("我是实现 Callable 接口多线程");
return "success";
}
}
启动线程
对应上面的定义线程来启动线程
java 启动线程
@Log4j2
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 启动继承方式实现的多线
new Thread(new T()).start();
// 启动实现 Runnable 接口的多线程
new Thread(new R()).start();
// 启动实现 Callable 接口多线程
FutureTask<String> target = new FutureTask<>(new C());
new Thread(target).start();
log.info("callable 线程的返回值" + target.get());
}
}
匿名定义&启动
// 启动继承 Thread 匿名内部类的任务
Thread t = new Thread(){
@Override
public void run() {
log.info("我是Thread匿名内部类的任务");
}
};
t.start();
// 启动实现 Runnable 匿名实现类的任务
new Thread(new Runnable() {
@Override
public void run() {
log.info("我是Runnable匿名内部类的任务");
}
}).start();
// 启动实现 Runnable 的 lambda 简化后的任务
new Thread(() -> log.info("我是 Runnable 的 lambda 简化后的任务")).start();
上下文切换
多核 CPU 下,多线程是并行工作的,如果线程数多,单个核又会并发的调度线程,运行时会有上下文切换的概念
CPU 执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。
线程的 CPU 时间片用完
垃圾回收
线程 sleep、yield、wait、join、park、synchronized、lock 等方法
当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态, JVM 中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,是线程私有的。
线程优先级
线程内部用1~10的数来调整线程的优先级,默认的线程优先级为 NORM_PRIORITY:5
CPU 比较忙时,优先级高的线程获取更多的时间片
CPU 比较闲时,优先级设置基本没用
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
// 设置线程优先级方法的签名
public final void setPriority(int newPriority) {}
yield()
yield() 方法会让运行中的线程切换到就绪状态,重新争抢 CPU 的时间片,争抢时是否获取到时间片看 CPU 的分配。
代码
yield 方法体现
@Log4j2
public class Demo2 {
public static void main(String[] args) {
// t1 线程
new Thread(() -> {
int count = 0;
while (true) {
log.info("----------1)" + count++);
}
}).start();
// t2 线程
new Thread(() -> {
int count = 0;
while (true) {
Thread.yield();
log.info("----------2)" + count++);
}
}).start();
}
}
从图中可以看出 t2 线程每次执行时进行了 yield(),线程1 执行的机会明显比线程 2 要多。
守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。
默认的线程都是非守护线程。
垃圾回收线程 就是典型的守护线程
主要的意思: 就算如果是守护线程只要调用者线程挂了那么自己就挂了(像古代君王的陪葬人)
方法签名:public final void setDaemon(boolean on)
Thread thread = new Thread(() -> {
while (true) {
}
});
// 具体的api。设为true表示未守护线程,当主线程结束后,守护线程也结束。
// 默认是false,当主线程结束后,thread继续运行,程序不停止
thread.setDaemon(true);
thread.start();
log.info("结束");
线程的阻塞
线程的阻塞可以分为好多种,从操作系统层面和 Java 层面阻塞的定义可能不同,但是广义上使得线程阻塞的方式有下面几种
BIO 阻塞,即使用了阻塞式的 IO 流
sleep(long time) 让线程休眠进入阻塞状态
a.join() 调用该方法的线程进入阻塞,等待 a 线程执行完恢复运行
sychronized 或 ReentrantLock 造成线程未获得锁进入阻塞状态
获得锁之后调用wait()方法 也会让线程进入阻塞状态
LockSupport.park() 让线程进入阻塞状态
sleep()
sleep 使线程休眠,会将运行中的线程进入阻塞状态。当休眠时间结束后,重新争抢 CPU 的时间片继续运行
方法签名: public static native void sleep(long millis) throws InterruptedException;
有两种形式的调用
sleep 两种调用形式
try {
// 1. 第一种使用 Thread.sleep() 方法
Thread.sleep(1000);
// 2. 第二种使用TimeUnit
TimeUnit.MINUTES.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
join()
join 是指调用该方法的线程进入阻塞状态,等待某线程执行完成后恢复运行
方法签名
java join()方法签名
public final void join() throws InterruptedException
/**
* millis 最长等待时间
*/
public final synchronized void join(long millis) throws InterruptedException{}
interrupt
线程的打断
相关方法签名
/**
* 用于中断线程
**/
public void interrupt() {}
/**
* 获取线程的打断标记 ,调用后不会修改线程的打断标记
**/
public boolean isInterrupted() {}
/**
* 获取线程的打断标记,调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)
**/
public static boolean interrupted() {}
线程的状态
线程的状态可从 操作系统层面分为五种状态 从 Java API 层面分为六种状态。
操作系统
初始状态:创建线程对象时的状态
可运行状态(就绪状态):调用 start() 方法后进入就绪状态,也就是准备好被 CPU 调度执行
运行状态:线程获取到 CPU 的时间片,执行 run() 方法的逻辑
阻塞状态: 线程被阻塞,放弃 CPU 的时间片,等待解除阻塞重新回到就绪状态争抢时间片
终止状态: 线程执行完成或抛出异常后的状态
Java API
线程对象被创建
Runnable 线程调用了start()方法后进入该状态,该状态包含了三种情况
就绪状态 : 等待 CPU 分配时间片
运行状态: 进入Runnable方法执行任务
阻塞状态: BIO 执行阻塞式 IO 流时的状
Blocked 没获取到锁时的阻塞状态
WAITING 调用wait()、join()等方法后的状态
TIMED_WAITING 调用 sleep(time)、wait(time)、join(time)等方法后的状态
TERMINATED 线程执行完成 或 抛出异常后的状态
锁
在 Java 中,锁有四种状态,级别从低到高依次为: 无状态锁、偏向锁、重量级锁状态。而且锁的状态只能逐步升级不能降级
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
常见问题 哲学家就餐问题
假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子.
其中的餐叉就是竞争资源
预防死锁
概念
互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完 成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对 自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。 理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。 所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。 此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
操作
避免多次锁定。尽量避免同一个线程对多个 Lock 进行锁定
具有相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。
使用定时锁。程序在调用 acquire() 方法加锁时可指定 timeout 参数,该参数指定超过 timeout 秒后会自动释放对 Lock 的锁定,这样就可以解开死锁了。
死锁检测。死锁检测是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的。
同步锁
线程安全
一个程序运行多个线程本身是没有问题的
问题有可能出现在多个线程访问共享资源
多个线程都是读共享资源也是没有问题的
当多个线程读写共享资源时, 如果发生指令交错,就会出现问题
临界区: 一段代码如果对共享资源的多线程读写操作,这段代码就被称为临界区。
线程安全: 指的是多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。
同步方法&同步代码块 synchronized
在 Java 中提供了 synchronized 来标识同步方法&同步代码块, 该关键字是用于保证线程安全的,是阻塞式的解决方案。让同一个时刻最多只有一个线程能持有对象锁,其他线程在想获取这个对象锁就会被阻塞,不用担心上下文切换的问题。
当一个线程执行完 synchronized 的代码块后 会唤醒正在等待的线程
synchronized 实际上使用对象锁保证临界区的原子性 临界区的代码是不可分割的 不会因为线程切换所打断
注意: 不要理解为一个线程加了锁 ,进入 synchronized 代码块中就会一直执行下去。如果时间片切换了,也会执行其他线程,再切换回来会紧接着执行,只是不会执行到有竞争锁的资源,因为当前线程还未释放锁。
基本使用
// 同步方法
private synchronized void a() {}
// 同步代码块
// this 是一个任意对象即可,但是在不同线程中 this 的对象要一致,
// 即在不同线程中 this 要是一个对象否则将起不到同步的作用
private void b(){
synchronized (this){ }
}
线程不安全
java 线程不安全
class T implements Runnable {
private static int sum = 0;
private static Object obj = new Object();
@SneakyThrows
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Thread.sleep(100);
sum++;
}
System.out.println(sum);
}
}
public class Demo3 {
public static void main(String[] args) {
final T t = new T();
new Thread(t).start();
new Thread(t).start();
}
}
线程安全
java 线程安全
class T implements Runnable {
private static int sum = 0;
private static Object obj = new Object();
@SneakyThrows
@Override
public void run() {
for (int i = 0; i < 10; i++) {
Thread.sleep(100);
synchronized (obj) {
sum++;
}
}
System.out.println(sum);
}
}
public class Demo3 {
public static void main(String[] args) {
final T t = new T();
new Thread(t).start();
new Thread(t).start();
}
}
再次注意 加锁是加在对象上,一定要保证是同一对象,加锁才能生效
线程通信
wait+notify
线程间通信可以通过共享变量+wait()¬ify()来实现
1. wait() 将线程进入阻塞状态 notify() 将线程唤醒
当多线程竞争访问对象的同步方法时,锁对象会关联一个底层的 Monitor 对象 (重量级锁的实现)
Thread-0 先获取到对象的锁,关联到 monitor 的 owner,同步代码块内调用了锁对象的 wait() 方法,调用后会进入 waitSet 等待,Thread-1 同样如此,此时 Thread-0 的状态为 Waitting
Thread2、3、4、5同时竞争,2 获取到锁后,关联了 monitor 的 owner,3、4、5 只能进入 EntryList 中等待,此时 2 线程状态为 Runnable,3、4、5状态为Blocked
2 执行后,唤醒 EntryList 中的线程,3、4、5进行竞争锁,获取到的线程即会关联 monitor 的 owner
3、4、5 线程在执行过程中,调用了锁对象的 notify() 或 notifyAll() 时,会唤醒waitSet的线程,唤醒的线程进入 EntryList 等待重新竞争锁
1. Blocked 状态和 Waitting 状态都是阻塞状态
2. Blocked 线程会在 owner 线程释放锁时唤醒
3. wait 和 notify 使用场景是必须要有同步,且必须获得对象的锁才能调用,使用锁对象去调用,否则会抛异常
IllegalMonitorStateException
wait() 释放锁 进入 WaitSet 可传入时间,如果指定时间内未被唤醒 则自动唤醒
notify() 随机唤醒一个 WaitSet 里的线程
notifyAll() 唤醒 WaitSet 中所有的线程
代码
java 线程定义
static final Object lock = new Object();
Runnable r1 = () -> {
log.info("开始执行");
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("继续执行");
}
};
java 线程的启动
new Thread(r1, "t1").start();
new Thread(r1, "t2").start();
// 主线程
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("开始唤醒");
synchronized (lock) {
// 同步代码内部才能调用
lock.notifyAll();
}
结果
wait 和 sleep的区别?
相同点:都会让线程进入阻塞状态
不同点:
wait 是 Object 的方法 sleep 是 Thread 的方法
wait会立即释放锁 sleep不会释放锁
wait后线程的状态是 Watting sleep 后线程的状态为 Time_Waiting
生产者消费者模型
生产者消费者模型指的是有生产者来生产数据,消费者来消费数据,生产者生产满了就不生产了,通知消费者取,等消费了再进行生产。
java 主方法
@SneakyThrows
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int k = 0; k < 1000; k++) {
queue.put(new Message(UUID.randomUUID().toString()));
}
}, "生产者" + i).start();
}
new Thread(() -> {
while (true) {
queue.take();
}
}, "消费者").start();
}
java MessageQueue.java
@Log4j2
public class MessageQueue {
private final LinkedList<Message> list = new LinkedList<>();
private int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
@SneakyThrows
public void put(Message message) {
synchronized (list) {
while (list.size() == capacity) {
log.info("队列已满, 生产者等待");
list.wait();
}
list.addLast(message);
log.info("生产消息:{}", message);
list.notifyAll();
}
}
@SneakyThrows
public Message take() {
synchronized (list) {
while (list.isEmpty()) {
log.info("队列已空,消费者等待");
list.wait();
}
Message message = list.removeFirst();
log.info("消费消息:{}", message);
list.notifyAll();
return message;
}
}
}
java Message.java
@AllArgsConstructor
@ToString
class Message {
private Object value;
}
同步锁
什么是同步锁 ?
同步锁举个简单的一个例子,在厕所上厕所一个人进入一个坑位并且上锁了,另外一个人只能在外面等直到里面的人释放锁来可以进入
在 Java 中有 ReentrantLock
来使用同步锁
private static final ReentrantLock LOCK = new ReentrantLock();
new Thread(() -> {
try {
LOCK.lock();
log.info("开始");
} catch (Exception e) {
}finally {
LOCK.unlock();
}
},"l1").start();
ReentrantLock 有以下优点
支持获取锁的超时时间
获取锁时可被打断
可设为公平锁
可以有不同的条件变量,即有多个 WaitSet,可以指定唤醒
ReentrantLock API
java ReentrantLock API
// 默认非公平锁,参数传 true 表示公平锁, 公平锁先来先服务
ReentrantLock lock = new ReentrantLock(false);
// 尝试获取锁
lock()
// 释放锁 应放在finally块中 必须执行到
unlock()
try {
// 获取锁时可被打断,阻塞中的线程可被打断
LOCK.lockInterruptibly();
} catch (InterruptedException e) {
return;
}
// 尝试获取锁 获取不到就返回false
LOCK.tryLock()
// 支持超时时间 一段时间没获取到就返回false
tryLock(long timeout, TimeUnit unit)
// 指定条件变量 休息室 一个锁可以创建多个休息室
Condition waitSet = ROOM.newCondition();
// 释放锁 进入waitSet等待 释放后其他线程可以抢锁
yanWaitSet.await()
// 唤醒具体休息室的线程 唤醒后 重写竞争锁
yanWaitSet.signal()
实例:一个线程输出a,一个线程输出b,一个线程输出c,abc 按照顺序输出,连续输出 5 次
java 主函数
public static void main(String[] args) {
AwaitSignal awaitSignal = new AwaitSignal(5);
// 构建三个条件变量
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
// 开启三个线程
new Thread(() -> {
awaitSignal.print("a", a, b);
}).start();
new Thread(() -> {
awaitSignal.print("b", b, c);
}).start();
new Thread(() -> {
awaitSignal.print("c", c, a);
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
awaitSignal.lock();
try {
// 先唤醒a
a.signal();
} finally {
awaitSignal.unlock();
}
}
CAS
CAS 是 compare and swap 的简写,即比较并交换。它是指一种操作机制,而不是某个具体的类或方法
在 Java1.8 平台上对这种操作进行了包装。在 sun.misc.Unsafe
类中,调用代码如下:
public final native boolean compareAndSwapObject(Object o,long l,Object o1,Object o2);
public final native boolean compareAndSwapInt(Object o, long l, int i, int i1);
public final native boolean compareAndSwapLong(Object o, long l, long l1, long l2);
对于
sun.misc.Unsafe
了解很多可以看这篇文章 [Java魔法类:Unsafe应用解析]
可见 CAS 其实存在一个循环的过程,如果有多个线程在同时修改这一个变量 V,在修改之前会先拿到这个变量的值,再和变量对比看是否相等,如果相等,则说明没有其它线程修改这个变量,自己更新变量即可。如果发现要修改的变量和期望值不一样,则说明再读取变量 V 的值后,有其它线程对变量 V 做了修改,那么,放弃本次更新,重新读变量 V 的值,并再次尝试修改,直到修改成功为止。这个循环过程一般也称作 自旋
流程图
CAS存在的缺点
只能保证一个共享变量的原子性
CAS不像synchronized和RetranLock一样可以保证一段代码和多个变量的同步。对于多个共享变量操作是CAS是无法保证的,这时候必须使用枷锁来是实现。
存在性能开销问题
由于CAS是一个自旋操作,如果长时间的CAS不成功会给CPU带来很大的开销。
ABA 问题
因为 CAS 是通过检查值有没有发生改变来保证原子性的,假若一个变量 V 的值为 A,线程 1 和线程 2 同时都读取到了这个变量的值 A,此时线程 1 将 V 的值改为了 B,然后又改回了 A,期间线程 2 一直没有抢到 CPU 时间片。知道线程 1 将 V 的值改回 A 后线程 2 才得到执行。那么此时,线程 2 并不知道 V 的值曾经改变过。这个问题就被成为 ABA 问题, 可使用版本号方式解决,在更新值同时更新版本号。
CPU对CAS的支持
在操作系统中 CAS 是一种系统原语,原语由多条指令组成,且原语的执行是连续不可中断的。因此 CAS 实际上是一条 CPU 的原子指令,虽然看上去 CAS 是一个先比较再交换的操作,但实际上这个过程是由 CPU 保证了原子操作。
Java中使用 CAS
从 JDK 1.5 开始,Java 在
java.util.concurrent.atomic
包下引入了一些 Atomic 相关的原子操作类,这些类避免使用加锁来实现同步,从而更加方便、高效的实现原子操作。
根据使用范围,可以将这些类分为四种类型,分别为
原子更新基本类型、原子更新数组、原子更新引用类型、原子更新属性
原子更新基本类型
atomic 包下原子更新基本数据类型包括 AtomicInteger、AtomicLong、AtomicBoolean 三个类。
以 AtomicInteger 为例子
// 获取当前值,然后自加,相当于i++
getAndIncrement()
// 获取当前值,然后自减,相当于i--
getAndDecrement()
// 自加1后并返回,相当于++i
incrementAndGet()
// 自减1后并返回,相当于--i
decrementAndGet()
// 获取当前值,并加上预期值
getAndAdd(int delta)
// 获取当前值,并设置新值
int getAndSet(int newValue)
演示代码
@SneakyThrows
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(10);
final Runnable runnable = () -> {
for (int i = 0; i < 100; i++) {
atomicInteger.incrementAndGet();
}
};
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
TimeUnit.SECONDS.sleep(10);
// 510
System.out.println(atomicInteger.get());
}
原子更新引用类型
引用类型的原子类包括 AtomicReference、AtomicStampedReference、AtomicMarkableReference 三个。
AtomicReference 引用原子类
AtomicStampedReference 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
AtomicMarkableReference 原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来。
@SneakyThrows
@Test
public void AtomicReferenceTest() {
AtomicReference<Student> atomicStudent = new AtomicReference<>();
Student student1 = new Student(18, "xiaou");
Student student2 = new Student(16, "xiaou");
atomicStudent.set(student1);
new Thread(() -> {
atomicStudent.compareAndSet(student2, new Student(21, "xiaou"));
}).start();
atomicStudent.compareAndSet(student1, student2);
TimeUnit.SECONDS.sleep(1);
// Student(age=21, name=xiaou)
System.out.println(atomicStudent.get());
}
// 辅助对象
@Data
@AllArgsConstructor
class Student {
volatile int age;
private String name;
}
原子更新对象属性
如果直选哟更新某个对象中的某个字段,可以使用更新对象字段的原子类。包括三个
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
1. 被操作的字段不能是 static 类型;
2. 被操纵的字段不能是 final 类型;
3. 被操作的字段必须是 volatile 修饰的;
4. 属性必须对于当前的 Updater 所在区域是可见的。
@SneakyThrows
@Test
public void AtomicIntegerFieldUpdaterTest() {
AtomicIntegerFieldUpdater<Student> updater =
AtomicIntegerFieldUpdater.newUpdater(Student.class, "age");
Student student1 = new Student(18, "xiaou");
updater.set(student1, 19);
System.out.println( "更新后的年龄是" + updater.get(student1));
}
// 辅助对象
@Data
@AllArgsConstructor
class Student {
volatile int age;
private String name;
}
原子更新数组
这里原子更新数组并不是对数组本身的原子操作,而是对数组中的元素。主要包括 3 个类:AtomicIntegerArray、AtomicLongArray 及 AtomicReferenceArray,分别表示原子更新整数数组的元素、原子更新长整数数组的元素以及原子更新引用类型数组的元素。
public class AtomicIntegerArray implements java.io.Serializable {
// final类型的int数组
private final int[] array;
// 获取数组中第i个元素
public final int get(int i) {
return (int)AA.getVolatile(array, i);
}
// 设置数组中第i个元素
public final void set(int i, int newValue) {
AA.setVolatile(array, i, newValue);
}
// CAS更改第i个元素
public final boolean compareAndSet(int i, int expectedValue, int newValue) {
return AA.compareAndSet(array, i, expectedValue, newValue);
}
// 获取第i个元素,并加1
public final int getAndIncrement(int i) {
return (int)AA.getAndAdd(array, i, 1);
}
// 获取第i个元素并减1
public final int getAndDecrement(int i) {
return (int)AA.getAndAdd(array, i, -1);
}
// 对数组第i个元素加1后再获取
public final int incrementAndGet(int i) {
return (int)AA.getAndAdd(array, i, 1) + 1;
}
// 对数组第i个元素减1后再获取
public final int decrementAndGet(int i) {
return (int)AA.getAndAdd(array, i, -1) - 1;
}
// ... 省略
}
java内存模型(JMM)
JMM 体现在以下三个方面
原子性 保证指令不会受到上下文切换的影响
可见性 保证指令不会受到 CPU 缓存的影响
有序性 保证指令不会受并行优化的影响
原子性
原子性 上述同步锁的 synchronized 代码块就是保证了原子性,就是一段代码是一个整体,原子性保证了线程安全,不会受到上下文切换的影响。
一个操作可能包含多个子操作,要么全部执行要么都不执行
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// ....
}
});
t.start();
Thread.sleep(1000);
// 线程t不会如预想的停下来
run = false;
}
如上图所示,线程有自己的工作缓存,当主线程修改了变量并同步到主内存时,t线程没有读取到,所以程序停不下来
初始状态,线程t刚开始从主内存读取了
run
的值到工作内存;因为线程t要频繁从主内存中读取
run
的值,JIT 编译器会将run
的值缓存至自己工作内存中的高速缓存中,减少对主存中`run`的访问,提高效率1 秒之后,主线程修改了
run
的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
可见性
可见性是指, 当多个线程并发访问共享变量时,一个线程对共享变量的修改,其他线程能够立刻看到。
CPU 从主内存中读取数据的效率相对来说不高,都有好几级缓存。每个线程读取共享变量时候,都将该变量加载进其对应 CPU 高速缓存中,修改该变量后 CPU 会立即更新该缓存,当不一定会立即将其写回主内存。此时其他线程访问该变量时从主内存读取到的数据是旧的数据,而非更新后的数据
顺序性
顺序性是指,程序执行的顺序按照代码的先后顺序执行。
volatile
volatile 不能解决原子性,即不能通过该关键字实现线程安全。
该关键字解决了可见性和有序性,volatile 通过内存屏障来实现的
写屏障
会在对象写操作之后加写屏障,会对写屏障的之前的数据都同步到主存,并且保证写屏障的执行顺序在写屏障之前
读屏障
会在对象读操作之前加读屏障,会在读屏障之后的语句都从主存读,并保证读屏障之后的代码执行在读屏障之后
volatile 应用场景:一个线程读取变量,另外的线程操作变量,加了该关键字后保证写变量后,读变量的线程可以及时感知。
对于上面不能及时停下来可以对 run
声明这么写
static volatile boolean run = true;
线程池
线程池的介绍
线程池是 Java 并发最重要的一个知识点,也是难点,是实际应用最广泛的。
线程的资源很宝贵,不可能无限的创建,必须要有管理线程的工具,线程池就是一种管理线程的工具,Java开发中经常有池化的思想,如 数据库连接池、Redis 连接池等。
线程池的好处
降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源
提高响应速度,任务到达时,无需创建线程即可运行
提供更多更强大的功能,可扩展性高
线程池的构造方法
/**
* @param corePoolSize 核心线程数
* @param maximumPoolSize 最大线程数
* @param keepAliveTime 救急线程的空闲时间
* @param unit 救急线程的空闲时间单位
* @param workQueue 阻塞队列
* @param threadFactory 创建线程的工厂,主要定义线程名
* @param handler 拒绝策略
**/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long ,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
线程池的状态
线程池通过一个 int 变量的高 3 位来表示线程池的状态,低 29 位来存储线程池的数量
| 状态名称 | 高三位 | 接收新任务 | 处理阻塞队列任务 | 说明 |
| --------- | ------ | ---------- | ---------------- | ------------------------------------------------------------ |
| Running | 111 | Y | Y | 正常接收任务,正常处理任务 |
| Shutdown | 000 | N | Y | 不会接收任务,会执行完正在执行的任务,也会处理阻塞队列里的任务 |
| stop | 001 | N | N | 不会接收任务,会中断正在执行的任务,会放弃处理阻塞队列里的任务 |
| Tidying | 010 | N | N | 任务全部执行完毕,当前活动线程是 0,即将进入终结 |
| Termitted | 011 | N | N | 终结状态 |
线程池的主要流程
线程池创建、接收任务、执行任务、回收线程的步骤
创建线程池后,线程池的状态是 Running,该状态下才能有下面的步骤
提交任务时,线程池会创建线程去处理任务
当线程池的工作线程数达到c orePoolSize 时,继续提交任务会进入阻塞队列
当阻塞队列装满时,继续提交任务,会创建救急线程来处理
当线程池中的工作线程数达到 maximumPoolSize 时,会执行拒绝策略
当线程取任务的时间达到 keepAliveTime 还没有取到任务,工作线程数大于 corePoolSize 时,会回收该线程
不是刚创建的线程是核心线程,后面创建的线程是非核心线程,线程是没有核心非核心的概念的
拒绝策略
调用者抛出RejectedExecutionException (默认策略)
让调用者运行任务
丢弃此次任务
丢弃阻塞队列中最早的任务,加入该任务
线程池的关闭
shutdown()
会让线程池状态为 shutdown,不能接收任务,但是会将工作线程和阻塞队列里的任务执行完 相当于优雅关闭
shutdownNow()
会让线程池状态为stop, 不能接收任务,会立即中断执行中的工作线程,并且不会执行阻塞队列里的任务, 会返回阻塞队列的任务列表
ThreadLocal
ThreadLocal
适用于每一个线程需要自己独立实例,而且实例的话需要在多个方法里被使用到,也就是变量在线程之间是隔离的但是在方法或者是类里面是共享的场景。可以在多线程的情况下防止自己的变量被其他线程篡改。
ThreadLocal 与 Synchonized 区别
Synchronized
是利用锁的机制,使得变量或者是代码块在某一时刻里只能被一个线程来进行访问。
ThreadLocal
是为每一个线程都提供了一个变量的副本,这样就是的每一个线程在某一时刻里访问到的不是同一个对象,这样就隔离了多个线程对数据的数据共享,`Synochronized` 正好相反,可以用于多个线程之间通信能够获得数据共享。
简单使用
static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()
demo
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private void print() {
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}
@Test
public void test1() {
threadLocal.set(1);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
threadLocal.set(Integer.valueOf(
Thread.currentThread().getName().split("-")[1]));
print();
}finally {
threadLocal.remove();
}
}).start();
}
}
java 输出
Thread-4:4
Thread-2:2
Thread-1:1
Thread-7:7
Thread-6:6
Thread-3:3
Thread-5:5
Thread-8:8
Thread-9:9
Thread-10:10
由于 ThreadLocal
里设置的值,只有当前线程自己看得见,这意味着你不可能通过其他线程为它初始化值。为了弥补这一点,ThreadLocal
提供了一个 withInitial() 方法统一初始化所有线程的 ThreadLocal
的值:
// ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
ThreadLocal 的实现原理
get 方法
java ThreadLocal get()源码
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 更新线程获取对应的 ThreadLocalMap
// ThreadLocalMap 保存着这个线程的所有的 ThreadLocal 变量
ThreadLocalMap map = getMap(t);
if (map != null) {
// ThreadLocalMap 的 key 就是当前 ThreadLocal 对象实例
// 多个 ThreadLocal 变量都是放在这个 map 中的
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
// 从 map 里取出来的值就是我们需要的这个 ThreadLocal 变量
return result;
}
}
// 如果 map 没有初始化,那么在这里初始化一下
return setInitialValue();
}
通过查看 ThreadLocalMap 类的源码可以发现每个 Entry 的 key 都是一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
// key 是个弱引用
super(k);
value = v;
}
}
这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个
ThreadLocal
对象,避免可能的内存泄露(注意,Entry 中的 value,还是强引用)
出现这样情况可能会出现内存泄漏
解决方案
try {
threadLocal.set(value);
print();
}finally {
threadLocal.remove();
}
ThreadLocal 为什么是 WeakReference 呢?
如果是强引用的话,即使 ThreadLocal
的值是为 null,但是的话 ThreadLocalMap
还是会有 ThreadLocal
的强引用状态,如果没有手动进行删除的话,ThreadLocal
就不会被回收,这样就会导致 Entry 内存的泄漏
ThreadLocalMap 中的 Hash 冲突处理
在 ThreadLocalMap 中解决 hash 冲突处理和 HashMap 中是不一样的 HashMap 中使用的是链表法解决冲突,而 ThreadLocalMap 使用的是线性探测法解决冲突
线性探测法
set 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据计算出来的 hash 值查找位置
int i = key.threadLocalHashCode & (len-1);
// 如果当前计算出来的位置没有被占用就不进入循环直接使用这个位置
// 如果计算出来的位置占用那么就一致找下去直到找到空位置
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果 key 一致认为是为了覆盖之前设置的值
if (k == key) {
e.value = value;
return;
}
// 如果 key = null 那么说明之前的 key 已经被回收了
// 进行清理工作并将当前的 key 和 value 设置进去
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 将当前要设置 key 和 value 设置到找到的位置
tab[i] = new Entry(key, value);
int sz = ++size;
// 扫描表并清理垃圾项
// 如果清理完垃圾项还是大于等于下一次扩充值的话就对表进行一次加一倍的扩充
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
共享线程的 ThreadLocal 数据
这个问题可以转换为 「父子线程数据传递的问题」
使用 InheritableThreadLocal
可以实现多个线程访问 ThreadLocal
的值,我们在主线程中创建一个 InheritableThreadLocal
的实例,然后在子线程中得到这个 InheritableThreadLocal
实例设置的值。
@Test
void test2() {
ThreadLocal<String> stringThreadLocal = new InheritableThreadLocal<>();
stringThreadLocal.set("xiaou");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// xiaou
System.out.println(stringThreadLocal.get());
}finally {
stringThreadLocal.remove();
}
}).start();
}
}
线程的创建
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 线程名称
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// 获取当前线程作为父线程
Thread parent = currentThread();
// 获取 Java 安全管理器
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
// 无论线程组是否被显式传入,都可以访问
g.checkAccess();
/*
* 我们有必要的权限吗
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
// 守护线程标识
this.daemon = parent.isDaemon();
// 获取父类的优先级
this.priority = parent.getPriority();
// 获取线程上下文类加载器
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
// 修改线程的优先级
// 该线程的优先级将被设置为指定的优先级和该线程的线程组允许的最大优先级中较小的一个
setPriority(priority);
// 线程的 inheritThreadLocals 变量不为空
// 而且父线程的 inheritableThreadLocals 也不为空
// 那么就父线程的 inheritThreadLocals 给当前线程的 inheritThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
// 存放指定的堆栈大小
this.stackSize = stackSize;
// 生成线程的 Id
tid = nextThreadID();
}