Java并发编程4-Java中的锁

1 synchronized的好处

隐式获取释放锁,简化了同步的管理,十分便捷。

2 锁的优势

(1)拥有锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

(2)扩展性和灵活性好。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。

3 锁得使用方式

Lock的使用也很简单,下面是Lock的使用的方式。

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

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

4 锁得常用API

094603_Onx4_3145136.png

5 重入锁

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

5.1 重入锁的简单实现(非JDK的实现)

public class SimpleReentrantLock {

	// 是否占有锁资源的标识位
	private boolean isLocked = false;
	// 当前锁资源被哪个线程持有
	private Thread lockedByThread = null;
	// 某线程上锁次数
	private int lockCount = 0;

	public void lock() {
		synchronized (this) {
			Thread currentThread = Thread.currentThread();
			// 如果锁资源被占有了,并且当前线程并不是占有锁资源的线程。
			while (isLocked && currentThread != lockedByThread) {
				try {
					wait();
				} catch (InterruptedException e) {
				}
			}
			isLocked = true;
			lockCount++;
			lockedByThread = currentThread;
		}

	}

	public void unlock() {
		// 如果当前线程就是获得锁资源的线程,那么就lockCount进行自减.
		if (Thread.currentThread() == this.lockedByThread) {
			lockCount--;
			if (lockCount == 0) {// 如果lockCount恢复到0时就释放资源
				isLocked = false;
				notify();
			}
		}
	}

}

5.2 重入锁的释放

    重入锁必须保证每次获取的锁都被释放才算是完全释放。

    重入锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

    例如线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。

5.3 重入锁的公平性

    如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。

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

    下面是示例代码:

package com.niuniu.study.spring;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.junit.Test;

public class FairAndUnfairTest {
	private static Lock fairLock = new ReentrantLock2(true);
	private static Lock unfairLock = new ReentrantLock2(false);

	@Test
	public void fair() {
		testLock(fairLock);
	}

	@Test
	public void unfair() {
		testLock(unfairLock);
	}

	private void testLock(Lock lock) {
		// 启动5个Job(略)
	}

	private static class Job extends Thread {
		private Lock lock;

		public Job(Lock lock) {
			this.lock = lock;
		}

		public void run() {
			// 连续2次打印当前的Thread和等待队列中的Thread(略)
		}
	}

	private static class ReentrantLock2 extends ReentrantLock {
		public ReentrantLock2(boolean fair) {
			super(fair);
		}

		public Collection<Thread> getQueuedThreads() {
			List<Thread> arrayList = new ArrayList<Thread>(
					super.getQueuedThreads());
			Collections.reverse(arrayList);
			return arrayList;
		}
	}
}

分别运行fair()和unfair()两个测试方法,输出结果如下所示:

===========================================================

121233_cUpo_3145136.png

===========================================================

    观察表所示的结果(其中每个数字代表一个线程),公平性锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁出现了一个线程连续获取锁的情况。

    为什么会出现线程连续获取锁的情况呢?这是因为当一个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。

    非公平性锁可能使线程“饥饿”,为什么它又被设定成默认的实现呢?这是因为公平性锁虽然保证了锁的获取按照FIFO原则,但是代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

6 读写锁

    读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

6.1 读写锁的好处

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

    假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。
    在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

(2)读写锁的性能都会比排它锁好。

    因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

6.2 读写锁的简单实现(非JDK的实现)

package com.niuniu.studyDemo;

import java.util.HashMap;
import java.util.Map;

public class SimpleReentrantLockReadWriteLock {

	// 读线程数
	private int readersCount = 0;
	// 写线程数(在0/1之间变化)
	private int writersCount = 0;
	// 请求写线程数
	private int requestWriters = 0;

	// 持有读锁线程容器
	private Map<Thread, Integer> readerThreads = new HashMap<Thread, Integer>();

	// 持有写锁的线程
	private Thread writeThread = null;

