JAVA日常学习—-锁

  • Post author:
  • Post category:java


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关键字不具有的特性:

lock接口额外特性
特性 描述
尝试非阻塞获得锁 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
获得锁,能够被中断 尝试获得锁的线程能够响应中断,被中断的线程会抛出终端异常,同时释放锁
获得锁,指定等待时间 等待指定时间去获得锁,如果超时则返回

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就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁

偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。



缺点:

同样的,如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。



版权声明:本文为qq_36112697原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。