Java 多线程学习笔记
Java 多线程并发
- 并发时需要解决得问题可能有多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系。因此你必须理解所有这些问题和特例,以便有效的使用并发。
- 用并发解决的问题大致上可以分为“速度”和“设计可管理性两种”
- 速度:如果你想要一个程序运行的更快,那么可以将其切开为多个片段,在单独的处理器上运行每个片段。当一个进程受阻时,另一个进程可以继续向前运行。
- 上下文切换:从一个任务切换到另一个任务。
- 事件驱动的编程:单处理器中性能提高的常见示例。
-
并发的三大性质:一个或多个操作,要么全部执行且不会被任何因素打断,要么就不执行
- synchronized定义同步块或者同步方法保证原子性
- Lock接口保证原子性
- Atomic类型保证原子性
-
可见性:一个线程对变量的修改,能及时的被其他线程看到
- 在多个线程的工作内存中都存在的变量,那么这个变量就是共享变量
- Java内存模型(Java Memory Model)描述了Java程序中各种变量(共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层信息。
- 所有的变量都存储在主内存中;
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本。
- 每个线程对共享变量的操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
- 不同线程之间无法直接访问彼此工作内存中的变量,不同线程间变量值的传递需要通过主内存来完成。
- 可见性实现原理
- 把线程中修改的变量值先更新到该线程独立的工作内存中,再将工作内存中的值刷新到主内存中。
- 主内存再将最新的共享变量的值刷新到其他工作内存中。
- 如何保证可见性
- 通过 synchronized 关键字定义同步代码块或者同步方法保障可见性。
- 通过 volatile 关键字标记内存屏障保证可见性。
- volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新(强制从公共堆栈中取值)。但是volatile 不能保证原子性。
- volatile可以实现禁止代码重排序。
- 通过 Lock 接口保障可见性。
- 通过 Atomic 类型保障可见性。
- 通过 final 关键字保障可见性:因为是不可变的所以可以保障可见性。
-
有序性:程序执行的顺序按照代码的先后顺序执行。
- synchronized保证线程操作的有序性。
- volatile禁止指令重排操作,通过内存屏障去完成禁止指令重排操作。
-
同步锁与死锁
- 同步锁:当多个线程同时访问一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是并发执行多个线程,在同一时间内只允许一个线程访问共享数据。Java中可以使用synchronized关键字来取得一个对象的同步锁。
- 死锁:多个线程同时被阻塞,他们中的一个或者全部都在等待某一个资源被释放。
Java线程的创建方式#
-
Thread类的本质是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方式就是通过Thread类的start()方法实现。start()方法是一个本地方法,它将启动一个新线程,并执行run()方法。
- Thread.java中的start()方法通知“线程规划器”-此线程已经准备就绪,准备调用线程对象的run()方法。让操作系统给安排一个时间来调用Thread中的run()方法。
-
使类继承Thread的方式创建线程:
class MyThread extends Thread{
@Override
public void run() {
try {
Thread.currentThread().setName("MyThread");
while (true){
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 使类实现Runnable接口
class MyThread2 implements Runnable{
@Override
public void run() {
try {
Thread.currentThread().setName("MyThread2");
while (true){
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过这两种方式启动线程:
public class ThreadStartApp {
public static void main(String[] args) {
new MyThread().start(); //继承Thread可以直接调用start方法。
new Thread(new MyThread2()).start(); //实现Runnable接口就需要传入Thread对象,Thread对象创建一个本地线程并执行run方法。
}
}
-
创建有返回值的线程
有返回值的任务必须实现Callable接口,类似的无返回值的任务必须实现Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合多线程就可以实现由返回结果的多线程了
public class CallableThread { public static void main(String[] args) throws Exception{ //创建一个线程池 int taskSize = 5; ExecutorService pool = Executors.newFixedThreadPool(taskSize); //创建有多个返回值的任务 List<Future<?>> list = new ArrayList<>(); for (int i = 0; i < taskSize; i++) { //执行任务并获取结果 Callable<String> c = new MyCallable(i + ""); Future<?> f = pool.submit(c); list.add(f); } //关闭线程池 pool.shutdown(); //获取并发任务的运行结果 for (Future<?> future : list) { System.out.println("res: " + future.get().toString()); } } }
-
基于线程池的方式
线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么可以使用缓存策略,也就是使用线程池
ExecutorService pool = Executors.newFixedThreadPool(10); //分配了10个线程 while (true) { pool.execute(new Runnable() { //提交多个线程任务,并执行 @Override public void run() { System.out.println(Thread.currentThread().getName() + "is running"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }); }
线程生命周期#
当一个线程被创建以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Bloked)和死亡(Dead)五种状态。尤其是线程启动以后,他不可能一直霸占着CPU独自运行(由操作系统内核分配CPU时间片),所以CPu需要在多条线程之间切换,于是线程状态也会多次在运行,阻塞之间切换。
-
新建(NEW)
当程序使用了new关键字创建一个线程以后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量。
-
就绪状态(RUNNABLE)
当线程对象调用start()方法之后,该线程就处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器(联系JVM内存知识),等待调度运行。
这里可以看出当调用了start启动后线程可能并不会立马运行,而是需要等待cpu资源、系统调度
-
运行状态(RUNNING)
如果处于就绪状态的线程被分配了CPU时间片,开始执行run方法的线程执行体,则线程处于运行状态。
-
阻塞状态(BLOKED)
阻塞状态是指线程因为某种原因放弃了cou使用权(例如等待IO资源,等待别的线程的计算结果或者等待锁之类的),也即让出了cpu timeslice(让出cpu时间片),暂时停止运行。直到线程进入可运行(runnable)状态(获得需要的资源后可以继续运行了)。阻塞的情况分为三种。
-
等待阻塞(o.wait->等待队列)
运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中,直到被唤醒。
-
同步阻塞(lock->锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中,待锁被释放然后争抢锁。
-
其他阻塞(sleep/join)
运行(running)的线程执行Thread.sleep(long ms)或t.join(),或者发出了I/O请求时,JVM会把该线程置位阻塞状态。当sleep()状态超时、join()等待终止或者超时时、或者I/O请求处理完毕,线程重新转为就绪状态(等待系统分配时间片)。
-
-
线程死亡(DEAD)
线程正常或非正常结束后就是死亡状态。
-
run()结束,call()方法,线程正常结束。
-
抛出异常
-
调用stop使线程死亡。
-
线程的切换状态#
-
创建一个线程后调用他的run()方法,系统会为此线程分配CPU资源,此时线程处于runnable(就绪/可运行)状态,如果线程抢占到CPU的资源,则此线程就处于running(运行)状态。
-
runnable:指操作系统可以分配时间片(线程可以抢占时间片)变为running运行状态,runnable和running状态可以互相切换,因为有可能线程运行一段时间后,其他高优先级的线程抢占了CPU资源,此时此线程就从running编程runnable状态。
进入runnable状态的四种情况:
- 调用sleep()方法后进过的时间超过了指定的休眠时间。
- 线程获得了试图同步的监视器。
- 线程正在等待某个通知,其他线程发出了通知。
- 处于挂起状态的线程调用了resume恢复方法。
-
阻塞状态(blocked):阻塞状态是指线程因为某种原因放弃了时cpu使用权,也让出了cpu时间片(此时操作系统会把时间片让给其他线程),暂时停止运行。直到线程进入可运行状态,才会有机会(抢占式调度cpu资源)获得cpu时间片。
blocked的五种原因:
- 线程调用了sleep方法(sleep方法不会释放锁)
- 线程调用了阻塞式IO
- 线程试图获得一个同步监视器,但该同步监视器正在被其他线程所有。
- 等待某个通知(notify)。
- 线程调用了suspend()方法将该线程挂起(容易造成死锁,应该尽量避免使用该方法)。
volatile关键字#
-
volatile的作用是在获取volatile修饰的字段是强制从主堆栈(main()线程)中获取值,在设置值的时候先在本地堆栈进行设置然后复制到主堆栈中实现变量在整个应用中的刷新。
-
同时使用volatile可以禁止指令重排序,volatile之前的代码不会被重排到volatile之后,volatile之后的代码也不会被重排到这个变量之前。
-
使用volatile实现线程间通信,需要不断的使用while轮询机制来检测某一个条件浪费cpu资源(类似于io中的忙轮询)。
例子:
ThreadA
public class ThreadA extends Thread{
private MyList myList;
public ThreadA(MyList myList){
super();
this.myList = myList;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("添加了"+i+"个元素");
myList.add();
Thread.sleep(1000);
}
System.out.println("线程:" + Thread.currentThread().getName()+" 退出了");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
ThreadB
public class ThreadB extends Thread{
private MyList myList;
public ThreadB(MyList myList){
super();
this.myList = myList;
}
@Override
public void run() {
try {
while (true){
if (myList.size() == 5){
throw new InterruptedException();
}
}
}catch (InterruptedException e){
System.err.println("list大小为5,线程B退出!");
}
}
}
MyList
public class MyList {
volatile private List<String> list = new ArrayList<>(); //实现线程间的可见性
public void add(){
list.add("name");
}
public int size(){
return list.size();
}
}
main线程
public class Main {
public static void main(String[] args) {
MyList myList = new MyList();
ThreadA threadA = new ThreadA(myList);
threadA.setName("A");
threadA.start();
ThreadB threadB = new ThreadB(myList);
threadB.setName("B");
threadB.start();
}
}
wait/notify机制#
- 等待/通知机制:相比于synchronized和volatile多个线程主动式的操作同一个一个变量(可能发生:cpu资源浪费、读取不到想要的值等情况),wait/norify机制提供了一个良好的线程通信机制。
- wait():wait方法是Object类的方法,他的作用是执行当前wait方法的线程等待,在wait所在的代码处暂停执行,并释放锁,直到被通知或被中断为止。(调用wait方法立即释放锁)
- 调用wait之前,线程必须获得该对象的对象级别锁,即:只能在同步代码块中调用wait方法。
- 通过通知机制唤醒等待线程的顺序是按照等待(调用wait)的顺序来的。
- 如果线程中调用wait方法并且当前线程没有持有任何锁,就会抛出IllegalMonitorStateException(运行时异常,无需catch)
- notify():用来通知那些可能等待锁的其他线程,如果有多个线程等待,按照wait()方法执行的顺序来对处于wait状态的线程发出一次通知。
- 当处于wait状态的线程被通知以后,不会立马获取该对象的锁,而是要等到调用notify的线程执行完毕以后才会持有锁。
- 当第一个wait线程被通知了以后执行完毕释放了持有的锁,并且没有调用notify语句,那么其他线程就会因为没有得到通知会继续处于wait状态。
一个完整的wait/notify Demo
public class WaitDemo implements Runnable{
private Object lock;
public WaitDemo(Object o){
lock = o;
}
@Override
public void run() {
try{
synchronized(lock){
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " do wait");
lock.wait();
System.out.println(threadName + " after notify");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class NotifyDemo implements Runnable{
private Object lock;
public NotifyDemo(Object o){
lock = o;
}
@Override
public void run() {
synchronized(lock){
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " run notify thread");
lock.notify();
System.out.println(threadName + " run after notify");
}
}
}
main
public class Demo2App {
public static void main(String[] args) {
Object lock = new Object();
new Thread(new WaitDemo(lock)).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new NotifyDemo(lock)).start();
}
}
使用wait/notify机制实现一个简单的多生产者多消费者模式
缓冲区:
public class Buf {
private List<Integer> stack = new ArrayList<>();
synchronized public void add(){
if (stack.size() < 50){
System.out.println("线程:" + Thread.currentThread().getName() + "执行push方法,size为" + size());
stack.add((int)(Math.random()*100));
}
}
synchronized public void pop(){
System.out.println("线程:" + Thread.currentThread().getName() + "执行pop方法,size为" + size());
stack.remove(0);
}
synchronized public int size(){
return stack.size();
}
}
生产者服务类:
public class ProductService {
private Buf buf;
public ProductService(Buf buf){
super();
this.buf = buf;
}
public void setMethod(){
try {
synchronized (this){
while (buf.size() == 50){
System.out.println("*****");
this.wait();
}
}
buf.add();
Thread.sleep(300);
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void checkBufStatus(){
try {
synchronized (this){
if (buf.size() < 50){
this.notifyAll();
}
}
System.out.println("push checkBuf = " + buf.size());
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
生产者线程:
public class ProductThread extends Thread{
private ProductService service;
public ProductThread(ProductService service){
super();
this.service = service;
}
@Override
public void run() {
while (true){
service.setMethod();
}
}
}
消费者服务类:
public class CustomService {
private Buf buf;
public CustomService(Buf buf){
super();
this.buf = buf;
}
public void getMethod(){
try {
synchronized (this){
while (buf.size() == 0){
System.out.println("+++++");
this.wait();
}
}
buf.pop();
Thread.sleep(300);
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void checkBufStatus(){
try {
synchronized (this){
if (buf.size() > 0){
this.notifyAll();
}
}
System.out.println("pop checkBuf = " + buf.size());
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
消费者线程:
public class CustomThread extends Thread{
private CustomService service;
public CustomThread(CustomService service){
this.service = service;
}
@Override
public void run() {
while (true){
service.getMethod();
}
}
}
测试主线程:
public class ProAndCusApp {
public static void main(String[] args) {
Buf buf = new Buf();
ProductService productService = new ProductService(buf);
for (int i = 0; i < 5; i++) {
new ProductThread(productService).start();
}
new ProductCheckThread(productService).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
CustomService customService = new CustomService(buf);
for (int i = 0; i < 20; i++) {
new CustomThread(customService).start();
}
new CustomCheckThread(customService).start();
}
}
输出:
线程:Thread-0执行push方法,size为0
线程:Thread-1执行push方法,size为1
线程:Thread-4执行push方法,size为2
push checkBuf = 3
线程:Thread-3执行push方法,size为3
线程:Thread-2执行push方法,size为4
...(此处省略)
- notifyAll()方法:按照一定顺序依次唤醒全部的线程(具体是正序还是倒序决定于JVM的具体实现)。
join方法#
-
很多情况下,主线程创建并启动子线程,如果子线程要进行大量的耗时运算,主线程往往将早于子线层之前结束,这时如果主线程想等待子线程完成之后再结束就需要用到join方法。
-
join:等待线程对象销毁
-
join方法和interrupt()方法同时使用出现异常:当在使用join的过程中,如果当前线程被中断,则当前线程出现异常。
-
join()的重载join(long)等待一定时间后不管子线程有没有执行完,主线程继续执行。
join源码:
join使用wait(long)实现,当使用join方法的时候持有锁被释放。
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
sleep与wait的区别
- 对于sleep()方法,我们首先要知道该方法是属于Thread类这种的。而wait()方法,则是属于Object类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是她的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。
- 在调用sleep()方法的过程中,线程不会释放对象锁。
- 调用wait()方法的时候,线程对象会立即释放锁,并进入等待此对象的等待锁定池,只有针对此对象调用notify()方法唤醒后本线程才进入对象锁定池准备获取对象锁进入运行状态。
Java后台线程#
-
守护线程-也称“服务线程”,它是后台线程,即为用户线程提供公共服务,在没有用户线程可服务的时候会自动离开。
-
优先级:守护线程优先级较低,用于为系统中的其他线程提供服务。
-
设置:通过设置setDeamon(true)来设置线程为“守护线程”。
-
在Deamon线程中产生的新线程也是Deamon的(具有继承关系)。
-
线程则是JVM级别的,以Tomcat为例,如果你在Web应用中启动一个线程,这个线程的生命周期并不会和Web应用程序保持同步。也就是说即使你停止了Web应用,这个线程依旧是活跃的。
-
垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何的Thread运行,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自栋离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
-
生命周期:守护进程(Deamon)是运行在后台的一种特殊线程。它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的时事件。也就是说守护线程不依赖于终端(后台线程),与系统共生死。当JVM找那个所有的线程都是守护线程的时候,JVM就可以退出了;如果还有1个及以上则不会退出。
Java锁#
乐观锁
- 乐观锁是一种乐观思想,即认为读多写少,遇到并发的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别有没有去更新这个数据(版本号version,在 MyBatis Plus 中,使用 @Version 实现乐观锁),采取在写时先读取出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
Java中的乐观锁基本通过使用CAS操作实现的,CAS是一种原子操作,比较当期值跟传入值是否一样,一样则更新,否则失败。
悲观锁
- 悲观锁就是悲观思想,即认为写多,遇到并发的可能性高,每次去拿数据的时候都认为数据是被写过的,所以每次在读数据的时候都会上锁,这样别人想读写这个数据就会block直接拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转化为悲观锁,例如:RetreentLock。
自旋锁
-
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,他们只需要等一等(自旋),等待持有锁的线程释放锁后即可获立即得锁,这样就避免了用户线程和内核的切换消耗。
-
线程自旋是需要消耗cpu的,说白了就是让cpu再做无用功,如果一值直获取不到锁,那线程也不能一直占用cpu自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
-
优缺点
- 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码来说性能大幅度的提升,因为自旋(保持内核态空转)的消耗会小于线程阻塞挂起再唤醒(用户态和内核态的切换)的操作的消耗,这些操作会导致线程发生两次上下文的切换。
- 但是如果锁竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不合适使用自旋锁了,因为自旋锁在获取锁前一直占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起的消耗。其他需要cpu的cpu也迟迟得不到cpu,造成cpu的浪费。这种情况下我们关闭自旋锁。
-
自旋锁时间阈值
- 自旋锁的目的是占着CPU的资源不释放,等到获取到锁立即进行处理。但是如果时间太长那么就会有大量的线程处于自选状态占用CPU资源,进而影响系统的性能。因此自旋的周期的选择额外重要!
- JDK1.6引入了自旋锁,适应性自旋锁意味着自旋的时间不固定了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者状态来决定的,基本认为一个线程上下文切换的时间是一个最佳时间,同时JVM还针对当前CPU的负荷情况做了较多的优化,如果平均负载小于cpu内核数则一直自旋,如果超过cpu内核数一半个线程在自旋,则后来线程直接阻塞,如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果cpu处于节电模式则停止自旋,自旋时间的最坏情况是CPU的存储延迟(CPUA存储了一个数据到CPUB得知这个数据的时间差),自旋时会适当放弃线程之间的优先级的差异。
偏向锁
- Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。用于在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销。
- 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
- 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
- 偏向锁的适用场景
- 始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;
在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;
- 始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;
轻量级锁
-
随着锁的升级,锁可以从偏向锁升级到轻量锁再升级为重量锁
-
轻量锁是针对于操作系统通过互斥量来实现传统锁来说的。目的是:在没有多线程竞争的前提下,减少传统的重量锁使用产生的性能消耗。
-
轻量锁适用于线程交替执行的同步块的情况,如果存在同一时间访问同一锁的情况(存在竞争),就会导致轻量锁膨胀为重量锁。
-
加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。==然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。
-
解锁
轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程。
-
锁的升级流程
重量级锁
- 重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。
- 每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
- 操作系统实现线程之间的掐混需要从用户态转换为和心态,成本很高相对耗时,所以synchronized效率比较低被称为重量级锁。
- Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
Synchronized同步锁
-
synchronized可以吧任意一个非NULL的对象当做锁。它属于独占式的悲观锁,同时属于可重入锁。
-
作用范围
- 作用于方法时,锁住的是对象的实例(this)。
- 作用于静态方法时,锁住的是Class实例(Demo.class),又因为Class相关数据存储在方法区(永久代/元空间),方法区是线程共享区域所以静态方法相当于全局锁,会锁所有调用该方法的线程。
- synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块(new Object())。她有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
-
核心组件:
- Wait Set:那些调用wait方法被阻塞的线程被放置在这里。
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中。
- Entry List:Contention List中那些有资格称为候选资源的线程被移动到Entry List中。
- OnDeck:任意时刻,最多只有一个线程正在竞争资源锁,该线程被称为OnDeck。
- Owner:当前已经获取到所有资源的线程被称为Owner。
- !Owner:当前释放锁的线程。
-
实现
- JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
- OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
- 处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
- Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
ReentrantLock 锁
-
ReentrantLock实现接口Lock中的方法,它是一种可重入锁,除了能完成synchronized所能完成的工作以外,还提供了诸如可响应中断锁,可轮询请求,定时锁等避免多线程死锁的方法。
-
Lock接口的主要方法:
public interface Lock { //执行此方法时,如果锁处于空闲状态,当前线程将获取到锁。相反,如果锁已经被其他线程持有,将禁用当前线程直到当前线程获取到锁。 void lock(); //尝试获取锁,如果锁可用则获取锁并返回true,如果锁不可用返回false。 boolean tryLock(); //尝试获取锁并等待一定时间,如果锁可用则获取锁并返回true,如果锁不可用返回false。 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //当前线程释放持有的锁 void unlock(); //返回条件对象,获取等待通知的组件。该组件和当前锁绑定。只有当前线程获取了锁才能调用该组件的await()方法,调用后将释放锁。 Condition newCondition(); }
- 例子:
public class MyService {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void await(){
try {
lock.lock();
System.out.println("锁住了");
condition.await();
System.out.println("等待结束");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
condition.signal();
lock.unlock();
System.out.println("释放锁");
}
}
public void signal(){
try {
lock.lock();
condition.signal();
}finally {
lock.unlock();
}
}
}
main:
public class Demo1App {
public static void main(String[] args) {
MyService myService = new MyService();
for (int i = 0; i < 3; i++) {
new Thread(myService::await).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myService.signal();
}
}
-
ReentrantLock公平锁
-
公平锁指的是锁的分配是公平的,通常先对锁提出获取请求的线程会先分配到锁,ReentrantLock在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
构造方法如下:
public ReentrantLock() { sync = new NonfairSync(); //默认是非公平锁 } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); //传入参数选择是否是公平锁 }
-
-
ReentrantLock非公平锁
- JVM按随机、就近原则分配锁的机制称为不公平锁,ReentrantLock默认就是非公平锁,执行效率高于公平锁。
-
Condition类
- await方法和Object类的wait方法等效
- signal方法和Object类的notify方法等效
- signalAll方法和notifyAll方法等效
- ReentrantLock可以唤醒指定条件的线程,而Objectr唤醒是随机的
Semaphore信号量(互斥锁)
-
Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于这个阈值,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore可以用来构建一些对象池,资源之类的,比如数据库连接池。
-
Semaphore可以用来实现互斥锁:即信号量为1的时候,同一时间只允许一个线程访问
public void doSomething(){ Semaphore semaphore = new Semaphore(1); try { semaphore.acquire(); //同时只有一个线程能够申请到许可 try { //TODO do something }catch (Exception e){ e.printStackTrace(); }finally { semaphore.release();//释放许可 } }catch (InterruptedException e){ e.printStackTrace(); } }
-
Semaphore也可以指定公平锁与非公平锁
Semaphore在构造时传入是否公平锁的选项
public Semaphore(int permits) {
sync = new NonfairSync(permits); //默认非公平锁
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
原子类
-
在多线程中i++或者++i之类的操作不具有原子性,是属于线程不安全的操作之一。通常我们会使用synchronized将该操作变成一个原子操作,当JVM为此类特意提供了一些同步类,使得使用更加方便。
-
在java.util.concurrent.atomic包下,atomic包里面一共提供了13个类,分为4种类型,分别是:
- 原子更新基本类型
- 原子更新数组
- 原子更新引用
- 原子更新属性
-
以AtomicInteger为例
-
部分源码:
public class AtomicInteger extends Number implements java.io.Serializable { //省略字段... private volatile int value; //实际的volatile值(保证了可见性和有序性)所以将其封装保证原子性 public AtomicInteger(int initialValue) { //指定值构造原子类 value = initialValue; } public AtomicInteger() { //无值类 } public final int get() { //获取值 return value; } public final void set(int newValue) { //获取值,若未初始化就是0 value = newValue; } //调用的afe类的实例unsafe的方法,本地方法,不做讨论 public final int getAndSet(int newValue) { //设置新值,并返回之前的值 return unsafe.getAndSetInt(this, valueOffset, newValue); } public final int getAndIncrement() { //值做-1动作,并返回之前的值 return unsafe.getAndAddInt(this, valueOffset, 1); } public final int getAndDecrement() { //值做+1动作,并返回之前的值 return unsafe.getAndAddInt(this, valueOffset, -1); } public final int getAndAdd(int delta) { //将给定值与当前值相加,并返回之前的值 return unsafe.getAndAddInt(this, valueOffset, delta); } public final int incrementAndGet() { //++,并返回+1的值 return unsafe.getAndAddInt(this, valueOffset, 1) + 1; //和上面就相差个+1 } public final int decrementAndGet() { //--,并返回-1的值 return unsafe.getAndAddInt(this, valueOffset, -1) - 1; //和上上面就相差个-1 } public final int addAndGet(int delta) { //将给定值和当前值相加,并返回新值 return unsafe.getAndAddInt(this, valueOffset, delta) + delta; //和getAndAdd相比就是操作之后方法内返回一个+delta的数 } //省略一些方法... }
get在前说明返回旧值,在后返回新值
-
可重入锁(递归锁)
-
在Java下ReentrantLock和synchronized都是可重入锁。
-
指的是同一线程,外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。
公平锁与非公平锁
- 公平锁(Fair)
- 加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得(指锁)。
- 非公平锁(Nonfair)
- 加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
- 非公平锁性能高:因为公平锁需要在多核的情况下维护一个队列。
- Java中的synchronized是非公平锁,ReentrantLock默认也是非公平锁(上面有展示,构造方法中)
ReadWriteLock 读写锁
- 为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。
- 规则:读写互斥,写读互斥,写写互斥,读读同步(只要涉及到写操作就存在竟态资源,需是互斥的)
ReadWriteLock 接口源码:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
共享锁与独占锁
- 独占锁
- 独占模式下,每次只有一个线程能持有锁,ReentrantLock就是以独占的方式实现互斥锁。独占锁是一种悲观保守的加锁策略,因为同时的读读操作并不会影响数据的一致性。
- 共享锁
- 共享锁允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁是一种乐观锁,放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
- ASQ内部类Node定义了两个常量SHARED和EXCUSIVE,他们分别表示AQS队列中等待线程的锁获取模式
- java并发包中提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
锁优化
- 减少锁持有时间
- 只用在有线程安全需求的程序上加锁
- 减少锁粒度
- 将大对象(被许多对象访问),拆成小对象,大大增加并行度,降低锁竞争。这时偏向锁,轻量级锁成功概率才会高。例如(ConcurrentHashMap)
- 锁分离
- 例如ReadWriteLock读写分离锁。
- 读写操作也可以延伸为只要操作互补干扰锁就可以分离。比如LinkedBlockingQueue从头部取出,从尾部放数据。
- 锁粗化
- 通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量段,即在使用完公共资源后,应该立即释放锁。但是,凡是都有个度,如果对一个锁不停地进行请求,同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
- 锁消除
- 在编译时期,如果发现不可能被共享的对象,则可以消除这些对象的锁操作(编码不规范引起的)。
线程上下文切换#
线程的上下文切换,巧妙的利用了时间片轮转的方式,CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一轮任务的状态之后,继续服务下一个任务。任务的状态保存及再加载,这段过程就叫做上下文切换。时间片轮转的方式使得多个任务在同一颗CPU上执行任务变成了可能。
-
线程
- **线程 (thread)是 操作系统 能够进行运算 调度 的最小单位。 它被包含在 进程 之中,是 进程 中的实际运作单位。 **
- 在Linux中线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间和其他的资源的轻量级进程。
-
进程
- 系统进行资源分配和调度的基本单位
-
上下文
- 指某一时间点CPU寄存器和程序计数器的内容
- CPU寄存器存数据
- 程序计数器保存CPU正在执行或者下一条将要被执行的指令地址。(所以上下文就是指数据与指令地址)
- 指某一时间点CPU寄存器和程序计数器的内容
-
PCB-“切换帧”
- 上下文切换可以认为是内核(Kernel)在CPU上对于进程(包括线程)进行切换,上下文切换过程中的信息是保存在进程控制模块(PCB)。PCB被称为“切换帧”,信息一直会保存到CPU的内存中,直到再次被他们使用。
-
上下文切换的活动
- 挂起一个进程,将这个进程在CPU中的状态(上下文)存于内存中某处
- 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复。
- 跳转到程序计数器所在的位置(中断时的代码行),以恢复该进程在晨曦中
-
引起上下文切换的原因
- 当前执行任务的时间片用完之后,CPU正常调度下一个任务。
- 当前执行任务碰到IO阻塞,调度器将此资源挂起,继续下一个任务。
- 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续执行下一个任务。
- 用户代码挂起当前任务,让出CPU时间(yield)
- 硬件中断(poweroff)
任务与线程池#
-
构造一个新的线程开销大,因为这涉及与操作系统的交互。如果你的应用程序中创建了大量的生命周期很短的线程,那么不应该吧每一个任务映射到一个线程,而使用线程池(thread pool)。线程池中包含许多准备运行的线程。为线程池提供一个Runnable,就会有一个线程调用run方法。当run方法退出时,这个线程不会死亡,而是留在线程池中为下一个请求提供服务。
* -
从JDK5开始,吧工作单元与执行机制分离开来。工作单元包括Runnable和Callble,而执行机制由Executor框架提供。
Callable与Future#
callable是一个参数化的类型,只有一个方法表示返回参数类型的计算值。
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Future保存异步计算的结果,可以启动一个计算,将Future对象交给某个线程,然后忘掉它。Future对象的owner在计算完成后就能获得结果。
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning); //尝试取消这个任务,如果任务已经开始,并且mayInterrupt参数值为true,它就会被中断。如果成功执行了操作,则返回true。
boolean isCancelled(); //如果任务在完成之前被取消,则返回true
boolean isDone(); //计算是否正在进行
V get() throws InterruptedException, ExecutionException; //调用get会阻塞直到计算完成
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException; //也会阻塞,但是超时会抛出TimeoutException
}
cancel一个任务涉及到两个步骤,必须找到并中断底层线程。另外任务实现(call方法)必须感知到中断,并放弃他的工作。如果一个Future对象不知道任务在哪个线程中执行,或者如果任务没有监视执行该任务的线程中断状态,那么取消没有任何效果。
线程池#
-
线程池是与工作队列密切相关的,其中在工作队列(Work Queue)中保存了所有等待执行的任务。工作者线程(Worker Thread)从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务
-
我们分别对传统的Java线程使用方式和使用执行器线程池的方式进行比较。
- 串行执行:糟糕的响应性,服务器资源利用率低
//一个串行化服务器 public static void main(String[] args) throws IOException { int port = 9999; ServerSocket serverSocket = new ServerSocket(port); while (true){ Socket accept = serverSocket.accept(); handleRequest(accept); } }
- 为每个任务创建一个线程:线程生命周期的开销大(线程的创建和销毁),资源消耗大(活跃的线程会消耗系统资源),不稳定(可能OOM)
//为每个请求创建一个线程 public static void main(String[] args) throws IOException { int port = 9999; ServerSocket serverSocket = new ServerSocket(port); while (true){ final Socket connection = serverSocket.accept(); //服务器可以同时接收多个请求 new Thread(()->handleRequest(connection)).start(); //任务的处理从主线程中分离出来了,并且可以并行处理,任务处理方法需要是线程安全的 } }
- 使用线程池的方式:
private static final int NTHREADS = 20; //线程池有固定的数量上限 private static final Executor exec = Executors.newFixedThreadPool(NTHREADS); //不会频繁的创建销毁线程 public static void main(String[] args) throws IOException { int port = 9999; ServerSocket serverSocket = new ServerSocket(port); while (true){ final Socket accept = serverSocket.accept(); exec.execute(()->handleRequest(accept)); //任务到达时不需要等到线程的创建而是可以直接执行 } }
这种方式将请求处理任务的提交和任务的实际执行解耦开,并且改变Executor的实现方式就能够改变服务器的行为。
- 自定义执行器实现串行化:
public class SingleThreadExecutor implements Executor { @Override public void execute(Runnable command) { command.run(); } }
- 自定义执行器实现对每一个提交的任务都创建一个新线程
public class ThreadPreTaskExecutor implements Executor { @Override public void execute(Runnable command) { new Thread(command).start(); } }
可以看到改变Executor的实现方式就可以修改服务器任务的执行行为,而任务提交不受影响
-
在开发过程中合理的使用线程池可以为开发带来好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的调用不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
-
线程池的处理流程:
- 主线程往线程池中提交一个任务,线程池判断当前线程池线程数量是否小于corePoolSize(核心线程数),如果小于则新创建一个线程用于执行任务。
- 如果当前核心线程池中的线程数不小于corePoolSize(核心线程数量),则将任务存储在工作队列中。
- 如果任务队列没满就直接将任务加入工作队列中等待线程执行,如果工作队列满了则判断线程池是否满了(当前线程数是否达到最大线程数maximumPoolSize)。
- 如果线程池没满就创建新线程用于执行此任务,如果线程池满了就按照策略处理无法执行的任务。
ThreadPoolExecutor
的execute方法:public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { //当当前工作线程小于核心线程数 if (addWorker(command, true)) //直接新建Worker线程执行任务command return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { //当前线程池在运行并且工作队列未满则将任务放入工作队列 int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); //传入false会和最大线程数量进行比较 } else if (!addWorker(command, false)) //如果工作队列放不下了,并且大于最大线程数量的话就会调用reject() reject(command); //执行饱和策略 }
分析:
reject()方法:
当队列和线程池都满了,说明线程池处于饱和状态,那么必须采用一种策略处理提交的新任务。
final void reject(Runnable command) { handler.rejectedExecution(command, this); } private volatile RejectedExecutionHandler handler; //饱和策略处理器 /* * RejectedExecutionHandler接口中的rejectedExecution方法 * void rejectedExecution(Runnable r, ThreadPoolExecutor executor); */
有四个类实现了饱和策略处理器中的方法:
当前线程池具体策略在创建时确定。
-
AbortPolicy 直接抛出异常
-
public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } }
-
CallerRunsPolicy 只用调用者线程来运行任务
-
public static class CallerRunsPolicy implements RejectedExecutionHandler { public CallerRunsPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } } }
-
DiscardPolicy 不处理,丢弃(摆烂)
-
public static class DiscardPolicy implements RejectedExecutionHandler { public DiscardPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } }
-
DiscardOldestPolicy 丢弃队列中最近的一个任务,并执行当前任务
-
public static class DiscardOldestPolicy implements RejectedExecutionHandler { public DiscardOldestPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } } }
Worker中的run()方法:
线程池中的工作线程如何接收任务并进行处理
- 当execute()创建一个线程时,会让这个线程执行当前任务。
- 这个线程执行完它的第一个任务之后还会不断从阻塞任务队列中不断的取出任务执行。
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { task.run(); //线程池中的线程不断从队列中获取任务并执行 } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }
分析ThreadPoolExecutor的构造方法参数:
public ThreadPoolExecutor(int corePoolSize, //核心线程数量(线程池的基本大小) int maximumPoolSize, //最大线程大小 long keepAliveTime, //线程活动保持时间:线程空闲后保持活动的时间 TimeUnit unit, //线程活动保持时间单位 BlockingQueue<Runnable> workQueue, //任务队列 ThreadFactory threadFactory, //用于创建线程的工厂 RejectedExecutionHandler handler//饱和策略 ) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
饱和策略上面已经介绍过了,这里只介绍几个任务队列
任务队列:
- ArrayBlockingQueue:是一个基于数据结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素尽行排序。(她的锁在构造时初始化)
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,通过ReentrantLock和两个Condition来实现并发安全与阻塞。吞吐量大于ArrayBlockingQueue。Executors.newFixedThreadPool和Executors.newSingleThreadExecutor使用了这个队列。(内部Node有两个常量锁)
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量高于LinkedBlockingQueue,Executors.newCachedThreadPool使用了这个队列。(使用LockSupport.park和LockSupport.unpark阻塞和恢复,无锁)
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。(有锁,构造时初始化)
-
往线程中提交任务:
- 一个是使用execute()方法,提交一个无需返回值的任务。
- 一个是使用submit()方法,提交一个有返回值的任务,返回一个Future对象。
-
关闭线程池:
- 通过shutdown()方法:遍历线程池中的工作线程,将线程池状态设置成SHUTDOWN状态,然后中断没有任务的线程。
- 通过shutdownNow()方法:遍历线程池中的工作线程,将线程池设置成STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表。
调用了这两个方法中的任意一个,isShutdown就会返回true,当所有线程都关闭后调用isTerminaed方法返回true。
使用哪一种方法来关闭线程池取决于提交到线程池中的任务特性决定。
-
合理分配线程池
-
任务的性质:CPU密集型,IO密集型
- CPU密集型任务应该分配尽量小的线程数(如cpu数量+1)。
- IO密集型任务应该分配较可能多的线程数(如cpu数量*2)。
- 使用
Runtime.getRuntime().avaiableProcessors()方法获得当前设备的CPU数
-
任务的优先级:高、中、低。
- 任务有优先级的可以使用
PriorityBlockingQueue
队列 - 如果一直有高优先级的任务加入,那么低优先级的任务可能一直不会执行。
- 任务有优先级的可以使用
-
任务的执行长短:长、中、短。
-
任务的依赖性:是否依赖其他系统资源、如数据库连接。
-
-
线程池监控
可以使用线程池提供的参数对线程池进行监控
- taskCount:线程池需要执行的任务数量
- completedTaskCount:线程池已完成的任务数量,小于等于taskCount
- largestPoolSize:线程池曾经创建过的最大线程数量。如果该数值等于线程池最大大小,则说明线程池满过。
- getPoolSize:当前线程池中的线程数量
- getActiveCount:活动的线程数
Executor框架#
- Executor提供多线程中的执行机制,工作单元包括Runnable和Callable。
- Executor的两级调度模型:在HotSpot VM的线程模型中,Java线程被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程,当Java线程停止时这个操作系统线程也会被回收。操作系统线程会调度所有线程并把他们分配给可用的CPU。
Executor框架的结构(Java多并发执行机制的结构):
- 任务。包括被执行任务需要实现的接口:Runnable或者Callable。
- 任务的执行。包括执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。(关键类:ThreadPoolExecutor,ScheduledThreadPoolExecutor,ForkJoinPool)
- 异步计算的结果。包括接口Future和实现Future的FutureTask类。
任务
- Runnable
@FunctionalInterface
public interface Runnable {
public abstract void run(); //执行任务接口
}
- Callable
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception; //执行并返回结果的接口
}
Executor包含的类的介绍:
Executor是一个接口,它是Executor框架的基础,她将任务的提交和任务的执行解耦。
- Executor源码:
Executor就一个方法需要实现,那就是执行任务方法。
public interface Executor {
void execute(Runnable command); //执行实现了Runnable接口的任务
}
ThreadPoolExecutor
上面对ThreadPoolExecutor的构造方法和其参数进行了说明,这里列举三个可以通过Executors构造的ThreadPoolExecutor线程池。
-
ThreadPoolExecutor
newFixedThreadPool传入指定线程数或者指定线程数和线程工厂。
使用的LinkedBlockingQueue阻塞队列,基于链表结构的阻塞队列。是一个无界队列,所以不会触发饱和策略。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, //构造核心线程数和最大线程数相等的线程池 0L, TimeUnit.MILLISECONDS, //0L表示可被回收的空闲的线程立马被终止 new LinkedBlockingQueue<Runnable>()); } public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); }
-
newSingleThreadExecutor
newSingleThreadExecutor由于是单工作线程所以不需要指定线程数,可以指定线程工厂。
使用的LinkedBlockingQueue阻塞队列,基于链表结构的阻塞队列。是一个无界队列,所以不会触发饱和策略。
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, //指定核心线程和最大都是1 0L, TimeUnit.MILLISECONDS, //0L表示可被回收的空闲的线程立马被终止 new LinkedBlockingQueue<Runnable>())); } public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }
-
newCachedThreadPool
使用SynchronousQueue阻塞队列,不存储元素的阻塞队列。是一个无缓冲等待队列,直接将任务交给线程执行,所以当提交任务速度高于线程处理任务的速度时就会不断创建新进程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU资源。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, //核心线程为0,最大线程为整形的最大值(和系统位数有关) 60L, TimeUnit.SECONDS, //缓存线程池中可被回收的工作线程有60s存活时间 new SynchronousQueue<Runnable>()); } }public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }
allowCoreThreadTimeOut()方法可以设置核心线程在空闲情况下被回收。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承自TreadPoolExecutor。它的主要作用是用来给定的延迟之后执行任务,或者定期执行任务。其功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是后台单线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。
- ScheduledThreadPoolExecutor的几个构造函数:
可以看到使用的队列是DelayedWorkQueue
,DelayedWorkQueue是一个ScheduledThreadPoolExecutor的静态内部类
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), handler);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
-
ScheduledThreadPoolExecutor运行机制
-
当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()或者scheduleWithFixedDelay()方法时,会向延迟工作队列中添加任务。
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (period <= 0) throw new IllegalArgumentException(); ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period)); RunnableScheduledFuture<Void> t = decorateTask(command, sft); sft.outerTask = t; delayedExecute(t); //延迟任务的执行(在这里加入延迟队列) return t; } public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (delay <= 0) throw new IllegalArgumentException(); ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(-delay)); RunnableScheduledFuture<Void> t = decorateTask(command, sft); sft.outerTask = t; delayedExecute(t); //延迟任务的执行(在这里加入延迟队列) return t; } private void delayedExecute(RunnableScheduledFuture<?> task) { if (isShutdown()) reject(task); else { super.getQueue().add(task); //添加进入延迟工作队列 if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) task.cancel(false); else ensurePrestart(); } }
-
JAVA阻塞队列原理#
阻塞队列,在阻塞队列中线程阻塞有这样的两种情况:
- 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
- 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
在阻塞队列不可用时,这两个附加操作提供了4种处理方式
抛出异常:抛出一个异常
特殊值:返回一个特殊值(null或false)
阻塞:调用的函数在完成 操作之前不返回
超时:在时间内阻塞超时则放弃
插入操作
- public abstract boolean add(E paramE):将指定元素插入此队列中(如果可行则不会违反容量限制),成功则返回true,如果当前没有可用的空间,则抛出IllegalStateException异常。如果该元素是NULL,则抛出NUllPointException。
- public abstract boolean offer(E paramE):将指定元素插入此队列中(如果可行则不会违反容量限制),成功时返回true,如果当前没有可用的空间,则返回false。
- public abstract void put(E paramE):将指定元素插入此队列中,将等待可用的空间(如果有必要),如果队列满了则阻塞等待。
- offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内阻塞,还不能往队列中加入BlockingQueue,则返回失败。(就是在上面的无限制等待之上加了个时间限制)
获取数据操作
- poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null。
- poll(long timeout, TimeUnit):指定时间内取位于队首的数据。超时未取得返回失败
- take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据加入。
- drainTo():一次性从BlockingQueue获取所有可用的对象(可指定获取的个数),不需要多次分批加锁或者释放锁。
Java中的阻塞队列
-
ArrayBlockingQueue
- 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了。(基于数组实现)
- 按照FIFO(先进先出)的原则对元素进行排序。
- 默认非公平队列。
-
LinkedBlockingQueue
- LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
- LinkedBlockingQueue 使用两个独立的锁对生产者端和消费者端进行控制,所以并发效率高(并行的操作队列中的数据(同时生产和消费))。
-
PriorityBlockingQueue
- 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
- 实现Comparable 用于排序,因为PriorityBlockingQueue是由优先级的队列。
-
DelayQueue
- 是一个支持延迟获取的无界阻塞队列,使用PriorityQueue实现。元素必须实现Delayed接口,只创建元素时可以指定多久才能从队列中获取当前元素,使用一个线程循环查询DelayQueue,只有在延迟期满时才能从队列中提取元素。
- 可以用于缓存系统设计:一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 可以用于定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如从TimeQueue就是使用DelayQueue实现的。
-
LinkedTransferQueue
-
一个由链表组成的无界阻塞队列。
public interface TransferQueue<E> extends BlockingQueue<E> { // 如果可能,立即将元素转移给等待的消费者。 // 更确切地说,如果存在消费者已经等待接收它(在 take 或 timed poll(long,TimeUnit)poll)中,则立即传送指定的元素,否则返回 false。 boolean tryTransfer(E e); // 将元素转移给消费者,如果需要的话等待。 // 更准确地说,如果存在一个消费者已经等待接收它(在 take 或timed poll(long,TimeUnit)poll)中,则立即传送指定的元素,否则等待直到元素由消费者接收。 void transfer(E e) throws InterruptedException; // 上面方法的基础上设置超时时间 boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException; // 如果至少有一位消费者在等待,则返回 true boolean hasWaitingConsumer(); // 返回等待消费者人数的估计值 int getWaitingConsumerCount(); }
-
-
LinkedBlokingDeque
- 链表组成的双向阻塞队列。可以从队列的两端插入和移出元素,减少了生产者插入和消费者取出的竞争。
CyclicBarrier
-
通过它可以实现一组线程等待至某个状态之后再全部同时执行。
-
public int await():挂起按成,直至所有线程都达到barrier状态再同时执行后续任务。
CountDownLatch
- 用于实现计数器的功能。
Semaphore的用法
- 信号量,控制同时访问的线程个数。
ThreadLocal#
-
public static可以实现线程变量的共享,而线程自己的变量使用ThreadLocal实现(线程隔离的,在Thread中存在一个ThreadLocalMap变量,ThreadLocal可以对其进行操作)
-
ThreadLocal的主要作用是将数据放入当前线程对象中的Map中,这个Map是Thread类的实例变量。ThreadLocal不管理,不存储任何数据它只是数据和Map之间的桥梁,用于将数据放到Map中。执行流程:数据->ThreadLocal->currentThread()->Map
- Map中的Key存储的是ThreadLocal对象,value就是存储的值。每个Thread中的Map值只对当前线程可见,其他线程不可以访问当前线程对象中Map的值。M当前线程Map随当前线程的销毁而销毁(若Map中的数据没有被引用,没有被使用那么随时GC回收)。
- Thread中有个字段:
ThreadLocal.ThreadLocalMap threadLocals = null;
访问级别为包,可以在ThreadLocal中访问到,我们对这个变量进行的操作就是在ThreadLocal中进行的。
-
常见的ThreadLocal使用场景为:解决数据库连接,Session管理等。(因为ThreadLocal用于储存线程数据)
ThreadLocal存取分析:
ThreadLocal.set方法:
public void set(T value) {
Thread t = Thread.currentThread(); //获得当前线程。
ThreadLocalMap map = getMap(t); //从当前线程中获取ThreadLocalMap。
if (map != null) //在这里表示第一次存数据的时候会创建Map,否则直接存入数据。
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) { //获取当前线程的ThreadLocalMap
return t.threadLocals;
}
void createMap(Thread t, T firstValue) { //新建一个ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //这里可以知道table其实就是Entry[]数组类型
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
static class Entry extends WeakReference<ThreadLocal<?>> { //Entry继承了弱引用,所以Map中的Entry数据没有被引用的时候就会被回收
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal.get方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //从entry数组中获取对应的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value; //强转并返回
return result;
}
}
return setInitialValue();
}
private T setInitialValue() { //如果在没有初始值的情况下获取值,就会直接设置一个新值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
InheritableThreadLocal#
但ThreadLoca
l是不能实现值继承的,如果要实现值继承则需要使线程继承InheritableThreadLocal
InheritableThreadLocal源码:
InheritableThreadLocal可以让子线程从父线程中取值
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
研究一下如何实现继承:
Thread中有个字段inheritableThreadLocals,属于父子线程间可继承的变量
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
线程的初始化方法中,如果属于父子线程关系且父线程中inheritableThreadLocals不为空,那么就会将父线程中的inheritableThreadLocals指向的ThreadLocalMap复制一份赋值给子线程inheritableThreadLocals字段。
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//...省略
if (inheritThreadLocals && parent.inheritableThreadLocals != null) //从这里可以知道如果
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//...省略
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value); //
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c; //将父线程中的数据复制到子线程中
size++;
}
}
}
}
synchronized和ReenrantLock的区别#
相同点:
- 都是用来协调线程对共享独享,变量的访问
- 都是可重入锁,同一线程可以多次获得一个锁
- 都保证了可见性和互斥性
不同点
- ReentrantLock显示的获得锁、释放锁,synchronized隐式的释放锁。
- ReentrantLock可响应中断,可轮回,synchronized是不可以响应中断的。
- ReentrantLock是API级别的,synchronized是JVM级别的。
- ReentrantLock可以实现公平锁。
- ReentrantLock通过Condition可以绑定多个条件。
- 底层实现不同,synchronized是同步阻塞,使用的悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略。
- Lock是一个接口,而synchronized是一个关键字。
- synchronized在发生异常时会自动释放锁,不会导致死锁的发生。Lock发生异常时如果没有主动unlock就可能会导致死锁额发生。
- Lock可以让等待锁的线程响应中断,synchronized不行,synchronized会使线程一直等待。
- Lock可以判断线程是否获得了锁。
- Lock可以实现读写锁。
ConcurrentHashMap并发#
-
ConcurrentHashMap减小了锁粒度(即缩减了锁的范围)
-
JDK1.7 ConcurrentHashMap分段锁
- 它内部细分了若干个小的HashMap,称之为段(Segmane)默认情况下一个ConcurrentHashMap被分为16个段,即就是锁的并发度。
- 如果需要在ConcurrentHashMap中添加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该存放在哪个段中,然后对该段加锁,并完成put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。
- JDK7中ConcurrentHashMap是通过ReentrantLock+CAS+分段思想来保证的并发安全的,ConcurrentHashMap的put方法会通过CAS的方式,把一个Segment对象存到Segment数组中,一个Segment内部存在一个HashEntry数组,相当于分段的HashMap,Segment继承了ReentrantLock,每段put开始会加锁。
- 在JDK7的ConcurrentHashMap中,首先有一个Segment数组,存的是Segment对象,Segment相当于一个小HashMap,Segment内部有一个HashEntry的数组,也有扩容的阈值,同时Segment继承了ReentrantLock类,同时在Segment中还提供了put,get等方法,比如Segment的put方法在一开始就会去加锁,加到锁之后才会把key,value存到Segment中去,然后释放锁。同时在ConcurrentHashMap的put方法中,会通过CAS的方式把一个Segment对象存到Segment数组的某个位置中。同时因为一个Segment内部存在一个HashEntry数组,所以和HashMap对比来看,相当于分段了,每段里面是一个小的HashMap,每段共用一把锁,同时在ConcurrentHashMap的构造方法中是可以设置分段的数量的,叫做并发级别concurrencyLevel.
-
JDK1.8 ConcurrentHashMap
- JDK8中ConcurrentHashMap是通过synchronized+cas来实现了。在JDK8中只有一个数组,就是Node数组,Node就是key,value,hashcode封装出来的对象,和HashMap中的Entry一样,在JDK8中通过对Node数组的某个index位置的元素进行同步,达到该index位置的并发安全。同时内部也利用了CAS对数组的某个位置进行并发安全的赋值。
-
JDK8中的ConcurrentHashMap为什么使用synchronized来进行加锁?
-
JDK8中使用synchronized加锁时,是对链表头结点和红黑树根结点来加锁的,而ConcurrentHashMap会保证,数组中某个位置的元素一定是链表的头结点或红黑树的根结点,所以JDK8中的ConcurrentHashMap在对某个桶进行并发安全控制时,只需要使用synchronized对当前那个位置的数组上的元素进行加锁即可,对于每个桶,只有获取到了第一个元素上的锁,才能操作这个桶,不管这个桶是一个链表还是红黑树。
-
想比于JDK7中使用ReentrantLock来加锁,因为JDK7中使用了分段锁,所以对于一个ConcurrentHashMap对象而言,分了几段就得有几个ReentrantLock对象,表示得有对应的几把锁。
-
而JDK8中使用synchronized关键字来加锁就会更节省内存,并且jdk也已经对synchronized的底层工作机制进行了优化,效率更好。
-
putVal源码:
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; //算出了key所在桶的hash for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //获取桶根节点的值 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) { //若节点不为空对添加节点元素的行为进行加锁,使用synchronized持有节点的锁。 } } } //省略。。。 } static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); }
-
-
JDK8的ConcurrentHashMap和JDK7的ConcurrentHashMap有什么区别?
-
JDK8中新增了红黑树
-
JDK7中使用的是头插法,JDK8中使用的是尾插法
-
JDK7中使用了分段锁,而JDK8中没有使用分段锁了
-
JDK7中使用了ReentrantLock,JDK8中没有使用ReentrantLock了,而使用了Synchronized
-
JDK7中的扩容是每个Segment内部进行扩容,不会影响其他Segment,而JDK8中的扩容和HashMap的扩容类似,只不过支持了多线程扩容,并且保证了线程安全
-
-
与HashMap的区别是什么?
- ConcurrentHashMap是HashMap的升级版,HashMap是线程不安全的,而ConcurrentHashMap是线程安全。而其他功能和实现原理和HashMap类似。
-
与Hashtable的区别是什么?
- Hashtable也是线程安全的,但每次要锁住整个结构,并发性低。相比之下,ConcurrentHashMap获取size时才锁整个对象。
- Hashtable对get/put/remove都使用了同步操作。ConcurrentHashMap只对put/remove同步。
- Hashtable是快速失败的,遍历时改变结构会报错ConcurrentModificationException。ConcurrentHashMap是安全失败,允许并发检索和更新。
线程调度#
抢占式调度
- 抢占式调度指的是每条线程执行的时间,线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,每条线程都分同样的执行时间片,也可能是某些线程执行的时间片比较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的竖着不会导致整个进程阻塞。
协同式调度
- 协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交给下一个人,下个人继续跑。线程的执行时间由线程本身控制,线程切换是可以预知的(这种模式下如果某一个线程出现问题就可能导致整个系统崩溃)。
JVM的线程调度实现(抢占式调度)
- Java使用抢占式调度,Java中线程会按优先级分配CPU时间片,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片(只是说获得时间片的可能性大点,这也是Java中线程执行的随机性的体现),也就是优先级高可能执行的时间多点(分配的时间片多点),优先级低执行的时间少点(分配的时间片少点)。
线程让出CPU的情况
- 当前线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片slice的执行权),例如调用yield()方法。(用户线程主动调用)
- 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。(阻塞)
- 当前线程结束,即运行完run()方法里面的任务。(线程消亡)
进程调度算法#
优先调度算法FCFS
- 通过一个
FIFO
队列来管理,当一个进程进入就绪队列之后,他的PCB
控制块会被加到FIFO
队列的尾部. cpu每次 会从FIFO 队列的头部拿去进程来执行。
最短作业优先服务SJF
- FCFS的问题在于,执行时间长的进程再队列最前面,只要将队列的顺序按照执行时间从小到大排序,那么进程的平均等待时间就是最小的。但是进程的执行时间是个未知数,我们无法知道一个进程接下来需要占用多久的cpu。可能造成某些CPU长时间得不到执行
高优先权先调度算法
-
Priority Scheduling 是一个特殊的SJF 算法。优先级通常为固定区间的数字,比如 [0,10].其中数字的大小与优先级高低的关系再不同系统中实现是不一样的,在linux系统中0是最高优先级。
-
优先级调度策略就是给每个进程附上优先级(SJF 就是最短执行时间的优先级最高),优先级调度可以是
抢占式
和非抢占式
的。 -
优先级的定义
-
静态优先级
- 优先级保持不变,可能造成饥饿现象,短进程一直占用着cpu,长进程可能长时间无法获得cpu.
-
动态优先级
-
通过老化(aging) 的方式:多级反馈队列(Multi-level Feedback QUeue,MLFQ)
-
根据进程占用的cpu时间,当进程占用cpu的总时间越长,则慢慢降低他的优先级
-
根据进程再就绪队列等待的时间,当进程再就绪队列等待的事件越长,则慢慢提升他的优先级
-
-
基于时间片轮转调度算法
- 时间片轮转法
- 每个进程都可以得到相同的CPU时间(CPU时间片)。当时间便到达晋城,将为剥夺CPU并加入就绪队列的尾部。因此该调度算法是一个抢占式调度算法。
- 多级反馈队列调度算法
- 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队的优先权逐个降低。优先权越高的队列中,为每个进程所规定的执行时间片就越小。
- 当一个新进程进入内存后,首先将她放入第一个队列的尾部,按FCFS原则排列等待调度。当轮到该进程执行时,如果他能在该时间片内完成,便可准备撤离系统,如果第一个时间片内执行未完成,就放入第二个队列并按FCFS原则排列等待如此往复。
- 第一队列为空的时候,调度程序才会调度第二队列中的进程运行。
- 规定第一个时间片的时间略大于人机交互所需处理的时间时,便能够较好的满足各种用户的需求。
什么是CAS#
-
CAS:Compare And Swap/Set 比较并交换
-
CAS的过程:如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置更新为新值。否则,处理器不做任何操作。
-
AtomicLong的添加操作为例:
public final long getAndAddLong(Object var1, long var2, long var4) { long var6; do { var6 = this.getLongVolatile(var1, var2); //获取期望值(旧值) } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4)); //我们在做加操作时期望值可能被修改,所以我们需要对我们获得时的var6的值与内存中存储的值进行比较,比较再将交换var6与var6+var4,即交换新值和旧值并返回旧值,但如果比较时发现期望值被修改了则进行自旋(再次进入while循环并比较) return var6; }
-
-
CAS优缺点
- 优点
- 非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
-
缺点
-
ABA问题: 线程C、D;线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A。
-
自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源
-
- 优点
什么是AQS#
-
AQS(AbstractQueuedSynchronizer):抽象的队列式同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类都依赖于它 ,例如:ReentrantLock/Semaphore/CountDownLatch。
-
思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
-
AQS维护了一个volatile int state(代表共享资源,强制从公共内存中获取的资源)和一个FIFO线程等待队列(多线程争用资源会被阻塞进入此队列)。
-
其state访问方式有三种:
-
state字段
private volatile int state;
-
getState()
protected final int getState() { return state; }
-
setState()
protected final void setState(int newState) { state = newState; }
-
compareAndSetState()
protected final void setState(int newState) { state = newState; }
-
-
-
AQS定义了两种资源共享方式:
- Exclusive独占资源-ReentrantLock
- 只有一个线程能够执行,如ReentrantLock
- Share共享资源-Semaphore/CountDownLatch
- Share共享资源多个线程能够同时执行,如Semaphore/CountDownLatch
- Exclusive独占资源-ReentrantLock
-
AQS只是一个框架,AQS这里只定义了一个接口具体资源的获取/释放方式交由自定义同步器去实现,并且自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。
- 实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
- 自定义同步器实现的时候主要实现下面几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- 一般来说自定义同步器要么是独占方法,要么是共享方法,只需实现独占或者共享方法中的一种即可
- 但AQS支持同时实现两种方式,如ReentrantReadWriteLock。
-
同步器的实现是ABS核心(state资源状态计数)
-
例如ReentrantLock,state初始化为0,表示未锁定状态。
-
A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlocck()到state=0(即释放锁)为止,其他线程才有机会获取该锁。并且获取该锁是可重入的(state会类加)。
-
FairSync中的tryAcquire:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //获取state if (c == 0) { //若等于0则获取锁 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { //可重入 int nextc = c + acquires; //state进行类加操作 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); //state类加后赋值 return true; } return false; //如果state不为0且该线程并未持有返回false。 }
-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了