Java并发编程
1.JavaSE多线程基础回顾
什么是线程?进程和线程的区别
- 线程是进程的执行单元,线程是操作系统能够进行运算调度的最小单位。
- 一个进程可拥有多个线程,而一个线程只能拥有一个父进程。
- 线程可独享自己的堆栈、程序计数器和局部变量;但线程必须与其父进程的其他线程共享代码段、数据段、堆空间等系统资源。
多线程创建的方式
我们计算机上的多线程是操作系统自动创建的,那么在java中我们要如何创建线程来让多个任务同时执行呢?
方法一
1.定义一个类继承Thread类
2.覆盖Thread类中的run方法
3.创建子类对象就是创建了一个线程
4.调用start方法,开启一个线程 (start 做两件事:开启线程、调用run方法)
解释:Thread 类是用于描述线程的类,run方法是Thread类中描述线程任务的方法。
用线程调用run方法和start方法有什么区别?
现在假设我们有一个类Demo 继承了Thread 类并且复写了它的run方法。现在如果我们创建了demo1对象。那么demo1.run实际上是没有开启线程的,仅仅是main线程中的一个对象调用了它自己方法而已。只有使用demo1.start方法,才会开启一个线程,同时执行run方法,多个线程同时执行。
方法二
1.定义类实现Runnable接口
2.覆盖接口中的run方法,将线程的任务代码封装到run方法中
3.通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递
4.调用线程对象的start方法开启线程
方法三实现Callable接口
采用方法二和方法三有什么好处
- 避免单继承的局限性
- 第二种方式仅仅是将任务封装成了对象,不用创建一个子类去具有父类的所有方法,更加明确。(将任务单独封装成了对象)
Runnable和Callable的区别
- Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
- Call方法可以抛出异常,run方法不可以。
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
线程状态
新建状态(New):当使用new关键字创建一个线程对象后。
就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
阻塞状态(Blocked):由于某种原因线程放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
注意要会描述下面这张图

线程的控制
- sleep方法是Thread类的一个静态方法,其作用是使运行中的线程暂时停止指定的毫秒数,从而该线程进入阻塞状态并让出处理器,将执行的机会让给其他线程。但是这个过程中监控状态始终保持,当sleep的时间到了之后线程会自动恢复。
- wait是Object类的方法,它是用来实现线程同步的。当调用某个对象的wait方法后,当前线程会被阻塞并释放同步锁,直到其他线程调用了该对象的notify方法或者notifyAll方法来唤醒该线程。所以wait方法和notify(或notifyAll)应当成对出现以保证线程间的协调运行。
- yield方法容易与sleep方法混淆,请务必牢记,yield的作用是让当前的线程暂停,但不会像sleep那样阻塞该线程,而是使该线程进入就绪状态。
线程的同步
方法一:同步代码块
同步代码块的语法格式如下:
synchronized(obj){
//需要同步的代码块
}
参数obj是一个引用类型的对象,也就是同步监视器。同步监视器obj可以是任何引用类型的对象,线程在开始执行同步代码块之前必须先获得对同步监视器的锁定。任何时刻只能有一条线程获得对同步监视器的锁定,当同步代码块执行完毕后,该线程自然释放对该同步监视器的锁定。
方法二:同步方法
所谓同步方法就是使用关键字synchronized来修饰某个方法。与同步代码块不同的是,使用同步方法不需要显式地指定同步监视器,即obj对象,因为同步方法中默认的同步监视器是this,也就是该对象本身。
方法三:同步锁
JDK1.5之后,Java又提供了同步锁机制。使用同步锁机制对临界资源保护时需要定义个Lock类型的对象,通常使用Lock的子类ReentrantLock(可重入锁)的对象来进行加锁和释放锁的操作。使用同步锁机制的语法格式如下:
private final ReentrantLock lock = new ReentrantLock();
lock.lock(); //加锁
try{
//访问临界资源,需要保证线程安全的代码
}
finally{
lock.unlock(); //释放锁
}
可以看到,访问临界资源的代码被放到了try块中,释放锁的操作被放到了finally块中。虽然这不是必需的,但是还是建议使用try...finally...块来确保同步锁一定会被释放,以避免死锁的发生。
线程调度
即系统为线程分配处理器使用权的过程。
| 协同式线程调度 | 抢占式线程调度 | |
|---|---|---|
| 控制权 | 线程本身(线程执行完后,主动通知系统切换) | 系统决定 |
| 优点 | 1.切换操作线程已知,控制简单2.不存在线程同步问题 | 线程执行时间可控,不会因为一个线程耽误整个进程 |
| 缺点 | 执行时间不可控,一个线程可能耽误整个进程 | 1.切换控制复杂2.存在线程同步问题 |
2.synchronized和ReentrantLock 的区别
1. 两者都是可重入锁
可重入锁:重入锁,也叫做递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁,比如: 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
2. synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
- synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的
- ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)
3. ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
- 等待可中断.通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
- ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”
4. 使用选择
- 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。
- synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放
3.synchronized关键字的底层原理(基础)以及跟lock锁之间的区别?
使用了synchronized关键字,在底层编译后的jvm指令中,会有monitorenter和monitorexit两个指令
那么monitorenter指令执行的时候会干什么呢?
每个对象都有一个关联的monitor,比如一个对象实例就有一个monitor,一个类的Class对象也有一个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁
他里面的原理和思路大概是这样的,monitor里面有一个计数器,从0开始的。如果一个线程要获取monitor的锁,就看看他的计数器是不是0,如果是0的话,那么说明没人获取锁,他就可以获取锁了,然后对计数器加1
这个monitor的锁是支持重入加锁的,什么意思呢,好比下面的代码片段
加一次锁monitor计数器就会加一 释放一次就会减一 一直到0表示锁为可以获取的状态
synchronized(myObject){
//一大堆代码
synchronized(myObject){
}
}
加锁,一般来说都是必须对一个对象进行加锁
如果一个线程第一次synchronized那里,获取到了myObject对象的monitor的锁,计数器加1,然后第二次synchronized那里,会再次获取myObject对象的monitor的锁,这个就是重入加锁了,然后计数器会再次加1,变成2
这个时候,其他的线程在第一次synchronized那里,会发现说myObject对象的monitor锁的计数器是大于0的,意味着被别人加锁了,然后此时线程就会进入block阻塞状态,什么都干不了,就是等着获取锁
接着如果出了synchronized修饰的代码片段的范围,就会有一个monitorexit的指令,在底层。此时获取锁的线程就会对那个对象的monitor的计数器减1,如果有多次重入加锁就会对应多次减1,直到最后,计数器是0

