• 深入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("	余额为: " + 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("	余额为: " + 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("	余额为: " + 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("	余额为: " + 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();
    	}
    }


  • 相关阅读:
    关于API微服务网关
    适用于企业的API管理平台
    简单的api测试
    Json,2020年api数据格式的Top 1
    API文档之团队协作
    如何进行API测试以提高程序质量
    API接口也要监控?
    春招实习_腾讯 wxg 一面 3.27 15:00
    春招实习_腾讯一面 & 二面_3.13
    春招实习_阿里一面
  • 原文地址:https://www.cnblogs.com/hehe520/p/6330000.html
Copyright © 2020-2023  润新知