深入Java线程管理(三):线程同步

一、 引入同步: 有一个很经典的案例,即银行取款问题。我们可以先看下银行取款的基本流程:

1)用户输入账户、密码,系统判断用户的账户、密码是否匹配。

2)用户输入取款金额。

3)系统判断账户金额是否大于取款金额。

4)如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败。

假设,此时有两个人,同时使用同一个账户并发取钱,我们模拟下取款流程:

public class Account
{
	// 封装账户编号、账户余额两个Field
	private String accountNo;
	private double balance;
	public Account(){}
	// 构造器
	public Account(String accountNo , double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}
	// 此处省略了accountNo和balance两个Field的setter和getter方法

	// accountNo的setter和getter方法
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}

	// balance的setter和getter方法
	public void setBalance(double balance)
	{
		this.balance = balance;
	}
	public double getBalance()
	{
		return this.balance;
	}

	// 下面两个方法根据accountNo来重写hashCode()和equals()方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if(this == obj)
			return true;
		if (obj !=null
			&& obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

接下来,提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出钞票,余额减少。

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);
			try {
				Thread.sleep(1);
			} catch (InterruptedException ex) {
				ex.printStackTrace();
			}
			// 修改余额
			account.setBalance(account.getBalance() - drawAmount);
			System.out.println("\t余额为: " + account.getBalance());
		} else {
			System.out.println(getName() + "取钱失败!余额不足!");
		}
	}
}

输出:

---------- java ----------
乙取钱成功!吐出钞票:800.0
甲取钱成功!吐出钞票:800.0
余额为: 200.0
余额为: -600.0

输出完成 (耗时 0 秒) - 正常终止

之所以会出现这样的错误,是因为线程调度具有不确定性,在账户余额只有1000时,取出了1600,而且账户余额出现了负值。

要解决该问题,java引入了同步监视器,在线程开始执行同步代码块之前,必须先获得同步监视器的锁定。

同步监视器的目的: 阻止多个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。

接下来,我们使用同步监视器锁定线程的执行体run()方法:

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);
				try
				{
				Thread.sleep(1);
				}
				catch (InterruptedException ex)
				{
				ex.printStackTrace();
				}
				// 修改余额
				account.setBalance(account.getBalance() - drawAmount);
				System.out.println("\t余额为: " + account.getBalance());
			}
			else
			{
				System.out.println(getName() + "取钱失败!余额不足!");
			}
		}
		//同步代码块结束,该线程释放同步锁
	}
}

除了使用同步代码块之外,我们还可以使用同步方法。同步方法无须显示指定同步监视器,同步方法的同步监视器是this,也就是对象本身。

通过通过方法可以非常方便的实现线程安全的类:

·该类的对象可以被多个线程安全的访问。

·每个线程调用该对象的任意方法之后都将得到正确的结果。

·每个线程调用该对象的任意方法之后,该对象的状态依然保持合理状态。

