java线程同步
一、使用线程进行数据传递
回调函数进行数据传递
这里举个例子,新建一个ThreadTest07.java代码:
import java.util.Random;
class data{
public int value = 0;
}
class work{
public void progress(data data,int n1, int n2, int n3){
{
int sum = 0;
sum += n1;
sum += n2;
sum += n3;
data.value = sum ;
}
}
}
public class ThreadTest07 implements Runnable{
private work work;
public ThreadTest07(work work){
this.work = work;
}
@Override
public void run() {
Random random = new Random();
data data = new data();
int a = random.nextInt(1000);
int b = random.nextInt(1000);
int c = random.nextInt(1000);
work.progress(data,a,b,c);
System.out.println(String.valueOf(a) + "+" + String.valueOf(b) + "+"
+ String.valueOf(c) + "=" + data.value);
}
public static void main(String[] args) {
new Thread(new ThreadTest07(new work())).start();
}
}
运行结果:
658+83+459=1200
这是一个使用线程来进行数据处理的例子,但是我们需要注意线程同步的问题,由一个例子引出线程同步的概念。
二、线程同步
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 从而保证了该变量的唯一性和准确性。解决办法:在线程使用一个资源的时候,我们为其加锁即可。访问资源的第一个线程为其加上锁以后,其它线程便不能访问那个资源,除非获得那个资源的线程对其解锁!
(一)、银行取钱算法:
这里我们介绍一下银行取钱的算法,也是很多教程都在用的关于多线程的例子。
我们大致的说一下代码流程:
- 新建一个账户的类,里面有账户信息、账户余额、创建账户、取钱方法,修改账户余额。
- 新建一个取钱的线程类,这个线程主要来进行取钱的操作。
- 主方法,主方法里创建账户,设置账户信息存款,创建两个用户,分别对同一个银行账户进行存取。
1、创建银行账户Account
class Account {
//账户号码
private String accountNo;
//账户余额
private double balance;
//获取账户号码
public String getAccountNo() {
return accountNo;
}
//获取账户余额
public double getBalance() {
return balance;
}
//创建账户的构造器
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
//取钱的方法
public void draw (double drawAmount){
//账户余额大于取钱数目
if(balance >=drawAmount){
//吐出钞票---打印正则取钱的的线程的名字+取钱数额
System.out.println(Thread.currentThread().getName() +" 取钱成功,吐出钞票"+drawAmount);
//修改余额
balance=balance-drawAmount;
System.out.println("\t余额 :"+balance);
}else {
System.out.println(Thread.currentThread().getName()+"取钱失败,余额不足!");
}
}
}
2、取钱的线程DrawThread
class DrawThread implements Runnable{
//银行账户
private Account account;
//取钱数额
private double drawAmonut;
//取钱线程的构造器
public DrawThread(Account account,double drawAmonut){
this.account = account;
this.drawAmonut = drawAmonut;
}
//这里进行取钱的操作
@Override
public void run() {
//获得账户,然后取钱。
account.draw(drawAmonut);
}
}
3、主线程Threadbank
public class Threadbank {
public static void main(String[] args) {
//创建一个银行账户
Account account= new Account("whq", 1000);
//模拟两个线程对同一个账户操作,开启两个线程
Thread t1=new Thread(new DrawThread(account,800),"甲");
Thread t2=new Thread(new DrawThread(account,800),"乙");
t1.start();
t2.start();
}
}
4、完整的代码如下:
class Account {
//账户号码
private String accountNo;
//账户余额
private double balance;
//获取账户号码
public String getAccountNo() {
return accountNo;
}
//获取账户余额
public double getBalance() {
return balance;
}
//创建账户的构造器
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
//取钱的方法
public void draw (double drawAmount){
//账户余额大于取钱数目
if(balance >=drawAmount){
//吐出钞票---打印正则取钱的的线程的名字+取钱数额
System.out.println(Thread.currentThread().getName() +" 取钱成功,吐出钞票"+drawAmount);
//修改余额
balance=balance-drawAmount;
System.out.println("\t余额 :"+balance);
}else {
System.out.println(Thread.currentThread().getName()+"取钱失败,余额不足!");
}
}
}
class DrawThread implements Runnable{
//银行账户
private Account account;
//取钱数额
private double drawAmonut;
//取钱线程的构造器
public DrawThread(Account account,double drawAmonut){
this.account = account;
this.drawAmonut = drawAmonut;
}
//这里进行取钱的操作
@Override
public void run() {
//获得账户,然后取钱。
account.draw(drawAmonut);
}
}
public class Threadbank {
public static void main(String[] args) {
//创建一个银行账户
Account account= new Account("whq", 1000);
//模拟两个线程对同一个账户操作,开启两个线程
Thread t1=new Thread(new DrawThread(account,800),"甲");
Thread t2=new Thread(new DrawThread(account,800),"乙");
t1.start();
t2.start();
}
}
运行结果如下(这里列举几个示例):
第一种:
甲 取钱成功,吐出钞票800.0
乙 取钱成功,吐出钞票800.0
余额 :-600.0
余额 :200.0
第二种:
甲 取钱成功,吐出钞票800.0
余额 :200.0
乙 取钱成功,吐出钞票800.0
余额 :-600.0
第三种:
甲 取钱成功,吐出钞票800.0
乙 取钱成功,吐出钞票800.0
余额 :200.0
余额 :-600.0
第四种(设计的正确答案):
甲 取钱成功,吐出钞票800.0
余额 :200.0
乙取钱失败,余额不足!
分析:
主线程这里是先让甲取出800,然后乙再去取出800,如果按照流程来思考,最终显示的正确结果应该为第四种,甲取钱成功,乙取不出钱。但是这里我们还列举了前三种结果,为什么会出现这种结果呢?有的都显示取钱成功,最终余额为-600。这是因为两个线程同时对同一块资源使用,可能在CPU进行分配的时候,甲线程刚刚开始,程序还没有运行结束,乙线程就开始了,同样是对同一个银行账户进行取钱。甲乙“同时”对同一个账户取钱,同时进行,所以出现了这种错误结果。所以由这个结果我们可以看到如果不对线程进行限制,那么有可能出现这种情况。我们需要对此进行改进,毕竟银行不能做亏本的买卖对吧!
(二)、解决方案
1、join()的方法
之前的文章了我们介绍过join(),他表示等待该线程终止,然后继续其他线程,我们修改一下Threadbank代码:
public class Threadbank {
public static void main(String[] args) {
//创建一个账户
Account account= new Account("whq", 1000);
//模拟两个线程对同一个账户操作
Thread t1=new Thread(new DrawThread(account,800),"甲");
Thread t2=new Thread(new DrawThread(account,800),"乙");
t1.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果为:
甲 取钱成功,吐出钞票800.0
余额 :200.0
乙取钱失败,余额不足!
这里表示先让甲执行完,然后再让乙去取钱。这也算是一种方案了。
2、线程同步synchronized--上锁
我们使用synchronized关键字对银行账户对象进行加锁,问题解决。因为:当甲对这个银行账户取钱时,就获得了这个账户的对象,然后对这个账户进行加锁,知道这个账户的取钱动作完成(或者是其它的原因导致程序退出),甲用户将释放这个锁,此后乙就可以对这个银行账户进行操作,然后加锁,再继续完成一系列的取钱操作!,思想就是先让一个人做完,然后释放资源,另一个在继续上锁,使用资源,用完再释放资源。
a、修改一下Account代码:这个synchronized称之为synchronized同步代码块
class Account {
//账户号码
private String accountNo;
//账户余额
private double balance;
public String getAccountNo() {
return accountNo;
}
public double getBalance() {
return balance;
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
//取钱的方法
public void draw (double drawAmount){
synchronized (accountNo){
//账户余额大于取钱数目
if(balance >=drawAmount){
//吐出钞票
System.out.println(Thread.currentThread().getName() +" 取钱成功,吐出钞票"+drawAmount);
balance=balance-drawAmount;
System.out.println("\t余额 :"+balance);
}else {
System.out.println(Thread.currentThread().getName()+"取钱失败,余额不足!");
}
}
}
}
在这里我们在判断取钱前,首先将银行账户上锁,只允许一个人对此账户操作,操作完成之后在释放此资源。
运行结果如下:
甲 取钱成功,吐出钞票800.0
余额 :200.0
乙取钱失败,余额不足!
b、修改一下Account代码:这个synchronized称之为synchronized同步方法
class Account {
//账户号码
private String accountNo;
//账户余额
private double balance;
public String getAccountNo() {
return accountNo;
}
public double getBalance() {
return balance;
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
//取钱的方法
public synchronized void draw (double drawAmount){
//账户余额大于取钱数目
if(balance >=drawAmount){
//吐出钞票
System.out.println(Thread.currentThread().getName() +" 取钱成功,吐出钞票"+drawAmount);
//修改余额
balance=balance-drawAmount;
System.out.println("\t余额 :"+balance);
}else {
System.out.println(Thread.currentThread().getName()+"取钱失败,余额不足!");
}
}
}
运行结果如下:
甲 取钱成功,吐出钞票800.0
余额 :200.0
乙取钱失败,余额不足!
运行结果和synchronized修饰的同步代码块效果是一样的.有一点需要注意的是:如果一个对象中的所有方法都用synchronized关键字修饰的话,则这个对象就称为同步锁!当调用一个对像的一个synchronized方法时,就会给这个对象上锁!其它对象就无法访问这个对象的synchronized方法!如果某个synchronized方法是static 方法的话,那么当线程访问该方法时,它锁的并不是synchronized所在的对象,而是synchronized方法所在对象的所对应的Class对象! Class对象是唯一的,不管你new 了多少个对像,Class对像是唯一的!