乐趣区

013期JavaSE面试题十三多线程3

开篇介绍

大家好,我是 Java 最全面试题库提裤姐,明天这篇是 JavaSE 系列的第十三篇,次要总结了 Java 中的多线程问题,多线程分为三篇来讲,这篇是第三篇,在后续,会沿着第一篇开篇的常识线路始终总结上来,做到日更!如果我能做到百日百更,心愿你也能够跟着百日百刷,一百天养成一个好习惯。

volatile 关键字的作用?

对于可见性,Java 提供了 volatile 关键字来保障可见性。当一个共享变量被 volatile 润饰时,它会保障批改的值会立刻被更新到主存,当有其余线程须要读取时,它会去内存中读取新值。次要的原理是应用了内存指令。

  • LoadLoad 重排序:一个处理器先执行一个 L1 读操作,再执行一个 L2 读操作;然而另外一个处理器看到的是先 L2 再 L1
  • StoreStore 重排序:一个处理器先执行一个 W1 写操作,再执行一个 W2 写操作;然而另外一个处理器看到的是先 W2 再 W1
  • LoadStore 重排序:一个处理器先执行一个 L1 读操作,再执行一个 W2 写操作;然而另外一个处理器看到的是先 W2 再 L1
  • StoreLoad 重排序:一个处理器先执行一个 W1 写操作,再执行一个 L2 读操作;然而另外一个处理器看到的是先 L2 再 W1

说一下 volatile 关键字对原子性、可见性以及有序性的保障

在 volatile 变量写操作的后面会退出一个 Release 屏障,而后在之后会退出一个 Store 屏障,这样就能够保障 volatile 写跟 Release 屏障之 前的任何读写操作都不会指令重排,而后 Store 屏障保障了,写完数据之后,立马会执行 flush 处理器缓存的操作。
在 volatile 变量读操作的后面会退出一个 Load 屏障,这样就能够保障对这个变量的读取时,如果被别的处理器批改过了,必须得从其余 处理器的高速缓存(或者主内存)中加载到本人本地高速缓存里,保障读到的是最新数据;在之后会退出一个 Acquire 屏障,禁止 volatile 读操作之后的任何读写操作会跟 volatile 读指令重排序。
与 volatie 读写内存屏障比照一下,是相似的意思。
Acquire 屏障 其实就是 LoadLoad 屏障 + LoadStore 屏障
Release 屏障 其实就是StoreLoad 屏障 + StoreStore 屏障

什么是 CAS?

CAS(compare and swap)的缩写。Java 利用 CPU 的 CAS 指令,同时借助 JNI 来实现 Java 的非阻塞算法, 实现原子操作。其它原子操作都是利用相似的个性实现的。
CAS 有 3 个操作数:内存值 V 旧的预期值 A 要批改的新值 B
当且仅当预期值 A 和内存值 V 雷同时,将内存值 V 批改为 B,否则什么都不做。
CAS 的毛病:

  • CPU 开销过大

在并发量比拟高的状况下,如果许多线程重复尝试更新某一个变量,却又始终更新不胜利,周而复始,会给 CPU 带来很到的压力。

  • 不能保障代码块的原子性

CAS 机制所保障的常识一个变量的原子性操作,而不能保障整个代码块的原子性。比方须要保障 3 个变量独特进行原子性的更新,就不得不应用 synchronized 了。

  • ABA 问题

这是 CAS 机制最大的问题所在。

什么是 AQS?

AQS,即 AbstractQueuedSynchronizer,队列同步器,它是 Java 并发用来构建锁和其余同步组件的根底框架。
同步组件对 AQS 的应用:
AQS 是一个抽象类,主是是以继承的形式应用。
AQS 自身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和开释的办法来供自定义的同步组件的应用。从图中能够看出,在 java 的同步组件中,AQS 的子类(Sync 等)个别是同步组件的动态外部类,即通过组合的形式应用。
形象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如罕用的 ReentrantLock/Semaphore/CountDownLatch
它保护了一个 volatile int state(代表共享资源)和一个 FIFO(双向队列)线程期待队列(多线程争用资源被阻塞时会进入此队列)

Semaphore 是什么?

Semaphore 就是一个信号量,它的作用是限度某段代码块的并发数。
semaphore 有一个构造函数,能够传入一个 int 型整数 n,示意某段代码最多只有 n 个线程能够拜访,如果超出了 n,那么请期待,等到某个线程执行结束这段代码块,下一个线程再进入。
由此能够看出如果 Semaphore 构造函数中传入的 int 型整数 n =1,相当于变成了一个 synchronized 了。

public static void main(String[] args) {  
        int N = 8; // 工人数  
        Semaphore semaphore = new Semaphore(5); // 机器数目  
        for(int i=0;i<N;i++)  
            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();  
                System.out.println("工人"+this.num+"占用一个机器在生产...");  
                Thread.sleep(2000);  
                System.out.println("工人"+this.num+"开释出机器");  
                semaphore.release();} catch (InterruptedException e) {e.printStackTrace();  
            }  
        }  
    }  

Synchronized 的原理是什么?

