胡八一之Java(十):多线程进阶

我们来模拟一个存取款操作,来看看线程的安全问题

首先,建一个Account类:

package bank;

public class Account {
   private String accountNo;
   private double balance;
   public Account() {
	   
   }
   public Account(String accountNo,double balance) {
	   this.accountNo=accountNo;
	   this.balance=balance;
   }
public String getAccountNo() {
	return accountNo;
}
public void setAccountNo(String accountNo) {
	this.accountNo = accountNo;
}
public double getBalance() {
	return balance;
}
public void setBalance(double balance) {
	this.balance = balance;
}
   
}

再设置一个取钱类:

package bank;

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

测试取钱是否安全?

package bank;

public class Test {

	public static void main(String[] args) {
		
	//存钱
     Account account=new Account("张三",1000);
     //取钱
     DrawThread t1=new DrawThread("李四",account,800);
     DrawThread t2=new DrawThread("王五",account,800);
     //启动线程
     t1.start();
     t2.start();
	}

}

运行结果如下:

王五取钱成功!吐出钞票800.0
李四取钱成功!吐出钞票800.0
	 余额为:200.0
	 余额为:-600.0

显然,取钱出错了,账户余额为负,出错原因是当线程一检测到余额不为负时,进入if判断,在还没有完成取钱操作时,线程二也检测到余额不为负,也进入到了if判断,可是账户里只有一千块,两个线程各取走了八百,所以最终余额为-600;

这就是多线程的不安全性,那么怎么来解决这个问题呢?Java引入了同步监视器,synchronized(obj),线程开始执行前,必须先获得对同步监视器的锁定。提供以下几种方法来控制线程同步:

一、同步代码块:

package bank;

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对象加上同步锁
		synchronized(account) {
		if (account.getBalance() >=drawAmount){
			System.out.println(getName()+"取钱成功!吐出钞票"+ drawAmount );
			try {
				Thread.sleep(10);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
			//修改余额
			account.setBalance(account.getBalance()-drawAmount);
			System.out.println("\t 余额为:"+account.getBalance());
		}
		else {
			System.out.println(getName()+"取钱失败!余额不足!");
		}
		}
	}
}

运行结果:

李四取钱成功!吐出钞票800.0
	 余额为:200.0
王五取钱失败!余额不足!

在加上同步锁以后,线程便安全了。这是为什么呢?是因为将共享资源作为同步监视器后,就能阻止两个线程对同一个共享资源进行并发访问,其逻辑为:加锁->修改->释放锁。

二、同步方法:

同步方法就是使用synchronized关键字对方法修饰,修饰后的方法被称为同步方法,这种方法无需显式指定同步监视器,同步方法的同步监视器就是this, 也就是该对象本身。

