目录
并发编程
一、请你谈谈你对volatile的理解
1、定义
volatile是java虚拟机提供的轻量级的同步机制
2、作用
volatile 关键字的主要作⽤:
保证变量的可⻅性
防⽌指令重排序。
3、底层原理
3.1JMM(Java内存模型)
JMM是一个抽象的概念,并不是真实的存在,它涵盖了缓冲区,寄存器以及其他硬件和编译器优化。
3.2从JVM指令解释有序性(内存屏障)
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
还是那句话,不能解决指令交错:
写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
而有序性的保证也只是保证了本线程内相关代码不被重排序
4.抛出问题:怎么解决原子性问题
CAS—>Atomic 原⼦类
synchronized
5.synchronized 和 volatile 的区别是什么?
volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
6.谈谈你在那些地方用过volatile
public final class Singleton
{
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance()
{
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null)
{
synchronized (Singleton.class)// t2
{
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) // t1
{
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
二、创建线程的方式
1、创建线程的四种方式
1.1继承 Thread 类;
1.2实现 Runnable 接口;
1.3实现Callable接口(可以获取线程执行之后的返回值)
1.4使用 Executors 工具类创建线程池
2、Thread 与 Runnable 的关系
Thread是把线程和任务合并在了一起
Runnable是把线程和任务分开了
用 Runnable 更容易与线程池等高级 API 配合
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
3、说一下 runnable 和 callable 有什么区别?
Runnable
public interface Runnable {
public abstract void run();
}
public interface Callable<V> {
V call() throws Exception;
}
相同点:
都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
主要区别
Runnable 接口 run 方法无返回值;Callable 接口 call
方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果 Runnable 接口 run
方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
三、说说线程的⽣命周期和状态?
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调⽤ start() ⽅法后开始运⾏,线程
这时候处于 READY(可运⾏) 状态。可运⾏状态的线程获得了 CPU 时间⽚(timeslice)后就处于
RUNNING(运⾏) 状态。
当线程执⾏ wait() ⽅法之后,线程进⼊ WAITING(等待) 状态。进⼊等待状态的线程需要依靠其他
线程的通知才能够返回到运⾏状态,⽽ TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,⽐如通过 sleep(long millis) ⽅法或 wait(long millis) ⽅法可以将 Java
线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调
⽤同步⽅法时,在没有获取到锁的情况下,线程将会进⼊到 BLOCKED(阻塞) 状态。线程在执⾏
Runnable 的 run() ⽅法之后将会进⼊到 TERMINATED(终⽌) 状态
四、谈谈你对线程安全的理解
1、我对线程安全的理解
当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的
2、什么时候需要考虑线程安全(举例)
那么,我们开发中,如果需要拼接字符串,使用StringBuilder还是StringBuffer?
场景一:
如果是多个线程访问同一个资源,那么就需要上锁,才能保证数据的安全性。
这个时候如果使用的是非线程安全的对象,比如StringBuilder,那么就需要借助外力,给他加synchronized关键字。或者直接使用线程安全的对象StringBuffer
场景二:
如果每个线程访问的是各自的资源,那么就不需要考虑线程安全的问题,所以这个时候,我们可以放心使用非线程安全的对象,比如StringBuilder
比如在方法中,创建对象,来实现字符串的拼接。
看场景,如果我们是在方法中使用,那么建议在方法中创建StringBuilder,这时候相当是每个线程独立占有一个StringBuilder对象,不存在多线程共享一个资源的情况,所以我们可以安心使用,虽然StringBuilder本身不是线程安全的。
总结:什么时候需要考虑线程安全?
1,多个线程访问同一个资源
2,资源是有状态的,比如我们上述讲的字符串拼接,这个时候数据是会有变化的
3、怎么保证线程安全
加锁
五、说说 sleep() ⽅法和 wait() ⽅法区别和共同点?
两者最主要的区别在于:sleep ⽅法没有释放锁,⽽ wait ⽅法释放了锁 。
两者都可以暂停线程的执⾏。
Wait 通常被⽤于线程间交互/通信,sleep 通常被⽤于暂停执⾏。
wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或者
notifyAll() ⽅法。sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(long
timeout)超时后线程会⾃动苏醒。
六、谈谈你对CAS的理解
1.定义
CAS是自旋锁(轻量级锁)最常用的实现方式,是一种乐观锁
2.机制
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
3.底层原理
自旋锁
举例:AtomicInteger
this---->当前AtomicInteger对象
valueOffset—>内存地址偏移量 即内存中地址的值
var1 :当前AtomicInteger对象
var2: 内存中的值
var4:自己工作内存中的值
JAVA实现CAS的原理:
compareAndSwapInt是借助C来调用CPU底层指令实现的。
再往下就是硬件方面的了, 无需了解
4.为什么用cas不用sychonized
关键在于性能问题。
synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
而Atomic操作类的底层正是用到了“CAS机制”。
CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
5.CAS的缺点
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3) ABA问题
这是CAS机制最大的问题所在。(后面有介绍)
6.谈谈原子类AtomicInteger的ABA问题?原子更新引用知道吗
AtomicStampedReference
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
1.//先工作内存中拿到的值和内存中的值比较
expectedReference == current.reference &&
2//工作内存中拿到的版本号和内存中的版本号比较
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
举例说:JMM解释。
七、ReentrantLock详解
参考链接:
(JDK)ReentrantLock手撕AQS
JUC锁: ReentrantLock详解
1.什么是可重入,什么是可重入锁? 它用来解决什么问题?
什么是可重入
简单地讲就是:“同一个线程对于已经获得到的锁,可以多次继续申请到该锁的使用权”
正经地讲就是:假如访问一个资源A需要获得其锁lock,如果之前没有其他线程获取该锁,那么当前线程就获锁成功,此时该线程对该锁后续所有“请求”都将立即得到“获锁成功”的返回,即同一个线程可以多次成功的获取到之前获得的锁。“可重入”可以解释成“同一个线程可多次获取”。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// state--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 支持锁重入, 只有 state 减为 0, 才释放成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}