传送门
https://juejin.cn/post/6996803830654435335#heading-11

https://github.com/YvetteLau/Blog/issues/35

1.volatile是什么

volatile是JVM提供的轻量级的同步机制

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排(保证有序性)

    2._JMM内存模型之可见性

    JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
    内存可见性:

  • 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域

  • Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行
  • 一个线程如果想要修改主内存中的变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存
  • 线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图

大厂面试题 - 图1
JMM volatile 的内存可见性

  • 通过前面对JMM的介绍,我们知道:各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的
  • 这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作
  • 但此时A线程工作内存中的共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题

代码示例:内存可见性

  1. public class VolatileDemo {
  2. volatile boolean stop = false;
  3. public void shutDown() {
  4. stop = true;
  5. }
  6. public void read() {
  7. while (!stop) {
  8. }
  9. System.out.println("你可以读到我了");
  10. }
  11. public static void main(String[] args) throws InterruptedException {
  12. VolatileDemo demo = new VolatileDemo();
  13. new Thread(demo::read).start();
  14. Thread.sleep(1000);
  15. new Thread(demo::shutDown).start();
  16. }
  17. }

3.volatile不保证原子性

原子性: 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。

  1. public class VolatileAtomicityDemo {
  2. /**
  3. * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
  4. */
  5. volatile int number = 0;
  6. public void addPlusPlus() {
  7. number++;
  8. }
  9. public static void main(String[] args) {
  10. VolatileAtomicityDemo demo = new VolatileAtomicityDemo();
  11. // 创建10个线程,线程里面进行1000次循环
  12. for (int i = 0; i < 10; i++) {
  13. new Thread(() -> {
  14. // 里面
  15. for (int j = 0; j < 1000; j++) {
  16. demo.addPlusPlus();
  17. }
  18. }, String.valueOf(i)).start();
  19. }
  20. // 需要等待上面10个线程都计算完成后,在用main线程取得最终的结果值
  21. // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
  22. while (Thread.activeCount() > 2) {
  23. // yield表示不执行
  24. Thread.yield();
  25. }
  26. // 查看最终的值
  27. // 假设volatile保证原子性,那么输出的值应该为: 10 * 1000 = 10000
  28. System.out.println(Thread.currentThread().getName() + "\t finally number value: " + demo.number);
  29. }
  30. }

最后的结果总是小于10000。

4.volatile不保证原子性理论解释

number++在多线程下是非线程安全的。
我们可以将代码编译成字节码,可看出number++被编译成3条指令。
大厂面试题 - 图2
假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于10000。
分析多线程写值,值丢失的原因

  1. 两个线程:线程 A和线程 B ,同时拿到主内存中 n 的值,并且都执行了加 1 的操作
  2. 线程 A 先执行 putfield 指令将副本的值写回主内存,线程 B 在线程 A 之后也将副本的值写回主内存
  3. 此时,就会出现写覆盖、丢失写值的情况

