`

Java 多线程同步问题的探究(三、Lock来了,大家都让开【1. 认识重入锁】)

阅读更多

在上一节中,

我们已经了解了Java多线程编程中常用的关键字synchronized,以及与之相关的对象锁机制。这一节中,让 我们一起来认识JDK 5中新引入的并发框架中的锁机制

我想很多购买了《Java程序员面试宝典》之类图书的朋友一定对下面 这个面试题感到非常熟悉:

问:请对比synchronized与java.util.concurrent.locks.Lock 的异同。
答案:主要相同点:Lock能完成synchronized所实现的所有功能
     主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放 锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

恩,让我们先鄙视一下应试教育。

言归正传,我们先来看一个多线程程序。它使用多个线程对一个Student对象进行访问,改变其中的变量值。 我们首先用传统的synchronized 机制来实现它:

package sky.cn.test4;

import java.util.Random;

public class ThreadDemo implements Runnable {
	
	Student student = new Student();
	int count = 0;
	
	public void accessStudent() {
		String currentThreadName = Thread.currentThread().getName();
		long startTime = System.currentTimeMillis();
		System.out.println(currentThreadName + " is running!");
		synchronized (this) {		//(1)使用同一个ThreadDemo对象作为同步锁
			System.out.println(currentThreadName + " got lock1@Step1!");
			try {
				count++;
				Thread.sleep(5000);
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				System.out.println(currentThreadName + " first Reading count: " + count);
			}
			System.out.println(currentThreadName + " release lock1@Step1!");
		}
		
		synchronized (this) { 		//(2)使用同一个ThreadDemo对象作为同步锁
			System.out.println(currentThreadName + " got lock2@Step2!");
			try {
				Random random = new Random();
				int age = random.nextInt(100);
				System.out.println("thread " + currentThreadName + " set age to: " + age);
				this.student.setAge(age);
				System.out.println("thread " + currentThreadName + " first read age is: " 
              + this.student.getAge());
				Thread.sleep(5000);
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				System.out.println("thread " + currentThreadName + " second read age is: "
               + this.student.getAge());
			}
			System.out.println(currentThreadName + " release lock2@step2!");
			long endTime = System.currentTimeMillis();
			System.out.println("thread " + currentThreadName + " cost " 
             + (endTime - startTime)/1000 + " seconds!");
		}
	}
	
	public void run() {
		accessStudent();
	}
	
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		Thread t1 = new Thread(td, "a");
		Thread t2 = new Thread(td, "b");
		Thread t3 = new Thread(td, "c");
		t1.start();
		t2.start();
		t3.start();
	}

	class Student {
		private int age = 0;
		
		public int getAge() {
			return age;
		}
		
		public void setAge(int age) {
			this.age = age;
		}
	}
}
 结果:
a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count: 1
a release lock1@Step1!
c got lock1@Step1!
c first Reading count: 2
c release lock1@Step1!
c got lock2@Step2!
thread c set age to: 80
thread c first read age is: 80
thread c second read age is: 80
c release lock2@step2!
thread c cost 15 seconds!
b got lock1@Step1!
b first Reading count: 3
b release lock1@Step1!
b got lock2@Step2!
thread b set age to: 73
thread b first read age is: 73
thread b second read age is: 73
b release lock2@step2!
thread b cost 25 seconds!
a got lock2@Step2!
thread a set age to: 80
thread a first read age is: 80
thread a second read age is: 80
a release lock2@step2!
thread a cost 30 seconds!
 
显然,在这个程序中,由于两段synchronized块使用了同样的对象做为对象锁 ,所以JVM优先使刚刚释放该锁的线程重新获得该 锁。这样,每个线程执行的时间是10秒钟,并且要彻底把两个同步块的动作执行完毕,才能释放对象锁。这样,加起来一共是 30秒。

我想一定有人会说:如果两段synchronized块采用两个不同的对象锁,就可以提高程序的并发性,并且,这 两个对象锁应该选择那些被所有线程所共享的对象。

那么好。我们把第二个同步块中的对象锁改为student (此处略去代码,读 者自己修改),程序运行结果为:
a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count: 1
a release lock1@Step1!
a got lock2@Step2!
c got lock1@Step1!
thread a set age to: 32
thread a first read age is: 32
c first Reading count: 2
c release lock1@Step1!
b got lock1@Step1!
thread a second read age is: 32
a release lock2@step2!
thread a cost 10 seconds!
c got lock2@Step2!
thread c set age to: 86
thread c first read age is: 86
b first Reading count: 3
b release lock1@Step1!
thread c second read age is: 86
c release lock2@step2!
thread c cost 15 seconds!
b got lock2@Step2!
thread b set age to: 40
thread b first read age is: 40
thread b second read age is: 40
b release lock2@step2!
thread b cost 20 seconds!
 从 修改后的运行结果来看,显然,由于同步块的对象锁不同了,
三个线程的执行顺序也发生了变化。在一个线程释放第一个同步块的同步锁之 后,第二个线程就可以进入第一个同步块,而此时,第一个线程可以继续执行第二个同步块。这样,整个执行过程中,有10秒钟 的时间是两个线程同时工作 的。另外十秒钟分别是第一个线程执行第一个同步块的动作和最后一个线 程执行第二个同步块的动作 。相比较第一 个例程,整个程序的运行时间节省了1/3。细心的读者不难总结出优化前后的执行时间比例公式:(n+1)/ 2n ,其中n为 线程数 。如果线程数趋近于正无穷,则程序执行效率的提高会接近50%。而如果一个线程的执行阶段被分割成m个 synchronized ,并且每个同步块使用不同的对象锁,而同步块的执行时间恒定,则执行时间比例公式可以写作:((m- 1)n+1)/ mn 那么当m趋于无穷大时,线程数n趋近于无穷大,则程序执行效率的提升几乎可以达到100%。(显然,我 们不能按照理想情况下的数学推导来给BOSS发报告,不过通过这样的数学推导,至少我们看到了提高多线程程序并发性的一种方案,而 这种方案至少具备数学上的可行性理论支持。)

可见,使用不同的对象锁,在不同的同步块中完成任务,
可以使性能大大提升。

很多人看到这不禁要问:这和新的Lock框 架有什么关系?


别着急。我们这就来看一看。

synchronized块 的确不错,但是他有一些功能性的限制

1. 它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。

2.synchronized 块对于锁的获得和释放是在相同的堆栈帧中进行的。多数情况下,这没问题(而且与异常处理交互得很    好),但是,确实存在一些更适合使用 非块结构锁定的情况。

java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。

JDK 官方文档中提到:
ReentrantLock是“一个可重入的互斥锁 Lock ,它具有与使用 synchronized  方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查 此情况是否发生。 ”

简单来说,ReentrantLock有一个与锁相关的获取计 数器 ,如果拥有锁的某个线程再次得到锁 ,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放 。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

ReentrantLock  类(重入锁)实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性 。此外,它还提供了在激烈争用情况下更佳的性 能 。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

我们把 上面的例程改造一下:

package sky.cn.test4;

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

public class ThreadDemo implements Runnable {
	
	Student student = new Student();
	int count = 0;
	Lock lock1 = new ReentrantLock(false);
	Lock lock2 = new ReentrantLock(false);
	
	public void accessStudent() {
		String currentThreadName = Thread.currentThread().getName();
		long startTime = System.currentTimeMillis();
		System.out.println(currentThreadName + " is running!");
		lock1.lock();	//使用重入锁
		System.out.println(currentThreadName + " got lock1@Step1!");
		try {
			count++;
			Thread.sleep(5000);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			System.out.println(currentThreadName + " first Reading count: " + count);
			lock1.unlock();
			System.out.println(currentThreadName + " release lock1@Step1!");
		}
		
		lock2.lock();	//使用另外一个不同的重入锁
		System.out.println(currentThreadName + " got lock2@Step2!");
//		/*
//		 * 注:如果在此处抛出异常,将不会释放lock2的锁,需要确定开启锁和try之前不会出现异常,
//		 * 否则就全包到try中去
//		 */
//		if ("a".equals(currentThreadName)) {
//			throw new RuntimeException("thread a throw an exception!");
//		}
		try {
			Random random = new Random();
			int age = random.nextInt(100);
			System.out.println("thread " + currentThreadName + " set age to: " + age);
			this.student.setAge(age);
			System.out.println("thread " + currentThreadName + " first read age is: " 
            + this.student.getAge());
			Thread.sleep(5000);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			System.out.println("thread " + currentThreadName + " second read age is: " 
            + this.student.getAge());
			lock2.unlock();
			System.out.println(currentThreadName + " release lock2@step2!");
			long endTime = System.currentTimeMillis();
			System.out.println("thread " + currentThreadName + " cost " 
            + (endTime - startTime)/1000 + " seconds!");
		}
	}
	
	public void run() {
		accessStudent();
	}
	
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		Thread t1 = new Thread(td, "a");
		Thread t2 = new Thread(td, "b");
		Thread t3 = new Thread(td, "c");
		t1.start();
		t2.start();
		t3.start();
	}

	class Student {
		private int age = 0;
		
		public int getAge() {
			return age;
		}
		
		public void setAge(int age) {
			this.age = age;
		}
	}
}

 从上面这个 程序我们看到:

对象锁的获得和释放是由手工编码完成的,
所以获得锁和释放锁的时机 比使用同步块具有更好的可定制性 。并 且通过程序的运行结果(运行结果忽略,请读者根据例程自行观察),我们可以发现,和使用同步块的版本相比,结果是相同的

这说明两点问题:
1. 新的ReentrantLock的确实现了和同步块相同的语义功能。而对象锁的获得和释放 都可以由编码 人员自行掌握
2. 使用新的ReentrantLock,免去 了为同步块放置合适的对象锁 所要进行的考量。
3. 使用新的ReentrantLock,最佳的实践就是结合try/finally块来进行。在try块之前使用lock方法 ,而 在finally中使用unlock方法

细心的读者又发现了:

在我们的例程中,创建ReentrantLock实例的时候,
我们的构造函数里面传递的参数是false。那么如果传递 true又回是什么结果呢?这里面又有什么奥秘呢?

请看本节的续 ———— Fair or Unfair? It is a question...

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics