Java多线程学习(六)Lock锁的使用

系列文章传送门:java

Java多线程学习(一)Java多线程入门程序员

Java多线程学习(二)synchronized关键字(1)面试

Java多线程学习(二)synchronized关键字(2)算法

Java多线程学习(三)volatile关键字编程

Java多线程学习(四)等待/通知(wait/notify)机制安全

Java多线程学习(五)线程间通讯知识点补充微信

系列文章将被优先更新于微信公众号“Java面试通关手册”,欢迎广大Java程序员和爱好技术的人员关注。数据结构

本节思惟导图: 多线程

本节思惟导图

思惟导图源文件+思惟导图软件关注微信公众号:“Java面试通关手册” 回复关键字:“Java多线程” 免费领取。并发

一 Lock接口

1.1 Lock接口简介

锁是用于经过多个线程控制对共享资源的访问的工具。一般,锁提供对共享资源的独占访问:一次只能有一个线程能够获取锁,而且对共享资源的全部访问都要求首先获取锁。 可是,一些锁可能容许并发访问共享资源,如ReadWriteLock的读写锁。

在Lock接口出现以前,Java程序是靠synchronized关键字实现锁功能的。JDK1.5以后并发包中新增了Lock接口以及相关实现类来实现锁功能。

虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,而且有助于避免涉及锁的许多常见编程错误,可是有时您须要以更灵活的方式处理锁。例如,用于遍历并发访问的数据结构的一些算法须要使用“手动”或“链锁定”:您获取节点A的锁定,而后获取节点B,而后释放A并获取C,而后释放B并得到D等。在这种场景中synchronized关键字就不那么容易实现了,使用Lock接口容易不少。

Lock是synchronized关键字的进阶,掌握Lock有助于学习并发包中的源代码,在并发包中大量的类使用了Lock接口做为同步的处理方式。

Lock接口的实现类: ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock

1.2 Lock的简单使用

Lock lock=new ReentrantLock();
  lock.lock();
   try{
    }finally{
    lock.unlock();
    }
复制代码

由于Lock是接口因此使用时要结合它的实现类,另外在finall语句块中释放锁的目的是保证获取到锁以后,最终可以被释放。

注意: 最好不要把获取锁的过程写在try语句块中,由于若是在获取锁时发生了异常,异常抛出的同时也会致使锁没法被释放。

1.3 Lock接口的特性和常见方法

Lock接口提供的synchronized关键字不具有的主要特性:

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,若是这一时刻锁没有被其余线程获取到,则成功获取并持有锁
能被中断地获取锁 获取到锁的线程可以响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁 在指定的截止时间以前获取锁, 超过截止时间后仍旧没法获取则返回

Lock接口基本的方法:

方法名称 描述
void lock() 得到锁。若是锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。
void lockInterruptibly() 获取锁,若是可用并当即返回。若是锁不可用,那么当前线程将被禁用以进行线程调度,而且处于休眠状态,和lock()方法不一样的是在锁的获取中能够中断当前线程(相应中断)。
Condition newCondition() 获取等待通知组件,该组件和当前的锁绑定,当前线程只有得到了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
boolean tryLock() 只有在调用时才能够得到锁。若是可用,则获取锁定,并当即返回值为true;若是锁不可用,则此方法将当即返回值为false 。
boolean tryLock(long time, TimeUnit unit) 超时获取锁,当前线程在一下三种状况下会返回: 1. 当前线程在超时时间内得到了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false.
void unlock() 释放锁。

二 Lock接口的实现类:ReentrantLock

ReentrantLocksynchronized关键字同样能够用来实现线程之间的同步互斥,可是在功能是比synchronized关键字更强大并且更灵活。

ReentrantLock类常见方法:

构造方法:

方法名称 描述
ReentrantLock() 建立一个 ReentrantLock的实例。
ReentrantLock(boolean fair) 建立一个特定锁类型(公平锁/非公平锁)的ReentrantLock的实例

ReentrantLock类常见方法(Lock接口已有方法这里没加上):

方法名称 描述
int getHoldCount() 查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。
protected Thread getOwner() 返回当前拥有此锁的线程,若是不拥有,则返回 null
protected Collection getQueuedThreads() 返回包含可能正在等待获取此锁的线程的集合
int getQueueLength() 返回等待获取此锁的线程数的估计。
protected Collection getWaitingThreads(Condition condition) 返回包含可能在与此锁相关联的给定条件下等待的线程的集合。
int getWaitQueueLength(Condition condition) 返回与此锁相关联的给定条件等待的线程数的估计。
boolean hasQueuedThread(Thread thread) 查询给定线程是否等待获取此锁。
boolean hasQueuedThreads() 查询是否有线程正在等待获取此锁。
boolean hasWaiters(Condition condition) 查询任何线程是否等待与此锁相关联的给定条件
boolean isFair() 若是此锁的公平设置为true,则返回 true 。
boolean isHeldByCurrentThread() 查询此锁是否由当前线程持有。
boolean isLocked() 查询此锁是否由任何线程持有。