5.volatile不保证原子性问题解决

  • 1、可加synchronized解决,但它是重量级同步机制,性能上有所顾虑。
  • 2、如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger

    1. public class VolatileAtomicityDemo {
    2. /**
    3. * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
    4. */
    5. volatile int number = 0;
    6. AtomicInteger atomicNumber = new AtomicInteger();
    7. public void addPlusPlus() {
    8. number++;
    9. }
    10. public void addAutoNum() {
    11. atomicNumber.getAndIncrement();
    12. }
    13. public static void main(String[] args) {
    14. VolatileAtomicityDemo demo = new VolatileAtomicityDemo();
    15. // 创建10个线程,线程里面进行1000次循环
    16. for (int i = 0; i < 10; i++) {
    17. new Thread(() -> {
    18. // 里面
    19. for (int j = 0; j < 1000; j++) {
    20. demo.addPlusPlus();
    21. demo.addAutoNum();
    22. }
    23. }, String.valueOf(i)).start();
    24. }
    25. // 需要等待上面10个线程都计算完成后,在用main线程取得最终的结果值
    26. // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
    27. while (Thread.activeCount() > 2) {
    28. // yield表示不执行
    29. Thread.yield();
    30. }
    31. // 查看最终的值
    32. // 假设volatile保证原子性,那么输出的值应该为: 10 * 1000 = 10000
    33. System.out.println(Thread.currentThread().getName() + "\t finally number value: " + demo.number);
    34. System.out.println(Thread.currentThread().getName() + "\t finally atomicNumber value: " + demo.atomicNumber);
    35. }
    36. }

    输出结果:

    1. main finally number value: 9877
    2. main finally atomicNumber value: 10000

    6.volatile指令重排案例

    观察以下程序:

    1. public class ReSortSeqDemo{
    2. int a = 0;
    3. boolean flag = false;
    4. public void method01(){
    5. a = 1;//语句1
    6. flag = true;//语句2
    7. }
    8. public void method02(){
    9. if(flag){
    10. a = a + 5; //语句3
    11. }
    12. System.out.println("retValue: " + a);//可能是6或1或5或0
    13. }
    14. }

    多线程环境中线程交替执行method01()和method02(),由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
    禁止指令重排小总结:
    volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个.

  1. 保证特定操作的执行顺序,
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

    由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
    对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。
    大厂面试题 - 图3
    对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
    大厂面试题 - 图4
    线性安全性获得保证

  • 工作内存与主内存同步延迟现象导致的可见性问题 - 可以使用synchronizedvolatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见
  • 对于指令重排导致的可见性问题和有序性问题 - 可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

    7.单例模式在多线程环境下可能存在安全问题

    首先查看多线程下的单例模式:

    1. public class SingletonDemo {
    2. private static SingletonDemo instance = null;
    3. private SingletonDemo () {
    4. System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
    5. }
    6. public static SingletonDemo getInstance() {
    7. if(instance == null) {
    8. instance = new SingletonDemo();
    9. }
    10. return instance;
    11. }
    12. public static void main(String[] args) {
    13. for (int i = 0; i < 10; i++) {
    14. new Thread(() -> {
    15. SingletonDemo.getInstance();
    16. }, String.valueOf(i)).start();
    17. }
    18. }
    19. }

    输出:[构造方法并不是只执行一次]

    1. "C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" "-javaagent:D:\360setup\idea\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar=62255:D:\360setup\idea\IntelliJ IDEA 2019.3.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;D:\threadDemos\atomic-collections\target\classes;D:\localRepository\log4j\log4j\1.2.17\log4j-1.2.17.jar;D:\localRepository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\localRepository\org\slf4j\slf4j-log4j12\1.7.25\slf4j-log4j12-1.7.25.jar;D:\localRepository\org\slf4j\slf4j-simple\1.7.25\slf4j-simple-1.7.25.jar;D:\localRepository\org\slf4j\slf4j-nop\1.7.2\slf4j-nop-1.7.2.jar;D:\localRepository\org\jsoup\jsoup\1.7.3\jsoup-1.7.3.jar" cn.com.beyond.atomic.order.SingletonDemo
    2. 0 我是构造方法SingletonDemo
    3. 1 我是构造方法SingletonDemo
    4. 3 我是构造方法SingletonDemo
    5. 2 我是构造方法SingletonDemo

    解决?
    DCL 单例模式: [DCL模式:Double Check Lock,即双端检索机制:在加锁前后都进行判断]

    1. public class SingletonDemo {
    2. private static SingletonDemo singletonDemo = null;
    3. private SingletonDemo() {
    4. System.out.println(Thread.currentThread().getName() + "\t 我是构造方法");
    5. }
    6. //DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
    7. public static SingletonDemo getInstance() {
    8. if(instance == null) {
    9. // a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
    10. synchronized (SingletonDemo.class) //b
    11. {
    12. //c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
    13. if(instance == null) {
    14. // d 此时才开始初始化
    15. instance = new SingletonDemo();
    16. }
    17. }
    18. }
    19. return instance;
    20. }
    21. public static void main(String[] args) {
    22. for (int i = 0; i < 10000; i++) {
    23. new Thread(() -> {
    24. SingletonDemo.getInstance();
    25. }, String.valueOf(i + 1)).start();
    26. }
    27. }
    28. }

    解决方法之一:用synchronized修饰方法getInstance(),但它属重量级同步机制,使用时慎重。

  1. DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排
  2. 原因:可能出现某一个线程执行到第一次检测,读取到的instance不为null时,但是instance的引用对象可能没有完成初始化。原因如下:
  3. 实例化代码instance=new SingletonDemo();可以分为以下3步完成(伪代码)
    1. memory=allocate(); //1.分配对象内存空间
    2. instance(memory) //2.初始化对象
    3. instance=memory; //3.设置instance指向刚分配的内存地址,此时instance!=null
    1. 步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改4变,因此这种重排优化是允许的。
      5.指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
      6.就比如说我们需要使用 instance 对象中的一个对象 heygo ,但是由于 instance 并未初始化完成,此时 heygo == null ,访问 instance.heygo 将抛出空指针异常

8.单例模式volatile分析

多线程下单例正确写法: 加上 volatile ,禁止指令重排

  1. public class SingletonDemo{
  2. private SingletonDemo(){}
  3. private volatile static SingletonDemo instance = null;
  4. public static SingletonDemo getInstance() {
  5. if(instance == null) {
  6. synchronized(SingletonDemo.class){
  7. if(instance == null){
  8. instance = new SingletonDemo();
  9. }
  10. }
  11. }
  12. return instance;
  13. }
  14. }

9.CAS

