java--多线程(线程安全)(三)

典型的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()方法代码后面继续下执行的

猜你喜欢

转载自blog.csdn.net/qq_40893056/article/details/82386967