Java面试八股文-多线程篇

目录

1、线程和进程的区别

2、Java里面的线程和操作系统的线程一样吗?

3、什么是并行与并发

4、线程有哪些状态?

5、什么是上下文切换?

6、线程切换要保存哪些上下文?

7、什么是线程安全?

8、为什么线程会不安全?

9、守护线程是什么?

10、什么是死锁?

11、多线程什么场景下会发生死锁?

12、如何预防和避免线程死锁?

13、为什么不能直接调用run()方法?

14、创建多线程的方式

15、Thread类和Runable接口的最大区别是什么?

16、Runnable和Callable创建线程有什么区别?

17、说说可重入锁,为什么需要可重入锁?

18、说说 sleep()方法和 wait()方法区别和共同点?

19、Java内部有哪些同步机制?

20、多线程环境下为什么要引入同步的机制?/一般怎么样才能做到线程安全?

21、synchronized 与 Lock的异同?

22、怎么使用 synchronized 关键字?

23、讲一下 synchronized 关键字的底层原理

24、谈谈 synchronized 和 ReentrantLock 的区别

25、ReentrantLock的实现原理?

26、volatile 关键字的作用/为什么要使用volatile呢?

27、volatile关键字是什么原理?

28、并发编程的三个重要特性

29、说说 synchronized 关键字和 volatile 关键字的区别

30、ThreadLocal 是干嘛的?

31、为什么要用线程池?

32、如何创建线程池?/创建线程池有哪几种方式?/线程池哪几种?

33、执行 execute()方法和 submit()方法的区别是什么呢?

34、线程池原理,有什么参数?

35、线程池的执行顺序是怎么样的呢?/一个任务进来后,线程池是怎么处理的?

36、线程池都有哪些状态?

37、shutdown()和shutdownNow()的区别是什么?

38、线程池的拒绝策略有哪几种?

39、线程安全的单例模式是怎么样的?

40、为什么要使用双段锁呢?/为什么先判断对象是否已经实例过?

41、Atomic的原理?


1、线程和进程的区别

  • 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。

2、Java里面的线程和操作系统的线程一样吗?

  • 不一样

3、什么是并行与并发

  • 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

  • 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事

4、线程有哪些状态?

  • 新建

    • 当一个Thread类或其子类对象被声明并创建时,新生的线程处于新建状态

  • 就绪

    • 处于新建状态的线程调用start()方法后,将进入线程队列等待CPU时间片,此时它已经具备了运行的条件,只是没分配到CPU资源,此时的线程处于就绪状态

  • 运行

    • 处于就绪状态的线程被调度并获得CPU资源时,便进入运行状态,执行run()方法中的代码

  • 阻塞

    • 正在运行的线程被人为挂起或执行输入输出操作时,如调用sleep、wait、suspend等方法后,让出CPU并临时中止自己的执行,进入阻塞状态

  • 死亡

    • 当线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法让它进入就绪状态

5、什么是上下文切换?

  • 线程在执行过程中会有自己的运行条件和状态,也称上下文,当出现如下情况的时候,线程会从占用 CPU 状态中退出:

    • 主动让出 CPU,比如调用了 sleep(), wait()等。

    • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。

    • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。

    • 被终止或结束运行

  • 这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场,并加载下一个将要占用 CPU 的线程上下文,这就是上下文切换

6、线程切换要保存哪些上下文?

  • 线程在切换的过程中需要保存当前线程id、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括SP、PC、EAX等寄存器,其主要功能如下:

    • SP:堆栈指针,指向当前栈的栈顶地址

    • PC:程序计数器,存储下一条将要执行的指令

    • EAX:累加寄存器,用于加法乘法的缺省寄存器

7、什么是线程安全?

  • 如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期是一样的,这就是线程安全。