CAS算法:CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下函数:CAS(V,E,N) 参数:V表示要更新的变量 E预期值 N新值
CAS原理解析:

  • 用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的。 在 java.util.concurrent 下面的源码中,Atomic, ReentrantLock 都使用了Unsafe类中的方法来保证并发的安全性。
  • CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁,JDK中大量使用了CAS来更新数据而防止加锁来保持原子更新。
  • CAS 操作包含三个操作数 :内存偏移量位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作

    10.线程池参数解释

  1. corePoolSizes:核心线程数。
  • 该线程会一直存在,即使没有任务需要执行
  • 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建线程池处理
  • 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
  1. maxPoolSize:最大线程数
  • 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
  • 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
  1. keepAliveTime、unit:线程空闲时间
  • 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过 setKeepAliveTime 来设置空闲时间
  • 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
  1. workQueue: 存放任务队列
  • 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
  1. ThreadFactory
  • 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
  1. RejectedExecutionHandler:拒绝策略
  • 任务拒绝策略,有两种情况,
  • 第一种是当我们调用shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝
  • 另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝

    11.线程池工作顺序

  1. 线程池创建,准备好core数量的核心线程,准备接受任务
  2. core满了,就将在进来的任务放进阻塞队列中,空闲的core就会自己去阻塞队列获取任务执行
  3. max满了就用 Reject ExecutionHandler拒绝任务
  4. max都完成,有很多空闲,在指定的keepAliveTime以后,释放一些max-core等线程

线程池面试题:
一个线程池core为7,max为20,queue:50,100个并发进来,任务如何分配?

  • 7个核心线程立即执行,50个进入到队列中,在开13即(20-7)个进行执行,剩余30个使用拒绝策略

    12,创建线程安全的HashMap

  • HashTable(不建议使用)

  • 使用Collections.synchronizedMap(new HashMap<>());
  • 使用 ConcurrentHashMap ,即Map map = new ConcurrentHashMap<>();

    13. 静态代理和动态代理的区别,什么场景使用?

    代理的目的是:为其他对象提供一个代理以控制某个对象的访问,将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的委托类的方法
    区别: 1)静态代理
    有程序员创建或者由特定的工具生成,在代码编译时就确定了被代理的类是哪一个是静态代理。静态代理通常只代理一个类
    2)动态代理
    在代码运行期间,运用反射机制动态创建生成。动态代理代理的是一个接口下的多个实现类
    实现步骤:
    a.实现 InvocationHandler接口创建自己的调用处理器,
    b.给 Proxy 类提供 ClassLoader 和代理接口类型数组创建动态代理类;
    c.利用反射机制得到动态代理类的构造函数;
    d.利用动态代理类的构造函数创建动态代理类对象
    使用场景:Retrofit 中直接调用接口的方法;Spring 的 AOP 机制;

    14、Java 中实现多态的机制是什么?

    多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编 译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用 修改源程序,就可以让引用变量绑定到各种不同的类实现上。Java 实现多态有三个必要条件: 继承、重写、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能 够具备调用父类方法和子类的方法。

    15、说说你对 Java 反射的理解

    在运行状态中,对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能 调用它的任意一个方法和属性。这种能动态获取信息及动态调用对象方法的功能称为 java 语言的反射机制。

    反射的作用:开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的,或只对系统应用开放,这里就可以利用 java的反射机制通过反射来获取所需的私有成员或是方法。
    获取反射的方式:
    1.通过new对象实现反射机制
    2.通过路径实现反射机制
    3.通过类名实现反射机制
    实际工作中一般使用hutool工具包中的ReflectUtil居多

1) 获取类的 Class 对象实例 Class clz = Class.forName(“com.zhenai.api.Apple”);
2) 根 据 Class 对 象 实 例 获 取 Constructor 对 象 Constructor appConstructor = clz.getConstructor();
3) 使 用 Constructor 对 象 的 newInstance 方 法 获 取 反 射 类 对 象 Object appleObj = appConstructor.newInstance();
4) 获取方法的 Method 对象 Method setPriceMethod = clz.getMethod(“setPrice”, int.class);
5) 利用 invoke 方法调用方法 setPriceMethod.invoke(appleObj, 14);
6) 通过 getFields()可以获取 Class 类的属性,但无法获取私有属性,而 getDeclaredFields()可 以获取到包括私有属性在内的所有属性。带有 Declared 修饰的方法可以反射到私有的方法, 没有 Declared 修饰的只能用来反射公有的方法,其他如 Annotation\Field\Constructor 也是如 此。

反射效率低的原因

  1. Method#invoke 方法会对参数做封装和解封操作
  2. 需要检查方法可见性
  3. 需要校验参数
  4. 反射方法难以内联
  5. JIT 无法优化

    反射的应用场景

    反射常见的应用场景这里介绍3个:
  • Spring 实例化对象:当程序启动时,Spring 会读取配置文件applicationContext.xml并解析出里面所有的 标签实例化到IOC容器中。
  • 反射 + 工厂模式:通过反射消除工厂中的多个分支,如果需要生产新的类,无需关注工厂类,工厂类可以应对各种新增的类,反射可以使得程序更加健壮。
  • JDBC连接数据库:使用JDBC连接数据库时,指定连接数据库的驱动类时用到反射加载驱动

    反射的优势及缺陷

    反射的优点

  • 增加程序的灵活性:运行期类型的判断,动态加载类,提高代码灵活度。

但是,有得必有失,一项技术不可能只有优点没有缺点,反射也有两个比较隐晦的缺点

  • 破坏类的封装性:可以强制访问 private 修饰的信息
  • 性能损耗:反射相比直接实例化对象、调用方法、访问变量,中间需要非常多的检查步骤和解析步骤,JVM无法对它们优化。