2.1 第一个ReentrantLock程序

ReentrantLockTest.java

public class ReentrantLockTest {

	public static void main(String[] args) {

		MyService service = new MyService();

		MyThread a1 = new MyThread(service);
		MyThread a2 = new MyThread(service);
		MyThread a3 = new MyThread(service);
		MyThread a4 = new MyThread(service);
		MyThread a5 = new MyThread(service);

		a1.start();
		a2.start();
		a3.start();
		a4.start();
		a5.start();

	}

	static public class MyService {

		private Lock lock = new ReentrantLock();

		public void testMethod() {
			lock.lock();
			try {
				for (int i = 0; i < 5; i++) {
					System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
				}
			} finally {
				lock.unlock();
			}

		}

	}

	static public class MyThread extends Thread {

		private MyService service;

		public MyThread(MyService service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.testMethod();
		}
	}
}
复制代码

运行结果:

运行结果
从运行结果能够看出, 当一个线程运行完毕后才把锁释放,其余线程才能执行,其余线程的执行顺序是不肯定的

2.2 Condition接口简介

咱们经过以前的学习知道了:synchronized关键字与wait()和notify/notifyAll()方法相结合能够实现等待/通知机制,ReentrantLock类固然也能够实现,可是须要借助于Condition接口与newCondition() 方法。Condition是JDK1.5以后才有的,它具备很好的灵活性,好比能够实现多路通知功能也就是在一个Lock对象中能够建立多个Condition实例(即对象监视器),线程对象能够注册在指定的Condition中,从而能够有选择性的进行线程通知,在调度线程上更加灵活

在使用notify/notifyAll()方法进行通知时,被通知的线程是有JVM选择的,使用ReentrantLock类结合Condition实例能够实现“选择性通知”,这个功能很是重要,并且是Condition接口默认提供的。

而synchronized关键字就至关于整个Lock对象中只有一个Condition实例,全部的线程都注册在它一个身上。若是执行notifyAll()方法的话就会通知全部处于等待状态的线程这样会形成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的全部等待线程

Condition接口的常见方法:

方法名称 描述
void await() 至关于Object类的wait方法
boolean await(long time, TimeUnit unit) 至关于Object类的wait(long timeout)方法
signal() 至关于Object类的notify方法
signalAll() 至关于Object类的notifyAll方法

2.3 使用Condition实现等待/通知机制

1. 使用单个Condition实例实现等待/通知机制:

UseSingleConditionWaitNotify.java

public class UseSingleConditionWaitNotify {

	public static void main(String[] args) throws InterruptedException {

		MyService service = new MyService();

		ThreadA a = new ThreadA(service);
		a.start();

		Thread.sleep(3000);

		service.signal();

	}

	static public class MyService {

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

		public void await() {
			lock.lock();
			try {
				System.out.println(" await时间为" + System.currentTimeMillis());
				condition.await();
				System.out.println("这是condition.await()方法以后的语句,condition.signal()方法以后我才被执行");
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
		}

		public void signal() throws InterruptedException {
			lock.lock();
			try {				
				System.out.println("signal时间为" + System.currentTimeMillis());
				condition.signal();
				Thread.sleep(3000);
				System.out.println("这是condition.signal()方法以后的语句");
			} finally {
				lock.unlock();
			}
		}
	}

	static public class ThreadA extends Thread {

		private MyService service;

		public ThreadA(MyService service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.await();
		}
	}
}
复制代码

运行结果:

运行结果
在使用wait/notify实现等待通知机制的时候咱们知道必须执行完notify()方法所在的synchronized代码块后才释放锁。在这里也差很少,必须执行完signal所在的try语句块以后才释放锁,condition.await()后的语句才能被执行。

注意: 必须在condition.await()方法调用以前调用lock.lock()代码得到同步监视器,否则会报错。

2. 使用多个Condition实例实现等待/通知机制:

UseMoreConditionWaitNotify.java

public class UseMoreConditionWaitNotify {
	public static void main(String[] args) throws InterruptedException {

		MyserviceMoreCondition service = new MyserviceMoreCondition();

		ThreadA a = new ThreadA(service);
		a.setName("A");
		a.start();

		ThreadB b = new ThreadB(service);
		b.setName("B");
		b.start();

		Thread.sleep(3000);

		service.signalAll_A();

	}
	static public class ThreadA extends Thread {

