Java并发编程 — 并发理论
📋 本文节选自《Java并发编程面试题》,返回 查看全部章节
二、并发理论
1. Java中垃圾回收有什么目的?什么时候进行垃圾回收?
- 垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。
- 垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。
2. 线程之间如何通信及线程之间如何同步
- 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。
- Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。
3. Java内存模型
- 共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
![ ][nbsp 2]
- 从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1、 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去;
2、 然后,线程B到主内存中去读取线程A之前已更新过的共享变量;
下面通过示意图来说明线程之间的通信
![ ][nbsp 3]
- 总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。
4. 如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?
- 不会,在下一个垃圾回调周期中,这个对象将是被可回收的。
- 也就是说并不会立即被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。
5. finalize()方法什么时候被调用?析构函数(finalization)的目的是什么?
- 垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throwsThrowable { } 在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间
- GC本来就是内存回收了,应用还需要在finalization做什么呢? 答案是大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,比如你调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。
- Finalizetion主要用来释放被对象占用的资源(不是指内存,而是指其他资源,比如文件(FileHandle)、端口(ports)、数据库连接(DB Connection)等)。然而,它不能真正有效地工作。
6. 什么是重排序
- 程序执行的顺序按照代码的先后顺序执行。
- 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
int a = 5; //语句1
int r = 3; //语句2
a = a + 2; //语句3
r = a*a; //语句4- 则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系。
- 显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。
7. 重排序实际执行的指令步骤
![ ][nbsp 4]
1、 编译器优化的重排序编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2、 指令级并行的重排序现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行如果不;存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3、 内存系统的重排序由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在;乱序执行。
- 这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题。
8. 重排序遵守的规则
- as-if-serial:
1、 不管怎么排序,结果不能改变;
2、 不存在数据依赖的可以被编译器和处理器重排序;
3、 一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序;
4、 单线程根据此规则不会有问题,但是重排序后多线程会有问题;
9. as-if-serial规则和happens-before规则的区别
- as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
- as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
- as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
10. 并发关键字 synchronized ?
- 在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。
- 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
为了帮助更多的粉丝朋友,找到个好工作,多拿几个offer。苏三最近建一些工作内推群,群里目前已经收集了很多内推岗位,大厂、中厂、小厂、外包都有。欢迎HR、开发、测试、运维和产品加入。

扫描下方微信,备注:网站+所在城市,即可拉你进工作内推群。

11. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
synchronized关键字最主要的三种使用方式:
- 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
- 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结:synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(Stringa)因为JVM中,字符串常量池具有缓存功能!
12. 单例模式了解吗?给我解释一下双重检验锁方式实现单例模式!”
**双重校验锁实现对象单例(线程安全)说明:**双锁机制的出现是为了解决前面同步问题和性能问题,看下面的代码,简单分析下确实是解决了多线程并行进来不会出现重复new对象,而且也实现了懒加载
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
- uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();这段代码其实是分为三步执行:
1、 为uniqueInstance分配内存空间;
2、 初始化uniqueInstance;
3、 将uniqueInstance指向分配的内存地址;
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时uniqueInstance 还未被初始化。使用volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
13. 说一下 synchronized 底层实现原理?
- Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成,每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就
- 会处于锁定状态并且尝试获取monitor的所有权 ,过程:
1、 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为;monitor的所有者。
2、 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.;
3、 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再;重新尝试获取monitor的所有权。
synchronized是可以通过 反汇编指令 javap命令,查看相应的字节码文件。
14. synchronized可重入的原理
- 重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
15. 什么是自旋
- 很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
- 忙循环:就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
16. 多线程中 synchronized 锁升级的原理是什么?
- synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
- 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
- 轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁;
- 重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
17. 线程 B 怎么知道线程 A 修改了变量
(1)volatile 修饰变量(2)synchronized 修饰修改变量的方法(3)wait/notify(4)while 轮询
18. 当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?
- 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。
19. synchronized、volatile、CAS 比较
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。(3)CAS 是基于冲突检测的乐观锁(非阻塞)
20. synchronized 和 Lock 有什么区别?
- 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
- synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
21. synchronized 和 ReentrantLock 区别是什么?
- synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
- synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在Java 6 中对 synchronized 进行了非常多的改进。
- 相同点:两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
- 主要区别如下:ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word
- Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:普通同步方法,锁是当前实例对象静态同步方法,锁是当前类的class对象同步方法块,锁是括号里面的对象
22. volatile 关键字的作用
- 对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happensbefore 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值。
- 从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
- volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
23. Java 中能创建 volatile 数组吗?
- 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
24. volatile 变量和 atomic 变量有什么不同?
- volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
- 而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
25. volatile 能使得一个非原子操作变成原子操作吗?
- 关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。
- 虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。
所以从Oracle Java Spec里面可以看到:
- 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。
- 如果使用volatile修饰long和double,那么其读写都是原子操作
- 对于64位的引用地址的读写,都是原子操作
- 在实现JVM时,可以自由选择是否把读写long和double作为原子操作
- 推荐JVM实现为原子操作
26. synchronized 和 volatile 的区别是什么?
- synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
- volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别
- volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
27. final不可变对象,它对写并发应用有什么帮助?
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger 和 BigDecimal 等。
只有满足如下状态,一个对象才是不可变的;
它的状态不能在创建后再被修改;
所有域都是 final 类型;并且,它被正确创建(创建期间没有发生 this 引用的逸出)。
不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
28. Lock 接口和synchronized 对比同步它有什么优势?
- Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
- 它的优势有:(1)可以使锁更公平(2)可以使线程在等待锁的时候响应中断(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间(4)可以在不同的范围,以不同的顺序获取和释放锁
- 整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
29. 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
- 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
30. 什么是 CAS
- CAS 是 compare and swap 的缩写,即我们所说的比较交换。
- CAS 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。
- CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。
java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)
31. CAS 的会产生什么问题?
- 1、ABA 问题:比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
- 2、循环时间长开销大:对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
- 3、只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。
32. 什么是原子类
- java.util.concurrent.atomic包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。
- 比如:AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。
简单来说就是原子类来实现CAS无锁模式的算法
33. 原子类的常用类
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
34. 说一下 Atomic的原理?
- Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
35. 死锁与活锁的区别,死锁与饥饿的区别?
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
Java 中导致饥饿的原因:
1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。
2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。