并发编程专栏
1.TOMCAT性能优化整理
2.JVM性能优化整理
3.MYSQL性能优化整理
4.并发编程整理(1)
5.并发编程整理(2)
文章目录
- 并发编程专栏
- 并发编程整理(1)
- 1、Synchronized用过吗,其原理是什么?
- 2、你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?
- 3、什么是可重入性,为什么说Synchronized是可重入锁?
- 4、JVM对Java的原生锁做了哪些优化?
- 5、为什么说Synchronized是非公平锁?
- 6、什么是锁消除和锁粗化?
- 7、为什么说Synchronized是-个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么特性?
- 8、乐观锁一定就是好的吗?
- 9、跟Synchronized相比,可重入锁ReentrantLock其实现原理有什么不同?
- 10、那么请谈谈AQS框架是怎么回事儿?
- 11、请尽可能详尽地对比下Synchronized和ReentrantLock的异同。
- 12、ReentrantLock 是如何实现可重入性的?
- 13、除了ReetrantLock, 你还接触过JUC中的哪些并发工具?
- 14、请谈谈ReadWriteLock和StampedLock。
- 15、如何让Java的线程彼此同步?你了解过哪些同步器?请分别介绍下。
- 16、CyclicBarrier和CountDownLatch看起来很相似,请对比下呢?
并发编程整理(1)
1、Synchronized用过吗,其原理是什么?
这是一道Java面试中几乎百分百会问到的问题,因为没有任何写过并发程序的开发者会没听说或者没接触过Synchronized。Synchronized是 由JVM实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块, 在编译前后被编译器生成了monitorenter和monitorexit两个字节码指令。这两个指令是什么意思呢?在虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+ 1;当执行monitorexit指令时将锁计数器-1;当计数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。Java中Synchronize通过在对象头设置标记,达到了获取锁和释放锁的目的。
2、你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?
“锁”的本质其实是monitorenter和monitorexit字节码指令的一个Reference
类型的参数,即要锁定和解锁的对象。我们知道,使用Synchronized可以
修饰不同的对象,因此,对应的对象锁可以这么确定。
- 如果Synchronized明确指定了锁对象,比如Synchronized(变量名)、Synchronized(this)等,说明加解锁对象为该对象。
- 如果没有明确指定:若Synchronized修饰的方法为非静态方法,表示此方法对应的对象为锁
对象;若Synchronized修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。
注意,当一个对象被锁住时,对象里面所有用Synchronized修饰的方法都将产生堵塞,而对象里非Synchronized修饰的方法可正常被调用,不受锁影响。
3、什么是可重入性,为什么说Synchronized是可重入锁?
可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。比如下面的伪代码,一个类中的同步方法调用另一个同步方法,假如Synchronized不支持重入,进入method2方法时当前线程获得锁,method2方法里面执行method1时当前线程又要去尝试获取锁,这时如果不支持重入,它就要等释放,把自己阻塞,导致自己锁死自己。对Synchronized来说,可重入性是显而易见的,刚才提到,在执行monitorenter指令时,如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器+1,其实本质.上就通过这种方式实现了可重入性。
4、JVM对Java的原生锁做了哪些优化?
在Java 6之前,Monitor 的实现完全依赖底层操作系统的互斥锁来实现,也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。
由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做了大量的优化。一 种优化是使用自旋锁,即在把线程进行阻塞操作之前先让线程自旋等待-段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。
现代JDK中还提供了三种不同的Monitor实现,也就是三种不同的锁:
- 偏向锁(Biased Locking)
- 轻量级锁
- 重量级锁
这三种锁使得JDK得以优化Synchronized的运行,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。
- 当没有竞争出现时,默认会使用偏向锁。 JVM会利用CAS操作,在对象头上的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应 用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
- 如果有另-线程试图锁定某个被偏斜过的对象,JVM就撤销偏斜锁,切 换到轻量级锁实现。
- 轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,就 使用普通的轻量级锁;否则,进一步升级为重量级锁。
5、为什么说Synchronized是非公平锁?
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
6、什么是锁消除和锁粗化?
- 锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程 序员自己加入的。
- 锁粗化:原则上,同步块的作用范围要尽量小。但是如果-系列的连续操 作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。 锁粗化就是增大锁的作用域。
7、为什么说Synchronized是-个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么特性?
Synchronized显然是一个悲观锁, 因为它的并发策略是悲观的:不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。乐观锁的核心算法是CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。CAS具有原子性,它的原子性由CPU硬件指令实现保证,即使用JNI调用Native方法调用由C++编写的硬件级别指令,JDK中提供了Unsafe类执行这些操作。
8、乐观锁一定就是好的吗?
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:
- 乐观锁只能保证一个共享变量的原子操作。如果多个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
- 长时间自旋可能导致开销大。假如CAS长时间不成功而- -直自旋,会给CPU带来很大的开销。
- ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。
9、跟Synchronized相比,可重入锁ReentrantLock其实现原理有什么不同?
其实,锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某种标记。Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原生的锁实现方式,而Reentrantlock以及所有的基于Lock接口的实现类,都是通过用一个volitile修饰的int型变量,并保证每个线程都能拥有对该int的可见性和原子修改,其本质是基于所谓的AQS框架。
10、那么请谈谈AQS框架是怎么回事儿?
AQS(AbstractQueuedSynchronizer类)是一个 用来构建锁和同步器的框架,各种Lock包中的锁(常用的有ReentrantLock、ReadWriteLock),以及其他如Semaphore、CountDownlatch,甚至 是早期的FutureTask等,都是基于AQS来构建。
- AQS在内部定义了一个volatile int state变量,表示同步状态:当线程调用lock方法时,如果state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将state=1;如果state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
- AQS通过Node内部类构成的一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。
- Node类是对要访问同步代码的线程的封装,包含了线程本身及其状态叫waitStatus(有五种不同取值,分别表示是否被阻塞,是否等待唤醒,是否已经被取消等),每个Node结点关联其prev结点和next结点,方便线程释放锁后快速唤醒下一个在等待的线程,是一个FIFO的过程。
- Node类有两个常量,SHARED 和EXCLUSIVE,分别代表共享模式和独占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量Semaphore就是基于AQS的共享模式实现的),独占模式是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待(如Reentranlock)。
- AQS通过内部类ConditionObject构建等待队列(可有多个),当Condition调用wait() 方法后,线程将会加入等待队列中,而当Condition调用signal() 方法后,线程将从等待队列转移动同步队列中进行锁竞争。
- AQS和Condition各自维护了不同的队列,在使用Lock和Condition的时候,其实就是两个队列的互相移动。
11、请尽可能详尽地对比下Synchronized和ReentrantLock的异同。
ReentrantLock是Lock的实现类,是一个互斥的同步锁。从功能角度,ReentrantLock比Synchronized的同步操作更精细(因为可以像普通对象一样使用),甚至实现Synchronized没有的高级功能,如:
- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
- 带超时的获取锁尝试:在指定的时间范围内获取锁,如果时间到了仍然无法获取则返回。 可以判断是否有线程在排队等待获取锁。
- 可以响应中断请求:与Synchronized不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
- 可以实现公平锁。
从锁释放角度,Synchronized 在JVM层面上实现的,不但可以通过一些监控工具监控Synchronized的锁定,而且在代码执行出现异常时,JVM 会自动释放锁定;但是使用Lock则不行,Lock 是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。
从性能角度,Synchronized 早期实现比较低效,对比ReentrantLock,大多数场景性能都相差较大。
但是在Java 6中对其进行了非常多的改进,在竞争不激烈时,Synchronized的性能要优于Reetrantlock;在高竞争情况下,Synchronized的性能会下降几十倍,但是ReetrantL .ock的性能能维持常
12、ReentrantLock 是如何实现可重入性的?
ReentrantLock内部自定义了同步器Sync(Sync既实现了AQS,又实现了AOS,而AOS提供了一种互斥锁持有的方式),其实就是加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下当前维护的那个线程ID和当前请求的线程ID是否一样,一样就可重入了。
13、除了ReetrantLock, 你还接触过JUC中的哪些并发工具?
通常所说的并发包(JUC)也就是java.util.concurrent 及其子包,集中了Java并发的各种基础工具类,具体主要包括几个方面:
- 提供了CountDownLatch、CyclicBarrier. Semaphore等,比Synchronized更加高级,可以实现更加丰富多线程操作的同步结构。
- 提供了ConcurrentHashMap、有序的ConcunrrentSkipListMap,或者通过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList等各种线程安全的容器。
- 提供了ArrayBlockingQueue、SynchorousQueue 或针对特定场景的PriorityBlockingQueue等,各种并发队列实现。
- 强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行 等。
14、请谈谈ReadWriteLock和StampedLock。
虽然ReentrantLock和Synchronized简单实用,但是行为,上有一定局限性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写操作,而是以并发读取为主,为了进一步优化并发操作的粒度,Java 提供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。ReadWriteLock代表了一对锁,下面是 一个基于读写锁实现的数据结构,当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优
势:
public class RWSample {
private final Map<String,String» m = new TreeMap<>();
private final Reent rantReadWriteLockrwl=new ReentrantReadWriteLock() ;
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public String get(String key){
r.lock();
System.out.println("读锁锁定" );
try{
return m.get(key);
} finally {
r.unlock();
}
}
public String put(String key, String entry) {
w.lock();
System.out.println("写锁锁定");
try {
return m.put(key, entry) ;
}finally {
w.unlock();
}
}
}
读写锁看起来比Synchronized的粒度似乎细一-些,但在实际应用中,其表现也并不尽如人意,主要还是因为相对比较大的开销。所以,JDK在后期引入了StampedLock,在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate 方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。
public class StampedSample {
private final StampedLock s1 = new StampedLock() ;
void mutate() {
long stamp = s1.writeLock ( ) ;
try {
write();
} finally {
s1.unlockWrite(stamp) ;
}
}
Data access() {
long stamp = s1.tryOptimisticRead():
Data data = read();
if (!s1.validate(stamp)) {
stamp = s1.readLock() ;
try {
data = read():
) finally {
s1.unlockRead(stamp):
}
}
return data ;
}
}
15、如何让Java的线程彼此同步?你了解过哪些同步器?请分别介绍下。
JUC中的同步器三个主要的成员:CountDownLatch、CyclicBarrier 和Semaphore,通过它们可以方便地实现很多线程之间协作的功能。
CountDownLatch叫倒计数,允许一个或多个线程等待某些操作完成。看几个场景:
- 跑步比赛,裁判需要等到所有的运动员(“其他线程”)都跑到终点(达到目 标),才能去算排名和颁奖。
- 模拟并发,我需要启动100个线程去同时访问某一个地址,我希望它们能同时并发,而不是一个一个的去执行。
用法:CountDownLatch构造方法指明计数数量,被等待线程调用countDown将计数器减1,等待线程使用await进行线程等待。一个简单的例子:
public class TestCountDownLatch {
private CountDownL atch countDownLatch =new CountDownLatch(4);
public static void main(String[] args) {
TestCountDownLatch testCountDounLatch =new TestCountDownLatch();
testCountDownLatch.begin();
}
private class Runner implenents Runnable {
private int result;
public Runner(int result) {
this.result =result ;
}
@Override
public void run(){
try{
Thread.sleep(result *1000);
countDownLatch.countDoun(); //
}catch (Inter ruptedException e) {
e.printStackTrace();
}
}
}
private void begin() {
System.out.println(“赛跑开始” );
Random random = new Random(System.currentTimeMillis());
for(inti.0;i<=4;1++){
int result = random.nextInt(3) + 1;
new Thread(new Runner(result)).start();
}
try {
countDownLatch.await(); //
}catch (InterruptedException e) {
e.printStackTrace();
}
Ѕуѕtеm. оut.рrіntln(“所有人都跑完了,裁判开始算成绩”);
}
}
CyclicBarrier叫循环栅栏,它实现让一组线程等待至某个状态之后再全部同时执行,而且当所有等待线程被释放后,CyclicBarrier 可以被重复使用。CyclicBarrier 的典型应用场景是用来等待并发线程结束。CyclicBarrier的主要方法是await(),await()每被调用- -次,计数便会减少1,并阻塞住当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier.上面阻塞的线程开始运行。
在这之后,如果再次调用await(),计数就又会变成N-1,新一轮重新开始,这便是Cyclic的含义所在。CyclicBarrier.await() 带有返回值,用来表示当前线程是第几个到达这个Barrier的线程。举例说明如下:
public class TestCyclicBarrier (
private CyclicBarrier cyclicBarrier . new CyclicBarrier(5);
public static void main(String[] args){
new TestCyclicBarrier.begin();
}
public void begin() {
for(int1.a;1<5;1++)(
new Thread(new Student()).start();
}
}
private class Student implements Runnable (
@Override
public void run() (
try {
Thread. sleep(2000) ;//该学生在赶往饭店路上
cyclicBarrier.await();//到了就等着,等其他人到齐了再一起进入饭店
) catch (Exception e) (
e printStackTrace();
}
//大家都到了进去吃饭
}
}
Semaphore,Java 版本的信号量实现,用于控制同时访问的线程个数,来达到限制通用资源访问的目的,其原理是通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。
public class Test (
public static void main(String[] args) (
Semaphore semaphore . new Semaphore(5); //机器数目及五个许可
for(int i=0;1<8;1++){//工人数8去抢许可
new Worker(i, semaphore).start();//
}
}
static class Worker extends Thread (
private int num ;
private Semaphore semaphore ;
public Worker(int num, Semaphore semaphore){
this.num = num ;
this.semaphore=semaphore ;
@Override
public void run(){
try (
semaphore.acquire(); //抢许可
Thread.sleep(2000) ;
semaphore.release();//释放许可
}catch (Inter ruptedException e) {
e printStackTrace( );
}
}
}
}
如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。
16、CyclicBarrier和CountDownLatch看起来很相似,请对比下呢?
它们的行为有一定相似度,区别主要在于:
- CountDownLatch是不可以重置的,所以无法重用,CyclicBarrier 没有这 种限制,可以重用。
- CountDownLatch的基本操作组合是countDown/await,调用await 的线程阻塞等countDown足够的次数,不管你是在一个线程还是多个线程 里countDown,只要次数足够即可CyclicBarrier的基本操作组合就是 await,当所有的伙伴都调用了await,才会继续进行任务,并自动进行重置。
CountDownLatch目的是让一个线程等待其他N个线程达到某个条件后,自己再去做某个事(通过CyclicBarrier的第二个构造方法public CyclicBarrier(int parties, Runnable barrierAction),在新线程里做事可以达到同样的效果)。而CyclicBarrier的目的是让N多线程互相等待直到所有的都达到某个状态,然后这N个线程再继续执行各自后续(通过CountDownLatch在某些场合也能完成类似的效果)。