并发编程05--Java中的锁(重入锁和读写锁)

Java中的锁

重入锁

重入锁ReentrantLock:表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

synchronized关键字隐式的支持重进入
比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,而不像Mutex由于获取了锁,而在下一次获取锁时出现阻塞自己的情况。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

class Mutex implements Lock {
    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否处于占用状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        // 当状态为0的时候获取锁
        @Override
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 释放锁,将状态设置为0
        protected boolean tryRelease(int releases) {
            if (getState() == 0) throw new
                    IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // 返回一个Condition,每个condition都包含了一个condition队列
        Condition newCondition() { return new ConditionObject(); }
    }
    // 仅需要将操作代理到Sync上即可
    private final Sync sync = new Sync();
    public void lock() { sync.acquire(1); }
    public boolean tryLock() { return sync.tryAcquire(1); }
    public void unlock() { sync.release(1); }
    public Condition newCondition() { return sync.newCondition(); }
    public boolean isLocked() { return sync.isHeldExclusively(); }
    public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

ReentrantLock虽然没能像sychrpnized关键字一样支持隐式的进入,但是在调用lock()方法时,已经获取到锁的线程能够再次调用lock()方法获取锁而不被阻塞.

如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁就是公平的.公平的锁,也就是等待时间最长的线程获取锁,也可以说锁获取是顺序的.

ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。

公平的锁机制旺旺没有非公平的效率高,到那时并不是任何场景都是以TPS作为唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

1.实现重进入

重进入:任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性需要解决以下两个问题.

  • 线程再次获取锁.所需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取.
  • 锁的最终释放.线程重复获取n次锁,随后在第n次释放该锁.其他线程能够获取到该锁.锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放,时,技术自减,当计数等于0时表示锁已经成功释放.

ReentrantLock是通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现为例.

并发编程05--Java中的锁(重入锁和读写锁)_第1张图片
ReentrantLock的nonfairTryAcquire方法

通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取状态成功.

成功获取锁的线程,只增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值,该方法的代码如下.


并发编程05--Java中的锁(重入锁和读写锁)_第2张图片
ReentrantLock的tryRelease方法

如果该锁被获取了n次,那么前(n-1)tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

公平与非公平获取锁的区别

公平性是针对获取锁而言的,如果一个锁是公平的,那么所得获取顺序就应该符合请求的绝对时间顺序,也就是FIFO.


并发编程05--Java中的锁(重入锁和读写锁)_第3张图片
ReentrantLock的tryAcquire方法

该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

非公平性锁可能使线程“饥饿”,为什么它又被设定成默认的实现呢?

  • 公平锁的上下文切换更过于频繁,相对的开销也就更大.

读写锁

读写锁在同一时刻可以被多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程被阻塞.

读写锁维护了,一对锁,一个读锁和一个写锁,通过分离读写锁和写锁,使得并发相比一般的排他锁有了很大提升.

读写锁能够简化读写交互场景的编程方式。

  • 假设在程序中定义一个共享的用作缓存的数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见.

在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了.

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock,


并发编程05--Java中的锁(重入锁和读写锁)_第4张图片
ReentrantReadWriteLock的特性

读写锁的实现分析

读写锁依赖同步器来实现同步功能,而读写状态就是同步器的同步状态.

同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键.

并发编程05--Java中的锁(重入锁和读写锁)_第5张图片
读写锁状态的划分方式

当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。

是通过位运算读写锁确定读和写的各自状态.

根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。

写锁的获取与释放

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态.


并发编程05--Java中的锁(重入锁和读写锁)_第6张图片
ReentrantReadWriteLock的tryAcquire方法

该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断.

  • 如果存在读锁,则写锁不能够被获取.
    原因在于:读写锁要确保写锁的操作对读锁可见,如果读锁在已被获取的情况下对写锁的获取,俺么正在运行的其他读线程就无法感知到当前写线程的操作.因此,只有等待其他线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞.

写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

读锁的获取与释放

读锁是一个支持可重入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总是会被成功获取,而所做的也只是(线程安全的)增加读状态.

  • 如果当前线程已经获取到了读锁,则会增加读状态.
  • 如果当前线程在获取读锁时,写锁已经被其他线程获取,则进入等待状态.


    并发编程05--Java中的锁(重入锁和读写锁)_第7张图片
    ReentrantReadWriteLock的tryAcquireShared方法

在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。

读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。

锁降级

锁降级指的是写锁降级成为读锁。
如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。

锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

和上述说法的区别在于获取读锁之前是否释放了写锁.

并发编程05--Java中的锁(重入锁和读写锁)_第8张图片
processData方法

上述示例中,当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false,此时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。

锁降级中读锁的获取是否有必要呢?

  • 是有必要的,主要是微课保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻

你可能感兴趣的:(并发编程05--Java中的锁(重入锁和读写锁))