9、在 Java 程序中怎么保证多线程的运行安全?

线程安全在三个方面体现:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,
synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,
volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂
乱无序,(happens-before 原则)。

 

 

10、Java 线程同步的几种方法?

1、使用 Synchronized 关键字;
2、wait 和 notify;
3、使用特殊域变量 volatile 实现线程同步;
4、使用可重入锁实现线程同步;
5、使用阻塞队列实现线程同步;
6、使用信号量 Semaphore。

 

二、synchronized

java线程面试题由浅入深 java线程安全面试_原子类

1. 说一说自己对于 synchronized 关键字的了解

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字
可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁
(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操
作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而
操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相
对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原
因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现
在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如
自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开
销。

 

2. 说说自己是怎么使用 synchronized 关键字,在项目中用到
了吗?

synchronized 关键字最主要的三种使用方式:
1.修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
2.修饰静态方法: 作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就
是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,
是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所
以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态
synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,
是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的
锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁;
3.修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的
锁。 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。
synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给
Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实
例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串
常量池具有缓冲功能!下面我已一个常见的面试题为例讲解一下 synchronized 关键字的
具体使用。
面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检
验锁方式实现单例模式的原理呗!”

 

双重校验锁实现对象单例(线程安全)

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 的指令重排,保证在多线程环境下也能正常运行。

 

4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优
化,可以详细介绍一下这些优化吗?

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋
锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状
态,它们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高
获得锁和释放锁的效率。

 

4.1 偏向锁
引入偏向锁的目的和引入轻量级锁的目的很像,它们都是为了没有多线程竞争的前提
下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在
无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同
步都消除掉。
偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接
下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步。
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请
锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意
的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

 

4.2 轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量
级锁的优化手段(JDK1.6 之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没
有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为
使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了 CAS 操作。
轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是
不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用
互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生 CAS 操作,因此
在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将
很快膨胀为重量级锁!

 

4.3 自旋锁和自适应自旋
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称
为自旋锁的优化手段。
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转
入内核态中完成(用户态转换到内核态会耗费时间)。
一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是
得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取
锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个
线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。

 

5. 谈谈 synchronized 和 ReenTrantLock 的区别?
简单回答(一般电话面试中可以这样简单回答,现场面试可以回答的详细些。视情况
而定吧!):
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这
是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized
更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,
ReentrantLock 比 synchronized 的扩展性体现在几点上:
另外,二者的锁机制其实也是不一样的:ReentrantLock 底层调用的是 Unsafe 的
park 方法加锁。synchronized 操作的应该是对象头中 mark word。

 

5.1 两者都是可重入锁
“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对
象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获
取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增
1,所以要等到锁的计数器下降为 0 时才能释放锁。

 

5.3 ReenTrantLock 比 synchronized 增加了一些高级功能相比
synchronized,ReenTrantLock 增加了一些高级功

主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可
以绑定多个条件);④ 性能已不是选择标准。

 

6. synchronized 和 volatile 的区别是什么?

1、volatile 本质是在告诉 Jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从
主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程
被阻塞住。
2、volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别
的。
3、volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证
变量的修改可见性和原子性。
4、volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
5、volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优
化。

 

7. 简单介绍下 volatile?
volatile 关键字是用来保证有序性和可见性的。这跟 Java 内存模型有关。
比如:我们所写的代码,不一定是按照我们自己书写的顺序来执行的,编译器会做重排
序,CPU 也会做重排序的,这样的重排序是为了减少流水线的阻塞的,引起流水阻塞,比
如数据相关性,提高 CPU 的执行效率。需要有一定的顺序和规则来保证,不然程序员自己
写的代码都不知道对不对了,所以有 happens-before 规则,其中有条就是 volatile 变量
规则:对一个变量的写操作先行发生于后面对这个变量的读操作;有序性实现的是通过插
入内存屏障来保证的。
可见性:首先 Java 内存模型分为,主内存,工作内存。比如线程 A 从主内存把变量从主
内存读到了自己的工作内存中,做了加 1 的操作,但是此时没有将 i 的最新值刷新会主内
存中,线程 B 此时读到的还是 i 的旧值。加了 volatile 关键字的代码生成的汇编代码发
现,会多出一个 lock 前缀指令。 Lock 指令对 Intel 平台的 CPU,早期是锁总线,这样代
价太高了,后面提出了缓存一致性协议,MESI,来保证了多核之间数据不一致性问题。

 

8. ReentrantLock 和 ReentrantReadWriteLock
ReentrantLock 是一个独占锁。同一时间只允许一个线程访问,而
ReentrantReadWriteLock 允许多个读线程同时访问,但是不允许写线程和读线程、写线
程和写线程同时访问。读写锁内部维护了两个锁:一个是用于读操作的 ReadLock,一个
是用于写操作的 WriteLock。
读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大
于写操作的时候,读写锁就非常有用了。
ReentrantReadWriteLock 基于 AQS 实现,它的自定义同步器(继承 AQS)需要在
同步状态 state 上维护多个读线程和一个写线程,该状态的设计成为实现读写锁的关键。
ReentrantReadWriteLock 很好的利用了高低位。来实现一个整型控制两种状态的功能,
读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写。
获取写锁的过程:

 

三、乐观锁和悲观锁

乐观锁和悲观锁详解
1. 乐观锁和悲观锁的基本概念
乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲
观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人
好于另外一种人。
1.1 悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的
时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线
程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就
用到了很多这种锁机制,比如 行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。

 

1.2 乐观锁:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但
是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和
CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类
似于 write_condition 机制,其实都是提供的乐观锁。在 Java 中
ReentrantReadWriteLock 特点:
升降级
java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS
实现的。

 

1.3 两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省
去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,
这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下
用悲观锁就比较合适。

 

3. 乐观锁的缺点
3.1 ABA 问题:
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A
值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时
间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改
过。这个问题被称为 CAS 操作的 "ABA" 问题。
JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的
compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于
预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
3.2 循环时间长开销大
自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU
带来非常大的执行开销。 如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的
提升,pause 指令有两个作用,第一:它可以延迟流水线执行指令(de-pipeline),使
CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟
时间是零。第二:它可以避免在退出循环的时候因内存顺序冲突(memory order
violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效
率。
3.3 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。 但是从 JDK
1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量
放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用 AtomicReference 类把
多个共享变量合并成一个共享变量来操作。
4. CAS 和 synchronized 的使用场景
简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),
synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)
(1)对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线
程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于
硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的
性能。
(2)对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而
浪费更多的 CPU 资源,效率低于 synchronized。
补充: Java 并发编程这个领域中 synchronized 关键字一直都是元老级的角色,很久
之前很多人都会称它为 “重量级锁” 。但是,在 JavaSE 1.6 之后进行了主要包括为了减
少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变
得在某些情况下并不是那么重了。
synchronized 的底层实现主要依靠 Lock-Free 的队列,基本思路是:自旋后阻塞,
竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况
下,可以获得和 CAS 类似的性能;而线程冲突严重的情况下,性能远高于 CAS。

 

四、Atomic 原子类
1. 原子类的基本介绍
这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个
操作一旦开始,就不会被其他线程干扰。所以,所谓原子类说简单点就是具有原子/原子操
作特征的类。
并发包 java.util.concurrent 的原子类都存放在 java.util.concurrent.atomic 下。根
据操作的数据类型,可以将 JUC 包中的原子类分为 4 类:四、Atomic 原子类
1. 原子类的基本介绍
这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个
操作一旦开始,就不会被其他线程干扰。所以,所谓原子类说简单点就是具有原子/原子操
作特征的类。
并发包 java.util.concurrent 的原子类都存放在 java.util.concurrent.atomic 下。根
据操作的数据类型,可以将 JUC 包中的原子类分为 4 类: