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);
        }

  • 相关阅读:
    JAVA复习笔记之GC部分
    elasticsearch学习笔记--原理介绍
    JavaScript--Array数组jion方法使用
    MyBatis探究-----缓存机制详解
    C#---- Winform控件PictureBox详解
    Mybatis探究-----一对一、一对多关联查询
    C#-----CancellationTokenSource取消线程
    MyBatis探究-----接口类映射XML文件中符号$和#的区别
    C#-----定时器的几种实现
    eclipse如何在不联网的情况下引入dtd约束文件
  • 原文地址:https://www.cnblogs.com/lingluo2017/p/10243610.html
Copyright © 2011-2022 走看看