多线程常见面试题一
参考:
1、Java线程安全的类
Stack
,HashTable
,StringBuffer
ynchronized
关键字给方法加上内置锁来实现线程安全3、原子类Atomicxxx
—包装类的线程安全类
如AtomicLong
,AtomicInteger
等等
Atomicxxx
是通过Unsafe
类的native方法实现线程安全的
通过{unsafeCAS操作Volitle修饰属性,保证可见}
4、BlockingQueue 和BlockingDeque
BlockingDeque接口继承了BlockingQueue接口,
BlockingQueue 接口的实现类有ArrayBlockingQueue ,LinkedBlockingQueue ,PriorityBlockingQueue 而BlockingDeque接口的实现类有LinkedBlockingDeque
BlockingQueue和BlockingDeque 都是通过使用定义为final的ReentrantLock作为类属性显式加锁实现同步的
2、volatile关键字作用
volatile 关键字的作用
可见性是指:一个线程对变量的修改对其他线程是可见的。volatile是通过定义特殊规则来实现可见性的:
- read、load、use动作必须连续出现。
- assign、store、write动作必须连续出现。
所以,使用volatile变量能够保证:
- 每次
读取前
必须先从主内存刷新最新的值。 - 每次
写入后
必须立即同步回主内存当中。
volatile 是通过 内存屏障来实现指令重排序的。
偏序关系
的Happens-Before内存模型
中,指令重排技术大大提高了程序执行效率。但同时也带来了一些问题。比如一个比较经典的问题就是基于 DCL 双锁检查的单例设计模式,如果没有把成员 instance 声明为 valotile, 那么在创建对象的时候将会对 创建对象操作这个底层实现进行排序优化,创建对象的抽象过程我们认为应该是先分配内存,然后初始化对象,最后返回对象的引用,但是实际上 cpu 会将这个过程进行 重排序,实际的创建过程是 先分配内存,然后返回对象引用,最后初始化对象。所以这个重排序就导致了,如果一个线程刚好处于创建单例对象的第二步和第三步之间,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。所以如果要实现安全的 单例,就可以使用 对instance 成员生声明为 volatile 来实现。
JMM内存屏障插入策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的前面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障
注意:
但对volatile的使用过程中很容易出现的一个问题是:
错把volatile变量当做原子变量。
出现这种误解的原因,主要是volatile关键字使变量的读、写具有了“原子性”。然而这种原子性仅限于变量(包括引用)的读和写,无法涵盖变量上的任何操作,即:
- 基本类型的自增(如
count++
)等操作不是原子的。 - 对象的任何非原子成员调用(包括
成员变量
和成员方法
)不是原子的。
如果希望上述操作也具有原子性,那么只能采取锁、原子类更多的措施。
参考:
3、有哪些锁?可重入不可重入?自旋锁互斥锁可重入?
3.1、独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。 (ReentrantLock、 Synchronized)
共享锁是指该锁可被多个线程所持有。 (ReadWriteLock 读锁是共享锁,写锁是独享锁。 )
3.2、 公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能会造成饥饿现象。
Synchronized 非公平锁。ReentrantLock 默认是非公平锁,不过可以通过构造函数传入 true 这个 boolean 值来指定该锁是公平锁,。非公平锁的优点在于吞吐量比公平锁大。
3.3、可重入锁
可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
ReentrantLock和Synchronized都是可重入锁。可重入锁的一个好处是可一定程度避免死锁,比如 A B 方法都锁定的是同一个对象,然后A 方法中调用了 B 方法,如果外层方法获取锁之后内层方法还需要获取锁,那么这个线程就会等待持有锁的线程释放锁,但是持有锁的线程是它本身,所以它在等待自己释放一个自己持有的锁,就陷入了死锁。
需要注意的是,可重入锁加锁和解锁的次数要相等。不过一般加锁和解锁都是成对出现的,所以这个一般不会出现问题。
3.4、乐观锁/悲观锁
乐观锁认为不存在很多的并发更新操作,不需要加锁。数据库中乐观锁的实现一般采用版本号,Java中可使用CAS实现乐观锁。
3.5、分段锁
分段锁是一种锁的设计,并不是一种具体的锁。对于 JDK 1.7 以 1.7 以前的 ConcuttentHashMap 就是通过分段锁实现高效的并发操作。
3.6、自旋锁和阻塞锁
自旋锁是指尝试获取锁的线程不会阻塞,而是采用一段空循环的方式等待持有锁的线程释放锁,然后获取锁。好处是减少上下文切换,缺点是一直占用CPU资源。
3.7、 偏向锁/轻量级锁/重量级锁
这是jdk1.6中对Synchronized锁做的优化
从jdk1.6开始为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。锁共有四种状态,级别从低到高分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。随着竞争情况锁状态逐渐升级、锁可以升级但不能降级。
4、针对 Synchronized 关键字的锁优化进制
4.1 锁粗化:
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,这样就只需要加锁一次就够了
4.2 锁消除:
如果你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
4.3 自旋锁:
4.4 偏向锁 / 轻量级锁
偏向锁的获取和撤销:
HotSpot作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。
线程1检查对象头中的Mark Word中是否存储了线程1,如果没有则CAS操作将Mark Word中的线程ID替换为线程1。此时,锁偏向线程1,后面该线程进入同步块时不需要进行CAS操作,只需要简单的测试一下Mark Word中是否存储指向当前线程的偏向锁,如果成功表明该线程已经获得锁。如果失败,则再需要测试一下Mark Word中偏向锁标识是否设置为1(是否是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将偏向锁指向当前线程
偏向锁的竞争结果:
根据持有偏向锁的线程是否存活
1.如果不活动,偏向锁撤销到无锁状态,再偏向到其他线程
2.如果线程仍然活着,则升级到轻量级锁
轻量级锁膨胀:
1.线程在执行同步块之前,JVM会在当前栈桢中创建用于存储锁记录的空间(Lock record),并将对象头中的Mark Word复制到锁记录中(Displaced Mark Word)。
2.然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针
3.如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁。在自旋次数超过一定次数,则将 对象头 升级为 重量级锁,当前线程不再自旋,陷入阻塞。
轻量级锁的释放
用 CAS 操作 把 Lock Record 中的副本拷贝到 对象头的 MarkWord 中,如果替换成功,则整个同步过程就顺利完成了;如果替换失败,说明现在的锁已经是重量级锁了,说明有其他线程尝试获取过该锁,就要在释放锁的同时,唤醒被挂起的线程。
5、什么是逃逸分析
逃逸分析的基本行为就是分析对象的动态作用域。当一个对象在方法中被定义后,它可能被外部方法所引用(例如作为形参传递到其它方法中去),称为方法逃逸。如果是被外部线程访问到,称为线程逃逸。如果能够证明一个对象不会逃逸到方法或者线程之外,则可能对这个对象进行一些高效的优化:
- 栈上分配
如果能够确定一个对象不会逃逸到方法之外,可以在栈上分配对象的内存,这样对象占用的内存空间可以随着栈帧出栈而销毁,减少gc的压力; - 同步消除
如果逃逸分析得出对象不会逃逸到线程之外,那么对象的同步措施可以消除。 - 标量替换
如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆解,那么程序执行的时候可能不创建这个对象,改为在栈上分配这个方法所用到的对象的成员变量。
常见的发生逃逸的场景有:
参考:即时编译(JIT)
6、AQS
AQS介绍
AQS的等待队列是一个CLH(Craig, Landin, and Hagersten lock queue)队列:竞争资源同一时间只能被一个线程访问, CLH为管理等待锁的线程的队列
一. AQS 是构建同步器的【框架】【核心思想】:线程请求资源情况
1:资源空闲则请求线程设置为工作线程,资源上锁情况
2:资源被占用则请求线程阻塞,加入CLH队列。等待资源空闲时竞争资源
二. AQS 定义两种资源共享模式
1. 独占锁 Exclusive :锁只能被一个线程占有例如: ReentrantLock 又分为公平锁和非公平锁
2. 共享锁 shared :多个线程共享锁例如: CountDownLatch 、Semaphore 三. AQS框架自定义模块尝试获取/释放独占资源 tryAcquire() tryRelease() 尝试获取/释放共享资源 tryAcquireShared() tryReleaseShared() 四. AQS 常见组件1. ReentrantLock A 线程调用 lock()方法若state=0,则资源空闲,state++,且 A线程可重复获取锁若state!=0,则资源被占有,当state=0时其他线程才能竞争2. CountDownLatch (1) 构造器初始化【state = N】当【子线程】调用countDown(),通过 CAS操作state自减1当state=0时,调用await的线程恢复正常继续执行 只有达到一定数量的线程,才能突破关卡,继续运行3. CyclicBarrier 构造方法state=n 每当一个线程调用 await()方法,则CAS操作state自减1当state=0时,所有调用await()的线程恢复 好比是所有线程约定一起出去玩,直到所有线程都到了才可以出发
AQS源码
1. aquire() public void aquire(){ if(!tryAcquire() // 尝试获取一次 && acquireQueued(addWaiter(Node.EXCLUSIVE),arg)) // acquireQueued 【作用】:自旋检测 (tryAcquire()&& node==head.next) // addWaiter【作用】:添加当前线程node至队列尾部 selfInterrupt(); } 【问题】:为何不仅调用 acuqireQueued(addWaiter()) 优先尝试最可能成功的代码,可减少执行的字节码指令
jdk中哪种数据结构或工具可以实现当多个线程到达某个状态时执行一段代码,栅栏和闭锁的区别
CountDownLatch
应用场景:
CyclicBarrier
区别:
如何使用信号量实现上述情况
7、ThreadLocal的原理(下面只是简单概括,详细原理查看《ThreadLocal原理,内存泄漏问题,怎么解决》)
8、为什么有了lock之后synchronized没被废弃掉,反而进行了锁的优化
在解决死锁问题的时候,提出了一个破坏不可抢占条件方案,但是这个方案 synchronized 没有办法解决。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但我们希望的是:
对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
如果我们重新设计一把互斥锁去解决这个问题,那该怎么设计呢?我觉得有三种方案。- 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
- 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
- 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
这三种方案可以全面弥补 synchronized 的问题。到这里相信你应该也能理解了,这三个方案就是“重复造轮子”的主要原因,体现在 API 上,就是 Lock 接口的三个方法。详情如下:
// 支持中断的 API void lockInterruptibly() throws InterruptedException; // 支持超时的 API boolean tryLock(long time, TimeUnit unit)throws InterruptedException; // 支持非阻塞获取锁的 API boolean tryLock();
Sychronized 修饰代码块 || 方法1.修饰代码块时通过【monitorenter 和 monitorExit 两条指令】,
分别指定同步代码块的开始位置和结束位置。
线程获取锁 = 获取位于对象头的monitor的持有权获取到锁,则计数器++。执行到monitorExit,则计数器-- 2.修饰方法 JVM通过 ACC_SYNCHRONIZED 辨别方法为同步方法 【面试口头】 Sychronized 是【JVM】层面的关键字。它是通过【字节码指令】实现的。 (1) Sychronized 修饰【代码块】时,montior-enter monitor-exit两个字节码指令表明 同步块的开始和结束位置。 (2) Sychronized 修饰【方法】时,JVM中通过ACC_SYCHRONIZED 标志同步方法
9、Lock和Condition
在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。ReentrantLock 和 Sychronized 区别
1.两者都是【可重入锁】 : 外层方法获得锁之后,内层方法如果获取的是同一把锁,则可以直接获取锁,无需阻塞,这样一定程度上可以 避免死锁问题2. Sychronized 依赖JVM实现,而ReentrantLock 依赖API实现(JDK层面) ReentrantLock 调用 lock() unlock() try/finally语句实现同步块,可以直接查看源代码 Sychronized 在JVM层面,通过字节码指令 monitorEnter monitorExit指定同步块的开始和结束位置3. ReentrantLock 实现高级功能 (1) ReentrantLock实现等待可中断:通过调用 lockInterruptibly() 中断等待锁的线程 (2) ReentrantLock可实现公平锁,而Sychronized仅实现非公平锁:公平锁 = 先等待的线程,先获得锁 (3) 等待/通知机制不同: Sychronized 通过 notiy() notifyAll() wait() 实现等待/通知机制 ReentrantLock 通过 Condition对象实现。一个lock可创建多个Condition对象,一个Condition对象可注册多个线程。 Condition 对象调用signal ||signalAll() 唤醒线程所在范围 = 注册的线程,而Sychronized 调用 notify() || notifyAll() 唤醒线程 = JVM选择的因此 ReentrantLock的等待通知机制更加灵活
10、Thread的start方法和run方法的区别?
run方法就是普通的一个方法,代码运行在当前主线程,start会启动一个新的线程,并运行run方法。