071、synchronized 常见的使用方法?
public class MyService {
public synchronized static void method1() {
}
public void method2() {
synchronized (MyService.class) {
}
}
public synchronized void method3() {
}
public void method4() {
synchronized (this) {
}
}
public void method5() {
synchronized ("hello") {
}
}
}
- method1() 和 method2() 持有的锁是同一个,即当前类 MyService 对应的 Class 对象
- method3() 和 method4() 持有的锁是同一个,即当前对象
- method5() 持有的锁是字符串 hello
面试时可以这样回答:
- 修饰实例方法时,使用当前实例对象作为锁,仅作用于当前实例对象。
- 修饰静态方法时,使用当前类的 class 对象作为锁,作用于该类的所有实例对象。
- 修饰代码块,可以使用关键字 this 作为锁,表示使用当前实例对象作为锁;也可以使用指定类的 class 对象作为锁;也可以直接创建一个实例对象作为锁。
注意:尽量不要使用字符串作为对象锁,因为在 JVM 中,字符串常量池具有缓存功能,有可能不同包不同类中的值相同的字符串常量引用的是同一个字符串对象,这样可能会造成意料之外的死锁情况。
072、双重检测锁实现单例模式?
public class Singleton {
// 1、定义一个 volatile 修饰的静态变量
private volatile static Singleton instance = null;
// 2、定义一个私有无参构造器
private Singleton() {
}
// 3、编写创建实例对象的静态方法
private static Singleton getInstance() {
// 判断对象是否已经实例化,没有实例化才能进入加锁代码
if (instance == null) {
synchronized (Singleton.class) {
// 在对象未实例化之前可能有好几个线程并发通过第一次判断,这里是保证对象只被实例化一次
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
不要第一个
instance == null
判断也可以实现单例,为什么还需要?如果没有这个判断,那么每一个线程进来都要进行一次获取锁和释放锁的操作,效率低且浪费资源。
volatile 关键字的作用是什么?
禁止指令重排。
instance = new Singleton();
这个操作实际上会分为三步执行:
- 为对象分配内存空间
- 初始化对象
- 将 instance 指向刚分配的内存地址
由于 JVM 具有指令重排的特性,实际上执行顺序可能会变为 1->3->2。单线程环境下指令重排没问题,多线程环境下是有可能出现问题的,比如当线程 T1 执行了 1 和 3,但 2 还没执行完时,如果此时线程 T2 调用 getInstance() 方法,发现 instance 不为 null,便会直接返回 instance ,但此时 instance 还未实例化完成,在使用时有可能会出现问题。
073、构造器可以使用 synchronized 修饰吗?
不可以,构造器本身就是线程安全的,不存在同步构造器这一说。
构造器只能被访问修饰符修饰,其他都不可以。
074、synchronized 底层实现?
synchronized 修饰代码块
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
synchronized 修饰代码块时使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指向同步代码块开始的位置,当执行 monitorenter
指令时,线程会试图获取锁也就是对象监视器 monitor 的持有权,如果成功获取锁会将锁的计数器加 1;monitorexit
指向同步代码块结束的位置,当执行 monitorexit
指令时,会将锁的计数器减 1,当锁的计数器变为 0 的时候,表明锁被释放。
synchronized 修饰方法
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized 修饰方法时使用 ACC_SYNCHRONIZED 标识,如果一个方法有该标识,JVM 会认定该方法为同步方法,在执行该方法前,会要求先获得对象锁;如果一个方法没有这个标识,JVM 会认定该方法为非同步方法,可以直接执行。
总之,两者的本质都是对对象监视器 monitor 的获取。
075、多个同步方法之间会相互影响吗?
会,两个不同的同步方法被两个线程分别调用,只要两个同步方法的对象锁一样,他们之间就会相互影响,代码如下:
public class Test {
public static void main(String[] args) {
new Thread(() -> {
ThreadMethod.methodA();
}).start();
new Thread(() -> {
ThreadMethod.methodB();
}).start();
}
}
class ThreadMethod {
public static synchronized void methodA() {
try {
System.out.println("methodA start");
Thread.sleep(5000);
System.out.println("5 秒过去了");
System.out.println("methodA end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void methodB() {
try {
System.out.println("methodB start");
Thread.sleep(1000);
System.out.println("1 秒过去了");
System.out.println("methodB end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制台输出如下:
methodA start
5 秒过去了
methodA end
methodB start
1 秒过去了
methodB end
076、谈谈 synchronized 和 ReentrantLock 的区别?
两者都是可重入锁,ReentrantLock 比 synchronized 多了一些高级功能,主要有以下几点:
- **线程在阻塞状态下可以响应中断。**ReentrantLock 提供了 lockInterruptibly() 方法,让线程可以在获取锁失败进入阻塞状态后仍然可以响应中断。详情参考文章:中断异常测试
- **可以实现公平锁。**ReentrantLock 提供了一个有参构造器,参数传入 true 表示是公平锁,传入 false 表示是非公平锁,使用方式如下:
Lock lock = new ReentrantLock(true);
- ReentrantLock 还提供了一个 tryLock() 方法,这个方法会尝试获取锁并立刻返回一个布尔值,成功获取锁就返回 true,失败的话就返回 false,但无论成功与否线程都会继续执行之后的代码(我们可以根据返回值进行判断,加一个 if 条件,返回值为 true 的话就执行之后的代码,为 false 的话就不执行);此外还有一个重载的
tryLock(long time, TimeUnit unit)
,该方法可以指定一个尝试时间。详情参考文章:Lock 之 tryLock - ReentrantLock 主要基于 CAS 实现,synchronized 则依赖于锁升级。
077、并发编程中的三个重要特性?
- 原子性:一个或多个操作为一个整体,要么都执行且不会受到任何因素的干扰而中断,要么都不执行,synchronized 可以保证代码块的原子性。
- 可见性:当多个线程共享同一变量时,若其中一个线程对该共享变量进行了修改,那么这个修改对其他线程是立即可见的。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
078、volatile 关键字的主要作用?
- 保证了可见性
- 禁止指令重排,保证了有序性
079、volatile 是如何保证可见性和有序性的?
Java 内存模型规定了所有的变量都存储在主内存中,同时每条线程都有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的副本,线程对该变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据,不同的线程之间也无法直接访问对方工作内存中的变量,不同线程之间变量值的传递均需要通过主内存来完成,如下图所示。
Java 内存模型对 volatile 变量定义了一些特殊规则,如下(V 表示一个 volatile 变量):
- 在工作内存中,每次使用 V 前都必须先从主内存中获取最新的值,用于保证能看见其他线程对 V 所做的修改
- 在工作内存中,每次修改 V 后都必须立刻同步回主内存中,用于保证其他线程可以看到当前线程对 V 所做的修改
- volatile 所修饰的变量不能被指令重排序优化,从而可以保证代码的执行顺序和编写顺序一样
前两条保证了可见性,第三条保证了原子性。
有一点需要注意,volatile 无法保证原子性,如果需要保证原子性,还是要借助 synchronized 和 ReentrantLock 等一些锁。
volatile 禁止指令重排有两层语义:
- 线程执行到 volatile 变量时,在其前面的操作都已完成,且结果对其后面的操作已经可见,并且后面的操作都还没有执行。
- 不能将 volatile 变量后面的语句放到其前面执行,也不能将其前面的语句放到后面执行。
080、volatile 的应用场景?
-
双重检测锁实现单例,使用 volatile 是为了禁止指令重排。
-
状态标记量
下面代码是为了保证可见性
volatile boolean flag = false; while(!flag){ doSomething(); } public void setFlag() { flag = true; }
下面代码是为了保证禁止指令重排
volatile boolean inited = false; // 线程1: // 加载资源 context = loadContext(); inited = true; // 线程2: while(!inited ){ sleep() } // 加载资源完后才能后才越过 while 循环到这里 doSomethingwithconfig(context);