将Account类加一个取钱的同步方法,然后在run方法里调用,如下:

  public synchronized void draw(double drawAmount) {
		if (getBalance() >=drawAmount){
		System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票"+ drawAmount );
		try {
			Thread.sleep(10);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		//修改余额
		balance=getBalance()-drawAmount;
		System.out.println("\t 余额为:"+getBalance());
	}
	else {
		System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足!");
	}
	}

run方法调用:

public void run(){
account.draw(drawAmount);
  }

运行结果:

王五取钱成功!吐出钞票800.0
	 余额为:200.0
李四取钱失败!余额不足!

第二种方法更适合与面向对象的思想,一个类应该包含它所有的操作,而不是将操作交给其他类实现。

可变类的线程安全是以运行效率作为代价的,所以我们为减少线程安全所带来的负面影响,应该采用以下策略:

1、不要对线程安全类里所有方法进行线程同步,只对共享资源的方法进行同步。

2、如果可变类有两种运行环境:单线程环境和多线程环境。应为其提供两种版本,即线程安全版本,和线程不安全版本。线程安全版本保证多线程环境下的安全性,不安全版本将使用在单线程环境中,以保证性能。

以下四种情况会释放同步监视器的锁定:

1、当前线程的同步方法,同步代码块执行完成后,当前线程即释放同步监视器。

2、当同步方法、同步代码块中遇到break return 中止了该方法,该代码块的执行,将释放同步监视器。

3、当前线程在同步代码块、同步方法中遇到了未处理的Error或Exception,则释放同步监视器。

4、当前线程在执行同步代码块、同步方法时调用了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。

以下两种情况不会释放同步监视器的锁定:

1、线程执行的同步代码块、同步方法中,调用了Thread.sleep(),Thread.yield()来暂停当前线程的执行,将不会释放同步监视器。

2、线程执行同步代码块时,其他线程调用了该线程的suspend()将该线程挂起,将不会释放同步监视器。

 

同步锁(Lock):

Lock是控制多线程对共享资源访问的一种工具。该工具比synchronized方法更加灵活,每次只能有一个线程对Lock加锁,线程访问共享资源前,应该先获得Lock对象。

package bank;

import java.util.concurrent.locks.ReentrantLock;

public class Account {
   private final ReentrantLock lock =new ReentrantLock();
   private String accountNo;
   private double balance;
   public Account() {
	   
   }
   public Account(String accountNo,double balance) {
	   this.accountNo=accountNo;
	   this.balance=balance;
   }
public String getAccountNo() {
	return accountNo;
}

public double getBalance() {
	return balance;
}

      public  void draw(double drawAmount) {
    	  //加锁
    	  lock.lock();
    	try {  
		if (getBalance() >=drawAmount){
		System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票"+ drawAmount );
		try {
			Thread.sleep(10);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		//修改余额
		balance=getBalance()-drawAmount;
		System.out.println("\t 余额为:"+getBalance());
	}
	else {
		System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足!");
	}
    	}
    	finally {
    		//修改完成释放锁
    		lock.unlock();
    	}
	}
  }

在Account类中定义锁对象,在draw()方法中显式的加锁,最后在finally块中确保释放锁。

 

死锁:

当两个线程相互等待对方释放同步监视器时就会发生死锁,java虚拟机没有监测,也没有采取措施来解决死锁,所以这就靠编程人员来避免死锁问题

下面程序便展示了死锁:

package bank;
class A{
	public synchronized void foo(B b) {
		System.out.println("当前线程名为:"+Thread.currentThread().getName()+"进入了A类的foo方法");
		try {
			Thread.sleep(200);
		}catch(InterruptedException e) {
			e.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 e) {
			e.printStackTrace();
		}
		System.out.println("当前线程名为:"+Thread.currentThread().getName()+"企图调用A类的last方法内部");
		a.last();
	}

	public synchronized void last() {
		System.out.println("进入了B类的last方法内部");
		
	}
}
public class MoreThread implements Runnable{
	A a =new A();
	B b =new B();
	public void init() {
		Thread.currentThread().setName("主线程");
		a.foo(b);
		System.out.println("进入了主线程之后");
	}
	public static void main(String[] args) {
		MoreThread d1=new MoreThread();
		//以d1为target启动新线程
		new Thread(d1).start();
		d1.init();
  
	}

	@Override
	public void run() {
		Thread.currentThread().setName("副线程");
		b.bar(a);
		System.out.println("进入了副线程之后");
		
	}

}

运行结果:

当前线程名为:主线程进入了A类的foo方法
当前线程名为:副线程进入了B类的bar方法
当前线程名为:主线程企图调用B类的last方法内部
当前线程名为:副线程企图调用A类的last方法内部

当主线程在持有a对象的锁在等待b对象的锁,但是副线程持有b对象的锁,但却在等待a对象的锁,这就形成了死锁。

 

线程通信:

在系统中,线程调度具有一定的透明性,程序无法准确控制线程的轮换执行,所以我们通过一些机制来保证线程的协调运行。

 

 

假设有两个线程,分别代表存款者和取款者,要求这存款者在存入钱后,取款者立即取出,并且重复这种操作。

可以通过设置旗标的方式解决。下面提供两种方法:

 

1、与synchronized配套的的线程通信:

Account类设置旗标:

package bank;



public class Account {
   
   private String accountNo;
   private double balance;
   private boolean flag=false;
   public Account() {
	   
   }
   public Account(String accountNo,double balance) {
	   this.accountNo=accountNo;
	   this.balance=balance;
   }
public String getAccountNo() {
	return accountNo;
}

public double getBalance() {
	return balance;
}

      public  synchronized void draw(double drawAmount) throws InterruptedException {
    	//如果flag为假,表明账户还没有人存钱进去,取钱方法阻塞
    	 if(!flag) {
    		 wait();
    	 }else {
    		 System.out.println("当前取钱的线程为:"+Thread.currentThread().getName()+"取钱:"+drawAmount);
    			//修改余额
    			balance-=drawAmount;
    			System.out.println("\t 余额为:"+getBalance());
    			//取钱后设置flag为false
    			flag=false;
    			//唤醒其他线程
    			notifyAll();
    	 }
    	
	     }
      public  synchronized void depoist(double depositAmount) throws InterruptedException {
    	//如果flag为真,表明账户还没有人取走钱,存钱方法阻塞
    	 if(flag) {
    		 wait();
    	 }else {
    		 System.out.println("当前取钱的线程为:"+Thread.currentThread().getName()+"存钱:"+depositAmount);
    			//修改余额
    			balance +=depositAmount;
    			System.out.println("\t 余额为:"+getBalance());
    			//存钱钱后设置flag为true
    			flag=true;
    			//唤醒其他线程
    			notifyAll();
    	 }
    	
	     }
  }

DrawThread类:

package bank;

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() {

		for(int i=0;i<100;i++) {
		 try {
			account.draw(drawAmount);
		} catch (InterruptedException e) {
			
			e.printStackTrace();
		}
		}
	}
}

DepositThread类:

package bank;

public class DepositThread extends Thread{
	   //模拟用户账户
		private Account account;
		//当前线程所希望取得钱数
		private double depositAmount;
		public DepositThread (String name,Account account,double depositAmount) {
			super(name);
			this.account=account;
			this.depositAmount=depositAmount;
		}
		
		public void run() {

			for(int i=0;i<100;i++) {
			 try {
				account.depoist(depositAmount);
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
			}
		}


}

运行部分结果:

当前取钱的线程为:张三存钱:800.0
	 余额为:800.0
当前取钱的线程为:李四取钱:800.0
	 余额为:0.0
当前取钱的线程为:赵六存钱:800.0
	 余额为:800.0
当前取钱的线程为:李四取钱:800.0
	 余额为:0.0
当前取钱的线程为:张三存钱:800.0
	 余额为:800.0

2、与Lock配套的线程通信:

只是Account类发生变化,所以只附上Account类的代码:

package bank;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
	//显式定义Lock对象
   private final ReentrantLock lock =new ReentrantLock();
   //获得指定Lock对象对应的Condition
   private final Condition cond = lock.newCondition();
   private String accountNo;
   private double balance;
   private boolean flag=false;
   public Account() {
	   
   }
   public Account(String accountNo,double balance) {
	   this.accountNo=accountNo;
	   this.balance=balance;
   }
public String getAccountNo() {
	return accountNo;
}

public double getBalance() {
	return balance;
}

      public  void draw(double drawAmount)  {
    	  //加锁
    	  lock.lock();
    	  try {
    	   //如果flag为假,表明账户还没有人存钱进去,取钱方法阻塞
    	 if(!flag) {
    		 cond.await();
    	 }else {
    		 System.out.println("当前取钱的线程为:"+Thread.currentThread().getName()+"取钱:"+drawAmount);
    			//修改余额
    			balance-=drawAmount;
    			System.out.println("\t 余额为:"+getBalance());
    			//取钱后设置flag为false
    			flag=false;
    			//唤醒其他线程
    			cond.signalAll();
    	 }
    	  }
    	  catch(InterruptedException e) {
    		  e.printStackTrace();
    	  }
    	  //使用finally释放锁
    	  finally {
    		  lock.unlock();
    	  }
	     }
      public   void depoist(double depositAmount) throws InterruptedException {
    	  //加锁
    	  lock.lock();
    	  try {
    	  //如果flag为真,表明账户还没有人取走钱,存钱方法阻塞
    	 if(flag) {
    		 cond.await();
    	 }else {
    		 System.out.println("当前取钱的线程为:"+Thread.currentThread().getName()+"存钱:"+depositAmount);
    			//修改余额
    			balance +=depositAmount;
    			System.out.println("\t 余额为:"+getBalance());
    			//存钱钱后设置flag为true
    			flag=true;
    			//唤醒其他线程
    			cond.signalAll();
    	 }
    	  }catch(InterruptedException e) {
    		  e.printStackTrace();
    	  }
    	  
    	  finally {
    		  lock.unlock();
    	  }
	     }
  }

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
毕业设计,基于SpringBoot+Vue+MySQL开发的体育馆管理系统,源码+数据库+毕业论文+视频演示 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本体育馆管理系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息,使用这种软件工具可以帮助管理人员提高事务处理效率,达到事半功倍的效果。此体育馆管理系统利用当下成熟完善的SpringBoot框架,使用跨平台的可开发大型商业网站的Java语言,以及最受欢迎的RDBMS应用软件之一的Mysql数据库进行程序开发。实现了用户在线选择试题并完成答题,在线查看考核分数。管理员管理收货地址管理、购物车管理、场地管理、场地订单管理、字典管理、赛事管理、赛事收藏管理、赛事评价管理、赛事订单管理、商品管理、商品收藏管理、商品评价管理、商品订单管理、用户管理、管理员管理等功能。体育馆管理系统的开发根据操作人员需要设计的界面简洁美观,在功能模块布局上跟同类型网站保持一致,程序在实现基本要求功能时,也为数据信息面临的安全问题提供了一些实用的解决方案。可以说该程序在帮助管理者高效率地处理工作事务的同时,也实现了数据信息的整体化,规范化与自动化。 关键词:体育馆管理系统;SpringBoot框架;Mysql;自动化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值