8、为什么线程会不安全?

  • 线程是一个抢占式执行的过程,具有随机性,是操作系统内核来实现的,程序员无法控制。

  • 由于多个线程修改同一个变量

  • 线程的非原子性操作,可能会有多条指令,在执行过程中相互穿插,也就无法保证线程的安全。

  • 内存的可见性

    • 比如: 两个线程同时操作一个内存,一个读内存,一个写内存,写操作的线程进行修改的时候,读线程可能读取到的是修改之前的值,也可能读取到的是修改之后的值,存在不确定性,这会带来线程不安全。

  • 指令重排序

    • 概念:为了让程序跑的更加地快,CPU调整了执行指令与指令之间的顺序(编译器的优化,是自动调整的)来提高运行的效率(逻辑不发生改变)。

    • 和编译器的优化有直接的关系,也和线程不安全直接相关。 如果是单线程的情况下,这样的调整没问题,但是在多线程的情况下就会发生线程安全问题。

9、守护线程是什么?

  • 守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。

10、什么是死锁?

  • 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

11、多线程什么场景下会发生死锁?

  • 产生死锁的四个必要条件:

    • 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源

    • 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放

    • 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放

    • 循环等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系

  • 这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

  • 此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

12、如何预防和避免线程死锁?

  • 如何预防死锁?破坏死锁的产生的必要条件即可:

    • 破坏请求与保持条件:一次性申请所有的资源。

    • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

    • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

  • 如何避免死锁?

    • 避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

13、为什么不能直接调用run()方法?

  • 因为调用start()方法才能启动线程并让线程进入就绪状态,直接执行run()方法的话不会以多线程的方式执行,只会把 run()方法当成一个 main 线程下的普通方法去执行。

14、创建多线程的方式

  • 继承Thread类的方式

    1. 创建一个继承于Thread类的子类

    2. 重写Thread类的run()-->将此线程执行的操作声明在run()中

    3. 创建Thread类的子类的对象

    4. 通过此对象调用start():①启动当前线程②调用当前线程的run()

  • 实现Runnable接口的方式

    1. 创建一个实现了Runnable接口的类

    2. 实现类去实现Runnable中的抽象方法:run()

    3. 创建实现类的对象

    4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

    5. 通过Thread类的对象调用start()

  • 实现Callable接口

    1. 创建一个实现Callable的实现类

    2. 实现call方法,将此线程需要执行的操作声明在call()中

    3. 创建Callable接口实现类的对象

    4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象

    5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()

    6. 使用get()方法获取Callable中call方法的返回值。get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。

  • 使用线程池

15、Thread类和Runable接口的最大区别是什么?

  • Thread是类,Runable是接口,实现的方式没有类的单继承性的局限性

  • 实现Runnable接口的方式更适合处理多个线程共享数据的情况。如果一个类继承Thread类,则不适合用于多线程资源共享;而实现了Runnable接口,就可以方便的实现资源的共享。

16、Runnable和Callable创建线程有什么区别?

  • Runnable接口不会返回结果或抛出检查异常

  • Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

17、说说可重入锁,为什么需要可重入锁?

  • 可重入锁,也叫做递归锁,就是可再进入的锁,重入性是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞。

  • 首先他需要具备两个条件:

    • 线程再次获取锁:锁需要识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功

    • 锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前线程被重复获取的次数,而被释放时,计数自减,当计数为0时表示锁已经成功释放。

  • 可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。比如,一个类中的同步方法调用另一个同步方法,假如Synchronized不支持重入,进入method2方法时当前线程获得锁,method2方法里面执行method1时当前线程又要去尝试获取锁,这时如果不支持重入,它就要等释放,把自己阻塞,导致自己锁死自己。

18、说说 sleep()方法和 wait()方法区别和共同点?

  • 两者都可以暂停线程的执行。

  • 两者最主要的区别在于:sleep()方法没有释放锁,而 wait()方法释放了锁

  • wait()通常被用于线程间交互/通信,sleep()通常被用于暂停执行。

  • wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。

19、Java内部有哪些同步机制?

  • 同步代码块

  • 同步方法

  • Lock锁

20、多线程环境下为什么要引入同步的机制?/一般怎么样才能做到线程安全?

  • 在多线程环境下,通过同步机制解决多线程的安全问题。所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。

