前言
完善整理java线程篇
1、什么叫线程
1.1:操作系统-任务调度
大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。
任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。
这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。
1.2:进程
进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
进程具有的特征:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
- 并发性:任何进程都可以同其他进程一起并发执行;
- 独立性:进程是系统进行资源分配和调度的一个独立单位;
- 结构性:进程由程序、数据和进程控制块三部分组成。
1.3:什么叫线程
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
1.4:线程与进程
从进程到线程 的历史
- 在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。它相当于一个进程里只有一个线程,进程本身就是线程。所以线程有时被称为轻量级进程(Lightweight Process,LWP)。
- 后来,随着计算机的发展,对多个任务之间上下文切换的效率要求越来越高,就抽象出一个更小的概念——线程,一般一个进程会有多个(也可是一个)线程。
两者对比
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
1.5:多核处理器
对单核处理器来说,同一时间点只有一个任务在执行,多核处理器可支持任务并行处理。 多核(心)处理器是指在一个处理器上集成多个运算核心从而提高计算能力,也就是有多个真正并行计算的处理核心,每一个处理核心对应一个内核线程。内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。
现在的电脑一般是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程
1.6:超线程
超线程技术就是利用特殊的硬件指令,把一个物理芯片模拟成两个逻辑处理核心,让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高的CPU的运行效率。这种超线程技术(如双核四线程)由处理器硬件的决定,同时也需要操作系统的支持才能在计算机中表现出来。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程(我们在这称它为用户线程),由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。
1.7:并发与并行
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生 举例子:开车和打电话 小A有两件事要做,开车和打电话。并发安排就是先把车开到路边,打完电话后继续开车(两件事分时间间隔执行);并行就是边开车边打电话(同一时间做两件事)。
1.8:线程的生命周期
1.8.1:状态切换
Thread类中的State枚举有六种值。 NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,ERMINATED;
- NEW 一个被创建的线程,但是还没有调用start方法。操作系统在创建进程时要进行的工作包括分配和建立进程控制块表项、建立资源表格并分配资源、加载程序并建立地址空间;
- RUNNABLE 一个正在被执行的线程的状态,正在占用时间片;
- BLOCKED 一个线程因为等待临界区的锁被阻塞产生的状态(也叫阻塞状态,可能的原因有1:线程通过调用sleep方法进入睡眠状态; 2:线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;3:线程试图得到一个锁,而该锁正被其他线程持有; 4:线程在等待某个触发条件 )
- WAITING 一个线程进入了锁,但是需要等待其他线程执行某些操作。时间不确定
- TIMED_WAITING 一个线程进入了锁,但是需要等待其他线程执行某些操作。时间确定
- ERMINATED 进程已结束,所以也称结束状态,释放操作系统分配的资源。
生命周期概述 线程的状态转换:
- 当一个线程创建以后,就处于新建状态。那什么时候这个状态会改变呢?只要它调用的start()方法,线程就进入了锁池状态。
- 进入锁池以后就会参与锁的竞争,当它获得锁以后还不能马上运行,因为一个单核CPU在某一时刻,只能执行一个线程,所以他需要操作系统分配给它时间片,才能执行。所以人们通常把一个线程在获得锁后,获得系统时间片之前的状态称之为可运行状态,但Java源代码里面并没有可运行状态这一说。
- 当一个持有对象锁的线程获得CPU时间片以后,开始执行这个线程,此时叫做运行状态。
- 当一个线程正常执行完,那么就进入终止(死亡)状态。系统就会回收这个线程占用的资源。
- 但是,线程的执行并不是那么顺利的。一个正在运行的线程有可能会进入I/O交互,还可能调用sleep()方法,还有可能在当前线程当中有其它线程调用了join()方法。这时候线程就进入了阻塞状态(但这也只是人们在理解的时候意淫加上去的,源代码里也没有定义这一个状态)。阻塞状态的线程是没有释放对象锁的。当I/O交互完成,或sleep()方法完成,或其它调用join()方法的线程执行完毕。阻塞状态的线程就会恢复到可运行状态,此时如果再次获得CPU时间片就会进入运行状态。 一个处于运行状态的线程还可能调用wait()方法、或者带时间参数的wait(long milli)方法。这时候线程就会将对象锁释放,进入等待队列里面(如果是调用wait()方法则进入等待状态,如果是调用带时间参数的则进入定时等待状态)。 一个线程如果的调用了带时间参数的wait(long milli)方法进入了定时等待状态,那么只要时间一到就会进入锁池状态,并不需要notify()或notifyAll()方法来唤醒它。如果调用的是不带时间参数的wait()则需要notify()或notifyAll()这两个方法来唤醒它然后进入锁池状态。进入锁池状态以后继续参与锁的竞争。
- 当一个处于运行状态的线程调用了suspend()方法以后,它就会进入挂起状态(这一方法已经过时不建议使用)。挂起状态的线程也没有释放对象锁,它需要调用resume()方法以后才能恢复到可运行状态。将线程挂起容易导致程序死锁。
1.8.2:jvm层面
启动原理
Java里面创建线程之后必须要调用start方法才能真正的创建一个线程,该方法会调用虚拟机启动一个本地线程,本地线程的创建会调用当前系统创建线程的方法进行创建,并且线程被执行的时候会回调 run方法进行业务逻辑的处理
停止原理
线程的终止有主动和被动之分,被动表示线程出现异常退出或者run方法执行完毕,线程会自动终止。主动的方式是 Thread.stop()来实现线程的终止,但是stop()方法是一个过期的方法,官方是不建议使用,理由很简单,stop()方法在中介一个线程时不会保证线程的资源正常释放,也就是不会给线程完成资源释放工作的机会,相当于我们在linux上通过kill -9强制结束一个进程。
使用thread.interrupt()来安全的中断。
public class InterruptedDemo implements Runnable {
@Override
public void run() {
long i = 0l;
while (!Thread.currentThread().isInterrupted()) {
//notice here
i++;
}
System.out.println("result:" + i);
}
public static void main(String[] args) throws InterruptedException {
InterruptedDemo interruptedDemo = new InterruptedDemo();
Thread thread = new Thread(interruptedDemo);
thread.start();
Thread.sleep(1000);
//睡眠一秒
thread.interrupt();
//notice here
}
}
Java里面创建线程之后必须要调用start方法才能真正的创建一个线程,该方法会调用虚拟机启动一个本地线程,本地线程的创建会调用当前系统创建线程的方法进行创建,并且线程被执行的时候会回调 run方法进行业务逻辑的处理
thread.interrupt()方法实际就是设置一个interrupted状态标识为true、并且通过ParkEvent的unpark方法来唤醒线程。
中断异常
Object.wait、Thread.sleep和Thread.join都会抛出InterruptedException。
首先,这个异常的意思是表示一个阻塞被其他线程中断了。
然后,由于线程调用了interrupt()中断方法,那么Object.wait、Thread.sleep等被阻塞的线程被唤醒以后会通过is_interrupted方法判断中断标识的状态变化。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。如果发现中断标识为true,则先清除中断标识,然后抛出InterruptedException
需要注意的是,InterruptedException异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,
比如
- 直接捕获异常不做任何处理
- 将异常往外抛出
- 停止当前线程,并打印异常信息
1.9:线程的调度
- 摘自:Java多线程学习(吐血超详细总结)
1、调整线程优先级
Java线程有优先级,优先级高的线程会获得较多的运行机会。
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量: - static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。 - static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。 - static int NORM_PRIORITY
分配给线程的默认优先级,取值为5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
2、线程睡眠
Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
3、线程等待
Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
4、线程让步
Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
5、线程加入(也叫合并)
join()方法,等待其他线程终止(即将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时可以使用join方法)。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
6、线程唤醒
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。
其他的调度:
- 摘自:Java线程: 线程调度 (有代码示例)
7、守护线程
调用setDaemon(true)将线程转换成守护线程。守护线程的唯一用途是为其他线程提供服务。比如说,JVM的垃圾回收、内存管理等线程都是守护线程。
计时线程就是一个例子,它定时的发送“计时器滴答”信号给其他线程或清空过时的高速缓存项的线程,最后只剩下守护线程时,JVM就退出了。
8、未捕获异常处理器
run方法不能抛出任何被检测的异常,但是,不被检测的异常就会导致线程的终止。但是不需要任何catch字句来处理被传播的异常。相反,死亡之前,异常被传递到一个用于未捕获异常处理器。该处理器实现一个Thread.UncaughtExceptionHandler接口的类。这个接口只有一个方法即:void uncaughtException(Thread t,Throwable e)
用setUncaughtaExceptionHandler方法为任何线程安装一个处理器。但是如果不为此独立的线程安装处理器,则ThreadGroup类对象即为此时的处理器。
ThreadGroup类实现了Thread.UncaughtExceptionHandler接口。它的uncaughtException方法做如下操作:
- 如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用。
- 否则,如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用该处理器。
- 否则,如果Throwable是ThreadDeath的一个实例,什么都不做。
- 否则,线程的名字以及Throwable的栈轨迹被输出到System.err上。此时可以看到多次的栈轨迹。
1.10:线程的实现
实现方式
- 继承Thread类实现多线程
- 实现Runnable接口
- 实现Callable接口通过Future包装器来创建Thread线程,这种是带返回值的线程
- 使用线程池ExecutorService
继承Thread类实现多线程
继承Thread类,然后重写run方法,在run方法中编写当前线程需要执行的逻辑。最后通过线程实例的start方法来启动一个线程
public class ThreadDemo extends Thread {
@Override
public void run() {
//重写run方法,提供当前线程执行的逻辑
System.out.println("Hello world");
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
}
}
实现Runnable接口
如果需要使用线程的类已经继承了其他的类,那么按照Java的单一继承原则,无法再继承Thread类来实现线程,所以可以通过实现Runnable接口来实现多线程
public class RunnableDemo implements Runnable {
@Override
public void run() {
//重写run方法,提供当前线程执行的逻辑
System.out.println("Hello world");
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
new Thread(runnableDemo).start();
}
}
实现Callable接口
在有些多线程使用的场景中,我们有时候需要获取异步线程执行完毕以后的反馈结果,也许是主线程需要拿到子线程的执行结果来处理其他业务逻辑,也许是需要知道线程执行的状态。那么Callable接口可以很好的实现这个功能
public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
return "hello world";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> callable = new CallableDemo();
FutureTask<String> task = new FutureTask<>(callable);
new Thread(task).start();
System.out.println(task.get());
//获取线程的返回值
}
}
在上面代码案例中的最后一行 task.get()就是获取线程的返回值,这个过程是阻塞的,当子线程还没有执行完的时候,主线程会一直阻塞直到结果返回
使用线程池
为了减少频繁创建线程和销毁线程带来的性能开销,在实际使用的时候我们会采用线程池来创建线程。详细的关于线程池的分析下面再陈述。
public class ExecutorServiceDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(1);
Future future = pool.submit(new CallableDemo());
System.out.println(future.get());
}
}
pool.submit有几个重载方法,可以传递带返回值的线程实例,也可以传递不带返回值的线程实例,源代码如下
-
Future submit(Callable var1); -
Future submit(Runnable var1, T var2); - Future<?> submit(Runnable var1);
2:多线程
2.1:线程安全
多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
线程安全的实现方法
- 摘自:【JVM.12】线程安全与锁优化
-
1:互斥同步
互斥同步是常见的一种并发正确性保障手段(悲观锁)。 最基本的互斥同步手段就是 synchronized 关键字, synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码都需要一个 refence 类型的参数来指明要锁定和解锁的对象。如果Java 程序中的 synchronized 明确指定了对象参数,那就是这个对象的 reference; 如果没有明确指定,那就根据 synchronized 修饰的是实例方法还是类方法,去取对应的对象实例或Class 对象来作为锁对象。
根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要尝试获取对象的锁。(这是个很关键的点:如果同一个类里多个 synchronized 方法,它们是共用一个锁的。那么一个类里面有两个synchronized方法不可以同步执行)
除了 synchronized 之外,我们还可以使用 java.util.concurrent(下文称 JUC)包中的重入锁(ReentrantLock)来实现同步,在基本用法上,ReentrantLock 与 synchronized 很相似,它们都具备一样的线程重入特性,只是代码写法上有点区别。不过,相比synchronized ,ReentrantLock 增加了一些高级功能,主要有一下三项:等待可中断、可实现公平锁、锁可以绑定多个条件。
等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。 synchronized 中的锁时非公平锁, ReentrantLock 默认情况下也是非公平锁,但是可以通过带布尔值的构造函数要求使用公平锁。
锁绑定多个条件是指一个ReentrantLock 对象可以绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait()、 notify()、 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联时,就不得不额外地添加一个锁,而ReentrantLock 则无需这样做,只需要多次调用 newCondition() 方法即可。
关于 synchronized 和 ReentrantLock 的性能问题,在 JDK1.6 之后 优先考虑使用 synchronized 来进行同步。 - 2:非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施,这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
为什么笔者说使用乐观并发策略需要“硬件指令集的发展”才能进行呢?因为我们需要操作和冲突检测这两个步骤具备原子性,靠什么来保证呢?如果这里再使用互斥同步来保证就失去了意义了,所以我们只能靠硬件来完成这件事情,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常有:
1:测试并设置(Test-and-Set)。
2:获取并增加(Fetch-and-Increment)。
3:交换(Swap)。
4:比较并交换(Compare-and-Swap,下文称 CAS)。
5:加载链接/ 条件存储(Load-Linked/ Store-Conditional)。
其中,前面3条是20世纪就已经存在于大多数指令集中的处理器指令,后面两条是新增的。
CAS 指令需要有3个操作数,分别是 内存位置 V、旧的预期值 A、新值 B。CAS 指令执行时,当且仅当V 符合就预期值A 时,处理器用新值B 更新V 的值,否则他就不执行更新,但无论是否更新了V 值,都会返回V 的旧值。
在JDK1.5 之后,Java程序中才可以使用 CAS操作,该操作由 sun.misc.Unsafe 类里面的 compareAndSwapInt() 和 compareAndSwapLong() 等几个方法包提供。
由于Unsafe 类不是提供给用户程序调用的类(只有启动类加载器 Bootstrap ClassLoader 加载的Class才能访问它),因此如果不采用反射手段,我们只能通过其他的Java API 来间接使用它,如 JUC包里面的 原子类。
不妨拿一段在上一章没解决的问题看看使用 CAS 操作来避免阻塞同步。
public class AtomicTest {
public static AtomicInteger race = new AtomicInteger(0);
public static void increase() {
race.incrementAndGet();
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(race);
}
}
尝试去看看 incrementAndGet() 方法的源代码。
尽管CAS 看起来很美,但是显然这种操作无法涵盖互斥同步的所有使用场景,存在这样一个漏洞:如果一个变量V 初次读取的时候是A 值,并且准备赋值的时候检查它仍然为A 值,那我们就能说它的值没有被其他线程改变过嘛?如果这期间它的值被改为B 后又改为了A ,那CAS操作就误认为它从没有改变过。这个漏洞称为“ABA”问题。但是大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步。
- 3:无同步方案
要保证线程安全,不是一定要进行同步,两者没有因果关系。同步时保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有些代码天生就是线程安全的。
可重入代码(Reentrant Code):可重入代码有一些共同特征,例如不依赖存储在堆上的数据和共用的系统资源、用到的状态量都是由参数中传入、不调用非可重入的方法等。如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
线程本地存储(Thread Local Storage):如果一段代码中需要的数据必须与其他代码共享,那就看看这些数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无需同步也能保证线程之间不出现数据争用的问题。
Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 关键字声明它为“易变得”;如果一个变量要被某个线程独享,可以通过 java.lang.ThreadLocal 类来实现线程本地存储的功能。每一个线程的Thread 对象中都有一个 ThreadLocalMap 对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值的 K-V 对。
2.2:线程组
可以批量管理线程或线程组对象,有效地对线程或线程组对象进行组织。用户创建的所有线程都属于指定线程组,如果没有显示指定属于哪个线 程组,那么该线程就属于默认线程组(即main线程组)。默认情况下,子线程和父线程处于同一个线程组。只有在创建线程时才能指定其所在的线 程组,线程运行中途不能改变它所属的线程组,也就是说线程一旦指定所在的线程组,就直到该线程结束。
2.3:线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
线程池到底解决了那些问题
- 降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗
- 提高响应速度:任务到达时不需要等待线程创建就可以立即执行
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。
执行流程
- 调用ThreadPoolExecutor的execute提交线程,首先检查CorePool,如果CorePool内的线程小于CorePoolSize,新创建线程执行任务,当前步骤需获取全局锁。
- 如果当前CorePool内的线程大于等于CorePoolSize,那么将线程加入到阻塞队列(如BlockingQueue)。
- 如果队列已满,并且小于MaxPoolSize的情况下,创建新的非核心线程来处理任务,该步骤也需要获取全局锁。
- 如果线程数大于等于MaxPoolSize则任务将被拒绝并执行拒绝策略。
2.4:线程组和线程池
- 线程组:线程组存在的意义,首要原因是安全。java默认创建的线程都是属于系统线程组,而同一个线程组的线程是可以相互修改对方的数据的。但如果在不同的线程组中,那么就不能“跨线程组”修改数据,可以从一定程度上保证数据安全。
- 线程池:线程池存在的意义,首要作用是效率。线程的创建和结束都需要耗费一定的系统时间(特别是创建),不停创建和删除线程会浪费大量的时间。所以,在创建出一条线程并使其在执行完任务后不结束,而是使其进入休眠状态,在需要用时再唤醒,那么 就可以节省一定的时间。如果这样的线程比较多,那么就可以使用线程池来进行管理。保证效率。
- 线程组和线程池共有的特点:1,都是管理一定数量的线程2,都可以对线程进行控制—包括休眠,唤醒,结束,创建,中断(暂停)–但并不一定包含全部这些操作。
2.5:线程池使用
jdk自带
1、ForkJoinPool
- 摘自多线程 ForkJoinPool
ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。
Java7 提供了ForkJoinPool来支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。 ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。 使用方法:创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTasktask) 或invoke(ForkJoinTask task)方法来执行指定任务了。 其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务。
2、ThreadPoolExecutor java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类
- 摘自Java并发编程:线程池的使用
构造方法(jdk1.8)public ThreadPoolExecutor(int var1, int var2, long var3, TimeUnit var5, BlockingQueue<Runnable> var6) { this(var1, var2, var3, var5, var6, Executors.defaultThreadFactory(), defaultHandler); } public ThreadPoolExecutor(int var1, int var2, long var3, TimeUnit var5, BlockingQueue<Runnable> var6, ThreadFactory var7) { this(var1, var2, var3, var5, var6, var7, defaultHandler); } public ThreadPoolExecutor(int var1, int var2, long var3, TimeUnit var5, BlockingQueue<Runnable> var6, RejectedExecutionHandler var7) { this(var1, var2, var3, var5, var6, Executors.defaultThreadFactory(), var7); } public ThreadPoolExecutor(int var1, int var2, long var3, TimeUnit var5, BlockingQueue<Runnable> var6, ThreadFactory var7, RejectedExecutionHandler var8) { this.ctl = new AtomicInteger(ctlOf(-536870912, 0)); this.mainLock = new ReentrantLock(); this.workers = new HashSet(); this.termination = this.mainLock.newCondition(); if (var1 >= 0 && var2 > 0 && var2 >= var1 && var3 >= 0L) { if (var6 != null && var7 != null && var8 != null) { this.corePoolSize = var1; this.maximumPoolSize = var2; this.workQueue = var6; this.keepAliveTime = var5.toNanos(var3); this.threadFactory = var7; this.handler = var8; } else { throw new NullPointerException(); } } else { throw new IllegalArgumentException(); } }
下面解释下一下线程池构造器中各个参数的含义
1:corePoolSize
核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()(初始化一个核心线程)或者prestartCoreThread()(初始化所有核心线程)方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
2:aximumPoolSize
线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
3:keepAliveTime
表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
4:unit
参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
- TimeUnit.DAYS; //天
- TimeUnit.HOURS; //小时
- TimeUnit.MINUTES; //分钟
- TimeUnit.SECONDS; //秒
- TimeUnit.MILLISECONDS; //毫秒
- TimeUnit.MICROSECONDS; //微妙
- TimeUnit.NANOSECONDS; //纳秒
5:workQueue
一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
- ArrayBlockingQueue;(基于数组的先进先出队列,此队列创建时必须指定大小)
- LinkedBlockingQueue;基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
- SynchronousQueue;这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。 ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
6:threadFactory
线程工厂,主要用来创建线程;ThreadFactory接口只有newThread方法,是用来生产线程的,子类需要实现这个方法来根据自己规则生产相应的线程。
7:handler
表示当拒绝处理任务时的策略,有以下四种取值:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
使用原理
当一个新任务提交到线程池时,简单来说线程池的处理流程如下:
1、判断核心线程池里的线程是否都在执行任务,如果不是则创建一个新的工作线程处理任务,否则进入下个流程。
2、判断工作队列是否已满,如果未满则将新提交的任务存储在该工作队列,否则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果不是则创建一个新的工作线程来执行任务,否则交由饱和策略处理。
-
1、线程池状态 (详见:1.9:线程的调度) 在ThreadPoolExecutor中定义了一个volatile变量:(private volatile int threadStatus = 0;),另外定义了几个static final变量表示线程池的各个状态,在Thread.java的内部枚举类State。
- 2、任务的执行
execute核心代码(jdk1.8)public void execute(Runnable var1) { if (var1 == null) { throw new NullPointerException(); } else { int var2 = this.ctl.get(); if (workerCountOf(var2) < this.corePoolSize) { //当前线程池大小小于核心大小,如果新增当前线程失败,就往下执行 if (this.addWorker(var1, true)) { return; } var2 = this.ctl.get(); } if (isRunning(var2) && this.workQueue.offer(var1)) { //当前线程池处于RUNNING状态且将任务放入任务缓存队列成功 int var3 = this.ctl.get(); if (!isRunning(var3) && this.remove(var1)) { //新增后,导致当前线程处于非RUNNING状态,就去掉刚添加的,并进行任务拒绝处理 this.reject(var1); } else if (workerCountOf(var3) == 0) { //如果当前线程池长度为0(keepAliveTime可能会造成这个情况),则新加一个空线程(也可说是预创建) this.addWorker((Runnable)null, false); } } else if (!this.addWorker(var1, false)) { //在加入当前线程池,失败的话,就进行任务拒绝处理 this.reject(var1); } } }
- 3、线程池中的线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
prestartCoreThread():初始化一个核心线程;
prestartAllCoreThreads():初始化所有核心线程核心代码: this.addWorker((Runnable)null, false);
传进去的参数是null,则最后执行线程会阻塞在runWorker调用的getTask方法中的
r = workQueue.take();
即等待任务队列中有任务。
-
4、任务缓存队列及排队策略
详见:2.5:线程池使用 的 workQueue -
5、任务拒绝策略
详见:2.5:线程池使用 的 handler -
6、线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务 -
7、线程池容量的动态调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:
setCorePoolSize()和setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
使用方法
在ThreadPoolExecutor类中有几个非常重要的方法:
- execute()
- submit()
- shutdown()
- shutdownNow()
execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。
shutdown()和shutdownNow()是用来关闭线程池的。
还有很多其他的方法:
比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法
封装类:Executors
- 摘自java中的线程池有哪些,分别有什么作用?
这像是个封装工具类,封装方法分别调用ThreadPoolExecutor和ForkJoinPool的构造函数。
1.newCachedThreadPool创建一个可缓存线程池程
2.newFixedThreadPool 创建一个定长线程池
3.newScheduledThreadPool 创建一个定长线程池
4.newSingleThreadExecutor 创建一个单线程化的线程池
一般情况下会使用Executors创建线程池,目前不推荐,线程池不建议使用Executors去创建,而是通过ThreadPoolExecutor方式,这样的处理方式可以更加明确线程池的运行规则,规避资源耗尽的风险。
1、newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
2、newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM
- 谷歌工具包
- 摘自线程池创建
使用了com.google.guavaThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat("demo-pool-%d").build(); ExecutorService executorService = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
- commons-lang3包
使用了commons-lang3包ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
- spring配置线程池
自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean调用execute(Runnable task)方法即可。
如果不配ThreadFactory,则会调用ThreadPoolExecutor的另一个构造方法。<!-- 添加默认实现 --> <bean id="threadFactory" class="java.util.concurrent.Executors$DefaultThreadFactory"/> <!-- 添加自定义实现--> <bean id="threadFactoryNew" class="com.fc.provider.ThreadFactoryConsumer"/> <!-- 创建线程池 --> <bean id="userThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <property name="corePoolSize" value="5" /> <property name="maxPoolSize" value="50" /> <property name="queueCapacity" value="1000" /> <property name="keepAliveSeconds" value="3000"/> <property name="threadFactory" ref="threadFactoryNew"/> <property name="rejectedExecutionHandler"> <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/> </property>
- springboot如何注入
- 摘自springboot线程池的使用和扩展
@Configuration
@EnableAsync
public class ExecutorConfig {
private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);
@Bean
public Executor asyncServiceExecutor() {
logger.info("start asyncServiceExecutor");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(5);
//配置最大线程数
executor.setMaxPoolSize(5);
//配置队列大小
executor.setQueueCapacity(99999);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("async-service-");
// rejection-policy:当pool已经达到max size的时候,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//执行初始化
executor.initialize();
return executor;
}
}
@Override
@Async("asyncServiceExecutor")
public void executeAsync() {
logger.info("start executeAsync");
try{
Thread.sleep(1000);
}catch(Exception e){
e.printStackTrace();
}
logger.info("end executeAsync");
}
- tomcat自带线程池
- 摘自详解 Tomcat 的连接数与线程池
在使用tomcat时,经常会遇到连接数、线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector)
Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据;然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine。当Engine处理完请求后,也会通过Connector将响应返回给客户端。
可以说,Servlet容器处理请求,是需要Connector进行调度和控制的,Connector是Tomcat处理请求的主干,因此Connector的配置和使用对Tomcat的性能有着重要的影响。这篇文章将从Connector入手,讨论一些与Connector有关的重要问题,包括NIO/BIO模式、线程池、连接数等。
根据协议的不同,Connector可以分为HTTP Connector、AJP Connector等。
3:并发编程
3.1:并发编程的目的
并发编程的目的是为了让程序运行得更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换的问题、死锁的问题,以及受限于硬件和软件的资源限制问题。
3.1.1:上下文切换
即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。
时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
如何减少上下文切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
- 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
- CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
- 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
3.1.2:死锁
锁是个非常有用的工具,运用场景非常多,因为它使用起来非常简单,而且易于理解。但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。
避免死锁的几个常见方法。
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
3.1.3:资源限制的挑战
1:什么是资源限制
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。
硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和socket连接数等。
:2:资源限制引发的问题
在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
例如,之前看到一段程序使用多线程在办公网并发地下载和处理数据时,导致CPU利用率达到100%,几个小时都不能运行完成任务,后来修改成单线程,一个小时就执行完成了。
3:如何解决资源限制的问题
对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。
比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这笔数据。
对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
4:在资源限制情况下进行并发编程
如何在资源限制的情况下,让程序执行得更快呢?
方法就是,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库操作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。
3.2:并发机制底层原理
3.2.1:volatile
在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。
可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
3.2.2:synchronized
在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。 但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了。 Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。
先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchonized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
那么锁到底存在哪里呢?锁里面会存储什么信息呢?
从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。
代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。
但是,方法的同步同样可以使用这两个指令来实现。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。
任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
#### 3.2.3:锁的升级与对比
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。
锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
偏向锁
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的所记录里存储锁偏向的线程ID,以后该线程再次加锁解锁时不需要进行CAS操作,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
- 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
- 偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查豉油偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的所记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
- 偏向锁默认启动,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数来关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
轻量级锁
- 线程在执行同步块之前,JVM会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中(Displaced Mark Word)。然后线程尝试使用CAS将对象头中的Mark Word奇幻为指向锁记录的指针。如果成功则当前线程获得锁,否则说明其他线程竞争锁,当前线程尝试使用自旋来获取锁。
- 轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功则表示没有竞争发生,否则表示当前锁存在竞争,锁会升级为重量级锁。
重量级锁
- 其他线程视图获取锁时都被阻塞,持有锁的线程释放锁之后唤醒这些线程,被唤醒的线程再抢。
锁 | 优点 | 缺点 | 适用场景 | 备注 |
---|---|---|---|---|
偏向锁 | 加锁和解锁无需额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 | |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 | |
重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
4:ThreadLocal
摘自: