创建线程的常用的几种方式:
新建状态(New):新创建了一个线程对象。
就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作 废的方法。
使用interrupt方法中断线程。
名称 | 解释 |
---|---|
public void interrupt() | 该方法只是设置当前线程的中断状态为true,发起一个协商而不会立刻停止线程 |
public static boolean interrupted() | 返回当前线程的中断状态,将当前线程的中断状态清空并重新设置为false |
public boolean interrupted() | 判断当前线程是否被中断,通过检查中断标志位 |
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的 效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start() 方法才会启动新线程。
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线 程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所 以把他们定义在Object类中因为锁属于对象。
用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockInterrupt {
static Lock lockA = new ReentrantLock();
static Lock lockB = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 线程 1:先获取 lockA 再获取 lockB
Thread t1 = new Thread(() -> {
try {
// 先获取 LockA
lockA.lockInterruptibly();
// 休眠 10 毫秒
TimeUnit.MILLISECONDS.sleep(100);
// 获取 LockB
lockB.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("响应中断指令");
} finally {
// 释放锁
lockA.unlock();
lockB.unlock();
System.out.println("线程 1 执行完成。");
}
});
// 线程 2:先获取 lockB 再获取 lockA
Thread t2 = new Thread(() -> {
try {
// 先获取 LockB
lockB.lockInterruptibly();
// 休眠 10 毫秒
TimeUnit.MILLISECONDS.sleep(100);
// 获取 LockA
lockA.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("响应中断指令");
} finally {
// 释放锁
lockB.unlock();
lockA.unlock();
System.out.println("线程 2 执行完成。");
}
});
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(1);
// 线程1:执行中断
t1.interrupt();
}
}
复制代码
底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
底层基于AQS+CAS+LockSupport锁实现
AQS抽象的队列同步器,是整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型的变量表示持有锁的状态;当有线程获取不到锁时,就将线程加入该队列中,通过CAS自旋和LockSupport.part()的方式,维护state变量,达到并发同步的效果。
AQS使用一个violatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要抢占资源的线程封装成一个Node节点来完成锁的分配,通过CAS完成对state值的修改;核心就是state+CLH带头节点的双端队列
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法 而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可 能在进入到暂停状态后马上又被执行。
ThreadPoolExecutor 最多包含以下七个参数:
ThreadPoolExecutor有如下常用方法:
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了 Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些 方法。
shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。
并不会立即创建核心线程,而是等到有任务提交时才会开始创建线程,除非调用了prestartCoreThread/prestartAllCoreThreads 事先启动核心线程。
prestartCoreThread: 启动一个核心线程,使其空闲等待工作。这会覆盖仅在执行新任务时启动核心线程的默认策略。如果所有核心线程都已启动,此方法将返回 false。如果线程已启动,则返回 true。
public boolean prestartCoreThread() {
return workerCountOf(ctl.get()) < corePoolSize &&
addWorker(null, true);
}
复制代码
prestartAllCoreThreads:启动所有核心线程,使它们空闲等待工作。这会覆盖仅在执行新任务时启动核心线程的默认策略。返回启动的线程数。
public int prestartAllCoreThreads() {
int n = 0;
while (addWorker(null, true))
++n;
return n;
}
复制代码
其实核心线程只是一个动态概念,在jdk中并没有给线程打上"core"标记。而在jdk1.6之前,线程池会尽量保证会有corePoolSize个线程存活,即使这些线程已经闲置了很长的时间,这样会造成一部分资源浪费;于是在1.6开始,jdk提供了一个allowCoreThreadTimeOut方法用于控制核心线程是否被销毁。
注意: 这种策略和corePoolSize=0是有区别的
综合来看,其实corePoolSize=0的效果基本等同于allowsCoreThreadTimeOut=true&&corePoolSize=1,只是实现细节不同。
分为CPU密集型和IO密集型
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。
keepAliveTime这个参数1.6之前控制的是非核心线程的存活时间,且该参数值不能小于0,否则在创建线程池时会抛出异常。而设置为0的含义其实是指非核心线程执行完属于自己的任务后即刻销毁。从1.6开始,若 allowsCoreThreadTimeOut=true,则keepAliveTime必须大于0,否则也会报错。
主要有4种拒绝策略:
上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是 多任务操作系统和多线程环境的基本特征。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称 作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。
如果有使用自定义线程池,那么使用的就是自定义线程池,没有则使用的是默认的ForkJoinPool线程池
如果在下一步使用的thenRunAsync方法,且没有传入自定义的线程池,就使用的是默认的,传入线程池就使用自定义的线程池。
其他thenApply和thenAccept亦是如此。thenRun使用的是上一步的线程池,如果上一步执行的太快,也会用main线程,thenRunAsync使用的是默认线程池
System.out.println(CompletableFuture.supplyAsync(() -> "returnA").thenRun(() -> {
// 执行异步任务,没有上一步的结果也没有返回值
}).join());
System.out.println(CompletableFuture.supplyAsync(() -> "returnA").thenApply(res -> {
// 执行异步任务,有上一步的结果也有返回值
return res + " aaa";
}).join());
System.out.println(CompletableFuture.supplyAsync(() -> "returnA").thenAccept(res -> {
// 执行异步任务,有上一步的结果,但没有返回值
System.out.println(res);
}).join());
复制代码
线程之间的通信有两种方式:共享内存和消息传递。
共享内存 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来 隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。
例如线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:
消息传递 在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行 通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify() ,或者 BlockingQueue 。
悲观锁: 就是默认认为在本线程使用该系统资源的时候就一定会有别的线程来进行争抢,就默认加锁。适合写多读少的场景,先加锁可以保证写操作是数据正确
乐观锁: 认为自己再使用数据的时候不会有别的线程来修改数据或资源,所以不会加锁。只是在更新数据的时候去判断一下有没有别的线程更新了这个数据,如果这个数据没有被更新,就将当前线程的数据写入,如果有更新,则根据不同的实现方式来执行不同的操作,比如放弃修改,重试抢锁等,适合读多写少的情况
判断规则:
Java中每一个对象都继承了Object类,Object类中有一个叫ObjectMonitor.java的监视器,java的底层是c++,在ObjectMonitor.java中又调用了ObjectMonitor.cpp,在ObjectMonitor.cpp中又调用了ObjectMonitor.hpp,在ObjectMonitor.hpp文件中有很多属性,比如_owner属性,就指向持有ObjectMonitor对象的线程
ObjectMonitor.hpp中的构造方法中的属性
属性 | 解释 |
---|---|
_owner | 指向持有ObjectMonitor对象的线程 |
_WaitSet | 存放处于wait状态的线程队列 |
_EntryList | 存在处于等待锁block状态的线程队列 |
_recursions | 锁的重入次数 |
_count | 用来记录该线程获取锁的次数 |
管程就是锁,在虚拟机中monitor使用的是ObjectMonitor实现,每一个对象都天生带有一个对象监视器,每一个被锁住的对象都活和Monitor相关
公平锁: 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人在队尾排队,这是公平的ReentrantLock reentrantLock = new ReentrantLock(true); true表示公平锁,先来先得
非公平锁: 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程有限获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
如果在业务场景上需要提高吞吐量的话,建议使用非公平锁,因为可以节省线程之间的切换时间,否则就使用公平锁,大家公平使用
是指同一个线程在外层方法获取锁的时候,在进入改线程的内层方法会自动获取锁(前提,锁的是同一个对象),不会因为之前已经获取过锁还没有释放而阻塞。
例如:synchronized修饰的一个递归方法,程序在第二次调用的时候不需要再次去获取锁,避免自己阻塞自己,所以在Java中ReentrantLock 和synchronized都是可重入锁,这样的主要是在一定程度上避免了死锁
当执行monitorenter时,如果目标锁对象的计数器(_count)为0,那么就代表该锁没有被别的线程所持有,然后Java虚拟机就会吧该锁的持有线程设置为当前线程,并把计数器加1
如果计数器不为0的时候,就会去判断该锁的owner属性锁指向的线程是不是当前线程,是的话就把计数器( count,recursions)加1,否则需要等待,直至持有线程释放该锁
当执行monitorexit时,java虚拟机就会把改锁的计数器(count),可重入recursions减去1,计数器为0的时候就代表锁已经被释放
用于创建锁和其他同步类的基本线程阻塞原语。本质上是一个线程阻塞的工具类,所有的方法是今天方法,LockSupport调用的是Unsafe中的native方法,可以在任意位置唤醒线程,其中提供可一个许可证的概念,但许可证只有一个,不会累加,可以通过park方法使其阻塞,unpark方法为某一个线程发放通行证
因为unpark就已经为该线程发放了一张通行证,在调用park方法的时候回去检查该线程有没有通行证,有就直接放行,没有则阻塞
因为凭证的数量始终只有一个,在park两次之后凭证不够两次消费,所以不能放行
举一个例子,就相当于i++这样的操作来说,在多线程的环境下,每个线程一开始都获得了i的值放回到自己的工作内存中之后,需要对i进行计算,然后再进行赋值,当在这个计算的时间中,有别的线程速度比较快,计算完成之后写入主内存,然后发出通知,当前线程收到通知的时候本次计算没有完成就又重新读取,重新计算,所以会导致计算丢失。也就是说在对一个值的取值,计算,复制的这三个步骤来说没有保证原子性,所以volatile也不具备原子性
综上所述,volatile变量不适合参加到依赖当前值的运算,如i=i+1;i++等的之类的,所以我们通常用volatile保存某个状态的boolean值或者int值
当我们用volatile修饰一个变量的时候,在他的class文件中我们可以发现该变量被加上了一个标签,也就是ACC_VOLATILE,然后吧字节码生成机器码的时候,就会按照JMM的规范,在相应位置插入内存屏障;
是一种屏障指令,它是的CPU或者编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束,也叫做内存栅栏或者栅栏指令
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。底层调用Unsafe类中的方法
虽然指令重排提高了并发的性能,但是Java虚拟机会对指令重排做出一些规则限制,并不能让所有 的指令都随意的改变执行位置,主要有以下几点:
LongAdder的基本思路就是分散热点,将value的值分散到一个Cell数组中,不同的线程会命中不同的Cell,各个现在只对自己槽中的那个值进行Cas操作,这样热点就被分散了,冲突的概率小很多,想要获得真正的long值,只要将各个槽中的变量值累加返回就行。
sum()方法会将Cell数组中的value和base累加作为返回值。
核心思想就是将AtomicLong的一个value的更新压力分散到多个vulue中去
简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了, ReentrantLock、CountDownLatch、Semaphore等等都用到了它。
AQS实际上以双向队列的形式 连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队 列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可 以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数, 可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待, 等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中 传入的int型整数n=1,相当于变成了一个synchronized了。
ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部 副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。 ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组, Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对 的能力。
弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无 法被回收,弱引用则会在下一次GC的时候被回收。 但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key 为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。 但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出 现这个问题的
Object o = new Object();
复制代码
对于我们new一个新的对象,这个对象在java的堆存储的内存布局可分为,对象头,实例数据,对齐填充(不够8的倍数,就填充)
锁的对象要是同一个,而不是多个对象
假如方法中收尾相接,而且前后相邻的同步代码块都是锁的统一个对象,JIT编译器就会把这几个同步块合成一个大块,加粗加大范围,一次申请使用即可,避免每次的申请和释放锁,提升了性能