21、synchronized 与 Lock的异同?

  • 相同:二者都可以解决线程安全问题

  • 不同:

    • Lock 是显式锁,需要手动开启和关闭,synchronized是隐式锁,出了作用域自动释放

    • Lock只有代码块锁,synchronized有代码块锁和方法锁

    • 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性

22、怎么使用 synchronized 关键字?

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

  • 修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object)表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)表示进入同步代码前要获得当前 class 的锁

23、讲一下 synchronized 关键字的底层原理

  • synchronized 关键字底层原理属于 JVM 层面。

  • synchronized 同步代码块的情况

    • synchronized 同步代码块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

    • 当执行 monitorenter 指令时,线程试图获取锁也就是获取对象监视器 monitor 的持有权。

    • 在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为0 则表示锁可以被获取,获取后将锁计数器设为1 也就是加1。

    • 对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放,其他线程可以尝试获取锁。

    • 如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

  • synchronized 修饰方法的的情况

    • synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

    • 如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

  • 两者的本质都是对对象监视器 monitor 的获取。

24、谈谈 synchronized 和 ReentrantLock 的区别

  • 共同点

    • 都是用来协调多线程对共享对象、变量的访问

    • 都是可重入锁,同一线程可以多次获得同一个锁

    • 都保证了可见性和互斥性

  • 不同点

    • ReentrantLock显示地获得,释放锁,synchronized隐式获得释放锁

    • ReentrantLock可响应中断,可轮回,synchronized不可以响应中断

    • ReentrantLock是API级别的,synchronized是JVM级别的

    • ReentrantLock可以实现公平锁

    • ReentrantLock通过Condition可以绑定多个条件

    • 底层实现不一样,synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略。

    • Lock是一个接口,而synchronized是java中的关键字

    • synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。

25、ReentrantLock的实现原理?

  • ReentrantLock 实现的前提是 AbstractQueuedSynchronizer(抽象队列同步器),简称 AQS。

  • AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

    • CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

  • AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

26、volatile 关键字的作用/为什么要使用volatile呢?

  • 防止 JVM 的指令重排

  • 保证变量的可见性:当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

27、volatile关键字是什么原理?

  • 为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。

  • 但是,对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。

  • 但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议

  • 缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

  • 所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

28、并发编程的三个重要特性

  • 原子性: 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。synchronized 可以保证代码片段的原子性。

  • 可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。

  • 有序性:代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

29、说说 synchronized 关键字和 volatile 关键字的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在。

  • volatile 关键字是线程同步的轻量级实现,所以 volatile性能比synchronized关键字要好。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块。

  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

30、ThreadLocal 是干嘛的?

  • ThreadLocal实现了每个线程都有自己的专属本地变量,可以将ThreadLocal类比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

  • 如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

31、为什么要用线程池?

  • 线程池提供了一种限制和管理资源(包括执行一个任务)的方式。每个线程池还维护一些基本统计信息,例如已完成任务的数量。

  • 使用线程池的好处

    • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

    • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

    • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

32、如何创建线程池?/创建线程池有哪几种方式?/线程池哪几种?

  • newFixedThreadPool(int nThreads)/FixedThreadPool

    • 创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。

  • newCachedThreadPool()/CachedThreadPool

    • 创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。

  • newSingleThreadExecutor()/SingleThreadExecutor

    • 这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;特点是能确保依照任务在队列中的顺序来串行执行。

  • newScheduledThreadPool(int corePoolSize)/ScheduledThreadPool

    • 创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

33、执行 execute()方法和 submit()方法的区别是什么呢?

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功;

  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值。

34、线程池原理,有什么参数?

  • 创建线程池的方法内部实际上是调用了 ThreadPoolExecutor的构造方法。线程池实现类 ThreadPoolExecutor 是 Executor 框架最核心的类。

  • 3 个最重要的参数

    • corePoolSize:核心线程数,定义了最小可以同时运行的线程数量。

    • maximumPoolSize:线程池最大线程数,当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

    • workQueue:等待队列,当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

  • ThreadPoolExecutor其他常见参数:

    • keepAliveTime:空闲线程存活时间,当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。

    • unit:keepAliveTime 参数的时间单位。

    • threadFactory:executor 创建新线程的时候会用到。

    • handler:饱和策略。

