美文网首页
04 番外(待补充AQS相关原理) Java多线程中的各种锁

04 番外(待补充AQS相关原理) Java多线程中的各种锁

作者: 攻城狮哦哦也 | 来源:发表于2019-08-13 20:14 被阅读0次

1 乐观锁 悲观锁

1.1 乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。那么我们如何实现乐观锁呢,一般来说有以下2种方式:

  • 使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
  • 乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

1.2 悲观锁

  • 对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度
  • 在整个数据处理过程中,将数据处于锁定状态
  • 悲观锁的实现往往依靠数据库提供的锁机制

2 内置锁 显式锁

2.1 内置锁

Synchronized 关键字结合对象的监视器,JVM 为我们提供了一种『内置锁』的语义,这种锁很简便,不需要我们关心加锁和释放锁的过程,我们只需要告诉虚拟机哪些代码块需要加锁即可,其他的细节会由编译器和虚拟机自己实现。

2.2 显式锁

Lock 显式锁
获取锁可以被中断,超时获取锁,尝试获取锁
Lock 接口位于 java.util.concurrent.locks 包下,基本定义如下:

public interface Lock {
    //获取锁,失败则阻塞
    void lock();
    //响应中断式获取锁
    void lockInterruptibly()
    //尝试一次获取锁,成功返回true,失败返回false,不会阻塞
    boolean tryLock();
    //定时尝试
    boolean tryLock(long time, TimeUnit unit)
    //释放锁
    void unlock();
    //创建一个条件队列
    Condition newCondition();
}

2.3 显式锁(Lock) 和 内置锁(synchronized)的比较

Synchronized

内置锁获得锁和释放锁是隐式的,进入synchronized修饰的代码就获得锁,走出相应的代码就释放锁。
通信
与Synchronized配套使用的通信方法通常有wait(),notify()。

  • wait()方法会立即释放当前锁,并进入等待状态,等待到相应的notify并重新获得锁过后才能继续执行;
  • notify()不会立刻立刻释放锁,必须要等notify()所在线程执行完synchronized块中的所有代码才会释放。
    灵活性
  • 内置锁在进入同步块时,采取的是无限等待的策略,一旦开始等待,就既不能中断也不能取消,容易产生饥饿与死锁的问题
  • 在线程调用notify方法时,会随机选择相应对象的等待队列的一个线程将其唤醒,而不是按照FIFO的方式,如果有强烈的公平性要求,比如FIFO就无法满足
    性能
    Synchronized在JDK1.5及之前性能(主要指吞吐率)比较差,扩展性也不如ReentrantLock。但是JDK1.6以后,修改了管理内置锁的算法,使得Synchronized和标准的ReentrantLock性能差别不大。

Lock(一个实现类ReentrantLock)

ReentrantLock是显示锁,需要显示进行 lock 以及 unlock 操作。

通信
与ReentrantLock搭配的通行方式是Condition,如下:

private Lock lock = new ReentrantLock();  
private Condition condition = lock.newCondition(); 
condition.await();//this.wait();  
condition.signal();//this.notify();  
condition.signalAll();//this.notifyAll();

Condition是被绑定到Lock上的,必须使用lock.newCondition()才能创建一个Condition。从上面的代码可以看出,Synchronized能实现的通信方式,Condition都可以实现,功能类似的代码写在同一行中。而Condition的优秀之处在于它可以为多个线程间建立不同的Condition,比如对象的读/写Condition,队列的空/满Condition,在JDK源码中的ArrayBlockingQueue中就使用了这个特性:

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();
}
private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}

编码

Lock lock = new ReentrantLock();
lock.lock();
try{

}finally{
    lock.unlock();
}

相比于Synchronized要复杂一些,而且一定要记得在finally中释放锁而不是其他地方,这样才能保证即使出了异常也能释放锁。

灵活性

  • lock.lockInterruptibly() 可以使得线程在等待锁是支持响应中断;lock.tryLock() 可以使得线程在等待一段时间过后如果还未获得锁就停止等待而非一直等待。有了这两种机制就可以更好的制定获得锁的重试机制,而非盲目一直等待,可以更好的避免饥饿和死锁问题
  • ReentrantLock可以成为公平锁(非默认的),所谓公平锁就是锁的等待队列的FIFO,不过公平锁会带来性能消耗,如果不是必须的不建议使用。这和CPU对指令进行重排序的理由是相似的,如果强行的按照代码的书写顺序来执行指令,就会浪费许多时钟周期,达不到最大利用率
    性能
  • 虽然Synchronized和标准的ReentrantLock性能差别不大,但是ReentrantLock还提供了一种非互斥的读写锁,
  • 也就是不强制每次最多只有一个线程能持有锁,它会避免“读/写”冲突,“写/写”冲突,但是不会排除“读/读”冲突,
  • 因为“读/读”并不影响数据的完整性,所以可以多个读线程同时持有锁,这样在读写比较高的情况下,性能会有很大的提升。

3 独占锁 共享锁

  • AQS独占和共享锁,ReentantLock为独占锁,ReentantReadWriteLock中readLock()为共享锁,writeLock()为独占锁。
  • 读锁与读锁可以共享
  • 读锁与写锁不可以共享
  • 写锁与写锁不可以共享

4 重入锁

可重入锁ReentrantLock
同一个线程可重复获取该锁,没获取一次加锁计数器加一

5 读写锁

概述

  • 同一时刻允许多个读线程同时访问,但是写线程访问的时候,所有其它的读和写都被阻塞,最适宜与读多写少的情况

详细

  • 之前提到锁(如Mutex和ReentrantLock)基本都是排它锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他线程均被阻塞。读写锁维护了一对锁,一个读锁和写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大提升。

6 公平锁 非公平锁

如果在时间上,先对锁进行获取的请求,一定先被满足,这个锁就是公平的,不满足,就是非公平的
非公平的效率一般来讲更高

7 死锁 活锁

7.1 死锁

  • 资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。
  • 死锁的根本成因:获取锁的顺序不一致导致。
    怀疑发生死锁:
  • 通过jps 查询应用的 id,
  • 再通过jstack id 查看应用的锁的持有情况
    解决办法:
  • 保证加锁的顺序性
    动态死锁
    动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的。
    解决:
    1、通过内在排序,保证加锁的顺序性
    2、通过尝试拿锁,也可以。

7.2 活锁

  • 尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。
    两个线程1、2,1线程需要拿到A锁,然后再拿B锁,2线程需要拿到B锁,然后再拿A锁,就会发生这样的情况,当1线程拿到A锁时,2线程也正好拿到了B锁,这时1线程尝试拿B锁时发现B锁已经被2线程持有了,而2线程尝试拿A锁时发现A锁已经被1线程持有了,这时两个线程都会将自己持有的锁释放,而后重新再拿锁。
    这样的话会导致活锁问题,两个线程一直谦让,虽然最后可以成功,但是会导致资源浪费,降低了性能。
    解决办法:
    每个线程休眠随机数,错开拿锁的时间。

相关文章

网友评论

      本文标题:04 番外(待补充AQS相关原理) Java多线程中的各种锁

      本文链接:https://www.haomeiwen.com/subject/jbdbjctx.html