ThreadLocal 用过么,用途是什么,原理是什么,用的时候要注意什么?
(1)每个Thread维护着一个ThreadLocalMap的引用
(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。
(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中
(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。
(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
CAS机制?
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。CAS机制当中使用了3个基本操作数:
(1)内存地址V,也就是AtomicInteger中的valueOffset。
(2)旧的预期值A,也就是getAndIncrement方法中的current。
CAS机制中,更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。下面我们来看一个具体的例子:
(1)在内存地址V当中,存储着值为10的变量。
(2)此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
(3)但是,在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
(4)此时,线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
(5)线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
(6)这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
对比Synchronized,我们可以发现,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
CAS机制缺点:
(1)ABA问题
如果V的初始值是A,在准备赋值的时候检查到它仍然是A,那么能说它没有改变过吗?也许V经历了这样一个过程:它先变成了B,又变成了A,使用CAS检查时,以为它没变,其实却已经改变过了。
(2)CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
(3)不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
怎么解决ABA?
使用比较旧的内存地址中的值以及版本号一起比较就好了。AtomicStampedReference类就实现了用版本号做比较的CAS机制
AtomicInteger(AtomicLong、AtomicReference)的内部实现:存在ABA问题
public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } }
这个方法的做法为先获取到当前的 value 属性值,然后将 value 加 1,赋值给一个局部的 next 变量,然而,这两步都是非线程安全的,但是内部有一个死循环,不断去做compareAndSet操作,直到成功为止,也就是修改的根本在compareAndSet方法里面。compareAndSet()方法的代码如下:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
传入的为执行方法时获取到的 value 属性值,update为加 1 后的值,compareAndSet所做的为调用 Sun 的 UnSafe 的 compareAndSwapInt方法来完成,此方法为 native 方法,compareAndSwapInt 基于的是CPU 的 CAS指令来实现的
线程池的关闭方式:
shutdown()
调用后,不可以再 submit 新的 task,已经 submit 的将继续执行shutdownNow()
调用后,试图停止当前正在执行的 task,并返回尚未执行的 task 的 list
Lock与Sychronized区别?
-
来源:
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现; -
异常是否释放锁:
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。) -
是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断; -
是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能; -
Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
-
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
-
synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度
如果让你实现一个并发安全的链表,你会怎么做?
可以使用读写锁ReentrantReadWriteLock(int 高低16位来实现)
static Map<String, Object> map = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();//获取一个key对应的value
public static final Object get(String key){
r.lock();
try { return map.get(key);
} finally {
r.unlock();
}
}//设置key对应的value,并返回旧的value
public static final Object put(String key,Object value){
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}}
锁升级过程?
markword 中存储有
无锁
偏向锁
(存储threadid,每次比较threadid 来判断要不要升级为轻量级)、
轻量级锁
(自旋,没有获取到的线程自旋,而不是阻塞,如果很多自旋次数很多,就会升级重量锁)
重量级锁
(直接阻塞,阻塞需要从用户态到内核态转换,消耗cpu)
jvm按照线程并发的情况选择 四种的一种锁
countdowlatch 和 cyclicbarrier 的内部原理和用法,以及相互之间的差别(比如countdownlatch 的 await 方法和是怎么实现的)?
1.CountDownLatch减计数,CyclicBarrier加计数。
2.CountDownLatch是一次性的,CyclicBarrier可以重用。
CountDownLatch Await方法解析:
// 此方法用来让当前线程阻塞,直到count减小为0才恢复执行
public void await() throws InterruptedException {
// 这里直接调用sync的acquireSharedInterruptibly方法,这个方法定义在AQS中
// 方法的作用是尝试获取共享锁,若获取失败,则线程将会被加入到AQS的同步队列中等待
// 直到获取成功为止。且这个方法是会响应中断的,线程在阻塞的过程中,若被其他线程中断,
// 则此方法会通过抛出异常的方式结束等待。
sync.acquireSharedInterruptibly(1);
}
/** 此方法是AQS中提供的一个模板方法,用以获取共享锁,并且会响应中断 */
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
// 首先判断当前线程释放被中断,若被中断,则直接抛出异常结束
if (Thread.interrupted()) throw new InterruptedException();
// 调用tryAcquireShared方法尝试获取锁,这个方法被Sycn类重写了,
// 若count == 0,则这个方法会返回1,表示获取锁成功,则这里会直接返回,线程不会被阻塞
// 若count < 0,将会执行下面的doAcquireSharedInterruptibly方法,
// 此处请去查看Sync中tryAcquireShared方法的实现
if (tryAcquireShared(arg) < 0)
// 下面这个方法的作用是,线程获取锁失败,将会加入到AQS的同步队列中阻塞等待,
// 直到成功获取到锁,而此处成功获取到锁的条件就是count == 0,若当前线程在等待的过程中,
// 成功地获取了锁,则它会继续唤醒在它后面等待的线程,也尝试获取锁,
// 这也就是说,只要count == 0了,则所有被阻塞的线程都能恢复运行
doAcquireSharedInterruptibly(arg); }
CountDownLatch CountDown方法解析:
/** * 此方法的作用就是将count的值-1,如果count等于0了,就唤醒等待的线程 */
public void countDown() {
// 这里直接调用sync的releaseShared方法,这个方法的实现在AQS中,也是AQS提供的模板方法,
// 这个方法的作用是当前线程释放锁,若释放失败,返回false,若释放成功,则返回false,
// 若锁被释放成功,则当前线程会唤醒AQS同步队列中第一个被阻塞的线程,让他尝试获取锁
// 对于CountDownLatch来说,释放锁实际上就是让count - 1,只有当count被减小为0,
// 锁才是真正被释放,线程才能继续向下运行
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 调用tryReleaseShared尝试释放锁,这个方法已经由Sycn重写,请回顾上面对此方法的分析
// 若tryReleaseShared返回true,表示count经过这次释放后,等于0了,于是执行doReleaseShared
if (tryReleaseShared(arg)) {
// 这个方法的作用是唤醒AQS的同步队列中,正在等待的第一个线程
// 而我们分析acquireSharedInterruptibly方法时已经说过,
// 若一个线程被唤醒,检测到count == 0,会继续唤醒下一个等待的线程
// 也就是说,这个方法的作用是,在count == 0时,唤醒所有等待的线程
doReleaseShared();
return true;
}
return false;
}
了解AQS?(队列同步器,非常方便的帮我们实现共享锁和排它锁,如countdownlatch利用的就是AQS的共享锁)
AQS通过一个同步队列来维护当前获取锁失败,进入阻塞状态的线程。这个同步队列是一个双向链表,获取锁失败的线程会被封装成一个链表节点,加入链表的尾部排队,而AQS
保存了链表的头节点的引用head
以及链表的尾节点引用tail
独占锁获取锁源码分析:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上面的方法执行流程如下:
-
首先调用
tryAcquire(是一个模板方法,我们自己去实现)
尝试获取一次锁,若返回true
,表示获取成功,则acquire
方法将直接返回;若返回false
,则会继续向后执行acquireQueued
方法; -
tryAcquire
返回false
后,将执行acquireQueued
,但是这个方法传入的参数调用了addWaiter
方法; -
addWaiter
方法的作用是将当前线封装成同步队列的节点,然后加入到同步队列的尾部进行排队,并返回此节点; -
addWaiter
方法执行完成后,将它的返回值作为参数,调用acquireQueued
方法。acquireQueued
方法的作用是让当前线程在同步队列中阻塞,然后在被其他线程唤醒时去获取锁; -
若线程被唤醒并成功获取锁后,将从
acquireQueued
方法中退出,同时返回一个boolean
值表示当前线程是否被中断,若被中断,则会执行下面的selfInterrupt
方法,响应中断;
addWaiter方法逻辑:将新线程封装成一个节点,加入到同步队列的尾部,若同步队列为空,则先在其中加入一个默认的节点,再进行加入;若加入失败,则使用死循环(也叫自旋)不断尝试,直到成功为止
acquireQueued方法逻辑:让线程在同步队列中阻塞,直到它成为头节点的下一个节点,被头节点对应的线程唤醒,然后开始获取锁,若获取成功才会从方法中返回
释放独占锁源码分析:
public final boolean release(int arg) { // 调用tryRelease尝试修改state释放锁,若成功,将返回true,否则false if (tryRelease(arg)) { // 若修改state成功,则表示释放锁成功,需要将当前线程移出同步队列 // 当前线程在同步队列中的节点就是head,所以此处记录head Node h = head; // 若head不是null,且waitStatus不为0,表示它是一个装有线程的正常节点, // 在之前提到的addWaiter方法中,若同步队列为空,则会创建一个默认的节点放入head // 这个默认的节点不包含线程,它的waitStatus就是0,所以不能释放锁 if (h != null && h.waitStatus != 0) // 若head是一个正常的节点,则调用unparkSuccessor唤醒它的下一个节点所对应的线程 unparkSuccessor(h); // 释放成功 return true; } // 释放锁失败 return false; }
release
也是一个模板方法,其中通过调用tryRelease
尝试释放锁,而tryRelease
也需要使用者自己实现。在之前也说过,头节点释放锁时,需要唤醒它的下一个节点对应的线程,让这个线程不再等待,去获取锁,而这个过程就是通过unparkSuccessor
方法实现的。
自己实现一个共享锁(读锁)?(利用AQS即可)代码如下:
/** * 抽象队列同步器(AQS)使用: * 实现一个同一时刻至多只支持两个线程同时执行的同步器 */ // 让当前类继承Lock接口 public class TwinLock implements Lock { // 定义锁允许的最大线程数 private final static int DEFAULT_SYNC_COUNT = 2; // 创建一个锁对象,用以进行线程同步,Sync继承自AQS private final Sync sync = new Sync(DEFAULT_SYNC_COUNT); // 以内部类的形式实现一个同步器类,也就是锁,这个锁继承自AQS private static final class Sync extends AbstractQueuedSynchronizer { // 构造方法中指定锁支持的线程数量 Sync(int count) { // 若count小于0,则默认为2 if (count <= 0) { count = DEFAULT_SYNC_COUNT; } // 设置初始同步状态 setState(count); } /** * 重写tryAcquireShared方法,这个方法用来修改同步状态state,也就是获取锁 */ @Override protected int tryAcquireShared(int arg) { // 循环尝试 for (; ; ) { // 获取当前的同步状态 int nowState = getState(); // 计算当前线程获取锁后,新的同步状态 // 注意这里使用了减法,因为此时的state表示的是还能支持多少个线程 // 而当前线程如果获得了锁,则state就要减小 int newState = nowState - arg; // 如果newState小于0,表示当前已经没有剩余的资源了 // 则当前线程不能获取锁,此时将直接返回小于0的newState; // 或者newState>0,就会执行compareAndSetState方法修改state的值, // 若修改成功将,将返回大于0的newState; // 若修改失败,则表示有其他线程也在尝试修改state,此时循环一次后,再次尝试 if (newState < 0 || compareAndSetState(nowState, newState)) { return newState; } } } /** * 尝试释放同步状态 */ @Override protected boolean tryReleaseShared(int arg) { for (; ; ) { // 获取当前同步状态 int nowState = getState(); // 计算释放后的新同步状态,这里使用加法, // 表示有线程释放锁后,当前锁可以支持的线程数量增加了 int newState = nowState + arg; // 使用CAS修改同步状态,若成功则返回true,否则自旋 if (compareAndSetState(nowState, newState)) { return true; } } } } /** * 获取锁的方法 */ @Override public void lock() { // 这里调用的是AQS的模板方法acquireShared, // 在acquireShared中将调用我们重写的tryAcquireShared方法 // 传入参数为1表示当前线程,当前线程获取锁后,state将-1 sync.acquireShared(1); } /** * 解锁 */ @Override public void unlock() { // 这里调用的是AQS的模板方法releaseShared, // 在acquireShared中将调用我们重写的tryReleaseShared方法 // 传入参数为1表示当前线程,当前线程释放锁后,state将+1 sync.releaseShared(1); } }
LinkedBlockingQueue与ConcurrentLinkedQueue区别?都是线程安全的队列
- 阻塞队列,典型例子是
LinkedBlockingQueue
适用阻塞队列的好处:多线程操作共同的队列时不需要额外的同步,另外就是队列会自动平衡负载,即哪边(生产与消费两边)处理快了就会被阻塞掉,从而减少两边的处理速度差距。采用Lock,condition实现的 - 非阻塞队列,典型例子是
ConcurrentLinkedQueue
当许多线程共享访问一个公共集合时,ConcurrentLinkedQueue
是一个恰当的选择。Queue
中元素按 FIFO 原则进行排序。采用 CAS操作,来保证元素的一致性