【多线程与并发】:Java中的锁

锁的概念

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁可以防止多个线程同时访问共享资源(但有些锁可以允许多个线程并发的访问共享资源,如读写锁)。

在JDK1.5之前,Java是通过synchronized关键字实现锁功能的:隐式地获取锁和释放锁,但不够灵活。

在JDK1.5,java.util.concurrent包中新增了Lock接口以及相关实现类,用来实现锁功能。它提供了与synchronized关键字类似的同步功能,但功能更强大和灵活:获取锁和释放锁的可操作性、可中断地获取锁、超时获取锁等,见下表:

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,如果这个时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁 获取到锁的线程能够响应中断(而synchronized则不会响应中断操作)
超时获取锁 在指定的截止时间之前获取锁,如果在截止时间到了仍无法获取锁,则返回。

Lock接口具体的方法及释义:

public interface Lock {

    /**
     * 获取锁
     *
     * 如果当前线程无法获取到锁(可能其他线程正在持有锁),则当前线程就会休眠,直到获取到锁
     */
    void lock();

    /**
     * 可中断地获取锁
     *
     * 如果如果当前线程无法获取到锁(可能其他线程正在持有锁),则当前线程就会休眠,
     * 直到发生下面两种情况的任意一种:
     * ①获取到了锁
     * ②被其他线程中断
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 尝试非阻塞地获取锁
     *
     * lock()和lockInterruptibly()在获取不到锁的时候,都会阻塞当前线程,直到获取到锁
     * 而该方法则不会阻塞线程,能立即获取到锁则返回true,获取不到则立即返回false
     *
     * 该方法的常用方式如下:
     *
     * Lock lock = ...;
     * if (lock.tryLock()) {
     * try {
     * // manipulate protected state
     * } finally {
     * lock.unlock();
     * }
     * } else {
     * // perform alternative actions
     * }}
     *
     * 这种使用方式,可以保证只在获取到锁的时候才去释放锁
     */
    boolean tryLock();

    /**
     * 超时获取锁
     *
     * 当前线程在以下三种情况下会返回:
     * ①当前线程在超时时间内获取到了锁,返回true
     * ②当前线程在超时时间内被中断,返回false(即该方法可以响应其他线程对该线程的中断操作)
     * ③超时时间结束,没有获取到锁,返回false
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 释放锁
     */
    void unlock();

    /**
     * 获取与该锁绑定的Condition
     *
     * 当前线程只有在获得了锁,才能调用Condition的wait()方法(表示我已经到了某一条件),
     * 调用Condition的wait()方法之后,当前线程会释放锁
     */
    Condition newCondition();
}

java.util.concurrent.locks的类图

java.util.concurrent.locks中的类和接口.png

其中:
AbstractOwnableSynchronizerAbstractQueuedLongSynchronizerAbstractQueuedSynchronizer是同步器,是锁实现相关的内容。
ReentrantLock(重入锁)ReentrantReadWriteLock(重入读写锁)是具体的实现类。
LockSupport是一个工具类,提供了基本的线程阻塞和唤醒功能。
Condition是实现线程间实现多条件等待/通知模式用到的。


同步器的实现原理

TODO


重入锁:ReentrantLock

重入锁,顾名思义,就是支持重新进入的锁:即某线程在获取到锁之后,可以再次获取锁而不会被阻塞。
ReentrantLock类是通过组合自定义同步器来来实现这种重入特性的,除此之外,该类还支持公平地获取锁(获取锁的顺序与请求锁的顺序是相同的,等待时间最长的线程最优先获取到锁),还支持绑定多个Condition。(synchronized关键字隐式地支持重进入,比如synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,不会出现阻塞自己的情况)。

ReentrantLock内部重进入的实现(非公平获取锁的情况)代码如下:

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()) {
        //如果是当前持有锁的线程再次获取锁,则将同步值进行增加并返回true
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
}

ReentrantLock公平锁的内部实现代码如下:

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

与非公平获取锁的方法nonfairTryAcquire(int acquires)相比,多了一个hasQueuedPredecessors()判断:同步队列中当前节点(当前想要获取锁的线程)是否有前驱节点,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要前驱线程获取并释放锁之后才能继续获取锁。

