典型的java线程安全例子(银行存储)
package com.xzy.Bank;
public class ThreadTest {
public static void main(String[] args) {
Account account = new Account("123456",1000);
DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);
Thread thread1 = new Thread(drawMoneyRunnable);
Thread thread2 = new Thread(drawMoneyRunnable);
thread1.start();
thread2.start();
}
}
class DrawMoneyRunnable implements Runnable{
private Account account;
private double drawAmount;
public DrawMoneyRunnable(Account account, double drawAmount) {
super();
this.account = account;
this.drawAmount = drawAmount;
}
@Override
public void run() {
if(account.getBalance() >= drawAmount) { //1
System.out.println("取钱成功,取出钱数为:"+drawAmount);
double balance = account.getBalance() - drawAmount;
account.setBalance(balance);
System.out.println("余额为:"+balance);
}
}
}
class Account{
private String accountNo;
private double balance;
public Account() {
super();
// TODO Auto-generated constructor stub
}
public Account(String accountNo, double balance) {
super();
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;
}
}
上面的例子模仿现实生活中,对同一账号,同时进行取钱的活动。多次运行程序后,可能会出现以下状况:
取钱成功,取出钱数为:700.0
取钱成功,取出钱数为:700.0
余额为:-400.0
余额为:300.0
问题在于java多线程环境下的执行不确定。cpu可能随机的在多个处于就绪状态中的线程中进行切换,因此,很有可能出现如下情况:当thread1执行到//1代码时,判断条件为true,此时CPU切换到thread2,执行到//1处代码,发现依然是真,然后执行完thread2,接着切换到thread2,最后执行完毕,此时就会出现上述情况。
为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。
- 同步方法
对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放对象,其他的线程才有可能获取此同步锁。
在上例中,共享资源为account对象,当使用同步方法时,可以解决线程安全问题。只需在run()方法前加上synchronized关键字即可
1 public synchronized void run() {
2
3 // ....
4
5 }
- 同步代码块
解决线程安全其实只需限制对共享资源访问的不确定即可。使用同步方法时,使得整个方法体都称为了同步执行状态,会使得可能出现同步范围过大的情况。针对这一现象,可以使用同步代码块。
同步代码块的格式为:
1 synchronized (obj) {
2
3 //...
4
5 }
其中,obj为锁对象,因此,选择哪一个对象作为锁至关重要。一般情况下,都是选择此共享资源对象作为所对象。
如上例中,最好使用account对象作为锁对象。(当然,选用this也是可以的,那是,因为创建线程使用了Runnable方式,如果直接继承Thread方式创建的线程,使用this对象作为同步锁其实没有起到任何作用,因为同步锁锁的是不同的对象,因此选择同步锁时需要非常小心)
- Lock对象同步锁
正因为对同步锁对象的选择需要如此小心,所以使用Lock对象共享锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样一对一的关系。Lock对象同步锁一般格式为:
1 class X {
2
3 // 显示定义Lock同步锁对象,此对象与共享资源具有一对一关系
4 private final Lock lock = new ReentrantLock();
5
6 public void m(){
7 // 加锁
8 lock.lock();
9
10 //... 需要进行线程安全同步的代码
11
12 // 释放Lock锁
13 lock.unlock();
14 }
15 }
故上例可以这么添加lock共享锁
class DrawMoneyRunnable implements Runnable{
private Account account;
private double drawAmount;
//显示Lock同步锁对象,此对象与共享资源具有一对一关系
private final Lock lock = new ReentrantLock();
public DrawMoneyRunnable(Account account, double drawAmount) {
super();
this.account = account;
this.drawAmount = drawAmount;
}
@Override
public void run() {
//加锁
lock.lock();
if(account.getBalance() >= drawAmount) {
System.out.println("取钱成功,取出钱数为:"+drawAmount);
double balance = account.getBalance() - drawAmount;
account.setBalance(balance);
System.out.println("余额为:"+balance);
}
//释放lock锁
lock.unlock();
}
}
- wait()/notify()/notifyAll()线程通信
上面三个方法虽然主要都用于多线程中,但实际上都是Object类的本地方法。因此,理论上任何Object对象都可以作为这三个方法的主调在,在实际的多线程编程中,只有同步锁对象调三个方法,才能完成多线程间的线程通信。
wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步所对象的notify()或notifyAll()方法来唤醒此线程
notify():唤醒在同步锁对象上等待的单个线程,如果多个线程都在此同步对象上等待,则会任意选择某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才有可能执行被唤醒的线程
notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才有可能执行被唤醒的线程。
package com.xzy.Bank;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
public static void main(String[] args) {
Account account = new Account("123456",0);
Thread drawMoneyThread = new DrawMoneyThread("取钱线程", account, 700);
Thread depositeMoneyThrea = new DepositeMoney("存钱线程", account, 700);
drawMoneyThread.start();
depositeMoneyThrea.start();
}
}
class DrawMoneyThread extends Thread{
private Account account;
private double amount;
public DrawMoneyThread(String threadName,Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
account.draw(amount, i);
}
}
}
class DepositeMoney extends Thread{
private Account account;
private double amount;
public DepositeMoney(String threadName,Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
account.deposite(amount, i);
}
}
}
class Account{
private String accountNo;
private double balance;
//标识账户是否已有存款
private boolean flag = false;
public Account() {
super();
// TODO Auto-generated constructor stub
}
public Account(String accountNo, double balance) {
super();
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;
}
/**
* 存钱
*/
public synchronized void deposite(double depositeAmount, int i) {
if(flag) {
//账户已有存钱进行此时当前线程等待阻塞
try {
System.out.println(Thread.currentThread().getName()+"--开始执行wait操作"+"--i="+i);
wait();
System.out.println(Thread.currentThread().getName()+"--执行了wait操作"+"--i="+i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
//开始存钱
System.out.println(Thread.currentThread().getName()+"--存款:"+depositeAmount+"--i="+i);
setBalance(balance+depositeAmount);
flag = true;
//唤醒其他线程
notifyAll();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--存款--执行完毕"+"--i="+i);
}
}
/**
* 取钱
*/
public synchronized void draw(double drawAmount,int i) {
if (!flag) {
//账号中还没有存钱进去,此时当前线程需要等待阻塞
try {
System.out.println(Thread.currentThread().getName()+"--开始执行wait操作"+"--i="+i);
wait();
System.out.println(Thread.currentThread().getName()+"--执行了wait操作"+"--i="+i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
//开始取钱
System.out.println(Thread.currentThread().getName()+"--取钱:"+drawAmount+"--i="+i);
setBalance(getBalance() - drawAmount);
flag = false;
//唤醒其他线程
notifyAll();
System.out.println(Thread.currentThread().getName()+"--取钱--执行完毕"+"--i="+i);
}
}
}
由此,我们需要注意如下几点:
1.wait() 方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行
2.notify()/notifyAll()方法执行后,将唤醒此同步对象上的(任意一个notify()/notifyAll())线程对象,但是,此时并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续 执行,直到当前线程完毕才释放同步锁对象
3.notify()/notifyAll()执行后,如果后面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁还没有释放,依然自己保留,到一定时候还会继续执行此线程
4.wait()/notify()/notifyAll()完成线程间的通信或协作都是基于不同对象锁,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系
5.当wait线程唤醒后并执行时,是接着上次执行的wait()方法代码后面继续下执行的