`
supportopensource
  • 浏览: 521726 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

线程的同步

阅读更多
一个经典的关于线程安全性的问题:银行取钱问题。

银行取钱的基本流程基本可以分为以下几个步骤:
(1)用户输入账户、密码,系统判断用户的账户、密码是否匹配。
(2)用户输入取款金额。
(3)系统判断账户余额是否大于取款金额。
(4)如果余额大于取款余额,取款成功;如果余额小于取款金额,则取款失败。

要求:按照以上步骤编写取款程序,并且要使用两个线程模拟取钱操作,即模拟两个人使用同一账户并发取钱。不用考虑检查账户和密码的操作,仅模拟后面三步操作。

编写程序的步骤如下:

1、定义一个账户类,该账户类封装了账户编号和余额两个属性。
源代码:Account.java
public class Account{
	//定义账户编号、账户余额两个属性
	private String accountNo;
	private double balance;
	//构造方法
	public Account(String accountNo,double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	//获得属性accountNo的值
	public String getAccountNo(){
		return accountNo;
	}
	//设置属性accountNo的值
	public void setAccountNo(String accountNo){
		this.accountNo = accountNo;
	}
	//获得属性balance的值
	public double getBalance(){
		return balance;
	}
	//设置属性balance的值
	public void setBalance(double balance){
		this.balance = balance;
	}
	//根据accountNo计算hashCode
	public int hashCode(){
		return accountNo.hashCode();
	}
	//重写equals()方法
	public boolean equals(Object obj){
		if(obj!=null&&obj.getClass()==Account.class){
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}


2、定义一个取钱的线程类,该线程类根据执行账户、取钱数目进行取钱操作。其中,取钱的逻辑是当余额不足时无法取出现金,当余额足够时系统吐出现金,金额减少。
源代码:DrawThread.java
public class DrawThread extends Thread{
	//模拟用户账户
	private Account account;
	//当前取钱线程所希望取钱的数目
	private double drawAmount;
	//构造方法
	public DrawThread(String name,Account account,double drawAmount){
		super(name);
		this. account = account;
		this.drawAmount = drawAmount;
	}
	//当多条线程修改同一个共享数据时,将涉及数据安全问题
	public void run(){
		//账户余额大于取钱的数目
		if(account.getBalance()>=drawAmount){
			//取出钞票
			System.out.println(getName()+"取钱成功!您取钱的数目为:"+drawAmount);
			//修改余额
			account.setBalance(account.getBalance()-drawAmount);
			System.out.println("您账户的余额为:"+account.getBalance());
		}
		else{
			System.out.println(getName()+"余额不足!取钱失败!");
		}
	}
}


3、主程序,创建一个账户,并启动两个线程从该账户账户取钱。
源代码:DrawTest.java
public class DrawTest{
	public static void main(String args[]){
		//创建一个账户
		Account account  = new Account("12345678",1000);
		//模拟两个线程对同一个账户取钱操作
		new DrawThread("甲",account,800).start();
		new DrawThread("乙",account,800).start();
	}
}

程序的运行结果如下(结果有多种):
结果一:
甲取钱成功!您取钱的数目为:800.0
您账户的余额为:200.0
乙取钱成功!您取钱的数目为:800.0
您账户的余额为:-600.0

结果二:
甲取钱成功!您取钱的数目为:800.0
乙取钱成功!您取钱的数目为:800.0
您账户的余额为:200.0
您账户的余额为:-600.0

结果三:
乙取钱成功!您取钱的数目为:800.0
您账户的余额为:200.0
甲取钱成功!您取钱的数目为:800.0
您账户的余额为:-600.0

结果四:
乙取钱成功!您取钱的数目为:800.0
甲取钱成功!您取钱的数目为:800.0
您账户的余额为:200.0
您账户的余额为:-600.0


说明:多次运行该程序,很难产生我们希望的正确的结果!这是因为线程调度的不确定性。

同步代码块

为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:
synchronized(obj){
…….
	//此处的代码就是同步代码块
}

其中,obj是一个同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

注意:任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。

虽然Java程序允许使用任何对象来作为同步监视器,但想一下同步监视器的目的:阻止两条线程对同一个共享资源进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,应该考虑使用账户(account)作为同步监视器。

修改取钱线程类的代码如下:
public class DrawThread extends Thread{
	//模拟用户账户
	private Account account;
	//当前取钱线程所希望取钱的数目
	private double drawAmount;
	//构造方法
	public DrawThread(String name,Account account,double drawAmount){
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	//当多条线程修改同一个共享数据时,将涉及数据安全问题
	public void run(){
		//使用account作为同步监视器,任何线程进入下面同步代码块之前,
		//必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
		//这种做法符合:加锁——>修改完成——>释放锁的逻辑
		synchronized(account){
			//账户余额大于取钱的数目
			if(account.getBalance()>=drawAmount){
				//取出钞票
				System.out.println(getName()+"取钱成功!您取钱的数目为:"+drawAmount);
				account.setBalance(account.getBalance()-drawAmount);
				System.out.println("您账户的余额为:"+account.getBalance());
			}
			else{
				System.out.println(getName()+"余额不足!取钱失败!");
			}
		}
		//同步代码块结束,该线程释放同步锁
	}
}

再次运行该程序,结果如下:
甲取钱成功!您取钱的数目为:800.0
您账户的余额为:200.0
乙余额不足!取钱失败!


说明:上述程序使用synchronized将run方法里方法体修改成了同步代码块,该同步代码块的同步监视器是account对象,这样的做法符合“加锁——>修改完成——>释放锁”的逻辑,任何线程想修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成,该线程将释放对该资源的锁定。通过这种方式就可以保证并发线程在任何一个时刻只有一条线程可以进入修改共享资源的代码区(也称为临界区),所以同一个时刻最多只有一条线程处于临界区内,从而保证了线程的安全。

同步方法

与同步代码块相对应的是,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰的某个方法。对于同步方法而言,无需显式指定同步监视器,同步方法的同步监视器是this,也就是该对象本身。

通过使用同步方法可以非常方便地将某类变成线程安全的类,线程安全的类具有如下特性:
(1)该类的对象可以被多个线程安全的访问。
(2)每个线程调用该对象的任何方法之后都将得到正确的结果。
(3)每个线程调用该对象的任何方法之后,该对象状态依然保持合理状态。

对于可变类和不可变类,不可变类总是线程安全的,因为它的对象的状态不可改变。而可变类需要额外的方法来保证其线程安全。例如上面的Account类就是一个可变类,它的account和balance两个属性都是可变的,当两个线程同时修改Account对象的balance属性时,程序就出现了异常。下面将Account类对balance的访问设置成线程安全的,那么程序只要把修改的balance方法修改成同步方法即可。
程序如下:
源代码:Account.java
public class Account{
	//定义账户编号、账户余额两个属性
	private String accountNo;
	private double balance;
	//构造方法
	public Account(String accountNo,double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	//获得属性accountNo的值
	public String getAccountNo(){
		return accountNo;
	}
	//设置属性accountNo的值
	public void setAccountNo(String accountNo){
		this.accountNo = accountNo;
	}
	//获得属性balance的值
	public double getBalance(){
		return balance;
	}
	//提供一个线程安全draw方法来完成取钱操作
	public synchronized void draw(double drawAmount){
		//账户余额大于取钱的数目
		if(getBalance()>=drawAmount){
			//取出钞票
			System.out.println(Thread.currentThread().getName()+"取钱成功!您取钱的数目为:"+drawAmount);
			balance-=drawAmount;
			System.out.println("您账户的余额为:"+balance);
			}
		else{
			System.out.println(Thread.currentThread().getName()+"余额不足!取钱失败!");
		}
	}
	//根据accountNo计算hashCode
	public int hashCode(){
		return accountNo.hashCode();
	}
	//重写equals()方法
	public boolean equals(Object obj){
		if(obj!=null&&obj.getClass()==Account.class){
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

说明:以上程序中增加了一个代表取钱操作的draw方法,并使用了synchronized关键字修饰了该方法,把该方法变成同步方法。同步方法的同步监视器是this,因此对于同一个Account账户来说,任何时刻只能有一条线程获得对Account对象的锁定,然后进入draw方法执行取钱操作——这样也可以保证多条线程并发存钱的线程安全。

注意:synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造方法、属性等。

因为Account类中已经提供了draw()方法,而且取消了setBalance()方法,DrawThread线程类也需要修改,该类只要调用Account对象的draw()方法来执行取钱操作就可以了。
程序如下:
源代码:DrawThread.java
public class DrawThread extends Thread{
	//模拟用户账户
	private Account account;
	//当前取钱线程所希望取钱的数目
	private double drawAmount;
	//构造方法
	public DrawThread(String name,Account account,double drawAmount){
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	//当多条线程修改同一个共享数据时,将涉及数据安全问题
	public void run(){
		//直接调用account对象的draw方法来执行取钱
		account.draw(drawAmount);
	}
}

程序的运行结果如下:
甲取钱成功!您取钱的数目为:800.0
您账户的余额为:200.0
乙余额不足!取钱失败!


提示:此时程序把draw方法定义在Account里,而不是直接在run方法中实现逻辑,这种做法更符合面向对象规则。在面向对象里有一种流行的设计方式:Domain Driven Design(即领域驱动设计,简称DDO),这种方式认为每个类都应该是完备的领域对象,例如Account它代表用户账户,它应该提供账户的相关方法,例如通过draw()方法来执行取钱操作,而不是直接将setBalance()方法暴露出来任人操作,这样才可以更好地保证Account对象的完整性和一致性。

可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采取如下策略:
(1)不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步,例如上面的Account类中accountNo属性就无需同步,所以程序只对draw方法进行同步控制。
(2)如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:
(1)当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
(2)当线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。
(3)当线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时将会释放同步监视器。
(4)当线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
在下面的情况下,线程不会释放同步监视器:
(1)线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
(2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。当然,我们应该尽量避免使用suspend和resume方法来控制线程。

同步锁(Lock)

从JDK1.5之后,Java提供了另一种线程同步的机制:它通过显式定义同步锁对象来实现同步,在这种机制下,同步锁应该使用Lock对象充当。

通常认为:Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且可以支持多个相关的Condition对象。

Lock是控制多线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。不过,某些锁可能允许对共享资源并发访问,如RendWriteLock(读写锁)。当然,在实现线程安全的控制中,通常喜欢使用ReentrantLock(可重入锁)。使用该Lock对象可以显示加锁、释放锁,通常使用Lock对象的代码格式如下:
class X{
	//定义锁对象
	private final ReentrantLock lock = new ReentrantLock();
	//……
	//定义需要保证线程安全的方法
	public void m(){
		//加锁
		lock.lock();
		try{
			//需要保证线程安全的代码
			//…...
			//…method body
		}
		//使用finally块来保证释放锁
		finally{
			lock.unlock();
		}
	}
}

使用Lock对象来进行同步时,锁定和释放锁出现在不同作用范围中时,通常建议使用finally块来确保在必要时释放锁。通过使用Lock对象我们可以把Account类改写成如下形式,它依然是线程安全的。
程序如下:
import java.util.concurrent.locks.*;
public class Account{
	//定义锁对象
	private final ReentrantLock lock =new ReentrantLock();
	private String accountNo;
	private double balance;
	//构造方法
	public Account(String accountNo,double balance){
		this.accountNo = accountNo;
		this.balance = balance;
	}
	//获得属性accountNo的值
	public String getAccountNo(){
		return accountNo;
	}
	//设置属性accountNo的值
	public void setAccountNo(String accountNo){
		this.accountNo = accountNo;
	}
	//获得属性balance的值
	public double getBalance(){
		return balance;
	}
	//提供一个线程安全draw方法来完成取钱操作
	public void draw(double drawAmount){
		lock.lock();
		try{
			//账户余额大于取钱的数目
			if(getBalance()>=drawAmount){
				//取出钞票
				System.out.println(Thread.currentThread().getName()+"取钱成功!您取钱的数目为:"+drawAmount);
				balance-=drawAmount;
				System.out.println("您账户的余额为:"+balance);
			}
			else{
				System.out.println(Thread.currentThread().getName()+"余额不足!取钱失败!");
			}
		}
		//使用finally块来确保释放锁
		finally{
			lock.unlock();
		}
	}
	//根据accountNo计算hashCode
	public int hashCode(){
		return accountNo.hashCode();
	}
	//重写equals()方法
	public boolean equals(Object obj){
		if(obj!=null&&obj.getClass()==Account.class){
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

程序运行结果如下:
甲取钱成功!您取钱的数目为:800.0
您账户的余额为:200.0
乙余额不足!取钱失败!


提示:使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合“加锁——>访问——>释放锁”的操作模式,而且使用Lock对象时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一个时刻只能有一条线程能进入临界区。

同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。

虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免了很多涉及锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。Lock提供了同步方法和同步代码块没有的其他功能,包括用于非块结构的tryLock方法,以及试图获取可中断锁lockInterruptibly()方法,还有获取超时失效锁的tryLock(long,TimeUnit)方法。

ReentrantlLock锁具有重入性,也就是说线程可以对它已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

分享到:
评论

相关推荐

    vc++中的线程锁(线程锁保持线程同步)

    在VC++编程环境中,线程同步是一个至关重要的概念,特别是在多线程程序设计中,以确保并发执行的线程能够安全地访问共享资源,避免数据竞争和其他潜在的问题。本篇文章将详细探讨线程锁在VC++中的应用,以及如何通过...

    C#线程同步的几种方法

    标题与描述概述的知识点主要集中在C#中的线程同步技术,包括`volatile`关键字、`lock`关键字以及`System.Threading.Interlocked`类的使用。在深入探讨这些知识点之前,我们首先需要理解为什么线程同步在多线程环境中...

    线程同步的五种方法

    线程同步是多线程编程中的重要概念,用于协调多个并发执行的线程,确保它们在访问共享资源时不会产生竞态条件或数据不一致性。在Windows编程中,提供了多种线程同步机制,包括互斥量、临界区、原子操作、事件以及...

    操作系统线程同步实验报告

    操作系统线程同步是多线程编程中的核心概念,旨在确保并发执行的线程在访问共享资源时不会引发数据不一致性和竞态条件。本实验报告详细探讨了这一主题,通过一个简单的银行账户转账的示例来揭示临界区问题及其解决...

    简单实现多线程同步示例(模拟购票系统)

    本示例“简单实现多线程同步示例(模拟购票系统)”旨在通过一个具体的实例,帮助开发者理解如何在Java中创建并管理多线程以及如何实现线程同步,确保数据的一致性和正确性。 首先,我们要明确多线程的基本概念。在...

    线程同步的四种方式

    在多线程编程中,线程同步是一种控制多个线程并发执行时访问共享资源的方式,以避免数据不一致和死锁等问题。以下是对线程同步的四种主要方式的详细解释: 1. **事件(Event)** 事件是Windows API提供的一种线程...

    线程同步解决火车站售票问题

    在这个“线程同步解决火车站售票问题”的例子中,我们可以通过线程同步机制来实现售票的有序、无冲突的过程。 首先,我们需要理解问题的核心:10个售票处(线程)需要共享1000张票(资源),并且每卖出一张票,必须...

    操作系统实验多线程同步(含C++源代码)

    操作系统中的多线程同步是一个关键概念,特别是在并发编程中,它涉及到如何协调多个线程以避免数据不一致性和竞态条件。在这个实验中,我们关注的是C++编程语言中的实现,以及操作系统如何处理线程的优先级。 首先...

    VC++线程同步实例

    在编程领域,线程同步是多线程编程中的一个重要概念,它确保了多个线程在访问共享资源时的正确性和一致性。在这个“VC++线程同步实例”中,我们将探讨如何利用VC++(Visual C++)来实现线程间的同步,以避免数据竞争...

    操作系统线程同步算法

    操作系统中的线程同步是多线程编程中一个关键的概念,它确保了多个线程在访问共享资源时的正确性,防止数据竞争和其他并发问题。在Windows操作系统中,提供了多种线程同步机制,如临界区、事件、信号量以及互斥量等...

    linux线程同步

    ### Linux线程同步详解 #### 一、引言 随着计算机技术的发展,多核处理器的普及使得并发编程成为提升程序性能的重要手段。在Linux环境下,多线程编程因其高效的资源利用和灵活的任务调度受到广泛欢迎。然而,多...

    Delphi多线程同步的例子

    本文将深入探讨Delphi中的多线程和线程同步,并以"SortThreads"和"delphi-thread-gui"这两个示例项目为例,讲解如何在实践中应用这些概念。 1. **多线程**:多线程允许应用程序同时执行多个独立的任务,提高程序的...

    多线程及线程同步

    然而,多线程环境下也带来了一些问题,尤其是资源竞争和数据一致性问题,这些问题需要通过线程同步机制来解决。本文将详细介绍如何通过临界区、互斥内核对象、事件内核对象和信号量内核对象来实现线程同步。 1. ...

    Java多线程同步.pdf

    "Java多线程同步.pdf" Java多线程同步是指在Java语言中,如何使用synchronized关键字和其他同步机制来确保多线程程序的正确执行。在Java语言中,synchronized关键字用于对方法或者代码块进行同步,但是仅仅使用...

    线程同步小例子

    在编程领域,线程同步是多线程编程中的一个核心概念,它涉及到如何有效地管理和协调多个并发执行的线程,确保它们能正确地共享资源,避免数据竞争和死锁等问题。这个“线程同步小例子”是基于孙鑫先生著作中的示例...

    线程同步技术剖析.pdf

    ### 线程同步技术深度解析 #### 引言 多线程编程是现代软件开发不可或缺的一部分,尤其在追求高效能和响应性时更是如此。然而,随着多线程的应用,线程同步问题逐渐凸显,成为确保程序稳定性和正确性的关键。线程...

    操作系统实验 多线程同步与互斥 java编写 有界面

    操作系统实验是计算机科学教育中的重要组成部分,它帮助学生理解和掌握操作系统的基本原理,特别是多线程同步与互斥的概念。在Java编程环境下,这些概念可以通过实际的代码实现来深入理解。 多线程是现代操作系统中...

    多线程的批量线程同步解决方案

    "多线程的批量线程同步解决方案"这个标题暗示我们探讨的是如何在多线程环境下有效地管理和同步多个任务,确保数据一致性与程序正确性。下面将详细阐述相关知识点。 一、多线程基础 多线程是指在一个进程中同时执行...

    Jni多线程同步事例

    在本例“Jni多线程同步事例”中,我们将探讨如何在JNI层面上实现多线程同步,特别是在一个生产者-消费者模型的场景下。 生产者-消费者模型是一种经典的并发问题,它涉及到两个或多个线程之间的协作。在该模型中,...

    c#线程同步的典型例子

    C#线程同步是多线程编程中的一个重要概念,它涉及到如何控制多个线程对共享资源的访问,以避免数据不一致性和竞态条件。在C#中,线程同步通常用于确保在某一时刻只有一个线程可以访问特定的代码块或资源,从而保证...

Global site tag (gtag.js) - Google Analytics