public class Account
{
	// 封装账户编号、账户余额两个Field
	private String accountNo;
	private double balance;
	public Account(){}
	// 构造器
	public Account(String accountNo , double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	// accountNo的setter和getter方法
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
	public double getBalance()
	{
		return this.balance;
	}

	// 提供一个线程安全draw()方法来完成取钱操作
	public synchronized void draw(double drawAmount)
	{
		// 账户余额大于取钱数目
		if (balance >= drawAmount)
		{
			// 吐出钞票
			System.out.println(Thread.currentThread().getName()
				+ "取钱成功!吐出钞票:" + drawAmount);
			try
			{
				Thread.sleep(1);
			}
			catch (InterruptedException ex)
			{
				ex.printStackTrace();
			}
			// 修改余额
			balance -= drawAmount;
			System.out.println("\t余额为: " + balance);
		}
		else
		{
			System.out.println(Thread.currentThread().getName()
				+ "取钱失败!余额不足!");
		}
	}

	// 下面两个方法根据accountNo来重写hashCode()和equals()方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if(this == obj)
			return true;
		if (obj !=null
			&& obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

上面程序中增加了一个代表取钱的draw()方法,并使用了synchronized关键字修饰,该方法变为同步方法,同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一个线程Account对象锁定,然后进入draw()方法执行取钱操作。

接下来,我们看下并发的线程类该如何写:

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方法来执行取钱
		// 同步方法的同步监视器是this,this代表调用draw()方法的对象。
		// 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。
		account.draw(drawAmount);
	}
}

线程类无须事前取钱操作,而是直接调用account的draw()方法来执行取钱操作。由于已经使用了synchronized关键字修饰了draw()方法,同步方法的同步监视器就是this,而this总代表调用该方法的对象——在上面的示例中,调用draw()方法的对象时account,因此多个线程并发修改一份account之前,必须先对account对象加锁。

二、 同步锁(Lock)

Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构。Lock是控制多个线程对共享资源进行访问的工具。

某些锁可能允许对共享资源的并发访问,比如ReadWriteLock(读写锁)。比较常用的Lock有ReentrantLock(可重入锁),使用它可以显式的加锁、释放锁。

public class Account
{
	// 定义锁对象
	private final ReentrantLock lock = new ReentrantLock();
	// 封装账户编号、账户余额两个Field
	private String accountNo;
	private double balance;
	public Account(){}
	// 构造器
	public Account(String accountNo , double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	// accountNo的setter和getter方法
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
	public double getBalance()
	{
		return this.balance;
	}

	// 提供一个线程安全draw()方法来完成取钱操作
	public void draw(double drawAmount)
	{
		// 加锁
		lock.lock();
		try
		{
			// 账户余额大于取钱数目
			if (balance >= drawAmount)
			{
				// 吐出钞票
				System.out.println(Thread.currentThread().getName()
					+ "取钱成功!吐出钞票:" + drawAmount);
				try
				{
					Thread.sleep(1);
				}
				catch (InterruptedException ex)
				{
					ex.printStackTrace();
				}
				// 修改余额
				balance -= drawAmount;
				System.out.println("\t余额为: " + balance);
			}
			else
			{
				System.out.println(Thread.currentThread().getName()
					+ "取钱失败!余额不足!");
			}
		}
		finally
		{
			// 修改完成,释放锁
			lock.unlock();
		}
	}

	// 下面两个方法根据accountNo来重写hashCode()和equals()方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if(this == obj)
			return true;
		if (obj !=null
			&& obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

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

三、死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施处理死锁情况,所以多线程编程时应该采取避免死锁出现。

死锁的举例:

class A
{
	public synchronized void foo( B b )
	{
		System.out.println("当前线程名: " + Thread.currentThread().getName()
			+ " 进入了A实例的foo方法" );     //①
		try
		{
			Thread.sleep(200);
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
			+ " 企图调用B实例的last方法");    //③
		b.last();
	}
	public synchronized void last()
	{
		System.out.println("进入了A类的last方法内部");
	}
}
class B
{
	public synchronized void bar( A a )
	{
		System.out.println("当前线程名: " + Thread.currentThread().getName()
			+ " 进入了B实例的bar方法" );   //②
		try
		{
			Thread.sleep(200);
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
			+ " 企图调用A实例的last方法");  //④
		a.last();
	}
	public synchronized void last()
	{
		System.out.println("进入了B类的last方法内部");
	}
}
public class DeadLock implements Runnable
{
	A a = new A();
	B b = new B();
	public void init()
	{
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}
	public void run()
	{
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}
	public static void main(String[] args)
	{
		DeadLock dl = new DeadLock();
		// 以dl为target启动新线程
		new Thread(dl).start();
		// 调用init()方法
		dl.init();
	}
}
时间: 2025-01-05 16:00:50

深入Java线程管理(三):线程同步的相关文章

线程管理(四)操作线程的中断机制

操作线程的中断机制 在之前的指南里,你学习了如何中断执行线程和如何对Thread对象的中断控制.之前例子中的机制可以很容易中断的线程中使用.但是如果线程实现的是由复杂的算法分成的一些方法,或者它的方法有递归调用,那么我们可以用更好的机制来控制线程中断.为了这个Java提供了InterruptedException异常.当你检测到程序的中断并在run()方法内捕获,你可以抛这个异常. 在这个指南中, 我们将实现的线程会根据给定的名称在文件件和子文件夹里查找文件,这个将展示如何使用Interrupt

线程管理(二)获取和设置线程信息

获取和设置线程信息 Thread类的对象中保存了一些属性信息能够帮助我们来辨别每一个线程,知道它的状态,调整控制其优先级. 这些属性是: ID: 每个线程的独特标识. Name: 线程的名称. Priority: 线程对象的优先级.优先级别在1-10之间,1是最低级,10是最高级.不建议改变它们的优先级,但是你想的话也是可以的. Status: 线程的状态.在Java中,线程只能有这6种中的一种状态: new, runnable, blocked, waiting, time waiting,

深入解析Java并发程序中线程的同步与线程锁的使用_java

synchronized关键字 synchronized,我们谓之锁,主要用来给方法.代码块加锁.当某个方法或者代码块使用synchronized时,那么在同一时刻至多仅有有一个线程在执行该段代码.当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段.但是,其余线程是可以访问该对象中的非加锁代码块的. synchronized主要包括两种方法:synchronized 方法.synchronized 块. synchron

java创建线程的三种方式及其对比

Java中创建线程主要有三种方式: 一.继承Thread类创建线程类 (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务.因此把run()方法称为执行体. (2)创建Thread子类的实例,即创建了线程对象. (3)调用线程对象的start()方法来启动该线程. package com.thread; public class FirstThreadTest extends Thread{ int i = 0; //重写run方法,run方法的方

java中线程同步,线程让步,线程休眠的区别和联系是什么

问题描述 java中线程同步,线程让步,线程休眠的区别和联系是什么 java中线程同步,线程让步,线程休眠的区别和联系是什么 线程的本质还是一个运行中的类, 解决方案 线程同步:是保证多线程安全访问竞争资源的一种手段,java中常用的是加锁机制即synchronized同步代码块实现的. 线程让步的方法是yield(),休眠方法是sleep().sleep()方法和yield()方法都是Thread类的静态方法,都会使当前处于运行状态的线程放弃CPU,把运行机会让给别的线程. 两者的区别在于:s

《Java程序员面试秘笈》—— 第1章 线程管理 1.1 简介

第1章 线程管理 Java 7并发编程实战手册 本章内容包括: ◆ 线程的创建和运行 ◆ 线程信息的获取和设置 ◆ 线程的中断 ◆ 线程中断的控制 ◆ 线程的休眠和恢复 ◆ 等待线程的终止 ◆ 守护线程的创建和运行 ◆ 线程中不可控异常的处理 ◆ 线程局部变量的使用 ◆ 线程的分组 ◆ 线程组中不可控异常的处理 ◆ 使用工厂类创建线程 1.1 简介 在计算机领域中,我们说的并发(Concurrency)是指一系列任务的同时运行.如果一台电脑有多个处理器或者有一个多核处理器,这个同时性(Simul

深入Java线程管理(二):线程的生命周期

Java线程的生命周期 一个线程的产生是从我们调用了start方法开始进入Runnable状态,即可以被调度运行状态,并没有真正开始运行,调度器可以将CPU分配给它,使线程进入Running状态,真正运行其中的程序代码.线程在运行过程中,有以下几个可能的去向: (1)调度器在某个线程的执行过程中将CPU分配给了其它线程,则这个线程又变为Runnable状态,等待被调度. (2)调度器将CPU分配给了该线程,执行过程中没有遇到任何阻隔,运行完成直接结束,也就是run()方法执行完毕. (3)线程在

【转载】JAVA内存模型和线程安全

本文转载自http://shift-alt-ctrl.iteye.com/blog/1845309   一.JAVA内存模型(JMM,JAVA Memory Model):     运行时涉及到两种内存,主内存和工作区内存,其中工作区内存通常为CPU的高速缓存区用来加快内存数据读取操作的(各线程独立).所有的变量内容都存在主内存中,当需要对内存数据进行操作时,数据将会从主存中load到工作区缓存并由CPU计算和赋值操作,然后再由工作区内存write到主存中,读取时如果工作区内存中已经有(load

JAVA内存模型和线程安全

一.JAVA内存模型(JMM,JAVA Memory Model):     运行时涉及到两种内存,主内存和工作区内存,其中工作区内存通常为CPU的高速缓存区用来加快内存数据读取操作的(各线程独立).所有的变量内容都存在主内存中,当需要对内存数据进行操作时,数据将会从主存中load到工作区缓存并由CPU计算和赋值操作,然后再由工作区内存write到主存中,读取时如果工作区内存中已经有(loaded)则直接使用;工作区内存保存了线程使用的变量的副本,线程不可以直接操作主内存,只能操作工作区内存,对

Android线程管理之ActivityThread

ActivityThread功能 它管理应用进程的主线程的执行(相当于普通Java程序的main入口函数),并根据AMS的要求(通过IApplicationThread接口,AMS为Client.ActivityThread.ApplicationThread为Server)负责调度和执行activities.broadcasts和其它操作. 在Android系统中,在默认情况下,一个应用程序内的各个组件(如Activity.BroadcastReceiver.Service)都会在同一个进程(