Synchronized 是由 JVM 实现的一种实现互斥同步的形式,查看被 Synchronized 润饰过的程序块编译后的字节码,会发现,被 Synchronized 润饰过的程序块,在编译前后被编译器生成了 monitorenter 和 monitorexit 两个字节码指令。
在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁:如果这个对象没有锁定,或者以后线程曾经领有了这个对象的锁,把锁的计数器 +1;
当执行 monitorexit 指令时,将锁计数器 -1;当计数器为 0 时,锁就被开释了。如果获取对象失败了,那以后线程就要阻塞期待,直到对象锁被另外一个线程开释为止。
Java 中 Synchronize 通过在对象头设置标记,达到了获取锁和开释锁的目标。

为什么说 Synchronized 是非偏心锁?

非偏心次要体现在获取锁的行为上,并非是依照申请锁的工夫前后给期待线程调配锁的,每当锁被开释后,任何一个线程都有机会竞争到锁,这样做的目标是为了进步执行性能,毛病是可能会产生线程饥饿景象。

JVM 对 java 的原生锁做了哪些优化?

在 Java6 之前, Monitor 的实现齐全依赖底层操作系统的互斥锁来实现.

因为 Java 层面的线程与操作系统的原生线程有映射关系, 如果要将一个线程进行阻塞或唤起都须要操作系统的帮助, 这就须要从用户态切换到内核态来执行, 这种切换代价非常低廉, 很耗处理器工夫, 古代 JDK 中做了大量的优化。
一种优化是应用 自旋锁 , 即在把线程进行阻塞操作之前先让线程自旋期待一段时间, 可能在期待期间其余线程曾经解锁, 这时就无需再让线程执行阻塞操作, 防止了用户态到内核态的切换。
古代 JDK 中还提供了三种不同的 Monitor 实现, 也就是三种不同的锁:

  • 偏差锁(Biased Locking)
  • 轻量级锁
  • 重量级锁

这三种锁使得 JDK 得以优化 Synchronized 的运行, 当 JVM 检测到不同的竞争情况时, 会主动切换到适宜的锁实现, 这就是锁的降级、降级。当没有竞争呈现时, 默认会应用偏差锁。
JVM 会利用 CAS 操作, 在对象头上的 Mark Word 局部设置线程 ID, 以示意这个对象偏差于以后线程, 所以并不波及真正的互斥锁, 因为在很多利用场景中, 大部分对象生命周期中最多会被一个线程锁定, 应用偏差锁能够升高无竞争开销。
如果有另一线程试图锁定某个被偏差过的对象,JVM 就撤销偏差锁, 切换到轻量级锁实现。
轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁, 如果重试胜利, 就应用一般的轻量级锁否则, 进一步降级为重量级锁。

Synchronized 和 ReentrantLock 的异同?

synchronized
是 java 内置的关键字,它提供了一种独占的加锁形式。synchronized 的获取和开释锁由 JVM 实现,用户不须要显示的开释锁,十分不便。然而 synchronized 也有一些问题:
当线程尝试获取锁的时候,如果获取不到锁会始终阻塞。
如果获取锁的线程进入休眠或者阻塞,除非以后线程异样,否则其余线程尝试获取锁必须始终期待。

ReentrantLock
ReentrantLock 是 Lock 的实现类,是一个互斥的同步锁。ReentrantLock 是 JDK 1.5 之后提供的 API 层面的互斥锁,须要 lock()和 unlock()办法配合 try/finally 语句块来实现。
期待可中断防止,呈现死锁的状况(如果别的线程正持有锁,会期待参数给定的工夫,在期待的过程中,如果获取了锁定,就返回 true,如果期待超时,返回 false)
偏心锁与非偏心锁多个线程期待同一个锁时,必须依照申请锁的工夫程序取得锁,Synchronized 锁非偏心锁,ReentrantLock 默认的构造函数是创立的非偏心锁,能够通过参数 true 设为偏心锁,但偏心锁体现的性能不是很好。

从性能角度
ReentrantLock 比 Synchronized 的同步操作更精密(因为能够像一般对象一样应用),甚至实现 Synchronized 没有的高级性能, 如:

  • 期待可中断当持有锁的线程长期不开释锁的时候,正在期待的线程能够抉择放弃期待, 对解决执行工夫十分长的同步块很有用。
  • 带超时的获取锁尝试在指定的工夫范畴内获取锁,如果工夫到了依然无奈获取则返回。
  • 能够判断是否有线程在排队期待获取锁。
  • 能够响应中断请求与 Synchronized 不同,当获取到锁的线程被中断时, 可能响应中断, 中断异样将会被抛出, 同时锁会被开释。
  • 能够实现偏心锁。

从锁开释角度
Synchronized 在 JVM 层面上实现的,岂但能够通过一些监控工具监控 Synchronized 的锁定,而且在代码执行出现异常时,JVM 会主动开释锁定,然而应用 Lock 则不行,Lock 是通过代码实现的, 要保障锁定肯定会被开释,就必须将 unLock() 放到 finally{}中。

从性能角度
Synchronized 晚期实现比拟低效,比照 ReentrantLock,大多数场景性能都相差较大。
然而在 Java6 中对其进行了十分多的改良,
在竞争不强烈时:Synchronized 的性能要优于 ReetrantLock;
在高竞争状况下:Synchronized 的性能会降落几十倍, 然而 ReetrantLock 的性能能维持常态。

退出移动版