公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换;
非公平锁虽然可能造成线程“饥饿”(即某线程可能需要等很久才得到锁),但线程切换极少,可以保证更大的吞吐量。


读写锁:ReentrantReadWriteLock

ReentrantLock在在同一时刻,只允许一个线程进行访问(无论读还是写)。而读写锁是指:在同一时刻,允许多个线程进行读操作,而写操作则会阻塞其他所有的线程(无论是读还是写,都会被阻塞)。读写锁维护了一对锁:读锁和写锁,通过分离读锁和写锁,使得并发性能相比一般的排他锁有了很大的提升。

Java中读写锁的实现类是ReentrantReadWriteLock,它支持:①重进入;②公平性选择;③锁降级:写锁可以降级为读锁,其提供了一些便于外界监控其内部状态的方法,如下:

int getReadLockCount()
返回当前读锁被获取的次数
注意:该次数并不等于获取读锁的线程数,
因为同一线程可以连续获得多次读锁,获取一次,返回值就加1,
比如,仅一个线程,它连续获得了n次读锁,那么占据读锁的线程数是1,但该方法返回n

int getReadHoldCount()
返回当前前程获取读锁的次数

boolean isWriteLock()
判断读锁是否被获取

int getWriteHoldCount()
返回当前写锁被获取的次数

使用举例:

public class Cache{

    //非线程安全的HashMap
    private static Map<String, Object> map = new HashMap<>();
    //读写锁
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    //读锁
    private static Lock readLock = reentrantReadWriteLock.readLock();
    //写锁
    private static Lock writeLock = reentrantReadWriteLock.writeLock();

    /**
     * 获取key对应的value
     *
     * 使用读锁,使得并发访问该方法时不会被阻塞
     */
    public static final Object get(String key){
        readLock.lock();
        try{
            return map.get(key);
        }finally {
            readLock.unlock();
        }
    }

    /**
     * 设置key对应的value
     *
     * 当有线程对map进行put操作时,使用写锁,阻塞其他线程的读、写操作,
     * 只有在写锁被释放后,其他读写操作才能继续
     */
    public static Object put(String key, Object value){
        writeLock.lock();
        try {
            return map.put(key, value);
        }finally {
            writeLock.unlock();
        }
    }

    /**
     * 清空map
     *
     * 当有线程对map进行清空操作时,使用写锁,阻塞其他线程的读、写操作,
     * 只有在写锁被释放后,其他读写操作才能继续
     */
    public static void clear(){
        writeLock.lock();
        try {
            map.clear();
        }finally {
            writeLock.unlock();
        }
    }
}

TODO:读写锁的实现原理


LockSupport工具类

LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,是构建同步组件的基础工具,它主要有两类方法:
①以park开头的方法:阻塞当前线程
②以unpark开头的方法:唤醒被阻塞的线程

void park()
阻塞当前线程,只有当前线程被中断或其他线程调用unpark(Thread thread),才能从park()方法返回

void parkNanos(long nanos)
阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回

void parkUntil(long deadline)
阻塞当前线程,直到deadline这个时间点(从1970年开始到deadline时间的毫秒数)

void unpark(Thread thread)
唤醒处于阻塞状态的thread线程

在JDK1.6中,该类增加了void park(Object blocker)void parkNanos(Object blocker, long nanos)void parkUntil(Object blocker, long deadline)方法,相比之前的park方法,多了一个blocker对象,该对象用来标识当前线程在等待的对象(阻塞对象),主要用来问题排查和系统监控(对线程dump时,可以提供阻塞对象的信息),可以用来代替原有的park方法。


Condition接口

任意一个Java对象都有一组监视器方法(定义在java.lang.Object上):wait()、wait(long timeout)、notify()、notifyAll(),这些方法与sychronized配合使用,可以实现等待/通知模式。
Condition接口也提供了类似的监视器方法,与Lock配合使用,可以实现等待/通知模式。

两者的区别如下:

