50.volatile(面试过阔睿)?
工作内存和主内存
变量定义时候前面加volatile 修饰的话,那么线程只要修改变量,就会在修改完本地内存的变量以后,强制将变量最新的值刷回主内存,让主内存的值变成最新的值。
如果此时别的线程的有工作内存的变量的本地缓存,也就是一个变量的副本,会强制让其他线程的工作内存变量直接缓存失效过期,不在允许读取和使用
如果线程在代码运行过程中需要再次读取变量值,从本地工作内存直接读取,会发现已经过期了。
然后她重新从主内存中加载最新的变量值,读取到主内存的新的值了。对⼀个变量加了 volatile 关键字修饰之后,只要⼀个线程修改了这个变量的值,⽴⻢强制刷回主内存。
接着强制过期其他线程的本地⼯作内存中的缓存,最后其他线程读取变量值的时候,强制重新从主内存来加载最新的值!
这就类似于MySQL的读取未提交的事务隔离级别,防止读取脏数据。
51.ThreadLocal 是什么?有哪些使用场景?
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
ThreadLocalMap(线程的一个属性)
1. 每个线程中都有一个自己的ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
2. 将一个共用的ThreadLocal 静态实例作为key,将不同对象的引用保存到不同线程的
ThreadLocalMap 中,然后在线程执行的各处通过这个静态ThreadLocal 实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
- ThreadLocalMap 其实就是线程里面的一个属性,它在Thread 类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;
52.说一下 synchronized 底层实现原理?
Synchronized 作用范围
- 作用于方法时,锁住的是对象的实例(this);
public class Synchronized {
public synchronized void husband(){
}
}
- 当作用于静态方法时,锁住的是Class 实例,又因为Class 的相关数据存储在永久带PermGen(jdk1.8 则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
public class Synchronized {
public void husband(){
synchronized(Synchronized.class){
}
}
}
3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
public class Synchronized {
public void husband(){
synchronized(new test()){
}
}
}
同步代码块
⼤家可以看到⼏处我标记的,我在最开始提到过对象头,他会关联到⼀个monitor对象。
当我们.⼊.个.⽅法的时候,执行monitorenter,就会获取当前一个对象的所有权,这个时候,monitor进入数为1,当前的这个线程就是这个monitor的owner;
如果你已经是这个monitor的owner了,你再次进入,就会吧这个进入数+1;
同理,当它执行完monitoreixt,对应的进入数就-1,直到为0,才可以被其他线程持有。
所有的互斥,其实在这里就是在看能否获得monitor的所有权,一旦成为owner就是获得者。
同步方法
不知道⼤家注意到⽅法的特殊标志位,ACC_SYNCHRONIZED;
同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会隐式调用刚才的两个指令:monitorente和monitorexit。
所以归根结底,还是monitor对象的争夺。
synchronized底层的源码就就是引入了ObjectMonitor,锁升级的过程就是调用不同的实现获取锁,失败就调用更高级的实现,最后升级完成。
Synchronized 核心组件
1) Wait Set:哪些调用wait 方法被阻塞的线程被放置在这里;
2) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到Entry List 中;
4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
5) Owner:当前已经获取到所资源的线程被称为Owner;
6) !Owner:当前释放锁的线程。
Synchronized 实现
1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,
ContentionList 会被大量的并发线程进行CAS 访问,为了降低对尾部元素的竞争,JVM会将
一部分线程移动到EntryList 中作为候选竞争线程。
2. Owner 线程会在unlock 时,将ContentionList 中的部分线程迁移到EntryList 中,并指定
EntryList 中的某个线程为OnDeck 线程(一般是最先进去的那个线程)。
3. Owner 线程并不直接把锁传递给OnDeck 线程,而是把锁竞争的权利交给OnDeck,
OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在
JVM 中,也把这种选择行为称之为“竞争切换”。
4. OnDeck 线程获取到锁资源后会变为Owner 线程,而没有得到锁资源的仍然停留在EntryList
中。如果Owner 线程被wait 方法阻塞,则转移到WaitSet 队列中,直到某个时刻通过notify
或者notifyAll 唤醒,会重新进去EntryList 中。
5. 处于ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统
来完成的(Linux 内核下采用pthread_mutex_lock 内核函数实现的)。
6. Synchronized 是非公平锁。 Synchronized 在线程进入ContentionList 时,等待的线程会先
尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是
不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck 线程的锁
资源。
参考:https://blog.youkuaiyun.com/zqz_zqz/article/details/70233767
7. 每个对象都有个monitor 对象,加锁就是在竞争monitor 对象,代码块加锁是在前后分别加
上monitorenter 和monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线
程加锁消耗的时间比有用操作消耗的时间更多。
9. Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向
锁等,效率有了本质上的提高。在之后推出的Java1.7 与1.8 中,均对该关键字的实现机理做
了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。
53.synchronized 和 Lock 有什么区别?
synchronized是关键字,是JVM层⾯的底层啥都帮我们做了,而LOCK是接口,是JDK层面有的丰富的API。
synchronized会⾃动释放锁,⽽Lock必须手动释放锁。
synchronized是不可中断的,Lock可以中断也可以不中断。
通过Lock可以知道线程有没有拿到锁,而synchronized不能
Lock可以使用读锁提高线程读效率
synchronized是⾮公平锁,ReentarantLocal可以控制是否是公平锁。
54.RenntrantLock
ReentrantLock但是在介绍.玩意之前,我觉得我有必要先介绍
AQS:也就是队列同步器,这是实现RentrantLock的基础。
AQS有一个state标记位,值为1时表示线程有占用,其他线程需要进入到同步队列等待,同步队列是一个双向列表。
当获得锁的线程需要等待某个条件时,会进⼊ condition 的等待队列,等待队列可以有多个。当 condition 条件满⾜时,线程会从等待队列重新进⼊同步队列进⾏获取锁的竞争。
ReentrantLock 就是基于 AQS 实现的,如下图所示,ReentrantLock 内部有公平锁和⾮公平锁两种实现,差别就在于新来的线程是否⽐已经在同步队列中的等待线程更早获得锁。
和 ReentrantLock 实现⽅式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁。
从图中可以看到,ReentranLock里面有一个内部类Sync,Sync继承AQS,AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的,它有公平锁FairSync和非公平锁NonfairSync两个子类。
Reentrantlock默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。
54.synchronized 和 volatile 的区别是什么?
55.synchronized 和 ReentrantLock 区别是什么?
1. ReentrantLock 通过方法lock()与unlock()来进行加锁与解锁操作,与synchronized 会
被JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出
现异常而无法正常解锁的情况,使用ReentrantLock 必须在finally 控制块中进行解锁操
作。
2. ReentrantLock 相比synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要
使用ReentrantLock。
56.说一下 atomic 的原理?
59.乐观锁和悲观锁?
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block 直到拿到锁。java 中的悲观锁就是Synchronized,AQS 框架下的锁则是先尝试cas 乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
59.自旋锁?
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),
等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗cup 的,说白了就是让cup 在做无用功,如果一直获取不到锁,那线程
也不能一直占用cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁
的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来
说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会
导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合
使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu 做无用功,占着XX 不XX,同时有大量
线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,
其它需要cup 的线程又不能获取到cpu,造成cpu 的浪费。所以这种情况下我们要关闭自旋锁;
自旋锁阈值1.6 引入了适应性自旋锁
自旋锁的目的是为了占着CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择
自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在1.6 引入了适应性自旋锁,适应
性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥
有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM 还针对当
前CPU 的负荷情况做了较多的优化,如果平均负载小于CPUs 则一直自旋,如果有超过(CPUs/2)
个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现Owner 发生了变化则延迟自旋
时间(自旋计数)或进入阻塞,如果CPU 处于节电模式则停止自旋,自旋时间的最坏情况是CPU
的存储延迟(CPU A 存储了一个数据,到CPU B 得知这个数据直接的时间差),自旋时会适当放
弃线程优先级之间的差异。
JDK1.6 中-XX:+UseSpinning 开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7 后,去掉此参数,由jvm 控制;
60.公平锁非公平锁?
JVM 按随机、就近原则分配锁的机制则称为不公平锁,ReentrantLock 在构造函数中提供了
是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,
ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
61.CyclicBarrier以及和CountDownLatch的区别源码解析
CyclicBarrier以及和CountDownLatch的区别
CountdownLatch阻塞主线程,等所有子线程完结了再继续下去。Cyclicbarrier阻塞一组线程,直至某个状态之后再全部同时执行,并且所有线程都被释放后,还能通过reset来重用。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)也叫线程栅栏。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。
对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的(reset()方法重置屏障点)。这一点与CountDownLatch不同
CyclicBarrier
是一种同步机制允许一组线程相互等待,等到所有线程都到达一个屏障点才退出await方法,它没有直接实现AQS而是借助ReentrantLock来实现的同步机制。它是可循环使用的,而CountDownLatch是一次性的,另外它体现的语义也跟CountDownLatch不同,CountDownLatch减少计数到达条件采用的是release方式,而CyclicBarrier走向屏障点(await)采用的是Acquire方式,Acquire是会阻塞的,这也实现了CyclicBarrier的另外一个特点,只要有一个线程中断那么屏障点就被打破,所有线程都将被唤醒(CyclicBarrier自己负责这部分实现,不是由AQS调度的),这样也避免了因为一个线程中断引起永远不能到达屏障点而导致其他线程一直等待。屏障点被打破的CyclicBarrier将不可再使用(会抛出BrokenBarrierException)除非执行reset操作。
CyclicBarrier源码分析
构造方法
CyclicBarrier提供两个构造方法CyclicBarrier(int parties)和CyclicBarrier(int parties, Runnable barrierAction):
第一个参数是CyclicBarrier要阻塞线程的数量,
CyclicBarrier(int parties, Runnable barrierAction)
由于线程之前的调度是由CPU决定的,所以默认的构造方法无法设置线程执行优先级,CyclicBarrier提供一个更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达同步点时,优先执行线程barrierAction,这样可以更加方便的处理一些高优先级的线程。
创建CyclicBarrier后,每个线程调用await方法告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞。接下来我们来看看await方法的具体实现。
由此看出await方法实际是调了一个dowait()方法,接下来我们来看下dowait方法都做了什么事情
在dowait的前段部分,主要完成了当所有线程都到达同步点(barrier)时,唤醒所有的等待线程,一起往下继续运行,可根据参数barrierAction决定优先执行的线程
在dowait的实现后半部分,主要实现了线程未到达同步点(barrier)时,线程进入Condition自旋等待,直到等待超时或者所有线程都到达barrier时被唤醒。
总结:
使用ReentrantLock保证每一次操作线程安全;
线程等待/唤醒使用Lock配合Condition来实现;
线程被唤醒的条件:等待超时或者所有线程都到达barrier。
CountDownLatch
使用AQS为基础进行操作线程,必须要重写AQS的两个方法 因为父类对应方法直接抛出异常。用于当一些线程执行完毕后,另一个线程才开始执行。比如赛跑,每个运动员跑完之后,才能宣布结束。
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc)) // 使用CAS操作个数减1
return nextc == 0;
}
}
}
private final Sync sync;
await
执行await会将当前线程添加到AQS的node链表中,因为同一个countDownLatch对象可以阻塞多个线程,所以使用的是共享锁。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) // 仅当state为0时会大于0 意思是: 如果await执行时 state仍然大于0 则将线程放入阻塞队列
doAcquireSharedInterruptibly(arg);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireSharedInterruptibly
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 创建共享线程节点 并使用自旋添加到队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
// 获取前置节点
final Node p = node.predecessor();
if (p == head) {
// 如果是第二节点 执行一次tryAcquireShared方法
int r = tryAcquireShared(arg);
if (r >= 0) { // 将自己头节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
countDown
每次执行countDown方法后,state会减1,当减至0时,开始唤起阻塞线程
public void countDown() {
sync.releaseShared(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 当state被减至0的时候 开始唤起Node
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) { // 如果只有一个线程被阻塞,初始化会创建一个空的header节点
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h); // 唤起线程
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒当前节点
}
62.Semaphore 信号量
Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池。
实现互斥锁 计数器为 1
我们也可以创建计数为1 的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。
// 创建一个计数阈值为5 的信号量对象
// 只能5 个线程同时访问
Semaphore semp = new Semaphore(5);
try { // 申请许可
semp.acquire();
try {
// 业务逻辑
} catch (Exception e) {
} finally {
// 释放许可
semp.release();
}
} catch (InterruptedException e) {
}
Semaphore 与ReentrantLock
Semaphore 基本能完成ReentrantLock 的所有工作,使用方法也与之类似,通过acquire()与
release()方法来获得和释放临界资源。经实测,Semaphone.acquire()方法默认为可响应中断锁,
与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被
Thread.interrupt()方法中断。
此外,Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire 与tryLock
不同,其使用方法与ReentrantLock 几乎一致。Semaphore 也提供了公平与非公平锁的机制,也
可在构造函数中进行设定。
Semaphore 的锁释放操作也由手动进行,因此与ReentrantLock 一样,为避免线程因抛出异常而
无法正常释放锁的情况发生,释放锁的操作也必须在finally 代码块中完成。
63.AtomicInteger
首先说明, 此处AtomicInteger , 一个提供原子操作的Integer 的类, 常见的还有
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,
区别在与运算对象类型的不同。令人兴奋地,还可以通过AtomicReference<V>将一个对象的所
有操作转化成原子操作。
我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。
通常我们会使用synchronized 将该操作变成一个原子操作,但JVM 为此类操作特意提供了一些
同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger
的性能是ReentantLock 的好几倍。