1. 什么是线程安全问题?
多线程操作公共对象时,如何保证对象数据不变脏。
2. synchronized和ReentrantLock锁的区别?
synchronized,在写法上变现为原生语法级别,是非公平锁,可重入锁,java 1.6版本前性能较差,
reentranLock, 也是可重入锁,写法上变现为API级别的锁,相对synchronized有更多高级的功能,主要有一下三个:
可实现公平锁:可以按照申请锁的时间顺序来获取锁
等待可中断:持有锁的线程长期不释放锁的情况下,等待的线程可以选择放弃,改为处理其他事情)。如果是等待可中断,就避免了死锁情况的发生。使用tryLock方法
锁可以绑定多个条件:可同时绑定多个conditon对象。可以像synchronize,wait,notity一样实现生产者消费者机制。
性能方面:java 1.6版本后 ,synchronized和reenTranlock相比,性能差不多
可重入锁可以解决的场景:比如现在有个方法A,它调用了方法B,并且这个两个方法又加了同一把锁。如果锁是不可重入的,那在执行方法B,线程想要再次获取该锁就会被阻塞,方法B也就执行不了啦。
synchronized如果加在方法上,锁的目标其实是这个对象,而不是对象的方法。举个例子,如果一个对象a有非公共方法A和方法B,且A和B都加了synchronized修饰。那么执行a.A的执行与a.B的执行就是互斥的,因为它们都需要争抢锁a。
我们知道sleep方法和wait方法都是可以使得线程阻塞的。让线程阻塞的考虑无非就是想控制代码执行的时间点,来达到控制执行速度或者是线程间配合执行的效果。sleep关键字,想要实现的效果是控制线程执行的时间点,不太考虑与其它线程的配合,所以它的设计也是不释放锁资源的。而wait不一样,从字面上看,wait也是在等待某件事的发生。ok,那么它的设计思路很明显倾向于线程间的配合执行,所以wait也一般和notify配合使用。这也解释了为什么wait是要释放锁资源的,因为如果执行wait的线程不释放锁资源,那与其配合的线程也无获取锁执行代码,从而也无法唤醒自己。
3. java中的锁有什么样的特性?
自旋特性:java1.6版本后,自旋默认开启,阻塞的线程在一段时间内会不断的尝试取获取锁资源,在尝试一定次数之后,如果还不能获取到锁资源,就将此线程挂起。当再次分配到cpu资源时,可以再次启动。
锁消除特性:不可能存在共享的数据,即使使用了加锁,也会被锁消除机制消灭掉。
锁粗化:如果有一系列操作对某个对象反复进行加锁和解锁的操作,会导致很多不必要的性能损耗。虚拟机会对这种场景进行优化,只保留一个加锁,加锁范围扩展到从第一加锁范围开水,到最后一次加锁范围结束
4. java中锁的级别?
轻量锁:从对象头中存放锁的标志位来看,对象被加轻量锁时,标志位是00。轻量锁时具备自旋的特性。
重量锁:从对象头中存放锁的标志位来看,对象被加重量锁时,标志位是10。如果有两条以上的线程争用一个锁,那锁就会从轻量锁升级为重量锁
偏向锁:优化无竞争条件下同步语句的性能,不会通过CAS操作去改变锁标记位,所以此时的锁标记位置还是01(和未加锁时一样),只会偏向第一个获取锁对象的线程,当有另外一个线程尝试去获取锁时,偏向模式结束。java 1.6版本后默认开启。与无加锁的状态对比,加上偏向锁后,对象头会绑上对应的线程id。
将锁分成轻量锁,重量锁,是为了能够实现什么新的特性吗?还是说能有什么性能上的提升?
我们知道,锁是有自旋特性的,获取不到锁的线程会不断尝试若干次去获取锁资源,那如果依然获取不到,才会将线程挂起。这是为了减少上下文切换带来的性能损耗。 但是如果,我们提前知道某个资源竞争非常激烈,现在去获取它肯定是抢不到的,是不是可以减少自旋带来的cpu消耗呢。这就是我理解为什么要将锁的状态分成偏向,轻量,重量的原因。如果无锁,线程可以直接获取到锁资源,并升级为轻量锁;如果是轻量锁,甚至节省不再需要通过多次cas的方式去设置threadID修改偏向锁状态,只需检测是否有指向该线程的偏向锁就可以;如果是偏向锁,其实表明已经有线程持有该锁了,但是没有其它的竞争者了,我再尝试几次获取锁,有可能可以拿到;那如果是重量锁,就表明锁的竞争比较激烈,可以先挂起线程,不用浪费cpu资源去自旋了。
5. java中的读写锁API
ReentrantReadWriteLock,利用ReentrantReadWriteLock对象获取到读锁对象和写锁对象,读操作使用读锁加锁,写操作使用写锁加锁。
6. 什么是死锁? 多线程相互死锁的场景是什么样的?怎么解决死锁?
死锁:线程之间相互持有对方需要的锁资源,导致线程永远处于等待资源的状态。
多线程死锁场景:假设现在有A,B,C三个线程,当前状况是A线程持有锁1,B线程持有锁2,C线程持有锁3。下一步的动作是A想要去获取锁2,B想要去获取锁3,C想要去获取锁1。那么当前线程就是处于相互死锁的状态。
思路:在加锁的时候,指定加锁的时长,时间到了自动释放锁资源;再尝试获取锁资源时,也可以指定等待时长,获取不到锁资源则报错提示。
解决方案:
a) 不同的方法使用不同的对象加锁
b) 如果两个方法必须使用多个相同的对象加锁,那么请加锁的顺序请保持一致。如有线程A,B,有对象m,n。A线程的加锁顺序是m,n,B线程的加锁顺序就一定要是m,n
c) 使用lock.tryLock(时长,单位),尝试一段时间去获取锁,获取到了返回true,获取不到返回false,在finally中将所有的锁释放掉。这样先执行的线程有一端逻辑会执行失败,后执行的线程可以执行成功。(使用具有等待可中断特性的锁,这样获取不到锁资源时,可以中断线程)
案例:db死锁报错,批量更新数据的操作,可能会发生死锁。如果两个线程批量更新的数据存在重叠,并且顺序不同。
7.Object.wait和Thread.sleep有什么不同。
这两个都可以在synchronized代码块中使用。wait会释放线程持有的锁资源,sleep不会释放线程持有的锁资源。
8. 说说Thread中join,yeild,interrupt的作用?
join: 在A线程中调用B线程的Thread.join(), 那么A线程就需要等待B线程执行完毕之后才可执行。控制线程的执行顺序。
yield:只是告诉操作系统可以让其他线程先运行,但是自己可以仍是运行态。执行yield的线程会主动让出cpu时间,但是它可能又立即抢到了cpu时间,所以yield的效果不稳定。
interrupt系列: 这几个方法配合使用,才可以达到中断线程的效果,它们分别是:
interrupt(): 通过thread.interrupt()调用,可以改变线程中断的标志状态。
interrupted(), 通过Thread.interrupted()调用,重制线程的状态。调用之后状态位,会被重新置为false。
isInterrupt(), 通过thread.isInterrupted调用, 可以获取线程的中断状态,中断为true,未中断范围false。调用后不会重置状态位。
8. 线程的5种状态?
新建状态: 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态: 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态: 如果就绪状态的线程获取 CPU 资源,就可以执行
run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态: 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞 状态。
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态: 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
在java代码中有通过枚举类来定义这几个状态:
New:线程新建后,但还有start。
RUNNABLE:线程start了,但是还没拿cpu资源
BLOCKED:线程拿不到锁资源
WAITING:调用了object.wait, thread.join, 线程等待状态
TIMED_WAITING:有等待时间的waiting状态,如调用了Thread.sleep()
TERMINED: 线程执行完成。
其中被synchronized阻塞,线程会处于Blocked状态
wait, join, await, sleep , LockSupport.park 不加时间时 线程会处于waiting状态
wait, await, sleep , LockSupport.park 线程将进入timed_waiting状态
这里也可以从主动和被动的角度来理解:
像通过主动的方法调用,比如Thread.sleep,Object.wait ,thread.join 的方式,这样的方式造成线程阻塞,对应java中的线程状态时waiting,如果有加时间参数的话就是timed_waiting.
被动阻塞:其实就是线程执行遇到需要获取锁的情况,但是因为获取不到锁资源,需要暂时挂起。像遇到synchronized,reenterLock.lock这种。
9. 什么是逃逸分析?
可以从两个方向来说:
方法逃逸:一个对象在方法中被定义之后是否会被其他的方法引用,例如作为参数传入其他方法中,称为方法逃逸。
线程逃逸:有一个线程中的变量可以被其他的线程访问到,那就是线程逃逸。
逃逸分析可以帮助做下面3个方面的优化,虽然这个应用还不太成熟。
a. 栈上分配:如果一个对象不会发生方法逃逸,那么这个对象的内存分配是否可以在栈中完成,因为堆中进行内存分配和垃圾回收会比较消耗时间。
b. 同步消除:如果一个对象不会发生线程逃逸的话,那么线程中的加锁操作其实是可以去掉的,因为加锁解锁的操作是会消耗线程资源的。
c. 标量替换:标量是无法分解的数据,如果一个对象不会发生方法或者线程逃逸的话,是否可以将这个对象拆成基本的标量,在栈中完成内存的分配。
10. synchronized的原理是什么样的?或者说synchronized是怎么实现加锁的?
以synchronized使用对象加锁来距举例,在对象的对象头中有个叫锁标记位的东西。
在对象未被加锁,或者处于偏向锁状态的时候是01
轻量级锁:00
重量级锁:10
GC状态: 11
加锁过程:
如果一个锁对象第一次被线程获取的话,采用的是偏向锁的模式,虚拟机会使用CAS操作将线程id记录在对象头中。当有另外一个线程尝试取获取锁的时候,偏向锁模式结束,可进一步升级为轻量级锁。
当线程进入同步代码块的时候,发现对象头的标记位是01,对象没有被锁定。这时虚拟机会在当前线程的栈帧中建立一个锁记录(lock record)的空间,该空间存放了该对象mark world的拷贝。
然后虚拟机会尝试使用CAS操作,将对象的Mark Word更新为指向Lock Record的指针。如果这个操作成功的话,就表示这个线程拥有了这个对象的锁,并且这个时候锁标记位会变成00。
当有两条以上的线程在争抢同一个锁的时候,这个锁会升级为重量级锁,锁标记位变成10。
解锁过程
将Lock Record中的Displaced Mark Word和对象头中的Mark Word进行交换。交换成功的话就是解锁成功,替换失败说明其它线程在尝试获取锁,在释放锁的同时,需要唤醒挂起的线程。
reentrantLock的实现原理
其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。