1.synchronized锁
- synchronized锁可以加在方法和代码块上。
-
当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。
-
当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。
-
如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
- 对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
-
synchronized释放锁时机:代码执行完毕 , 发生异常。
2.Lock锁
JDK1.5之后并发包提供了Lock接口以及其实现类来实现锁功能。
位于java.util.concurrent.locks包下。
lock接口的实现类:ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock
lock接口提供了synchronized关键字不具有的特性:
特性 | 描述 |
---|---|
尝试非阻塞获得锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
获得锁,能够被中断 | 尝试获得锁的线程能够响应中断,被中断的线程会抛出终端异常,同时释放锁 |
获得锁,指定等待时间 | 等待指定时间去获得锁,如果超时则返回 |
2.1 lock获取锁
lock接口提供了四种方法获取锁:
- Lock()方法:尝试获取锁,如果锁被其他线程获得,则等待。lock方法需要主动释放锁,发生异常时不会主动释放锁,所以一般采用如下方式使用lock()
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
-
tryLock()
方法:这个方法是有返回值的,方法会尝试获得锁,获得成功则返回true,失败返回false。无论如何,该方法都会立即返回。 -
tryLock(long time, TimeUnit unit)
方法:与tryLock类似,如果在指定的时间内拿到锁,则返回true,否则返回false。 -
LockInterruptibly()
:当通过这个方法获得锁时,如果线程正在等待获得锁,这个线程可以被中断,即终端线程的等待状态。比如线程A得到了锁,线程B调用LockInterruptibly()等待锁,则此时可以调用B.interrupt()方法中断B等待获得锁。另外Interrupt()方法只能中断阻塞中的线程,不能中断执行中的线程。 -
ReentrantLock:
可重入锁
2.2 ReadWriteLock
ReadWriteLock是一个接口,其中声明了两个方法:Lock readLock(),Lock writeLock(),其中一个用来获取读锁,一个用来回去写锁,可以将读写分离,使多个线程可以同时进行
读
操作。
实现接口的类:
ReentrantReadWriteLock
读写锁可以支持多个线程同时进行读操作,但只能有一个线程进行写操作。
如果一个线程申请了读锁,另一给线程申请写锁,则会等待知道读锁释放。
如果一个线程申请了写锁,另一个线程不管申请读锁还是写锁,都会等待直到写锁释放。
2.3 lock和synchronized
- lock是接口,sychronized是关键字
- sychronized在发生异常时会释放锁,不会造成死锁。lock在发生异常时不会释放锁,会造成死锁,所以需要在finally中进行释放。
- lock能够响应中断,sychronized不能响应中断,等待的线程会一直等待下去。
- lock方法可以知道有没有成功获得锁。
- lock可以提高多个线程进行读操作的效率
3 锁相关概念
3.1 可重入锁
ReentrantLock和sychronized都是可重入锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。可重入锁的一个优点是可一定程度避免死锁。
3.2 可中断锁
3.3 公平锁
3.4 重量级锁
重量锁的同步成本很高,包括系统调用引起的内核态和用户态切换、线程阻塞造成的线程切换等。
3.5 自旋锁
目的:降低线程切换成本。
当线程竞争锁失败时,打算阻塞自己,这时候步进行阻塞,而是自旋(进入一个空的有限for循环),并同时竞争锁。如果自旋结束之前获得了锁,那么获得锁成功,否则阻塞自己。
适用于锁的持有时间短,或者锁的持有时间长但是竞争不激烈的场景。
缺点:
单核处理器上,不存在实际的并行,当前线程不阻塞自己的话,已获得锁的线程就不能执行,锁永远不会释放,此时不管自旋多久都是浪费;进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费。
自旋锁要占用CPU,如果是计算密集型任务,这一优化通常得不偿失,减少锁的使用是更好的选择。
如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。这通常发生在
锁持有时间长,且竞争激烈
的场景中,此时应主动禁用自旋锁。
3.6 自适应自旋锁
如果线程通过自旋获得了锁,则增加自旋时间。如果没有通过自旋获得锁,则减少自旋时间。
适用于
自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间
缺点
然而,自适应自旋也没能彻底解决该问题,
如果默认的自旋次数设置不合理(过高或过低),那么自适应的过程将很难收敛到合适的值
。
3.7 轻量级锁
如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。
轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗
,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
缺点:
如果
锁竞争激烈
,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费
3.8 偏向锁
如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。
偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗
。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
“偏向”的意思是,
偏向锁假定将来只有第一个申请锁的线程会使用锁
(不会有任何线程再来申请锁),因此,
只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功
,记录锁状态为偏向锁,
以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁
。
偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。
缺点:
同样的,如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。