Java多并发之锁机制


本文参考Java并发编程艺术
锁机制是Java多并发的核心之一,凡是谈及多并发多线程问题,锁机制是在所难免的,本文将对Java并发包中与锁相关的API和组件,以及这些API和组件的使用方式和实现细节。

一、Lock接口

1.1 什么是锁?

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

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

在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放,在try外抛出异常,则停止执行,不会执行finally块。

1.2 Lock对比synchronized关键字及常用API

Lock接口提供的synchronized关键字不具备的主要特性如下图:
在这里插入图片描述

Lock是一个接口,它定义了锁获取和释放的基本操作,Lock的API如下图:
在这里插入图片描述

1.3 Lock接口源码

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

从源码可以看出,Lock接口仅仅是给出了一些锁获取判断操作的基本方法。

二、队列同步器

Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。

2.1 什么是队列同步器

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

2.2 队列同步器的接口与示例

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态:
· getState():获取当前同步状态。
· setState(int newState):设置当前同步状态。
· compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
同步器可重写的方法与描述如下图所示:
在这里插入图片描述
实现自定义同步组件时,将会调用同步器提供的模板方法,这些(部分)模板方法与描述如下所示:
在这里插入图片描述
同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。
只有掌握了同步器的工作原理才能更加深入地理解并发包中其他的并发组件,所以下面通过一个独占锁的示例来深入了解一下同步器的工作原理。
顾名思义,独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁,如代码如下所示:

class Mutex implements Lock {
	// 静态内部类,自定义同步器
	private static class Sync extends AbstractQueuedSynchronizer {
		// 是否处于占用状态
		protected boolean isHeldExclusively() {
			return getState() == 1;
		}
		// 当状态为0的时候获取锁
		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));
	}
}

Mutex是一个自定义的同步组件,其内部自定义了一个静态内部类Sync ,该内部类继承了同步器,重写了AQS类的方法,在重写的方法中,调用getState()、setState(int state)、compareAndSetState(int expect, int update)来访问或者更新同步状态,此外Mutex类中其他方法,通过静态内部类Sync属性变量来调用AQS中的模板方法,实现Mutex自定义同步组件的相关操作。

三、重入锁

3.1 什么是重入锁

重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
什么意思呢?简单理解就是一个线程对已获得的锁的情况下,还可以继续对该锁进行获取,典型的例子就是synchronized关键字,我们考虑这种情况,如果synchronized用来修饰方法,而该方法是递归方法,那么每次递归则都会对资源进行synchronized隐式加锁,但是在进入递归之前,前一级方法并没有结束,也就是前一层递归没有释放synchronized锁,子级递归就又加锁了,这并不会阻塞当前线程,这就是可重入锁。
如果不是重入锁,在没有释放锁的同时还获取锁,就会阻塞线程;比如前面的代码例子,当一个线程调用Mutex的lock()方法获取锁之后,如果再次调用lock()方法,则该线程将会被自己所阻塞,原因是Mutex在实现tryAcquire(int acquires)方法时没有考虑占有锁的线程再次获取锁的场景,而在调用tryAcquire(int acquires)方法时返回了false,导致该线程被阻塞。简单地说,Mutex是一个不支持重进入的锁。
另外,锁的公平性, 就是根据时间顺序分配锁,先请求者先获得,那么这个锁就是公平的,反之则是不公平的。

3.2 实现重进入

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

  1. 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  2. 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
    我们以ReentrantLock锁为例分析锁的重入性。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()) {
		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;
	// 计数器计算后发现为0,则最后一次释放锁,其他线程可以获取该锁
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}

如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为时,将占有线程设置为null,并返回true,表示释放成功。
总之,锁的冲进入,就是根据一个计数器的值来进行判断该锁的加锁/释锁的次数,并且多次加锁/释锁的前提条件是判断是否为同一线程,在此基础上,实现锁的重入。

四、读写锁

4.1 什么是读写锁

之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。也就是说 当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。
Java并发包提供读写锁的实现是ReentrantReadWriteLock,它提供的特性如下:
在这里插入图片描述

4.2 读写锁的接口与示例

首先看看读写锁ReadWriteLock接口的源码:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

从源码中,我们可以看出ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法,比较简单,我们一般使用的是它的一个实现类ReentrantReadWriteLock。
我们来看看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();
		}
	}
}

上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法和clear()方法,在更HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续。Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。
这里简单说下笔者在学习这一段遇到的一个小疑惑:在我们的印象中,锁一般都是互斥的,具有排他性,读写锁也不例外(笔者认为写锁具有排他性,因此读写锁也具有),现在分别考虑读锁和写锁,如果多线程读锁可以同步进行,那么为什么还需要读锁呢?是不是就可以不需要读锁,直接并发获取读操作?反正不修改变量不会导致一致性问题,经过一番思考,笔者认为,这个读书实际上是为写锁的排他性服务的,试想,如果当前有线程在进行读操作,然后有一个线程此时要进行写操作也就是获取写锁,那么我们就要阻塞其他正在读的线程或者写的线程,我们是根据什么来判断正在读的线程是哪些呢?如果没有读锁,我们就很难判断,但是有读锁,相当于给读线程打上了标记,我们就可以知道这些线程是哪些了,然后阻塞相应的线程即可,实现写锁的互斥。

4.3 锁降级

锁降级指的是写锁降级成为读锁(写锁—>读锁,反之不是)。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程(简而言之就是写锁获取和释放中间插入一个读锁获取的过程)。
接下来我们看一段代码:

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接口

5.1 什么是Condition

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。通过Condition我们可以唤醒特定的线程对象。
在这里插入图片描述

5.2 代码举例

我们编写代码,让三个线程A、B、C按照A->B->C->A的方式轮流执行:

public class MutilaTion {
    public static void main(String[] args) throws InterruptedException {
        Data data = new Data();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                data.outA();
            }
        }
        ).start();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                data.outB();
            }
        }
        ).start();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
            data.outC();
        	}
        }
        ).start();
    }
}
class Data{
    private int flag = 1;
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();


    public void outA(){
        lock.lock();
        try {
            while (flag != 1){
                try {
                    conditionA.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            flag = 2;
            System.out.println("线程A执行===========》》》》》AAAAA");
            conditionB.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void outB(){
        lock.lock();
        try {
            while (flag != 2){
                try {
                    conditionB.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            flag = 3;
            conditionC.signal();
            System.out.println("线程B执行===========》》》》》BBBBB");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void outC(){
        lock.lock();
        try {
            while (flag != 3){
                try {
                    conditionC.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            flag = 1;
            conditionA.signal();
            System.out.println("线程C执行===========》》》》》CCCCC");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

输出结果:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值