五花八门的“锁”

锁(用于控制对共享资源的访问)

Lock接口!!!例如ReentrantLock;

为什么需要使用Lock或者synchronized不够用?

  1. synchronized效率低,不能设置超时等待,不能中断正常试图获取锁的线程;
  2. 不够灵活,加锁和释放锁的时机单一,需要执行完任务或者出现异常;
  3. 无法知道是否成功获取到锁

方法

lock()

  • 获取锁,如果锁被其他线程占用,进行等待;
  • Lock不会像synchronized一样在异常时自动释放
  • 使用时使用try—finally,在finally中释放锁

tryLock()/tryLock(long time,TimeUnit unit)

  • 尝试获取锁,返回boolean;
  • 可以根据是否获得到锁决定后续的行为
  • 立即返回,就算拿不到锁也不会等待;
  • tryLock(long time,TimeUnit unit),超时放弃

lockInterruptibly()

相当于tryLock(long time,TimeUnit unit)的时间设置为无限,进行等待锁,可以中断,十分灵活;

unlock()

习惯解锁,放在finally中;

可见性保证

happens-before原则;

Lock的加锁解锁和synchronized有同样的内存语义,下一个线程加锁可以看到前一个线程解锁前发生的所有操作;

锁的分类

五花八门的“锁”_第1张图片

1. 乐观(非互斥同步)锁和悲观(互斥同步)锁

互斥同步锁的劣势

  1. 阻塞和唤醒带来的性能劣势
  2. 永久阻塞
  3. 优先级反转(例如优先级低的线程拿到了锁并且释放慢,但是优先级高的线程就得等待优先级低的线程执行锁释放);

二者概念

乐观锁(CAS实现):认为自己操作的对象不会有人干扰,不锁同步资源更新的时候去检查数据是否被其他人修改过,如果没有被修改就正常执行,如果被修改过就不能继续执行了,会选择放弃、报错等策略;例如原子类、并发容器等

悲观锁:认为自己操作的对象总是会有人干扰,锁住同步资源不让其他人访问就不会出错;例如synchronized和Lock接口

二者开销(不确定)

总的来说:悲观锁>乐观锁;

悲观锁的开销比较固定,开始也是高于乐观锁的,并且一劳永逸;

虽然乐观锁一开始开销小,但是如果自旋时间长或者不停重试,资源开销也会增加;

适用场景

悲观锁:适合并发写入多的情况,临界区持有锁时间较长,避免大量无用自旋的消耗,典型场景:临界区有IO操作;临界区代码复杂;临界区竞争激烈;

乐观锁:适合并发写入少,大部分是读取的情况,性能提高大;

2. 可重入锁和不可重入锁

可重入锁(递归锁):同一个线程可以多次获取同一把锁;例如synchronized,ReentrantLock;

可重入锁好处:

  1. 避免死锁(如果两个方法被同一个锁锁住,如果线程A执行第一个方法,再去执行第二个方法的时候,如果没有可重入性质,那么就会死锁);
  2. 提升封装性(不用一次次的加解锁);

ReentrantLock方法

getHoldCount:返回当前的锁已经被拿到几次;

isHeldByCurrentThread:可以看出锁是否被当前线程所持有;

getQueueLength:可以返回正在等待这把锁的队列有多长;

源码分析

五花八门的“锁”_第2张图片

3. 公平锁和非公平锁

公平锁(ReentrantLock构造器传入true):按照线程请求锁的顺序来分配锁;

非公平锁(ReentrantLock默认非公平锁):不完全按照请求顺序来分配锁,在一定情况下可以插队;

非公平锁好处

避免唤醒线程带来的空档期,可以提高效率;

==tryLock()==可以不遵守公平锁策略!!!
五花八门的“锁”_第3张图片

二者对比

五花八门的“锁”_第4张图片

源码分析

五花八门的“锁”_第5张图片

4. 共享锁和排它锁(典型ReentrantReadWriteLock)

排它锁:写锁,独享锁、独占锁;既能读又能写,自己获取后其他线程无法获取;例如synchronized;

共享锁:读锁;获得共享锁后,只能查看数据,无法修改和删除数据,并且其他线程也可以获取到这个共享锁来查看数据;

读写锁的作用

  • 读是安全的,多个线程读数据是没有必要加锁的,如果使用同步锁,读数据也是需要获取锁,就造成了没有意义的开销;
  • 更加灵活;读的时候用读锁,写的时候用写锁;

读写锁规则

五花八门的“锁”_第6张图片