	// 获取读锁
	public void readLock() {
		synchronized (this) {
			Thread currentThread = Thread.currentThread();
			// 如果存在写线程或请求写线程,或者当前线程没有持有读锁,那么就等待.
			while (writersCount > 0 || requestWriters > 0
					|| !isCurrentThreadHoldReadLock(currentThread)) {
				try {
					wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			readerThreads.put(currentThread,
					(getCurrentThreadCount(currentThread) + 1));
		}
	}

	// 释放读锁
	public void unlockReadLock() {

		synchronized (this) {

			Thread currentThread = Thread.currentThread();
			int count = getCurrentThreadCount(currentThread);
			if (count == 1) {
				readerThreads.remove(currentThread);
			} else {
				readerThreads.put(currentThread, (count - 1));
			}
			notifyAll();
		}
	}

	/**
	 * 当前线程是否持有读锁
	 * 
	 * @param currentThread
	 * @return
	 */
	public boolean isCurrentThreadHoldReadLock(Thread currentThread) {
		return readerThreads.containsKey(currentThread);
	}

	/**
	 * 获得当前线程上锁次数
	 * 
	 * @param currentThread
	 * @return
	 */
	public int getCurrentThreadCount(Thread currentThread) {

		Integer count = readerThreads.get(currentThread);
		if (count == null)
			return 0;
		return count.intValue();

	}

	// 获取写锁
	public void writeLock() {
		synchronized (this) {
			Thread currentThread = Thread.currentThread();

			requestWriters++;
			// 如果存在写线程或读线程并且当前线程没有持有锁就等待
			while (readersCount > 0 || writersCount > 0
					|| (writeThread != null && currentThread != writeThread)) {
				try {
					wait();
				} catch (InterruptedException e) {
				}
			}
			requestWriters--;
			writersCount++;
			writeThread = currentThread;
		}
	}

	// 释放写锁
	public void unlockWriteLock() {

		synchronized (this) {
			writersCount--;
			if (writersCount == 0) {
				writeThread = null;
			}
			notifyAll();
		}
	}
}

6.3 ReentrantReadWriteLock

Java并发包提供读写锁的接口类是ReadWriteLock,它定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法,其实现是ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法,这些方法如下表所示。

122957_0NtB_3145136.png

接下来,通过一个缓存示例说明读写锁的使用方式,示例代码如下所示。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.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使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。

6.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();
		}
}

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

    锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

7 Condition接口

    Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。

    Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。
    Condition的使用方式比较简单,需要注意在调用方法前获取锁,使用方式如下代码所示。

    Lock lock = new ReentrantLock();
	Condition condition = lock.newCondition();

	public void conditionWait() throws InterruptedException {
		lock.lock();
		try {
			condition.await();
		} finally {
			lock.unlock();
		}
	}

	public void conditionSignal() throws InterruptedException {
		lock.lock();
		try {
			condition.signal();
		} finally {
			lock.unlock();
		}
	}

    如示例所示,一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
    Condition定义的(部分)方法以及描述如下表所示。

130709_fNYW_3145136.png

130719_uve1_3145136.png

    获取一个Condition必须通过Lock的newCondition()方法。下面通过一个有界队列的示例来深入了解Condition的使用方式。有界队列是一种特殊的队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现“空位”,如下面代码所示。

package com.niuniu.study.spring;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedQueue<T> {
	private Object[] items;
	// 添加的下标,删除的下标和数组当前数量
	private int addIndex, removeIndex, count;
	private Lock lock = new ReentrantLock();
	private Condition notEmpty = lock.newCondition();
	private Condition notFull = lock.newCondition();

	public BoundedQueue(int size) {
		items = new Object[size];
	}

	// 添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
	public void add(T t) throws InterruptedException {
		lock.lock();
		try {
			while (count == items.length)
				notFull.await();
			items[addIndex] = t;
			if (++addIndex == items.length)
				addIndex = 0;
			++count;
			notEmpty.signal();
		} finally {
			lock.unlock();
		}
	}

	// 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
	@SuppressWarnings("unchecked")
	public T remove() throws InterruptedException {
		lock.lock();
		try {
			while (count == 0)
				notEmpty.await();
			Object x = items[removeIndex];
			if (++removeIndex == items.length)
				removeIndex = 0;
			--count;
			notFull.signal();
			return (T) x;
		} finally {
			lock.unlock();
		}
	}
}

    上述示例中,BoundedQueue通过add(T t)方法添加一个元素,通过remove()方法移出一个元素。以添加方法为例。

    首先需要获得锁,目的是确保数组修改的可见性和排他性。当数组数量等于数组长度时,表示数组已满,则调用notFull.await(),当前线程随之释放锁并进入等待状态。如果数组数量不等于数组长度,表示数组未满,则添加元素到数组中,同时通知等待在notEmpty上的线程,数组中已经有新元素可以获取。
    在添加和删除方法中使用while循环而非if判断,目的是防止过早或意外的通知,只有条件符合才能够退出循环。回想之前提到的等待/通知的经典范式,二者是非常类似的。

 

转载于:https://my.oschina.net/u/3145136/blog/842529

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值