对比项 Object Monitor Methods Condition
前置条件 获取对象的锁 调用Lock.lock()获取锁→调用Lock.newCondition()获取Condition对象
调用方式 直接调用,如object.wait() 直接调用,如condition.await()
等待队列个数 1个 多个
当前线程释放锁并进入等待状态 支持 支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断 不支持 支持
当前线程释放锁并进入超时等待状态 支持 支持
当前线程释放锁并进入等待状态到将来的某个时间点 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的全部线程 支持 支持

Condition中的方法如下:(一般会将Condition对象作为成员变量)
说明:当前线程调用await()方法后,当前线程会释放锁并在此等候,当其他线程调用signal()方法通知当前线程后,当前线程才从await()方法中返回,并且在返回前已经获取了锁(re-acquire)。

public interface Condition {

    /**
     * 当前线程进入等待状态直到被通知(signalled)或中断(interrupted)
     *
     * 如果当前线程从该方法返回,则表明当前线程已经获取了Condition对象所对应的锁
     *
     * @throws InterruptedException
     */
    void await() throws InterruptedException;

    /**
     * 与await()不同是:该方法对中断操作不敏感
     *
     * 如果当前线程在等待的过程中被中断,当前线程仍会继续等待,直到被通知(signalled),
     * 但当前线程会保留线程的中断状态值
     *
     */
    void awaitUninterruptibly();

    /**
     * 当前线程进入等待状态,直到被通知或被中断或超时
     *
     * 返回值表示剩余时间,
     * 如果当前线程在nanosTimeout纳秒之前被唤醒,那么返回值就是(nanosTimeout-实际耗时),
     * 如果返回值是0或者负数,则表示等待已超时
     *
     */
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    /**
     * 该方法等价于awaitNanos(unit.toNanos(time)) > 0
     */
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 当前线程进入等待状态,直到被通知或被中断或到达时间点deadline
     *
     * 如果在没有到达截止时间就被通知,返回true
     * 如果在到了截止时间仍未被通知,返回false
     */
    boolean awaitUntil(Date deadline) throws InterruptedException;

    /**
     * 唤醒一个等待在Condition上的线程
     * 该线程从等待方法返回前必须获得与Condition相关联的锁
     */
    void signal();

    /**
     * 唤醒所有等待在Condition上的线程
     * 每个线程从等待方法返回前必须获取Condition相关联的锁
     */
    void signalAll();
}

使用Condition实现一个有界阻塞队列的例子:当队列为空时,队列的获取操作将会阻塞当前线程,直到队列中有新增元素;当队列已满时,队列的插入操作就会阻塞插入线程,直到队列中出现空位。(其实这个例子就是简化版的ArrayBlockingQueue

class BoundedBlockingQueue<T> {
    //使用数组维护队列
    private Object[] queue;
    //当前数组中的元素个数
    private int count = 0;
    //当前添加元素到数组的位置
    private int addIndex = 0;
    //当前移除元素在数组中的位置
    private int removeIndex = 0;

    private Lock lock = new ReentrantLock();
    private Condition notEmptyCondition = lock.newCondition();
    private Condition notFullCondition = lock.newCondition();

    private BoundedBlockingQueue() {
    }

    public BoundedBlockingQueue(int capacity) {
        queue = new Object[capacity];
    }

    public void put(T t) throws InterruptedException {
        lock.lock();//获得锁,保证内部数组修改的可见性和排他性
        try {
            //使用while,而非if:防止过早或意外的通知,
            //加入当前线程释放了锁进入等待状态,然后其他线程进行了signal,
            //则当前线程会从await()方法中返回,再次判断count == queue.length
            //todo:哪些情况下的过早或意外???
            while (count == queue.length) {
                notFullCondition.await();//释放锁,等待队列不满,即等待队列出现空位
            }
            queue[addIndex] = t;
            addIndex++;
            if (addIndex == queue.length) {
                addIndex = 0;
            }
            count++;
            notEmptyCondition.signal();
        } finally {
            //确保会释放锁
            lock.unlock();
        }
    }

    @SuppressWarnings("unchecked")
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmptyCondition.await();//释放锁,等待队列不为空,即等待队列中至少有一个元素
            }
            Object x = queue[removeIndex];
            removeIndex++;
            if (removeIndex == queue.length) {
                removeIndex = 0;
            }
            count--;
            notFullCondition.signal();//通知那些等待队列非空的线程,可以向队列中插入元素了
            return (T) x;
        } finally {
            //确保会释放锁
            lock.unlock();
        }
    }
}