读写锁可以理解成一把锁,有两种情况:要么是多个线程读,要么是一个线程写,并且二者不同时出现;

ReentrantReadWriteLock实现

  • 不允许读锁插队(现在运行的是读锁,队列中有读锁和写锁,由于可以同时读,读锁能否插队?);
  • 允许降级(写—>读),不允许升级(读—>写);

插队策略

  1. 公平锁:读写锁都不允许插队;
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
  1. 非公平锁:
  • 写锁可以随时插队(如果线程正在读,写锁是无法插队的,只能进入等待队列);
  • 为了防止饥饿,读锁不准插队;读锁只能在等待队列头结点不是想获取写锁的时候可以插队(等待队列第一个是写锁就不能插队,是读锁就可以插队);

源码分析

公平情况下的读写锁

  	/**
     * Fair version of Sync
     */
//公平锁情况下的读写锁  
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
      //写锁是否要阻塞(阻塞就是排队)
        final boolean writerShouldBlock() {
          //查看队列中是否有线程等待,有就返回true,去排队
            return hasQueuedPredecessors();
        }
      //读锁是否要阻塞(阻塞就是排队)
        final boolean readerShouldBlock() {
             //查看队列中是否有线程等待,有就返回true,去排队
            return hasQueuedPredecessors();
        }
    }

非公平情况下的读写锁

/**
 * Nonfair version of Sync
 */
//非公平情况下的读写锁
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
  //写锁是否排队
    final boolean writerShouldBlock() {
      //写锁不需要排队
        return false; // writers can always barge
    }
   //读锁是否排队
    final boolean readerShouldBlock() {
      //去查看队列中第一个是不是排它锁(写锁)
      //如果是写锁,返回true,排队
      //如果是读锁,返回false,插队
        return apparentlyFirstQueuedIsExclusive();
    }
}

读写锁的升降级策略

支持锁降级(写—>读),不支持升级(读—>写);

Tips:ReentrantReadWriteLock不支持锁升级的原因是避免死锁(例如AB两个线程都是读锁想升级写锁,但是只能有一个写锁进行写入,A要升级就要求B释放读锁,B同理,陷入死锁);

适用场景

ReentrantLock适合一般场合;ReentrantReadWriteLock适合读多写少场合,可以进一步提高并发效率;

5. 自旋锁和阻塞锁

假如等待的锁很快就会被释放(同步代码块中代码简单),就划不来每次都去让CPU切换线程的状态,切换状态的时间比同步代码块执行时间还长;

概念

自旋锁:如果机器有多个处理器,能够让两个或以上的线程并行执行,就可以让后面请求的线程不放弃CPU进行自旋,一直去检测锁是否释放;如果前面的线程释放锁,就可以不必阻塞而直接获得锁,从而减少线程切换带来的开销,这就是自旋锁;

阻塞锁:如果没拿到锁的情况下,直接把线程阻塞,直到被唤醒;

自旋锁缺点

如果锁占用时间过长,自旋造成的开销较大(随着时间增长,自旋锁开销也线性增长),浪费处理器资源;

自旋锁实现原理(底层是CAS)

JDK1.5及以上的JUC下的atmoic基本都是基于自旋锁实现的;

自旋锁适用场景

  1. 适用于多核服务器,并发度不是特别高;
  2. 适用于临界区比较简单的情况下;

6. 可中断锁与不可中断锁

  • synchronized是不可中断锁;
  • Lock是可中断锁,调用tryLock(time)lockInterruptibly都能响应中断(调用线程的interrupt方法中断);

概念

可中断锁:线程A执行锁中的代码,当线程B在等待线程A释放锁的时候,等待时间过长,线程B不想等待了,于是就可以进行中断,线程B就可以执行其他事情了;

JDK6锁优化

  • 自旋锁和自适应(尝试自旋,自旋xx次,不成功就转为阻塞锁;如果这次自旋没得到,下次直接进入阻塞状态);
  • 锁消除(代码块不存在并发,无需加锁)
  • 锁粗化(如果对一个对象反复加锁解锁,扩大范围合为一个synchronized)
  • 偏向锁
  • 轻量级锁
  • 重量级锁

写代码时如何优化锁和性能提高

  1. 缩小同步代码块;
  2. 尽量不要锁住方法,可以使用代码块;
  3. 减少请求锁的次数;
  4. 避免人为制造“热点”;
  5. 锁中尽量不要包含锁;
  6. 选用合适的锁类型或合适的工具类(例如多读少写用读写锁,并发度不高用原子类);

你可能感兴趣的:(JAVA基础,多线程,java,并发编程)