`

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...

分享到:
评论

相关推荐

    Java多线程同步.pdf

    ReentrantLock类提供了更高级的同步功能,包括可重入锁、公平锁、非公平锁等。 Java多线程同步机制的实现 在Java语言中,多线程同步机制的实现可以通过synchronized关键字、ReentrantLock类、 Semaphore类、...

    基于Java多线程同步的安全性研究.pdf

    接着,文章讨论了Java多线程同步机制中可能出现的安全性问题,如可见性、有序性和互斥性问题,并提出了解决这些问题的方法,包括使用ThreadLocal和Lock对象。 在文章的后半部分,讨论了Java多线程执行过程的机制和...

    Java多线程-避免同步机制带来的死锁问题及用Lock锁解决线程安全问题

    ### Java多线程-避免同步机制带来的死锁问题及用Lock锁解决线程安全问题 #### 死锁 ##### 1. 说明 在多线程编程中,死锁是一种常见的问题,指的是两个或多个线程在执行过程中,因为竞争资源而造成的一种相互等待...

    Python应用实战:python多线程-多线程安全问题&lock与rlock.zip

    本篇文章将深入探讨Python中的多线程安全问题以及如何使用锁(Lock)和可重入锁(RLock)来解决这些问题。 首先,我们要理解什么是线程安全。线程安全是指在多线程环境下,一个函数或方法被多个线程调用时,不会...

    Java多线程同步具体实例讲解 .doc

    Java多线程同步是编程中一个非常重要的概念,特别是在并发编程和高并发系统设计中起到关键作用。在Java中,为了保证线程安全,避免数据竞争和不一致的状态,我们通常会使用同步机制来控制对共享资源的访问。本文将...

    Java多线程同步机制的应用分析.pdf

    使用synchronized关键字可以实现线程之间的互斥访问,而使用ReentrantLock类可以实现线程之间的可重入锁。 在实际应用中,Java多线程同步机制可以用于解决多种问题,例如售票系统、银行系统等。通过使用同步机制,...

    Java多线程同步问题分析.pdf

    ReentrantLock是Lock接口的一个实现,它具有重入性,即线程可以获取已经持有锁的多次,这与synchronized的行为相似。 在售票系统实例中,如果使用ReentrantLock,可以更精确地控制哪些线程可以访问票的资源,同时...

    java 多线程同步

    Java多线程同步是Java编程中关键的并发概念,它涉及到如何在多个线程访问共享资源时保持数据的一致性和完整性。`java.util.concurrent`包是Java提供的一个强大的并发工具库,它为开发者提供了多种线程安全的工具,...

    java多线程之并发锁

    ReentrantLock 是一个可重入锁,这意味着线程可以多次获取同一个锁,直到线程释放所有的锁。ReentrantLock 的锁机制可以防止线程死锁和饥饿的发生。 FoodCenter 类和 ThreadDog、ThreadPig 类都是使用 Lock 机制来...

    Java多线程高并发篇(一)--重入锁

    在Java多线程高并发编程中,重入锁(ReentrantLock)是一个至关重要的概念,它提供了比Java内置锁(synchronized)更细粒度的控制,并且具有更高的可读性和可扩展性。本篇文章将深入探讨重入锁的相关知识点。 首先...

    JAVA多线程编程技术PDF

    Lock接口提供了更细粒度的锁控制,支持可中断和可重入的锁。volatile确保变量在多线程环境中的可见性和有序性,避免缓存一致性问题。 此外,线程间通信也是多线程编程的重要方面。Java提供了wait(), notify()和...

    Java多线程编程核心技术_完整版_java_

    Java多线程编程是Java开发中的重要组成部分,它允许程序同时执行多个任务,极大地提高了程序的效率和响应性。在Java中,多线程主要通过继承Thread类或实现Runnable接口来实现。本教程《Java多线程编程核心技术》将...

    java多线程经典案例

    Java多线程是Java编程中的重要概念,它允许程序同时执行多个任务,极大地提升了程序的效率和性能。在Java中,实现多线程有两种主要方式:通过实现Runnable接口或者继承Thread类。本案例将深入探讨Java多线程中的关键...

    java多线程同步例子

    java多线程同步互斥访问实例,对于初学者或是温故而知新的同道中人都是一个很好的学习资料

    java多线程进度条

    在Java编程中,多线程是一项关键特性,它允许程序同时执行多个任务,提升系统效率。在处理耗时操作如大文件下载、数据处理或网络请求时,展示进度条能够提供用户友好的交互体验,让使用者了解任务的完成状态。本主题...

    JAVA单线程多线程

    ### JAVA中的单线程与多线程概念解析 #### 单线程的理解 在Java编程环境中,单线程指的是程序执行过程中只有一个线程在运行。这意味着任何时刻只能执行一个任务,上一个任务完成后才会进行下一个任务。单线程模型...

    java多线程Demo

    在多线程环境下,可能会出现数据竞争问题,为了解决这个问题,Java提供了多种同步机制,如synchronized关键字、wait/notify机制、Lock锁(ReentrantLock)等。synchronized用于控制对共享资源的访问,而wait/notify...

    Java多线程同步机制在售票系统的实现

    ### Java多线程同步机制在售票系统的实现 #### 一、引言 随着计算机硬件的发展,多核处理器已经成为主流配置,这为多线程编程提供了更广阔的应用场景。多线程能够充分利用多核处理器的优势,提高程序的并发性和...

    java 多线程 同步详解

    Java多线程同步详解 在Java编程中,多线程是一种常见的并发执行方式,它可以提高程序的执行效率,充分利用CPU资源。然而,多线程环境下数据的安全性问题不容忽视,这就引出了Java中的同步机制。本文将深入探讨Java...

    基于同步机制解决Java多线程安全问题的应用 (1).zip

    总结,Java通过synchronized、volatile、Lock接口和ThreadLocal等同步机制,为开发者提供了强大的工具来处理多线程环境中的安全问题。理解和熟练掌握这些机制,是构建高效、稳定并发程序的关键。

Global site tag (gtag.js) - Google Analytics