本文是菜鸟学习笔记,如您发现错误,请指出,谢谢~
1、CPU中包含哪些结构?
首先从计算机原理回忆,一颗单核CPU 一般包含AUL(Arithmetic Logic Unit运算器:用于计算机运行过程中的算数运算、逻辑运算等),寄存器(存储二进制代码,AUL从寄存器中读取值进行运算),PC(程序计数器,记录下一条要执行的指令)等等
2、为什么要设计多级缓存?
AUL从寄存器中读取数据的速度是远超过从内存中读取的(小于1ns 和 约 80ns ,大约差了100倍),为了进一步提升CPU 的效率,避免因从内存中读取造成的时间浪费,在寄存器和内存之间增加了cache缓存;目前一般用的都是三级缓存,其中L1和L2 缓存在CPU核的内部,L3在CPU的内部,供多核CPU的不同核之间共享数据;
3、多级缓存的读取是什么样的?
缓存是一级一级取出的,假如我去遍历一个数组arr[];从L1缓存中取arr[0]的时候没有,逐层去找,一直在内存中才找到,那继续取arr[1]在L1缓存中扔没有,还是要像刚刚一样取一次,就会浪费大量的时间,解决办法就是,每次都会读取一定长度的数据,称作缓存行(64位 64*8bit),这样就会大大提高读取效率。(MySQL中的分页查询也是类似的)
4、什么是MESI协议?
在多核工作的时候,如果CPU核1 和 核 2 同时修改某个缓存行的数据,就会引发多线程问题;
MESI (Modified-Exclusive-Shared-Invalid)协议是一种广为使用的缓存一致性协议, x86处理器所使用的缓存一致性协议就是基于MESI协议的。
MESI协议对内存数据访问的控制类似读写锁,它使得针对同一地址的读内存操作是并发的,而针对同一地址的写内存操作是独占的,即针对同一内存地址进行的写操作在任意一个时刻只能够由一个处理器执行。 在MESI协议中, 一个处理器往内存中写数据时必须持有该数据的所有权。
更多MESI 可以参考:深入学习缓存一致性问题和缓存一致性协议MESI(一) - 掘金
5、缓存行读取机制会带来什么问题?
多线程并发的目的是保证安全性的前提下尽可能的提升效率,刚刚说64位的缓存行是高速缓存读取的基本单位,如果两个CPU核同时需要修改一个缓存行中的数据,根据MESI协议,缓存行的目的状态会在多核之间会不断的标记,其实是会降低效率的;有没有好的处理办法呢?
disruptor 中的 RingBuffer 给了一个比较好的思路,即:在 缓存的前后各增加7个Long 类型的无意义数据进行占位,空间换时间,保证一个缓存行不会被多个核同时进行修改。这样就不会频繁的更换缓存行的标记,造成时间上的浪费了;
(因为图片上传问题,仅用文字描述自己的理解,详细的笔记可以参考以下链接:
提升--04---并发编程之---有序性---volatile两大作用_高高for 循环的博客-CSDN博客)
6、指令重排序造成的问题:
如图,前三种是未发生指令重排序的时候,多并发下 xy 最后可能的值,如果发生了指令重排序,那么就会出现第四种情况。
7、指令重拍造成的this溢出问题:
首先了解一下Java中new一个对象分为以下几步,汇编码如下:
new的时候开辟一块内存空间,私有变量m赋值为默认值为0;invokespecial 的时候调用初始化方法,将m赋值为8;astore_1 将t 和 m=8 关联起来;
下图就是一个this 溢出问题的代码,在构造器中new了一个线程,打印this.num,如果在创建对象的时候,invokespecial 指令和 astore_1 指令发生了重排序,那么打印出来的值就不是num=8,而是num=0;
8、DCL (Double check lock )双重检查锁 需不需要volatile 关键字呢?
答案是需要的,首先要理解synchronized关键字可以保证原子性和可见性,但是没有办法保证顺序性,因此有可能发生指令重排序;如上说的new 对象过程中发生了指令重排序,如果一个线程在synchronized 锁里面执行到一半时,失去了时间片,刚好此时 astore_1 执行完,对象指向了还还未初始化完毕的对象(已经不为null了,但是还没有完全完成初始化),这个时候其他线程拿到时间片,外层判空的时候发现不为null,就会直接返回 线程1初始化一半的对象;
那么既然已经说了synchronized关键是保证原子性的,那么原子里面的代码执行一半也能被其他线程使用吗?这里就要明确一个问题,上锁代码和不上锁代码能不能同时执行?答案是可以的!因此,DCL是需要加volatile关键字的!(volatile关键字 两种作用:保证可见性 和 禁止指令重排序)
更详细的图解可以参考:对DCL(双重检查锁 Double Check Lock)及Volatile原理的理解_少年啊!的博客-CSDN博客
DCL 的双重检查代码:
public static T01 getInstance(){
//业务代码
//.......
if(INSTANCE==null){
synchronized (T01.class){
if(INSTANCE==null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new T01();
}
}
}
return INSTANCE;
}
9、volatile 是怎么保证指令执行的顺序呢?
首先jvm级别,识别到volatile关键词,会执行jvm内存屏障,包括 loadload 屏障、storestore 屏障、loadstore屏障、storeload 屏障(其中load是读,store是写),
jvm识别到volatile关键词后,:
a) 会在写之前加 storestore,写之后加storeload,保证在写之前完成其他的写,在自己写完之后才能继续其他的读
b) 会在读之后加上loadload 和 loadstore ,保证在自己读完之后其他的才能读,自己读完之后,其他的才能写
10、启动线程的三种方式
- 继承 Thread 类 ,重写run 方法,调用 start() 方法开启一个新的线程;注意,强调要调用start方法开启新的线程,如果调用run方法,只是普通的方法调用
- 实现一个Runnable 接口,重写run 方法,先实例化一个实现runnable接口的MyThread2实例,然后用Thread创建一个实例,然后在调用start方法;
- 实现Callable接口,重写call 方法,可以有返回值,返回值用泛型表示,使用futureTask实例的get方法可以得到线程的返回值;(Future接口表示异步任务,是一个可能没有完成的异步任务结果,所以说Callable用于产生结果,Future用于接收结果。)
- 使用线程池(后续介绍)
package JUC;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 继承 Thread 类 ,重写run 方法,调用 start() 方法开启一个新的线程;
* ****注意,是要调用start方法开启新的线程,如果调用run方法,只是普通的方法调用****
*/
public class TestThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 第一种方法
MyThread1 myThread1 = new MyThread1();
myThread1.start();
// 第二种方法
MyThread2 myThread2 = new MyThread2();
Thread thread = new Thread(myThread2);
thread.start();
// 第三种方法
MyThread3<String> myThread3 = new MyThread3<>();
FutureTask<String> futureTask = new FutureTask<>(myThread3);
Thread thread1 = new Thread(futureTask);
thread1.start();
String s = (String) futureTask.get();
System.out.println(s);
}
}
/**
* Thread 类本质上是实现了 Runnable 接口的一个实例, start()方法是一个 native 方法
*/
class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("MyThread1 is running!");
}
}
/**
* 实现一个Runnable 接口的时候,需要按照以下步骤开启一个新的线程
* 1、先实例化一个实现runnable接口的MyThread2实例,然后用Thread创建一个实例,然后调用start方法;
*/
class MyThread2 implements Runnable {
@Override
public void run() {
System.out.println("MyThread2 is running !");
}
}
/**
* 实现Callable接口,可以有返回值,返回值用泛型表示
* 实现Callable接口开启线程方式如下:
* MyThread3<String> myThread3 = new MyThread3<>();
* FutureTask<String> futureTask = new FutureTask<>(myThread3);
* Thread thread1 = new Thread(futureTask);
* thread1.start();
* <p>
* 使用futureTask实例的get方法可以得到线程的返回值;
*
* @param <String>
*/
class MyThread3<String> implements Callable<String> {
@Override
public String call() throws Exception {
String string = (String) "MyThread3 is running!";
return string;
}
}
10.1 run()方法和start()方法有和区别?
每个线程都是通过某个特定的Thread对象对应的run()方法来完成其操作的,run方法称为线程体,通过调用Thread类的start方法来启动一个线程。
start()方法用于启动线程,run()方法用于执行线程的运行代码,run()可以反复调用,而start()方法只能被调用一次。
start()方法来启动一个线程,真正实现了多线程的运行。调用start()方法无需等待run()方法体代码执行结束,可以直接继续执行其它的代码;调用start()方法线程进入就绪状态,随时等该CPU的调度,然后可以通过Thread调用run()方法来让其进入运行状态,run()方法运行结束,此线程终止,然后CPU再调度其它线程。
10.2 为什么调用start()方法会执行run()方法,为什么不能直接调用run()方法
这是一个常问的面试题,new Thread,线程进入了新建的状态,start方法的作用是使线程进入就绪的状态,当分配到时间片后就可以运行了。start方法会执行线程前的相应准备工作,然后在执行run方法运行线程体,这才是真正的多线程工作。
如果直接执行了run方法,run方法会被当作一个main线程下的普通方法执行,并不会在某个线程中去执行它,所以这并不是多线程工作。
小结:调用start方法启动线程可使线程进入就绪状态,等待运行;run方法只是thread的一个普通方法调用,还是在主线程里执行。
11、自旋锁和系统锁的适用场景?
线程执行时间短(加锁的代码)、线程数目少的时候,适合自旋锁,相反的,执行时间长,线程数目多,适合用系统锁(重量级锁)。
12、synchronized的使用方法
synchronized : 锁的是对象(不能用string常量),而不是代码; 如果没有写对象,默认是 this (XX.class);锁定方法和非锁定方法时可以同时执行的。
为什么不能用String常量?有可能你和别人写的代码锁定的是同一个对象。
用对象做锁的时候,如果对象变成了另外一个对象,则加锁就会出问题,因此,一般定义锁对象的时候,需要加上 final 关键字 : final Object o = new Object();
13、池化思想
首先说一下池化思想:为了得到最大的收益,最小的风险,将资源统一在一起进行管理的思想; 一般是对对象的池化,集中比较典型的池化策略:
1、内存池:预先申请内存,提高内存申请速度,减少内存碎片
2、连接池:预先申请数据量连接,提示连接速度,降低系统开销
3、线程池:预先申请线程,重复利用线程资源,任务到达时无需等待线程的创建而立即执行
4、字符串常量池
5、实例池:循环使用对象,减少资源在初始化和释放时的损耗
可以看到,使用池化思想有以下优点:
1、提升效率:因为资源都是预先分配的,可以大大提高使用时的效率
2、降低资源消耗:由于资源可以重复利用,便可以降低系统的资源消耗
3、提升资源的可管理性:资源统一到一起进行管理,可以避免某些应用无限制申请资源导致资源的分配失衡,从而可以提高系统的稳定性。
4、对于线程池而言,还可以控制最大并发数量(后续会讲到)
14、为什么使用线程池?
为什么使用线程池?肯定是为了提高资源的利用率,那么如果不使用线程池的话,线程的使用是什么样子的呢?:1、手动创建线程对象,2、执行任务 3、任务执行完毕,手动释放线程对象;想要执行一个任务首先要创建一个线程对象,执行完了还得把这个线程对象释放掉,可以想象这个效率是非常低的,而使用线程池呢,线程对象可以重复利用,就会大大增加使用线程的效率;
15、线程池的执行流程
线程池的基本思想就是预先创建若干个线程对象放在缓冲池里面,有任务需要执行时,从缓冲池中拿出一个线程使用,任务执行完毕后在把使用的线程还到缓冲池中,这样这些线程对象就可以不断地重复使用了。
比如一个线程池中我们规定创建3个线程,那么当任务过来时,就可以从线程池里面拿到预先创建好的线程进行使用,使用完毕后,返回线程池;(只要是任务不是一下子并发过来,这种情况下,我们三个线程就可以处理很多个任务,大大提升了资源利用率)
刚刚提到的内容中,有个前提条件,任务不是同时进来的,那么如果我一下子进来四个任务,而线程池里面只有三个线程该怎么办呢?这种情况下,多出来的线程就会进去等待队列进行等待,其他任务执行完毕返回线程后,等待队列中的任务在拿到线程执行任务;
那么如果等待队列也满了怎么办呢?比如说等待队列的长度设置成3个,加上可以直接使用的三个线程,供可以处理6个任务,如果一下子来了7个任务怎么办呢? 这种情况下,线程池会自动创建新的线程(非核心线程)供任务使用,此时,四个任务同时执行,三个任务在等待队列 (代码试验是最后一个任务会直接使用新创建的线程,而不是从队列中拿出等待的任务,实现的是非公平锁)
线程池创建额外的新线程也不是没有数量限制的,线程池中规定了允许创建的最大线程数量,假设最大数量为5,加上等待队列的三个,可以处理的任务最多为8个,如果同时来了9个任务怎么办呢?线程池一般会实现规定一个处理策略,如抛出异常等
如下代码为创建线程池的一种方法,ThreadPoolExecutor()入参的含义结合上述描述进行理解。
public class TestThreadPool {
public static void main(String[] args) {
/**
* 共有七个参数
* corePoolSize : 核心线程数
* maximumPoolSize :线程池中允许的最大线程数
* keepAliveTime:线程数目大于核心线程数时,多余空闲线程在终止前等待新任务的最长时间
* unit:保存时间的单位
* workQueue :用于任务执行前保存任务的队列
* threadFactory :执行器创建新线程的工厂 Executors.defaultThreadFactory() 默认工厂
* handler :由于达到线程边界和队列容量而阻止执行时使用的处理程序 new ThreadPoolExecutor.AbortPolicy()抛出异常
*/
ExecutorService executorService = new ThreadPoolExecutor(3, 5, 1L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 7; i++) {
int finalI = i;
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "===>办理业务" + finalI);
});
}
executorService.shutdown();
}
}
16、线程池的一些标识属性,有哪些状态?
// 声明了当前线程池的状态,声明了线程池的线程数 高3位是线程池状态,低29位是线程池的线程个数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 值为29,方便后面做位运算
private static final int COUNT_BITS = Integer.SIZE - 3; // 值为29,方便后面做位运算
//位运算得出最大容量,1左移29位然后减一,值为 0001 1111 1111 1111 1111 1111 1111 1111
//刚刚说了高三为为线程池状态,因此后29位就是最大线程池个数
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
// 1110 0000 0000 0000 000 0000 000 0000 正常接收新任务
private static final int RUNNING = -1 << COUNT_BITS;
// 000 不接受新任务,但是正常进行的任务和阻塞队列的任务还是会处理 调用shutdown 方法
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 001 线程池为 stop状态,不接受新任务,也不会去处理阻塞内部的任务,同时中断正在执行的任务, shutdownNow 方法
private static final int STOP = 1 << COUNT_BITS;
// 010 线程池为过渡的状态,当前线程池即将销毁
private static final int TIDYING = 2 << COUNT_BITS;
// 011 线程池为销毁状态
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
// 得到线程池的状态
private static int runStateOf(int c) {
return c & ~CAPACITY;
}
// 得到当前线程池的线程数量 -- 正在工作的线程数量
private static int workerCountOf(int c) {
return c & CAPACITY;
}
17 、excute()方法源码
public void execute(Runnable command) {
if (command == null) {
throw new NullPointerException();
}
// 得到32位的int类型
int c = ctl.get();
// 判断工作线程数是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 可以创建核心线程数 (说明线程池刚创建时,线程数为0,只有拿到任务时才回去创建)
if (addWorker(command, true)) {
return; // 创建OK,返回执行
}
// 创建失败,重新获取ctl (if里面没有锁,如果两个线程同时进入创建,会有一个失败,需要重新获取ctl)
c = ctl.get();
}
// 判断是不是running状态,将任务添加到阻塞队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次判断是否为running状态,如果不是,就移除任务
if (!isRunning(recheck) && remove(command)) {
reject(command);
}
// 如果是running状态,但是工作线程为0,阻塞队列里面有任务
else if (workerCountOf(recheck) == 0) {
addWorker(null, false); // 创建一个任务为空的线程,处理阻塞队列中的任务
}
}
// 创建非核心线程处理任务,如果失败,则执行居家策略
else if (!addWorker(command, false)) {
reject(command);
}
}
18 、 addWorker()方法源码
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
// 经过大量的判断 将工作线程数的标识+1
for (; ; ) {
int c = ctl.get();
int rs = runStateOf(c); // 获取线程池状态
// Check if queue empty only if necessary.
// >= SHUTDOWN 说明不是running状态 + !(shutdown状态,传入的任务为null,阻塞队列不为空)
if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty())) {
// 满足if的条件,构建工作线程失败 ---> 1 rs 为STOP或者更高的状态,不需要创建线程处理
// 2 任务为空,不需要处理
// 3 阻塞队列为空,也不需要处理
return false;
}
for (; ; ) {
// 获取工作线程个数
int wc = workerCountOf(c);
// 线程数大于最大容量 或者 线程数是否超过核心线程或者最大线程数 ---> 不需要创建线程,返回false
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) {
return false;
}
// CAS的方法将工作线程加1,成功就退出外侧的for循环
if (compareAndIncrementWorkerCount(c)) {
break retry;
}
// CAS 失败, 从新获取ctl
c = ctl.get(); // Re-read ctl
// 重新判断状态,如果状态变了,继续下次外侧循环 ,如果状态没有变,重新执行内部循环
if (runStateOf(c) != rs) {
continue retry;
}
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 创建worker,将任务传入worker对象
w = new Worker(firstTask);
// 从worker中获取线程t
final Thread t = w.thread;
if (t != null) {
// 获取线程池全局锁,避免添加任务时,其他线程把线程池干掉了
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
// 线程池状态为 running 或者 shutdown状态但是任务为null (处理阻塞队列中的任务)
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable 是否是运行状态
{
throw new IllegalThreadStateException();
}
// 将线程添加到集合中, private final HashSet<Worker> workers
workers.add(w);
int s = workers.size();
if (s > largestPoolSize) { // 工作线程数大于之前记录的最大数,就替换一下
largestPoolSize = s;
}
// 添加工作线程成功的标记
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start(); // 启动工作线程, 标记启动成功
workerStarted = true;
}
}
} finally {
if (!workerStarted) {
// 启动工作线程失败,调用这个方法,删除之前添加的工作线程和线程数目减一
addWorkerFailed(w);
}
}
// 返回工作是否启动
return workerStarted;
}
19、 w = new Worker(firstTask)时,调用了 runWorker()方法, 那么runWorker() 的源码是什么样的呢?
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 任务不为空,任务为空时,通过getTask()从阻塞队列中获取任务
while (task != null || (task = getTask()) != null) {
w.lock();
// 如果ctl 大于等于stop ... 将线程终端
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);
}
}
20、什么是CAS
compare and swap ,比较然后交换。
那么CAS是如何进行工作的呢?可以通过一个通俗的例子进行介绍: 假设现在有一个更衣室,一次只允许一个人进入,更衣室的门上有一个牌子,我们规定:牌子的值为0的时候,表示可以进入,牌子的值为1的时候,不能进入。 现在小明和小红都想去更衣室换衣服,他们在远处看到更衣室牌子为0,里面没有人,因此两个人都跑过去想要使用更衣室,但是小明跑的比小红快,在小红刚到的时候,小明已经把牌子翻成了1,小红就没办法进去了,但是他不会直接放弃不用这个更衣室了,就出去继续逛衣服,时不时的返回来看看更衣室是不是空的可以进入(自旋)。小红出去逛了10圈,都逛累了(自旋时CPU消耗巨大),回来看房间还是不可用,就干脆不逛了,老老实实排队(升级为重量级锁,jdk1.6之前。),当然,也可以根据逛商店的人多少自己决定逛大圈还是逛小圈,而不是一种圈逛十次(自适应自旋锁1.6以后)。
通过以上描述,我们应该可以比较容易理解CAS了,多线程并发时,每个线程都会生成两个值,old value 和 new value,old value 值标识最初读到的共享资源状态值,假设A线程运气较好,率先获得CPU时间片,他会将当前资源状态值和 old value 进行compare比较,如果一直,则改成new value (swap),而B运气不好,发现old value 和当前状态不一样,就无法更改,改为自旋。
看一下CAS实现的C++代码,我们发现好像cas函数本身没有实现任何的同步措施,是不是涉及不完善呢? 其实CAS是CPU的原语操作,他一定是原子性的,上述所有的假设都是基于原子性的操作,没有CAS的原子性,也就没有之前的一切设想了。
int cas(long *addr, long oldValue, long newValue)
{
/* Executes atomically. */
if(*addr != old)
return 0;
*addr = new;
return 1;
}
21、CAS 中的 ABA问题:
假设现在 有1 和2 两个线程,贡献资源为A,他们两个线程都想把A改成B,根据之前对CAS的描述理解,这两个线程应该是一个更改成功,另外一个更改失败。
现在假设以下场景,线程1 获得CPU时间片,此时线程2在自旋阻塞状态,线程1 将A更改成B,并释放资源,这个时候线程3过来,他想把共享资源从B改成A,此时2还在阻塞状态,线程3直接获得时间片,将B改成A,释放共享资源;然后线程2醒了,去比较的时候发现刚好和old value 相等,于是,就又把A改成B。
如何解决上述的ABA问题呢,只要加上版本号即可,每次变化的时候,版本号加1 ,即便值没变,但是版本号肯定会变的,这样比较的时候就会不一样了,即解决了ABA问题。
22、并发中的重中之重:AQS AbstractQueuedSynchronizer 抽象的,内部存在排队的 同步器
AQS采用了 volatile +CAS的机制来实现的;
AQS有一个成员属性state,这个就是我们之前所说的CAS中用于判断共享资源是否被占用的标记位,volatile 修饰则保证了其线程之间的可见性。那为什么这个值是用int修饰而不是boolean呢?boolean来标识资源被占用是或否,语义更加明确,内存也更小。 这是因为线程获取锁其实有两种模式,独占锁和共享锁,如果一个线程以独占模式获取锁的时候,其他线程必须等待,用boolean类型其实也是可以的,但是如果一个线程以共享模式获取锁的时候,其他也想以共享模式获取锁的线程也是可以一起访问共享资源的,说明在共享模式下,可能会有多个线程在共享同一个资源,所以需要用int类型来标识线程的占用数量。
private volatile int state;
AQS中还定义了两个Node 类型的节点,见名知意,一个头节点,一个尾节点。AQS中是存在一个队列用于对等待的线程进程管理的,这个队列是一个FIFO双向链表。
private transient volatile Node head;
private transient volatile Node tail;
那么Node 是如何定义的呢?且看如下Node 这个内部类的定义。
可以看到Node 中主要存储了节点在队列中的等待状态(waitStatus),前后指针,线程对象(thread)等属性信息。其中waitStatus这个属性是一个枚举值,包括了以下几个状态 :
0:节点初始化默认值或者节点已经释放锁
1:canclled 表示当前节点获取锁的请求已经取消了
-1:signal 表示当前节点的后续节点需要被唤醒
-2:condition 表示当前节点在等待某一个 condition对象,和条件模式相关
-3:propagate 传递共享模式下锁释放状态,和共享模式相关
Node中的方法也很简单,除了构造器之外,predecessor()就是获取前置节点。
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
我们可以想象在实际应用中,会有以下两种场景 :1、尝试获取锁,不管有没有获取到,立刻返回;2、必须获取锁,如果当时锁被其他线程占用,则进行等待。 AQS中就有两种方法对应了这两种场景 ,分别是tryAcquire() 和 acquire()方法。
首先先看tryAcquire()方法:这个方法是被protected修饰的方法,参数值是一个int,代表对 int state 的增加操作,返回值是boolean,代表是否成功获得锁。这个方法的内部只抛出了一个异常,说明AQS其实规定这个类必须要被override的,不然就会抛出异常,为什么一定要上层自己实现呢?因此尝试获取锁这个操作可能会包含一些特点的业务逻辑,比如是否支持“可重入”等。
如果上层调用tryAcquire返回true,线程获得锁,这时就可以对相应的资源进行操作,结束后释放锁,如果返回false,并且不想等待的话,就可以自己进行相应处理,如果选择等待,那么就可以直接调用acquire方法。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
复杂的acquire方法是怎么样的呢?这个方法是final 修饰的,说明是不允许被重写的。if中的判断包括了两个方面:
1、!tryAcquire(int arg) 刚刚已经说过了,如果获得锁,直接返回即可
2、acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ,这个是个嵌套方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先看一下addWaiter(Node.EXCLUSIVE) 方法,这个方法的作用就是讲当前的线程封装成一个Node然后加入等待队列,返回值就是这个node。
一起看一下实现:
先创建了一个node对象,获取当前队列的尾节点,作为当前节点的前置节点。先进行判断,如果前置节点不为空,就把当前节点插到尾节点,然后返回当前节点。内层if里面条件是原子性的,但是内层代码块里面并不是原子操作,这个会不会有问题呢?(留到后面慢慢理解-。-)
如果当前队列没有尾节点,就进入入队方法,enq(node), 这个方法里面是对队列进行初始化并不断通过自旋的方式尝试将节点插入到队列中。(其实 addWaiter方法里面的尝试入队列 和 enq 方法中的else 分支代码是一样的,可能是因为效率提升吧)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
23、并发编程的三要素(线程安全的保证)
- 原子性:指一个或多个操作要么全部执行成功,要么全部执行失败。(JDK Atomic开头的原子类、synchronized、LOCK)
- 可见性: 一个线程对共享变量的修改,另外一个线程也可以看到(synchronized,volatile)
- 有序性:程序的执行顺序按照代码的先后顺序 (Happens-Before 规则)
24、并发、并行和串行
- 并发:多个任务在一个CPU上执行,按照细分的时间片轮流交替执行,由于时间很短,看上去像是同时进行的
- 并行:单位时间内,多个处理器或者多核处理器同时处理多个任务,真正意义上的同时进行
- 串行:有多个任务,由一个线程按照顺序执行。
25、线程和进程
- 进程:内存中运行的程序,每个进程都有自己独立的内存空间,一个进程可以有多个线程,例如在Windows中,一个 .exe 就是一个进程
- 线程:进程中的一个控制单元,复杂当前进程中的程序的执行,一个进程至少有一个线程,也可以有多个线程,这些线程可以共享数据。
- 根本区别:进程是操作系统分配资源的基本单元,线程是处理器任务调度和执行的基本单元
- 资源开销:每个进程都有自己独立的代码和内存空间(程序上下文),程序之间的切换有较大的开销;线程可以看做是轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的程序计数器和运行栈,线程之间的切换开销较小
26、用户线程和守护线程
用户线程:运行在前台,执行具体的任务,如程序的主线程,连接网络的子线程等都是用户线程
守护线程(daemon):运行在后台,为其他前台线程服务,一旦所有线程都执行结束,守护线程就会随着JVM一起结束运行。
27、线程死锁
死锁是指两个或以上线程在执行的过程中,由于竞争资源或者彼此通信造成的阻塞现象,在无外力的作用下,都无法推进,此时系统就处于死锁状态。
比如线程1获得锁后执行线程2,线程2又拿到了锁,但是线程2依赖于线程1执行完后才释放锁,就出现了死锁
28、形成死锁的四个必要条件?
互斥:一个资源只能被一个线程占用,直到该线程释放这个资源
请求与保持:一个线程因请求被占有资源发生堵塞时,对获得的资源保持不释放
不剥夺条件:线程已经获取的资源在未使用完之前不能被其他线程强行剥夺,只能等自己使用完才释放
循环等待:发生死锁时,所等待的线程必定形成一个环路,死循环造成永久阻塞
29、如何避免死锁?
破坏上述死锁的四个必要条件之一即可:
互斥条件:无法破坏,因为锁本身就是要让线程之间产生互斥
破坏请求与保持条件:一次申请所有的资源
破坏不剥夺条件:占有部分资源的线程尝试申请其他资源申请不到的时候,可以主动释放占有的资源
破坏循环等待条件:按照顺序来申请资源
30、什么是上下文切换?
当前任务的CPU时间片使用完,CPU时间片切换到另外一个任务之前会保存自己的状态,以便下次在切换回这个任务的时候可以继续执行下去,任务从保存到再次加载执行就是一次上下文切换。
31、线程声明周期的6种状态
- 新创建:又称初始化状态,这个时候Thread才刚刚被new出来,还没有被启动。
- 可运行状态:表示已经调用Thread的start方法启动了,随时等待CPU的调度,此状态又被称为就绪状态。
- 锁堵塞状态:当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态
- 无限等待状态:一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒
- 计时状态:调用sleep(参数)或wait(参数)后线程进入计时状态,睡眠时间到了或wait时间到了,再或者其它线程调用notify并获取到锁之后开始进入可运行状态。另一种情况,其它线程调用notify没有获取到锁或者wait时间到没有获取到锁时,进入堵塞状态。
- 被终止:死亡状态,表示已经正常执行完线程体run()中的方法了或者因为没有捕获的异常而终止run()方法了。
public enum State {
NEW, // 新创建,还没有调用start
RUNNABLE, // 可运行状态
BLOCKED, // 阻塞
WAITING, // 等待
TIMED_WAITING,//计时等待
TERMINATED;// 被终止
}
32、针对阻塞状态详解
阻塞状态指的是代码不继续执行,而是在等待,阻塞状态解除后,重新进入就绪状态。因此,阻塞状态肯定是从运行状态变过来的,阻塞的方法有以下几种:
- sleep()方法 -静态,是占用资源在睡觉的,可以限制等待多久;
- Thread.sleep(500); // 表示当前线程阻塞500毫秒
- 可以模拟网络延迟,倒计时等等
- sleep时间到了之后线程进入就绪状态
- sleep存在异常:InterruptException
- wait() 方法,和 sleep() 的不同之处在于,是不占用资源的,可以限制等待多久;
- sleep()是Thread类下面的静态方法,而wait()方法是Object类下的非静态方法
- sleep()不释放锁,wait()释放锁
- sleep()常用于暂停执行,wait()长用于线程之间的通信
- sleep()方法调用后,线程经过设定的时间后就会自动苏醒,而wait()不会,当然wait()也可以通过传入参数使其苏醒wait(参数),但是苏醒后会释放锁,如果没有获取到锁就进入阻塞状态。
- wait() 方法和 notify()或者notifyAll() 搭配,一个等待,一个唤醒,如生产者消费者模型
- notify():唤醒一个处于等待状态的线程(无线等待或计时等待),如果多个线程在等待,并不能确切的唤醒一个线程,与JVM确定唤醒那个线程,与其优先级有关。
- notityAll():唤醒所有处于等待状态的线程,但是并不是将对象的锁给所有的线程,而是让它们去竞争,谁先获取到锁,谁先进入就绪状态。
- yield()方法- 静态。礼让线程,让当前正在执行的线程暂停,不阻塞线程,而是直接将线程从运行状态转变为就绪状态,让CPU调度器重新调用;这个方法无法保证达到线程切换的效果,因为CPU可能再次调用当前线程;
- join() 方法,插队线程,阻塞其他的线程,待此线程执行完之后,再执行其他的线程。
- join 的最重要的部分,就是插队可以保证,这个线程自己一定会先执行完,这是在很多地方需要的逻辑。
- 有些 IO 阻塞,比如 write() 或者 read() ,因为IO方法是通过操作系统调用的。
33、Java用到的调度算法是什么?
有两种调度模型:分时调度和抢占式调度
- 分时调度:就是让所有的线程轮流获得CPU的使用权,并且平均分配到各个线程占有CPU的时间片。
- 抢占式调度:Java虚拟机采用抢占式调度模型,是指优先让线程池中优先级高的线程首先占用CPU,如果线程池中优先级相同,那么随机选择一个线程,使其占有CPU,处于这个状态的CPU会一直运行,优先级高的分的CPU的时间片相对会多一点。
- Java线程调度策略采用抢占式调度;
34、 什么是线程调度(Thread Scheduler)和时间分片(Time Slicing )
线程调度是一个操作系统服务,它负责为储在Runnable状态的线程分配CPU时间片,一旦我们创建一个线程并启动它,它的执行便依赖线程调度器的实现。
时间分片是指CPU可用时间分配给Runnable的过程,分配的时间和线程优先级或线程等待时间有关。
35、为什么线程通信方法wait(),notify(),notifyAll()要被定义到Object类中?
java中的任何对象都可以当做锁对象,锁是对象级别的而不是线程级别的,任何对象都继承于Object类,所以定义在Object类中最合适;(有人会说,既然是线程放弃对象锁,那也可以把wait()放到Thread类中,新定义线程继承Thread类,也无需重新定义wait(),然而,这样做有一个很大的问题,因为一个线程可以持有多把锁,你放弃一个线程时,到底要放弃哪把锁,当然了这种设计不能不能实现,只是管理起来比较麻烦。)
36、为什么wait(),notify(),notifyAll()必须在同步方法/代码块中调用?
- 在JAVA中,所有的对象都能够被作为‘监视器monitor’——指的是一个拥有一个独占锁,一个入口队列,和一个等待队列的实体entity。
- 对象的非同步方法能在任意时刻被任意线程调用
- 对象的同步方法在任意时刻有且仅有一个拥有这个对象独占锁的线程能够调用他们,如一个线程想要调用某个对象的同步方法时,如果这个独占锁被其他线程占有,那么当前线程将处于阻塞状态。
- 只有调用线程拥有对象的独占锁时,才能够调用该对象的wait(),notify(),notifyAll()方法,如果在未获取对象锁的时候尝试调用这个三个方法,会抛出一个异常:java.lang.IllegalMonitorStateException:current thread not owner;
- 一个线程在某个对象的同步方法中运行时调用了对象的wait()方法,那么这个线程将释放这个对象的独占锁并被放入这个对象的等待队列;注意,
wait()
方法强制当前线程释放对象锁。这意味着在调用某对象的wait()
方法之前,当前线程必须已经获得该对象的锁。 - 当某线程调用某对象的
notify()
或notifyAll()
方法时,任意一个(对于notify()
)或者所有(对于notifyAll()
)在该对象的等待队列中的线程,将被转移到该对象的入口队列。接着这些队列(译者注:可能只有一个)将竞争该对象的锁,最终获得锁的线程继续执行。 如果没有线程在该对象的等待队列中等待获得锁,那么notify()
和notifyAll()
将不起任何作用。在调用对象的notify()
和notifyAll()
方法之前,调用线程必须已经得到该对象的锁。 - 调用
wait()
方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。 - 调用
notify()
或notifyAll()
方法的原因通常是,调用线程希望告诉其他等待中的线程:“特殊状态已经被设置”。 这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
37、 如何停止一个正在运行的线程?
- stop方法;----过期弃用
- interrupt方法--- 终止线程
- run方法执行结束 ---- 正常退出
38、如何在两个线程之间共享数据?
在两个线程之间共享变量就可以实现共享数据,一般来说,共享变量要求变量本身是线程安全的,然后在线程中对变量使用
39、同步代码块和同步方法怎么选?
- 一般而言,选择同步代码块要比同步方法要好;
- 要清楚需要同步的区域,如果整个方法都需要同步,两种其实是一样的,但是大多数情况下,我们仅需要某一部分是同步的即可;
- 同步代码块中,我们可以自由的选择任何一个对象实例作为锁,但是同步方法就只能是这个对象的实例,代码块自由定义锁,可以避免多个同步实例方法彼此之间的影响
- JVM层面不同
方法:
public synchronized void save(){
}
代码块
synchronized(object){
}
40、什么是线程安全?Servlet是线程安全吗?
线程安全是指某个方法在多线程的环境下被调用时,能够正确处理多线程之间的共享变量,能程序能够正确完成。
Servlet不是线程安全的,它是单实例多线程的,当多个线程同时访问一个方法时,不能保证共享变量是安全的。
Struts2是多实例多线程的,线程安全,每个请求过来都会new一个新的action分配这个请求,请求完成后销毁。
springMVC的controller和Servlet一样,属性单实例多线程的,不能保证共享变量是安全的。
Struts2好处是不用考虑线程安全问题,springMVC和Servlet需要考虑。
如果想既可以提升性能又可以不能管理多个对象的话建议使用ThreadLocal来处理多线程
41、 线程的构造方法,静态块是被哪个线程类调用的
线程的构造方法,静态块是被哪个线程类调用的?
该线程在哪个类中被new出来,就是在哪个被哪个类调用,而run方法是线程类自身调用的。
例子:mian函数中new Thread2,Thread2中new Thread1
- thread1线程的构造方法,静态块是thread2线程调用的,run方法是thread1调用的。
- thread2线程的构造方法,静态块是main线程调用的,run方法是thread2调用的。
42、线程同步和线程互斥的区别
- 线程同步:当一个线程对共享数据进行操作的时候,在没有完成相关操作时,不允许其它的线程来打断它,否则就会破坏数据的完整性,必然会引起错误信息,这就是线程同步。
- 线程互斥:而线程互斥是站在共享资源的角度上看问题,例如某个共享资源规定,在某个时刻只能一个线程来访问我,其它线程只能等待,知道占有的资源者释放该资源,线程互斥可以看作是一种特殊的线程同步。
- 实现线程同步的方法:
- 同步代码块:sychronized(对象){} 块
- 同步方法:sychronized修饰的方法
- 使用重入锁实现线程同步:reentrantlock类的锁又互斥功能,Lock lock = new ReentrantLock(); Lock对象的lock和unlock为其加锁
43、你对线程优先级有什么理解?
每个线程都具有优先级的,一般来说,高优先级的在线程调度时会具有优先被调用权。我们可以自定义线程的优先级,但这并不能保证高优先级又在低优先级前被调用,只是说概率有点大。
线程优先级是1-10,1代表最低,10代表最高。
Java的线程优先级调度会委托操作系统来完成,所以与具体的操作系统优先级也有关,所以如非特别需要,一般不去修改优先级。