		private MyserviceMoreCondition service;

		public ThreadA(MyserviceMoreCondition service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.awaitA();
		}
	}
	static public class ThreadB extends Thread {

		private MyserviceMoreCondition service;

		public ThreadB(MyserviceMoreCondition service) {
			super();
			this.service = service;
		}

		@Override
		public void run() {
			service.awaitB();
		}
	}
	
}
复制代码

MyserviceMoreCondition.java

public class MyserviceMoreCondition {

	private Lock lock = new ReentrantLock();
	public Condition conditionA = lock.newCondition();
	public Condition conditionB = lock.newCondition();

	public void awaitA() {
		lock.lock();
		try {
			System.out.println("begin awaitA时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionA.await();
			System.out.println(" end awaitA时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void awaitB() {
		lock.lock();
		try {			
			System.out.println("begin awaitB时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionB.await();
			System.out.println(" end awaitB时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void signalAll_A() {
		lock.lock();
		try {			
			System.out.println(" signalAll_A时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionA.signalAll();
		} finally {
			lock.unlock();
		}
	}

	public void signalAll_B() {
		lock.lock();
		try {		
			System.out.println(" signalAll_B时间为" + System.currentTimeMillis()
					+ " ThreadName=" + Thread.currentThread().getName());
			conditionB.signalAll();
		} finally {
			lock.unlock();
		}
	}
}
复制代码

运行结果:

运行结果:
只有A线程被唤醒了。

3. 使用Condition实现顺序执行

ConditionSeqExec.java

public class ConditionSeqExec {

	volatile private static int nextPrintWho = 1;
	private static ReentrantLock lock = new ReentrantLock();
	final private static Condition conditionA = lock.newCondition();
	final private static Condition conditionB = lock.newCondition();
	final private static Condition conditionC = lock.newCondition();

	public static void main(String[] args) {

		Thread threadA = new Thread() {
			public void run() {
				try {
					lock.lock();
					while (nextPrintWho != 1) {
						conditionA.await();
					}
					for (int i = 0; i < 3; i++) {
						System.out.println("ThreadA " + (i + 1));
					}
					nextPrintWho = 2;
					//通知conditionB实例的线程运行
					conditionB.signalAll();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		};

		Thread threadB = new Thread() {
			public void run() {
				try {
					lock.lock();
					while (nextPrintWho != 2) {
						conditionB.await();
					}
					for (int i = 0; i < 3; i++) {
						System.out.println("ThreadB " + (i + 1));
					}
					nextPrintWho = 3;
					//通知conditionC实例的线程运行
					conditionC.signalAll();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		};

		Thread threadC = new Thread() {
			public void run() {
				try {
					lock.lock();
					while (nextPrintWho != 3) {
						conditionC.await();
					}
					for (int i = 0; i < 3; i++) {
						System.out.println("ThreadC " + (i + 1));
					}
					nextPrintWho = 1;
					//通知conditionA实例的线程运行
					conditionA.signalAll();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		};
		Thread[] aArray = new Thread[5];
		Thread[] bArray = new Thread[5];
		Thread[] cArray = new Thread[5];

		for (int i = 0; i < 5; i++) {
			aArray[i] = new Thread(threadA);
			bArray[i] = new Thread(threadB);
			cArray[i] = new Thread(threadC);

			aArray[i].start();
			bArray[i].start();
			cArray[i].start();
		}

	}
}
复制代码

运行结果:

Condition实现顺序执行运行结果
经过代码很好理解,说简单就是在一个线程运行完以后经过condition.signal()/condition.signalAll()方法通知下一个特定的运行运行,就这样循环往复便可。

注意: 默认状况下ReentranLock类使用的是非公平锁

2.4 公平锁与非公平锁

Lock锁分为:公平锁非公平锁。公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获取锁的,和公平锁不同的就是先来的不必定先的到锁,这样可能形成某些线程一直拿不到锁,结果也就是不公平的了。

FairorNofairLock.java

public class FairorNofairLock {

	public static void main(String[] args) throws InterruptedException {
		final Service service = new Service(true);//true为公平锁,false为非公平锁

		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				System.out.println("★线程" + Thread.currentThread().getName()
						+ "运行了");
				service.serviceMethod();
			}
		};

		Thread[] threadArray = new Thread[10];
		for (int i = 0; i < 10; i++) {
			threadArray[i] = new Thread(runnable);
		}
		for (int i = 0; i < 10; i++) {
			threadArray[i].start();
		}

	}
	static public class Service {

		private ReentrantLock lock;

		public Service(boolean isFair) {
			super();
			lock = new ReentrantLock(isFair);
		}

		public void serviceMethod() {
			lock.lock();
			try {
				System.out.println("ThreadName=" + Thread.currentThread().getName()
						+ "得到锁定");
			} finally {
				lock.unlock();
			}
		}

	}
}
复制代码

运行结果:

公平锁运行结果
公平锁的运行结果是有序的。

把Service的参数修改成false则为非公平锁

final Service service = new Service(false);//true为公平锁,false为非公平锁

复制代码

非公平锁运行结果
非公平锁的运行结果是无序的。

三 ReadWriteLock接口的实现类:ReentrantReadWriteLock

3.1 简介

咱们刚刚接触到的ReentrantLock(排他锁)具备彻底互斥排他的效果,即同一时刻只容许一个线程访问,这样作虽然虽然保证了实例变量的线程安全性,但效率很是低下。ReadWriteLock接口的实现类-ReentrantReadWriteLock读写锁就是为了解决这个问题。

读写锁维护了两个锁,一个是读操做相关的锁也成为共享锁,一个是写操做相关的锁 也称为排他锁。经过分离读锁和写锁,其并发性比通常排他锁有了很大提高。

多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操做的过程就是互斥的。)。在没有线程Thread进行写入操做时,进行读取操做的多个Thread均可以获取读锁,而进行写入操做的Thread只有在获取写锁后才能进行写入操做。即多个Thread能够同时进行读取操做,可是同一时刻只容许一个Thread进行写入操做。

3.2 ReentrantReadWriteLock的特性与常见方法

ReentrantReadWriteLock的特性:

特性 说明
公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量上来看仍是非公平优于公平
重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁以后,可以再次获取读锁。而写线程在获取了写锁以后可以再次获取写锁也可以同时获取读锁
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁可以降级称为读锁

ReentrantReadWriteLock常见方法: 构造方法

方法名称 描述
ReentrantReadWriteLock() 建立一个 ReentrantReadWriteLock()的实例
ReentrantReadWriteLock(boolean fair) 建立一个特定锁类型(公平锁/非公平锁)的ReentrantReadWriteLock的实例

常见方法: 和ReentrantLock类 相似这里就不列举了。

3.3 ReentrantReadWriteLock的使用

1. 读读共享

两个线程同时运行read方法,你会发现两个线程能够同时或者说是几乎同时运行lock()方法后面的代码,输出的两句话显示的时间同样。这样提升了程序的运行效率。

private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

	public void read() {
		try {
			try {
				lock.readLock().lock();
				System.out.println("得到读锁" + Thread.currentThread().getName()
						+ " " + System.currentTimeMillis());
				Thread.sleep(10000);
			} finally {
				lock.readLock().unlock();
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
复制代码

2. 写写互斥

把上面的代码的

lock.readLock().lock();
复制代码

改成:

lock.writeLock().lock();
复制代码

两个线程同时运行read方法,你会发现同一时间只容许一个线程执行lock()方法后面的代码

3. 读写互斥

private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

	public void read() {
		try {
			try {
				lock.readLock().lock();
				System.out.println("得到读锁" + Thread.currentThread().getName()
						+ " " + System.currentTimeMillis());
				Thread.sleep(10000);
			} finally {
				lock.readLock().unlock();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public void write() {
		try {
			try {
				lock.writeLock().lock();
				System.out.println("得到写锁" + Thread.currentThread().getName()
						+ " " + System.currentTimeMillis());
				Thread.sleep(10000);
			} finally {
				lock.writeLock().unlock();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

复制代码

测试代码:

Service service = new Service();

		ThreadA a = new ThreadA(service);
		a.setName("A");
		a.start();

		Thread.sleep(1000);

		ThreadB b = new ThreadB(service);
		b.setName("B");
		b.start();

复制代码

运行两个使用同一个Service对象实例的线程a,b,线程a执行上面的read方法,线程b执行上面的write方法。你会发现同一时间只容许一个线程执行lock()方法后面的代码。记住:只要出现写操做的过程就是互斥的。

4. 写读互斥

和读写互斥相似,这里不用代码演示了。记住:只要出现写操做的过程就是互斥的。

参考:

《Java多线程编程核心技术》

《Java并发编程的艺术》

相关文章
相关标签/搜索