35、线程池的执行顺序是怎么样的呢?/一个任务进来后,线程池是怎么处理的?

  • 向线程池提交任务时,会首先判断线程池中的线程数是否大于设置的核心线程数,如果不大于,就创建一个核心线程来执行任务。

  • 如果大于核心线程数,就会判断缓冲队列是否满了,如果没有满,则放入队列,等待线程空闲时执行任务。

  • 如果队列已经满了,则判断是否达到了线程池设置的最大线程数,如果没有达到,就创建新线程来执行任务。

  • 如果已经达到了最大线程数,则执行指定的拒绝策略。

Java面试八股文-多线程篇_第1张图片

36、线程池都有哪些状态?

  • 线程池有5种状态:

    • Running

    • ShutDown

    • Stop

    • Tidying

    • Terminated

  • img

37、shutdown()和shutdownNow()的区别是什么?

  • shutdown():关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。

  • shutdownNow():关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

38、线程池的拒绝策略有哪几种?

  • 若线程池中的核心线程数被用完且阻塞队列已满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。

  • jdk内置的拒绝策略有4种:

    • AbortPolicy:直接抛出异常,组织线程正常运行

    • CallerRunsPolicy :如果被丢弃的线程任务未关闭,则执行该线程任务

    • DiscardOldestPolicy:移除线程队列中最早的一个线程任务,并尝试提交当前任务

    • DiscardPolicy:丢弃当前的线程任务而不做任何处理。

  • 默认的拒绝策略在ThreadPoolExecutor中作为内部类提供。

39、线程安全的单例模式是怎么样的?

饿汉式

public class Singleton {
    private static Singleton instance = new Singleton();
 
    private Singleton() {
    }
 
    public static Singleton getInstance() {
        return instance;
    }
}

双重校验锁实现对象单例

public class Singleton {
​
    private volatile static Singleton instance;
​
    private Singleton() {
    }
​
    public  static Singleton getInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (instance == null) {
            //类对象加锁,Singleton.class为同步锁
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • instance = new Singleton();这段代码其实是分为三步执行:

  1. 为 instance 分配内存空间

  2. 初始化 instance

  3. 将 instance 指向分配的内存地址

  • 但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

  • 第一次判断是否为null:

    • 第一次判断是在Synchronized同步代码块外,理由是单例模式只会创建一个实例,并通过getUniqueInstance方法返回singleton对象,所以如果已经创建了singleton对象,就不用进入同步代码块,不用竞争锁,直接返回前面创建的实例即可,这样大大提升效率。

  • 第二次判断是否为null:

    • 第二次判断原因是为了保证同步,假若线程A通过了第一次判断,进入了同步代码块,但是还未执行,线程B就进来了(线程B获得CPU时间片),线程B也通过了第一次判断(线程A并未创建实例,所以B通过了第一次判断),准备进入同步代码块,假若这个时候不判断,就会存在这种情况:线程B创建了实例,此时恰好A也获得执行时间片,如果不加以判断,那么线程A也会创建一个实例,就会造成多实例的情况。

      40、为什么要使用双段锁呢?/为什么先判断对象是否已经实例过?

      • 在单例对象被创建后,因为方法加了锁,所以要等当前线程得到对象释放锁后,下一个线程才可以进入getInstance()方法获取对象,也就是线程要一个一个的去获取对象。而采用双重同步锁,在synchronized代码块前加了一层判断,这使得在对象被创建之后,多线程不需进入synchronized代码块中,可以多线程同时并发访问获取对象,这样效率大大提高。

      41、Atomic的原理?

      • AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

      • Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

      • CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

你可能感兴趣的:(面试,职场和发展,java,程序人生,开发语言)