1、多线程间的原子性、可见性、有序性是什么意思?
原子性:执行一个或者多个操作的时候,要么全部执行,要么都不执行,并且中间过程中不会被打断。独占锁 和 CAS 能够保证原子性。 可见性:指多线程访问同一个变量的时候,一个线程修改了变量的值,其他线程能够立刻看得到修改的值。锁 和 volatile 能够保证可见性。 有序性:程序执行的顺序按照代码先后的顺序执行。锁 和 volatile 能够保证有序性。
2、happens-before 原则有哪些?
Java 内存模型具有一些先天的有序性,它通常叫做 happens-before 原则。如果两个操作的先后顺序不能通过 happens-before 原则推倒出来,那就不能保证它们的先后执行顺序,虚拟机就可以随意打乱执行指令。 happens-before 原则有: 程序次序规则:单线程程序的执行结果得和看上去代码执行的结果要一致。 锁定规则:一个锁的 lock 操作一定发生在上一个 unlock 操作之后。 volatile 规则:对 volatile 变量的写操作一定先行于后面对这个变量的读操作。
## 举例理解:
如果在静态变量 s 之前加上 volatile 修饰符:
volatile static int s = 0 ;
线程 A 执行如下代码:
s = 3 ;
这时候我们引入线程 B,执行如下代码:
System. out. println ( "s=" + s) ;
当线程 A 先执行的时候,把 s = 3 写入主内存的事件必定会先于线程 B 读取 s 的事件。所以线程 B 的输出一定是 s = 3 。
传递规则:A 发生在 B 前面,B 发生在 C 前面,那么 A 一定发生在 C 前面。 线程启动规则:线程的 start 方法先行发生于线程中的每个动作。 线程中断规则:对线程的 interrupt 操作先行发生于中断线程的检测代码。 线程终结原则:线程中所有的操作都先行发生于线程的终止检测。 对象终止原则:一个对象的初始化先行发生于他的 finalize() 方法的执行。 前四条规则比较重要。
3、volatile 的原理?
可见性 :对 volatile 变量进行写操作的时候,JVM 会向处理器发送一条 Lock 前缀的指令,将这个缓存变量写入到系统主存。多处理器的环境下,如果其他处理器的缓存还是旧的,为了保证各个处理器一致,会通过嗅探在总线上传播的数据来检测自己的数据是否过期,如果过期,会强制重新将系统内存的数据读取到处理器缓存。
# 缓存一致性协议:
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,
当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,
当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里
有序性 :Lock 前缀的指令相当于一个内存栅栏,它确保指令排序的时候,不会把后面的指令排到内存栅栏的前面,也不会把前面的指令排到内存栅栏的后面。【禁止指令重排优化】
4、synchronized 方法锁、对象锁、类锁?
方法锁 :类中非静态方法上的锁。注意:方法锁是对象锁,但是对象锁不一定是方法锁。
private synchronized void test ( ) {
}
对象锁 :类中非静态方法上的锁或者用 this 或者某一个对象做锁。方法锁也是对象锁。
public void method2 ( ) {
synchronized ( this ) {
}
}
public void method3 ( Object object) {
synchronized ( object) {
}
}
类锁 :类中静态方法上的锁或者用 XXX.class 做锁。
public static synchronized void method4 ( ) {
}
public void method4 ( Object object) {
synchronized ( SynchronizedDemo. class ) {
}
}
5、synchronized 的参数放入对象和 Class 有什么区别?
锁住的对象不同:成员方法锁住的实例对象,静态方法锁住的是 Class。 访问控制不同:如果锁住的是实例,只会针对同一个对象方法进行同步访问,多线程访问同一个对象的 synchronized 代码块是串行的,访问不同对象是并行的。如果锁住的是类,多线程访问的不管是同一对象还是不同对象的 synchronized 代码块是都是串行的。 串行是指多个任务时,各个任务按顺序执行,完成一个之后才能进行下一个。并行指的是多个任务可以同时执行,异步是多个任务并行的前提条件。
6、synchronized 的原理?
当 Java 源码被 javac 编译成字节码的时候,会在同步块的入口位置和退出位置分别插入 monitorenter 和 monitorexit 字节码指令,通过这种方式来实现同步操作。 任何一个对象都有一个 monitor 与之相关联,JVM 基于进入和退出 mointor 对象来实现代码块同步和方法同步,实际上 JVM 也只区分这两种不同用法,修饰代码块和修饰方法,两者实现细节不同,分别如下: 代码块同步 :在编译字节码的时候,代码块起始的地方插入 monitorenter 指令,异常和代码块结束处插入 monitorexit 指令,线程在执行 monitorenter 指令的时候尝试获取 monitor 对象的所有权,获取不到的情况下就是阻塞。方法同步 :synchronized 方法在 method_info 结构有 AAC_synchronized 标记,JVM 就是根据该标示符来实现方法的同步的;当方法调动时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。简单来说在 JVM 中 monitorenter 和 monitorexit 字节码依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的,但是使用 Mutex Lock 需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的,所以在 jdk 1.6 以后对 synchronized 实现了大量的优化 ,例如: 自旋锁 :由于线程阻塞后进入排队队列和唤醒都需要 CPU 从用户态转为核心态,花费时间较多,尤其频繁的阻塞和唤醒对 CPU 来说也是负荷很重的工作,同时,统计发现很多线程锁定状态只持续很短时间,如果这时候其他线程进入等待队列之后再唤醒太费时间了,因此,出现了自旋锁。锁消除 :锁消除就是虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除,即对于代码数据的逃逸分析,如果数据无法逃逸并且私有的话,锁其实是没必要的,可以消除。锁粗化 :也就是减少不必要的紧连在一起的 lock、unlock 操作,将多个连续的锁扩展成一个范围更大的锁。偏向锁 :偏向某个线程,记录一个线程 id,这个线程在获取这个锁的时候不需要获取或者释放锁。轻量级锁 :当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
7、synchronized 和 volatile 的区别?
volatile 只能修饰变量,synchronized 可以修饰方法和代码块 volatile 保证可见性和有序性,不能保证原子性。synchronized 可以保证可见性、有序性和原子性。 volatile不需要加锁,比 synchronized 更加轻量级,不会阻塞线程 volatile 能防止指令重排序 以下两个场景中可以使用 volatile来 代替 synchronized。 除此之外,都需要使用其他方式来保证原子性,如synchronized
## volatile 适用场景:
- 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束
volatile static int start = 3 ;
volatile static int end = 6 ;
线程A执行如下代码:
while ( start < end) {
}
线程B执行如下代码:
start += 3 ;
end += 3 ;
这种情况下,一旦在线程 A 的循环中执行了线程 B,start 有可能先更新成6 ,造成了一瞬间 start == end,从而跳出 while 循环的可能性。
8、synchronized 和 Lock 的区别?
Lock 是 Java 中的接口;而 synchronized 是 Java 中的关键字,是 Java 内置语言的实现; synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 Locl#unLock() 方法去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。 Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。 Lock 可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。所以说,在具体使用时要根据适当情况选择。
9、volatile、synchronized、Lock 之间的区别?
volatile:具有可见性、有序性,不具有原子性(线程不安全)。 synchronized 和 Lock:都具有原子性、可见性、有序性(线程安全)。 但是 volatile 和 synchronized 的有序性是不同的: volatile 关键字禁止 JVM 编译器已及处理器对其进行指令重排序。 synchronized 保证顺序性是串行化的结果,但同步块里的语句是会发生指令重排。
10、Object 类中 wait()、notify() 和 notifyAll() 方法区别?
wait()、notify()、notifyAll() 都是 Object 对象的方法,他们必须在被 synchronized 同步的方法或代码块中调用,否则会报错。 wait():会使该线程进入等待状态(阻塞状态)。 notify():在所有等待线程中随机唤醒一个线程,让它获得锁。 notifyAll():唤醒所有等待的线程,让它们一起竞争锁,最后其中之一获得锁。 注意:notify() 或者 notifyAll() 方法并不是真正释放锁,必须等到 synchronized 方法或者语法块执行完才真正释放锁。
11、Condition 类中 await()、signal() 和 signalAll() 方法区别?
await()、signal() 和 signalAll() 都是 Condition 对象的方法,可以搭配任意一种 Lock 使用,如 ReentractLock(可重入锁)。 await():释放当前锁,使该线程进入等待状态(阻塞状态)。 signal():在所有等待线程中随机唤醒一个线程,让它获得锁。 signalAll():唤醒所有等待的线程,让它们一起竞争锁,最后其中之一获得锁。
12、wait()/notify()/notifyAll() 和 await()/signal()/signalAll() 方法区别?
wait() 结合 notify()/notifyAll() 的功用和 await() 结合 signal()/signalAll() 基本相同,区别是基于 Condition 的 await()、signal()、signalAll() 使得我们可以在同一个锁的代码块内, 优雅地实现基于多个条件的线程间挂起与唤醒操作。
13、悲观锁 VS 乐观锁
悲观锁 :对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。(Java 中 synchronized 关键字和 Lock接口 的实现类都是悲观锁)乐观锁 :在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。(Java 最常采用的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现的)
14、CAS 是什么?底层原理?
CAS 全称 Compare And Swap(比较与交换),是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。(java.util.concurrent 包中的原子类就是通过 CAS 来实现了乐观锁,例如:AtomicInteger等) CAS 算法有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。一般情况下是一个自旋操作,即不断的重试。 底层原理:是借助 CPU 底层指令比较与交换(cmpxchg)来实现原子操作。
15、自旋锁 VS 适应性自旋锁
前置知识:阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。 自旋锁 :是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。适应性自旋锁 :如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
16、无锁、偏向锁、轻量级锁、重量级锁
无锁 :没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。(CAS 算法就是基于无锁的)偏向锁 :一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。轻量级锁 :当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。重量级锁 :若当前只有一个等待线程,则该线程通过自旋进行等待,但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。锁状态升级流程 :无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
17、公平锁和非公平锁区别?
公平锁 :指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。非公平锁 :指多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。注意:ReentrantLock 默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
18、可重入锁 VS 非可重入锁?
可重入锁 :在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。(ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁)非可重入锁 :在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法时,必须先释放进入外层方法的锁,才可以获取进入内层方法的锁。(NonReentrantLock 是非可重入锁)
19、独享锁 VS 共享锁
独享锁 :独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。(ReentrantLock 是独享锁)共享锁 :共享锁是指该锁可被多个线程所持有,如果线程 T 对数据 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加独享锁。获得共享锁的线程只能读数据,不能修改数据。(ReentrantReadWriteLock 是共享锁)