zoukankan      html  css  js  c++  java
  • Java并发编程的艺术笔记(五)——Java中的锁

    一.Lock接口的几个功能:

    显示的获取和释放锁

    尝试非阻塞的获取锁

    能被中断的获取锁

    超时获取锁

    使用方式:

    Lock lock = new ReentrantLock();
    lock.lock();
    try {
    } finally {
    lock.unlock();
    }

    Lock的API

    二.AQS

    定义:用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。是实现锁的关键

    同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些

    模板方法将会调用使用者重写的方法。

    重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

    ·getState():获取当前同步状态。

    ·setState(int newState):设置当前同步状态。

    ·compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性

    同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。

    队列同步器的实现:

    1.同步队列

    同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

    同步器的addWaiter()方法:

        private Node addWaiter(Node mode) {
            Node node = new Node(Thread.currentThread(), mode);
            // 快速尝试在尾部添加
            Node pred = tail;
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            enq(node);
            return node;
        }
    
        private Node enq(final Node node) {
            for (;;) {
                Node t = tail;
                if (t == null) { // Must initialize
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else {
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }

    节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)

     三.重入锁

    支持一个线程对资源的重复加锁,还支持公平与非公平的获取锁。下面着重分析ReentrantLock

    实现重进入:锁识别获取锁的是不是当前占据这个锁的线程,是则成功获取锁。

    锁释放:进入时加1,释放时减1。

    公平锁与非公平锁:是否遵循FIFO。

    默认非公平锁:nonfairTryAcquire(int acquires)方法,当一个线程请求锁时,只要获取了同步状态即成功获取锁。好处:线程切换次数少,减少开销。

    //获取同步状态,成功则获取锁
    final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    if (compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
    }
    } else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0)
    throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
    }
    return false;
    }

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

    释放同步状态时减少同步状态值:

    protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
    throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
    free = true;
    setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
    }

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

    //公平锁
    protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
    }
    } else if (current == getExclusiveOwnerThread()) {
    int nextc = c + acquires;
    if (nextc < 0)
    throw new Error("Maximum lock count exceeded");
    setState(nextc);
    return true;
    }
    return false;
    }

    公平锁与非公平锁的不同:判断条件多了

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

    非公平锁减少线程切换的次数,减少了开销,增大了吞吐量。

    四.读写锁

    读写锁是非独占锁,维护了一对锁,一个读锁一个写锁,通过锁的分类使并发性得到了提高。

    实现:ReentrantReadWriteLock。

    读写锁使用方式:

    public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();
    // 获取一个key对应的value
    public static final Object get(String key) {
    r.lock();
    try {
    return map.get(key);
    } finally {
    r.unlock();
    }
    }
    // 设置key对应的value,并返回旧的value
    public static final Object put(String key, Object value) {
    w.lock();
    try {
    return map.put(key, value);
    } finally {
    w.unlock();
    }
    }
    // 清空所有的内容
    public static final void clear() {
    w.lock();
    try {
    map.clear();
    } finally {
    w.unlock();
    }
    }
    }

    4.1 读写状态的设计:维护多个读线程,一个写线程。“按位切割使用”变量,32位,高16位表示读,低16位表示写。

    读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。

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

    4.2 写锁的获取与释放

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

     
    protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // 存在读锁或者当前获取线程不是已经获取写锁的线程
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
                return false;
            }
            setExclusiveOwnerThread(current);
            return true;
        }

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

    后续读写线程可见。

    4.3读锁的获取与释放

    读锁是一个支持重进入的共享锁。如果当前线程已经获取了读锁则增加读状态,如果当前线程在获取读锁时写锁被其他线程获取,则等待。

    读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,这使获取读锁的实现变得复杂。

    4.4锁降级

    写锁变为读锁。是指获取(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

    public void processData() {
            readLock.lock();
            if (!update) {
                // 必须先释放读锁,为了保持可见性
                readLock.unlock();
                // 锁降级从写锁获取到开始
                writeLock.lock();
                try {
                    if (!update) {
                        // 准备数据的流程(略)
                        update = true;
                    }
                    readLock.lock();
                } finally {
                    writeLock.unlock();
                }
                // 锁降级完成,写锁降级为读锁
            }
            try {
                // 使用数据的流程(略)
            } finally {
                readLock.unlock();
            }
        }

     五. Condition接口

    等待/通知机制的两种实现方式:

    1.wait/notify 不够精细,生产者和消费者之间可能是没有联系的

    2.Condition结合Lock

    从本质上来说,Condition是对Object监视器的场景性能优化

    调用方式:

    直接调用,如condition.await()

    package concurrent;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ConditionDemo {
    
        public static void main(String[] args) {
            final ReentrantLock lock = new ReentrantLock();
            final Condition condition = lock.newCondition();
            
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "拿到锁了");
                    System.out.println(Thread.currentThread().getName() + "等待信号");
                    
                    try {
                        //当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,
                        //当前线程才从await()方法返回,并且在返回前已经获取了锁。
                        condition.await();
                    } catch (Exception e) {
                        
                    }
                    
                    System.out.println(Thread.currentThread().getName() + "拿到信号");
                    lock.unlock();
                }
            },"线程1").start();
            
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "拿到锁了");
                    
                    try {
                        System.out.println("睡3秒");
                        Thread.sleep(3000);
                    } catch (Exception e) {
                        
                    }
                    
                    System.out.println(Thread.currentThread().getName() + "发出信号");
                    condition.signal();
                    lock.unlock();
                }
            },"线程2").start();
        }
    
    }

    打印日志:

    线程1拿到锁了
    线程1等待信号
    线程2拿到锁了
    睡3秒
    线程2发出信号
    线程1拿到信号

    强大之处:

    1. 一个lock对象可以通过多次调用 lock.newCondition() 获取多个Condition对象,也就是说,在一个lock对象上,可以有多个等待队列,而Object的等待通知在一个Object上,只能有一个等待队列。

    2.可响应中断

    Condition的实现是同步器的内部类,因此每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用。

    当调用await()方法时,相当于步队列的首节点(获取了锁的节点)移动到Condition的等待队列中

    public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 当前线程加入等待队列
            Node node = addConditionWaiter();
            // 释放同步状态,也就是释放锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

    同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列中。

    signal()

    public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

  • 相关阅读:
    BF算法和KMP算法
    Python课程笔记 (五)
    0268. Missing Number (E)
    0009. Palindrome Number (E)
    0008. String to Integer (atoi) (M)
    0213. House Robber II (M)
    0198. House Robber (E)
    0187. Repeated DNA Sequences (M)
    0007. Reverse Integer (E)
    0006. ZigZag Conversion (M)
  • 原文地址:https://www.cnblogs.com/lingluo2017/p/10243610.html
Copyright © 2011-2022 走看看