TODO:Condition的实现分析


参考

大部分来自《Java并发编程的艺术》,部分参考JDK中的注释说明。

作者:maxwellyue
链接:https://www.jianshu.com/p/6e0982253c01
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

原文地址:https://www.cnblogs.com/xiaoshen666/p/11258543.html

时间: 2024-09-29 09:40:05

【多线程与并发】:Java中的锁的相关文章

Java并发编程:Java中的锁和线程同步机制

锁的基础知识 锁的类型 锁从宏观上分类,只分为两种:悲观锁与乐观锁. 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作.Java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败. 悲观

【多线程】不懂什么是 Java 中的锁?看看这篇你就明白了!

本文来源:Java建设者 原文地址:https://mp.weixin.qq.com/s/GU42BjM5jY2CEMVD_PAZBQ Java 锁分类 Java 中的锁有很多,可以按照不同的功能.种类进行分类,下面是我对 Java 中一些常用锁的分类,包括一些基本的概述 从线程是否需要对资源加锁可以分为 悲观锁 和 乐观锁 从资源已被锁定,线程是否阻塞可以分为 自旋锁 从多个线程并发访问资源,也就是 Synchronized 可以分为 无锁.偏向锁. 轻量级锁和 重量级锁 从锁的公平性进行区分

java 中的锁 -- 偏向锁、轻量级锁、重量级锁

理解锁的基础知识 如果想要透彻的理解java锁的来龙去脉,需要先了解以下基础知识. 基础知识之一:锁的类型 锁从宏观上分类,分为悲观锁与乐观锁. 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作. java中的乐观锁基本都是通过CAS操作实现的,CAS

JAVA中关于锁机制

本文转自 http://blog.csdn.net/yangzhijun_cau/article/details/6432216 一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中). 取到锁后,他就开始执行同步代码(被synchronized修饰的代码):线程执行完同步代码后马上就把锁还给同步对象,其他在锁

一篇blog带你了解java中的锁

前言 最近在复习锁这一块,对java中的锁进行整理,本文介绍各种锁,希望给大家带来帮助. Java的锁 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作.java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传

JAVA中的锁机制

Java中的锁 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂.因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字(译者注:这说的是Java 5之前的情况). 自Java 5开始,java.util.concurrent.locks包中包含了一些锁的实现,因此你不用去实现自己的锁了.但是你仍然需要去了解怎样使用这些锁,且了解这些实现背后的理论也

Java中的锁(转)

Java中的锁 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂.因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字(译者注:这说的是Java 5之前的情况). 自Java 5开始,java.util.concurrent.locks包中包含了一些锁的实现,因此你不用去实现自己的锁了.但是你仍然需要去了解怎样使用这些锁,且了解这些实现背后的理论也

21、Java并发性和多线程-Java中的锁

以下内容转自http://ifeve.com/locks/: 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂.因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字(译者注:这说的是Java 5之前的情况). 自Java 5开始,java.util.concurrent.locks包中包含了一些锁的实现,因此你不用去实现自己的锁了.但是你仍然需要去

java中synchronize锁 volatile thread.join()方法的使用

对于并发工作,你永远不知道一个线程何时运行,你需要某种方式来避免两个任务访问相同的资源,即要避免资源竞争,至少在关键代码上不能出现这样的情况,否则多个线程同时对某个内存区域操作会导致数据破坏. 程序代码中的临界区是需要互斥访问的,同一时刻只能有一个线程来访问临界区,也就是线程对临界区的访问时互斥的. 竞争条件:当多个线程同时访问某个共享的内存区域并且对其进行读写操作时,就会出现数据破坏.这就是竞争条件.避免竞争条件的方法是synchronized加锁. 样例,设有一个现成,该线程的任务是对共享变