上下文切换
即使是单核处理器也支持多线程执行代码,CPU 通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停的切换线程来执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到加载的过程就是一次上下文切换。
线程的状态
第一是创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
第二是就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
第五是死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪。
多线程的几种实现方式
实现 Runnable 接口public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
Runnable 实例作为Thread的构造参数创建一个 Thread 实例,然后调用 Thread 实例的 start() 方法来启动线程。public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
Thread 类部分源码:
Thread 继承了 Runnable 接口。传入的Runnable参数最后赋值给了Thread类的属性target。public class Thread implements Runnable {
private Runnable target; // 当前线程将要执行的动作
# 构造方法
private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
...
this.target = target; // 赋给了Thread的属性target
}
}
Thread.start()方法启动线程:start()实际上是通过本地方法start0()启动一个新线程,新线程会调用run()方法。public synchronized void start() {
// 如果线程不是"就绪状态",则抛出异常!
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 将线程添加到ThreadGroup中
group.add(this);
boolean started = false;
try {
// 通过start0()启动线程,新线程会调用run()方法
start0();
// 设置started标记=true
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
Thread类重写的run() 方法:@Override
public void run() {
if (target != null) {
target.run();
}
}
继承 Thread 类
同样也需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
public class MyThread extends Thread {
public void run() {
// ...
}
}public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
实现 Callable 接口
Callable接口通过FutureTask包装器来创建Thread线程。Callable接口介绍:
(1)java.util.concurrent.Callable是一个泛型接口,只有一个call()方法
(2)call()方法抛出异常Exception异常,且返回一个指定的泛型类对象使用Callable接口实现多线程的步骤
(1)第一步:创建Callable子类的实例化对象
(2)第二步:创建FutureTask对象,并将Callable对象传入FutureTask的构造方法中
(注意:FutureTask实现了Runnable接口和Future接口)
(3)第三步:实例化Thread对象,并在构造方法中传入FurureTask对象
(4)第四步:启动线程public class MyCallable implements Callable {
public Integer call() {
return 123;
}
}public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
实现Runnable接口 VS 继承 Thread
实现接口会更好一些,因为:Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
类可能只要求可执行就行,继承整个 Thread 类开销过大。
volatile
volatile是轻量级的synchronized,它在多处理器并发中保证了共享变量的“可见性”。可见性的意思是但一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。volatile比synchronized的使用和执行成本低,因为它不会引起线程上下文的切换和调度。
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀命令。
volatile 的 Lock 前缀指令:将当前处理器缓存行的数据写回到系统内存。
这个写会内存的操作会使再其他CPU里缓存了该内存地址的数据无效。
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
ThreadLocal
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题.变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。
CAS (Compare And Swap)
CAS是一种乐观锁,机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
锁优化
Java 对象头
自旋锁
自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
偏向锁
轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
当一个线程访问同步代码块并获取锁时,会在对象头和帧栈中的锁记录里存储锁偏向的线程ID,以后该线程再进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word 里的线程ID是否是当前线程。如果测试成功,表示线程已经获得了锁。
如果测试失败,则需要再测一下Mark Word中偏向锁的表示是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则使用CAS将对象头的偏向锁执行当前线程。
轻量级锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为 Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋锁来获取锁。
如果仍未获取到锁,则升级为重量级锁。
轻量级锁解锁
使用CAS操作将Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
线程池
优势
(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。
实现原理
1、判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。
使用参数corePoolSize(线程池基本大小)
maximumPoolSize(线程池最大大小)
keepAliveTime(线程存活保持时间)
workQueue(任务队列): 用于传输和保存等待执行任务的阻塞队列。
threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。
参考资料