然后后面block住阻塞的线程,会再次尝试获取锁,但是只有一个线程可以获取到锁
synchronized和lock的区别

4.聊聊你对CAS的理解以及底层实现原理?
上一个讲解的非常好的CAS博客地址:什么是CAS机制
下面是我对CAS的理解和总结:Compare And Swap(比较和交换)
首先说一说CAS能解决的问题。我们都知道当多个线程对同一个数据进行操作的时候,如果没有同步就会产生线程安全问题。为了解决线程线程安全问题,我们需要加上同步代码块,操作,如加上synchronized。但是某些情况下这并不是最优选择。
synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。这个过程是一个串行的过程,效率很低。
尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过渡,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”。
而原子操作类的底层正是用到了“CAS机制”。
CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。(具体实现详细的见上面的博客中介绍)
从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
说了这么多,CAS是否是完美的呢,答案也是否定的。下面是说一说CAS的缺点:
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3) ABA问题
这是CAS机制最大的问题所在。
5.ConcurrentHashMap实现线程安全的底层原理是什么?
ConcurrentHashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树是为了提高查找效率。
jdk1.7:Segment+HashEntry来进行实现的;
jdk1.8:摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的
首先聊一聊ConcurrentHashMap存在的必要性,即它能解决的问题。在编程中我们常常要对一个hashMap进行多个线程的操作,这个时候为了避免线程安全问题,我们就要给她加上同步。但是这个时候又会有新的问题产生。
我们知道hashMap的底层实现实际上是数组
多个线程过来,线程1要put的位置是数组[5],线程二要put的位置是[21]
synchronized(map({
map.put(xxx,xxx)
}
我们可以看到向两个不同的位置添加元素,也被锁管理了,这明显是没有必要的,会造成效率低下。我们需要解决这个问题。JDK并发包里推出了ConcurrentHashMap,默认实现了线程的安全性。
下面聊一聊,它是如何实现的。
在JDK 1.7版本,它的实现方式是分段加锁,将HashMap在底层的数组分段成几个小数组,然后给每个数组分别加锁。
JDK1.8以及之后,做了一些优化和改进,锁粒度的细化。
这里仍然是一个大的数组,数组中每个元素进行put都是有一个不同的锁,刚开始进行put的时候,如果两个线程都是在数组[5]这个位置进行put,这个时候,对数组[5]这个位置进行put的时候,采取的是CAS策略。同一时间,只有一个线程能成功执行CAS,其他线程都会失败。这就实现了分段加锁的第一步,如果很多个线程对数组中不同位置的元素进行操作,大家是互相不会影响的。
如果多个线程对同一个位置进行操作,CAS失败的线程,就会在这个位置基于链表+红黑树来进行处理,synchronized([5]),进行加锁。
综上所述,JDK1.8之后,只有对相同位置的元素操作,才会加锁实行串行化操作,对不同位置进行操作是并发执行的。
6.你对JDK中的AQS理解吗?AQS的实现原理是什么?
多个线程同时访问一个共享数据可以用synchronized,CAS,ConcurrentHashMap。同时也可以用本小节学的Lock,它的底层基于AQS技术。Abstract Queue Synchronizer,抽象队列同步器
在创建锁时候 可以创建公平锁和非公平锁
创建非公平锁
ReentrantLock lock = new ReentrantLock();//非公平锁
创建公平锁
ReentrantLock lock = new ReentrantLock(true);//公平锁
lock.lock();
lock.unlock();
非公平锁,当线程1结束运行释放锁以后,它去唤醒等待队列中的线程2,但是还没等线程2 CAS成功,这时候冒出来一个线程3插队,优先实现加锁,线程2 CAS失败,继续等待。
公平锁,按照上述场景,当线程3在来时会进行判断等待队列中是否还有线程,如果有它就不能插队,会进入等待队列中排队。

6.说说线程池的底层工作原理?

首先说一说为什么要有线程池(优点)。
系统是不可能频繁的创建线程又销毁线程的,这样会非常影响性能,所以我们需要线程池。
ExecutorService threadPool = Executors.newFixedThreadPool(3);//corePoolSize=3
threadPool.submit(new Callable<>() {
@Override
public Object call() throws Exception {
return null;
}
});
执行原理:( 创建线程池时,线程池里面是没有线程的。)提交任务后,会首先判断线程池中的线程的数量是否小于corePoolSize(也就是上面的3),如果小于,就会创建一个线程来执行这个任务。
当任务来时,先判断线程池里面是否有空闲线程,有空闲线程则提交任务,直到当线程数量等于corePoolSize,无则在队列阻塞等待,(如果阻塞队列满了,那就创建新的线程执行当前任务,直到线程池中的线程数达到maxPoolSize,这时再有任务来,由饱和(拒绝)策略来处理提交的任务)
7.说说线程池的核心配置参数是干什么的?应该怎么用?
当我们调用上一节的函数生成fixed线程池的时候
ExecutorService threadPool = Executors.newFixedThreadPool(3);
它的底层执行的代码如下
return new ThreadPoolExecutor(
nThreads,//corePoolSize
nThreads,//maximumPoolSize(当线程到达corePoolSize,且等待队列满 则可继续创建直到=max...,当任务执行完后,等待设定的等待时间,此期间若无任务提交 则会销毁)
long keepAliceTime,//表示等待的时间
TimeUint.MiLLISECONDS,//代表keepAliceTime(等待时间)单位为毫秒
new LinkedBlockingQueue<Runnable>(n),//设置线程池放任务的等待队列的个数为n
RejectedExecutionHandler handler); //饱和策略
上面几个的参数分别是,corePoolSize,maximumPoolSize,keepAliveTime,queue这几个东西,如果你不用fixed之类的线程池,完全可以使用这个构造函数创造自己的线程池。
corePoolSize:3
maximumPoolSize:200
keepAliveTime:60s
new ArrayBlockQueue<Runnable>(200) //这是一个有界队列
如果我们把queue创建成有界队列,假设corePoolSize所有线程都在繁忙的工作,这个时候仍然有大量的任务进入队列,队列满了,此时怎么办?
这个时候,如果你的maximumPoolSize是比corePoolSize大的,此时线程池就会继续创建额外的线程放入线程池中,来处理这些任务。这些额外创建的线程如果处理完了一个任务也会尝试从队列中获取任务来执行。线程池总共可以创建的线程的数量就是maximumPoolSize
线程池的队列满了会发生什么?
但是还有一种情况,如果任务非常多,额外线程全部创建完了,队列还是满的,此时还是有新的任务来,又该怎么办?
此时只能reject掉,有几种不同的reject策略,可以传入RejectedExecutionHandler
(1)AbortPolicy (2)DiscardPolicy (3)DiscardOldestPolicy (4) CallerRunsPolicy (5) 自定义
如果后续慢慢没有任务了,额外创建的线程出去空闲状态,那么线程会等待最大存活时间,如果在这个时间内没有获取新的任务,它就会销毁。实际上maximumPoolSize就是起到一个缓冲的作用。
综上所述,如果定制自己的线程池,要考虑到corePoolSize的数量,队列类型,最大线程数量,拒绝策略,还有线程释放的时间。
特别补充:我们常用的fixedThreadPool是无界队列,maximumPoolSize 和 corePoolSize是一样的。队列永远不会满。或者我们采取有界对列,可以将maximumPoolSize设置的很大,来缓冲。
8.谈谈你对Java内存模型的理解?
read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)
- lock(锁定):作用于主内存中的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存中的变量,它把一个变量从主内存传输到线程的工作内存中。
- load(载入):作用于工作内存中的变量,它把read操作读取的值放入工作内存的变量副本中
- use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时都会执行这个操作。
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时都会执行这个操作。
- store(存储):作用于工作内存中的变量,它把一个变量的值传递到主内存中。
- write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
多个线程之间是不能互相传递数据通信的,它们之间的沟通只能通过共享变量来进行。Java内存模型(JMM)规定了jvm有主内存,所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该条线程所使用到的变量在主内存中的副本。线程的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。当线程操作某个对象时,执行顺序如下: (1) 从主存复制变量到当前工作内存 (read and load) (2) 执行代码,改变共享变量值 (use and assign) (3) 用工作内存数据刷新主存相关内容 (store and write)
例如下面代码:两个线程均对data操作,data本身为0,当线程1操作时,要先read主内存中data值然后load加载到工作内存中,然后执行代码use,改变共享变量值assign使工作内存中data变为1,然后store 最后写入主内存。
public class HelloWord(){
private int data = 0;
public void increment(){
data++;
}
}
HelloWorld helloWorld = new HelloWorld();
//线程1
new Thread(){
public void run(){
helloWorld.increment();
}
}.start();
//线程2
new Thread(){
public void run(){
helloWorld.increment();
}
}.start();
上面这段代码在内存中的过程如下图所示。

9.你知道Java内存模型中的原子性、有序性、可见性是什么吗?
连环炮:Java内存模型 > 原子性、可见性、有序性 > volatile > happens-before/内存屏障
可见性 就是如果有多个线程对一个数据进行操作时,如果一个线程成功修改了数据,那么其他线程能够立即更新工作内存中的该数据,即随时保持最新数据状态。这就叫有可见性,反之没有可见性。(如上个例子当线程1更新data=1,后立刻强制更新线程2获取data值为更新后的值)
原子性 就是当有一个线程在对内存中的某个数据进行操作的时候,必须要等这个线程完全操作结束后,其他线程才能够操作,这就是原子性。反之就是没有原子性,多线程默认是没有原子性的,需要我们通过各种方式来实现原子性,如同步等等。(lock、unlock之间的内存操作具备原子性)
有序性 就是代码的顺序应该和指令的顺序相同。在执行过程中不会发生指令重排,这就是有序性,反之就是没有有序性。
10.能从Java底层角度聊聊volatile关键字的原理吗?
volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
如果直接问volatile关键字,想要解释清楚的话,要从Java内存模型开始讲起。
volatile关键字是用来解决可见性和有序性问题,在有些罕见的条件下,可以有限的保证原子性。 但是它主要不是用来保证原子性的。
volatile保证可见性的原理,如果多个线程操作一个被volatile修饰的变量,当其中一个线程成功对组内存中的数据完成修改以后,它会将其他线程工作内存中的该变量的数据设为失效状态,迫使其它线程重新从主内存中读取变量数据,从而实现有可见性。
在很多的开源中间件系统的源码里,大量的使用了volatile。常常使用的一个场景是对于一个变量,有的线程要更新它有的线程要读取它来进行判断操作,这个时候就需要使用volatile关键字,来保证读取到最新的数据。
11.你知道指令重排以及happens-before原则是什么吗?
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
1、程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
2、锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作。必须保证先写再读。
4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,那么可以得出操作A
先行发生于操作C。
5、线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
6、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生。
7、线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测现场是否已经终止执行
8、对象终结规则:一个对象的初始完成先行发生于他的finalize()方法的开始。
上面这8条原则的意思即是,如果程序中的代码满足上述条件,就一定会按照这个规则来保证指令的顺序。
规则制定了一些特殊情况下,不允许编译器、指令器对我们写的代码进行指令重排,必须保证代码的有序性。
除了上述功能外,volatile还要其他的能够预防指令重排的规定,例如volatile前面的代码一定不能指令重排到volatile变量操作的后面,它后面的代码不能指令重排得到volatile前面。
12.volatile底层是如何基于内存屏障保证可见性和有序性的?
volatile+原子性:不能够保证原子性,只有在一些极端情况下能保证原子性。
保证原子性:synchronized,lock,加锁。
(1)lock指令:volatile保证可见性
对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完后会立即将这个值写回主内存,同时因为MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被修改了。
如果发现被修改了,那么CPU就会将自己的本地缓存数据过期掉,然后从主内存中重新加载最新的数据。
(2)内存屏障:volatile禁止指令重排序
并发这一块的知识非常的深,synchronized、volatile,的层都对应着一套复杂的cpu级别的硬件原理,大量的内存屏障原理;lock API,concurrentHashmap,都是各种复杂的jdk级别的源码。如果有时间可以自己多花时间买书看。

浙公网安备 33010602011771号