问:synchronized和ReentrantLock的区别?
ReentrantLock(可重入锁)
- 位于java.util.concurrent.locks包(著名的juc包是由Doug lea大神写的AQS抽象类框架衍生出来的应用)
- 和CountDownLatch、FutureTask、Semaphore一样基于AQS实现
- 能够实现比synchronized更细粒度的控制,如控制fairness
- 调用lock()后,必须调用unlock()释放锁
- 性能未必比synchronized高,并且也是可重入的
ReentrantLock公平性设置
ReentrantLock fairLock = new ReentrantLock(true);
参数为ture时,倾向于将锁赋予等待时间最久的线程
公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用,通常公平性没有想象的那么重要,java默认的调用策略很少会有饥饿情况的发生,与此同时若要保证公平性,会增加额外的开销,导致一定的吞吐量下降)
非公平锁:获取锁的顺序是无序的,synchronized是非公平锁
例子:
package interview.thread;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author: cctv
* @Date: 2019/5/21 11:46
*/
public class ReentrantLockDemo implements Runnable {
private static ReentrantLock lock = new ReentrantLock(false);
@Override
public void run() {
while (true) {
lock.lock();
System.out.println(Thread.currentThread().getName() + " get lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLockDemo rtld = new ReentrantLockDemo();
Thread t1 = new Thread(rtld);
Thread t2 = new Thread(rtld);
t1.start();
t2.start();
}
}
公平锁 new ReentrantLock(true);
非公平锁 new ReentrantLock(false);
ReentrantLock将锁对象化
- 判断是否有线程,或者某个特定线程再排队等待获取锁
- 带超时的获取锁尝试
- 感知有没有成功获取锁
是否能将wait\notify\notifyAll对象化
- java.util.concurrent.locks.Condition
总结synchronized和ReentrantLock的区别:
- synchronized是关键字,ReentrantLock是类
- ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
- ReentrantLock可以获取各种锁信息
- ReentrantLock可以灵活的实现多路通知
- 机制:synchronized操作MarkWord,ReentrantLock调用Unsafe类的park()方法
问:什么是Java内存模型中的happens-before?
java内存模型(即Java Memory Model 简称JMM)是一种抽象概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM中的主内存
- 存储java实例对象
- 包括成员变量、类信息、常量、静态变量
- 属于数据共享区域,多线程并发操作时会引发线程安全问题
JMM中的工作内存
- 存储当前方法的所有本地变量信息,本地变量对其他线程不可见(工作内存是存储的主内存的变量的拷贝)
- 字节码行号指示器、native方法信息
- 属于线程私有的数据区域,不存在线程安全问题
JMM和java内存区域划分是不同的概念层次:
- JMM描述的是一组规则,围绕原子性、有序性、可见性展开
- 相似点:都存在共享区域和私有区域
主内存和工作内存的数据存储类型以及操作方式归纳:
- 方法里的基本数据类型本地变量将直接存储在工作内存的栈帧结构中
- 引用类型的本地变量:引用存储在工作内存中,实例存储在主内存中
- 成员变量、static变量、类信息均会被存储在主内存中
- 主内存共享的方式线程各拷贝一份数据到工作内存,操作完成后刷新回主内存
JMM如何解决可见性问题?
首先要讲下重排序:在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
指令重排序需要满足的条件:
- 单线程环境下不能改变程序运行的结果
- 存在数据依赖关系的不允许重排序(无法通过happens-before原则推导出来的,才能进行指令重排序)
happens-before的八大原则:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
如果两个操作不满足上述任意一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序
如果操作A happens-before B 那么操作A在内存上所做的操作对操作B都是可见的
volatile:jvm提供的轻量级同步机制,解决了内存可见性问题,但并不是线程安全的(可以配合synchronized达到线程安全目的)
- 保证被volatile修饰的共享变量对所有线程总是可见的
- 禁止指令重排序优化
问:volatile变量如何立即可见?
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,该线程只能从主内存中读取变量
问:volatile变量如何禁止重排优化?
内存屏障(Memory Barrier)
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性
通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化,强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
package interview.thread;
/**
* 单例模式的双重检测实现
*
* @Author: cctv
* @Date: 2019/5/21 17:19
*/
public class Singleton {
// 禁止指令重排序优化
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
//第一次检测
if (instance == null) {
//同步
synchronized (Singleton.class) {
// 第二次检测
if (instance == null) {
// 多线程环境下可能会出现问题的地方(会出现指令重排序,导致instance先赋值后初始化Singleton)
instance = new Singleton();
}
}
}
return instance;
}
}
volatile和synchronized的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
问:谈谈 CAS(Compare and Swap)?
一种高效实现线程线程安全的方法
- 支持原子更新操作,适用于计数器,序列发生器等场景
- 属于乐观锁机制,号称lock-free(其实底层还是有加锁)
- CAS操作失败时由开发者决定是否继续尝试,还是执行别的操作,所以失败线程不会阻塞挂起
cas思想:
包含三个操作数--内存位置V 预期原值A 和 新增B
- 直接使用JUC的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选
- Unsafe类虽提供了CAS服务,但因为能够操纵任意内存地址的读写而有隐患
- java9以后可以使用Variable Handle API来替代Unsafe
缺点:
- 若循环时间长,则开销很大
- 只能保证一个共享变量的原子操作
- ABA问题(解决方法:AtomicStampedReference,它可以控制变量的版本保证正确性)