三、多线程安全实例

     线程同步会在偶然的多线程并发访问情况下出现线程安全的情况,例如银行的取钱存钱操作等都是需要保证数据的一致性,和每次操作的线程安全。

一、线程安全问题


             现在使用2个线程来模拟银行取款操作,模拟2个人使用同一个账户并发取钱的问题。

  1.     定义Account类
     /*
         * Creation : 2015年10月15日
         */
        package com.tan.thread.bank;
    
        public class Account {
            private String accoutnNo; // 账号编号
            private double balance;// 余额
    
            public Account(String accoutnNo, double balance) {
                super();
                this.accoutnNo = accoutnNo;
                this.balance = balance;
            }
    
            public String getAccoutnNo() {
                return accoutnNo;
            }
    
            public void setAccoutnNo(String accoutnNo) {
                this.accoutnNo = accoutnNo;
            }
    
            public double getBalance() {
                return balance;
            }
    
            public void setBalance(double balance) {
                this.balance = balance;
            }
    
            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }
                if (obj != null && obj.getClass() == Account.class) {
                    Account a = (Account) obj;
                    return a.getAccoutnNo().equals(accoutnNo);
                }
                return false;
            }
    
            @Override
            public int hashCode() {
                return accoutnNo.hashCode();
            }
    
        }
    
    


  2. 定义取钱的线程类 DrawThread
    /*
         * Creation : 2015年10月15日
         */
        package com.tan.thread.bank;
    
        /**
         * 取钱操作
         */
        public class DrawThread extends Thread {
            private Account account;// 模拟账户
            private double drawMoney; // 取钱金额
    
            public DrawThread(String name, Account account, double drawMoney) {
                super(name);
                this.account = account;
                this.drawMoney = drawMoney;
            }
    
           @Override
            public void run() {
                if (account.getBalance() >= drawMoney) {
                    System.out.println(getName() + "取钱成功!取了  " + drawMoney + "元");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    account.setBalance(account.getBalance() - drawMoney);
                    System.out.println(getName() + ": 账户" + account.getAccoutnNo() + "剩余:" + account.getBalance());
                } else {
                    System.out.println(getName() + "取钱失败");
                }
            }
               
            }
        }

  3. 主测试类

               // 创建一个账户,让两个线程来操作
                Account account = new Account("0001", 1000);
                new DrawThread("A", account, 800).start();
                new DrawThread("B", account, 800).start();

  4. 结果
      A取钱成功!取了  800.0元
        B取钱成功!取了  800.0元
        A: 账户0001剩余:200.0
        B: 账户0001剩余:-600.0
    

          问题出来了,账户出现了负值,这个就不是希望的结果..

二、使用同步代码块

  1.     取款的线程类
        /*
         * Creation : 2015年10月15日
         */
        package com.tan.thread.bank;
    
        /**
         * 取钱操作
         */
        public class DrawThread extends Thread {
            private Account account;// 模拟账户
            private double drawMoney; // 取钱金额
    
            public DrawThread(String name, Account account, double drawMoney) {
                super(name);
                this.account = account;
                this.drawMoney = drawMoney;
            }
    
            //加锁-》修改锁-》释放锁
            //使用可能并发访问的共享资源作为同步监视器
            @Override
            public void run() {
              synchronized (account) {
                  if (account.getBalance() >= drawMoney) {
                      System.out.println(getName() + "取钱成功!取了  " + drawMoney + "元");
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      account.setBalance(account.getBalance() - drawMoney);
                      System.out.println(getName() + ": 账户" + account.getAccoutnNo() + "剩余:" + account.getBalance());
                  } else {
                      System.out.println(getName() + "取钱失败");
                  }
              }
              //释放同步锁
               
            }
        }
    


    注意 任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

三、使用同步方法

  1. 修改后的Account类
    /*
     * Creation : 2015年10月15日
     */
    package com.tan.thread.bank;
    
    public class Account2 {
        private String accoutnNo; // 账号编号
        private double balance;// 余额
    
        public Account2(String accoutnNo, double balance) {
            super();
            this.accoutnNo = accoutnNo;
            this.balance = balance;
        }
    
        public String getAccoutnNo() {
            return accoutnNo;
        }
    
        public void setAccoutnNo(String accoutnNo) {
            this.accoutnNo = accoutnNo;
        }
    
        public double getBalance() {
            return balance;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj != null && obj.getClass() == Account2.class) {
                Account2 a = (Account2) obj;
                return a.getAccoutnNo().equals(accoutnNo);
            }
            return false;
        }
    
        @Override
        public int hashCode() {
            return accoutnNo.hashCode();
        }
    
        /**
         * 添加一个取钱的同步方法 该方法的同步监视器是this,对于同一个账户,任何时刻只有一个线程获得对Account2对象的锁定,
         * 然后进入draw方法执行取钱操作,这样就可以保证多个线程并发取钱的线程安全。
         */
        public synchronized void draw(double drawAmount) {
            if (balance >= drawAmount) {
                System.out.println(Thread.currentThread().getName() + "取钱成功,本次取出" + drawAmount);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance -= drawAmount;
                System.out.println(Thread.currentThread().getName() + "取钱之后账户还剩余:" + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足!");
            }
        }
    
    }
    


  2. 取钱操作的线程类

    /*
     * Creation : 2015年10月15日
     */
    package com.tan.thread.bank;
    
    /**
     * 取钱操作
     */
    public class DrawThread2 extends Thread {
        private Account2 account2;// 模拟账户
        private double drawMoney; // 取钱金额
    
        public DrawThread2(String name, Account2 account2, double drawMoney) {
            super(name);
            this.account2 = account2;
            this.drawMoney = drawMoney;
        }
    
        @Override
        public void run() {
            account2.draw(drawMoney);
        }
    }
    


  3. 测试如上示例

  4. 提示
    在Account类里面定义draw方法而不是在run方法中实现取钱逻辑这种做法更加符合面向对象的规则,符合DDD(领域驱动设计)


四、使用同步锁

       在实现线程安全的控制中,比较常用的是可重入锁(ReentrantLock),使用该Lock对象可以显示的加锁、释放锁。

  1. 修改后的Account类
    /*
     * Creation : 2015年10月15日
     */
    package com.tan.thread.bank;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Account3 {
    
        // 该锁具有可重入性,一个线程可以对已被加锁的ReentrantLock再次加锁
        private final ReentrantLock lock = new ReentrantLock(); // 定义锁对象
    
        private String accoutnNo; // 账号编号
        private double balance;// 余额
    
        public Account3(String accoutnNo, double balance) {
            this.accoutnNo = accoutnNo;
            this.balance = balance;
        }
    
        public String getAccoutnNo() {
            return accoutnNo;
        }
    
        public void setAccoutnNo(String accoutnNo) {
            this.accoutnNo = accoutnNo;
        }
    
        // 余额不可以随便修改,故而只能提供get方法
        public double getBalance() {
            return balance;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj != null && obj.getClass() == Account3.class) {
                Account3 a = (Account3) obj;
                return a.getAccoutnNo().equals(accoutnNo);
            }
            return false;
        }
    
        @Override
        public int hashCode() {
            return accoutnNo.hashCode();
        }
    
        public void draw(double drawAmount) {
            <strong><span style="color:#FF0000;">lock.lock();</span></strong>
            try {
                if (balance >= drawAmount) {
                    System.out.println(Thread.currentThread().getName() + "取钱成功,本次取出" + drawAmount);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    balance -= drawAmount;
                    System.out.println(Thread.currentThread().getName() + "取钱之后账户还剩余:" + balance);
                } else {
                    System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足!");
                }
            } finally {
               <strong><span style="color:#FF0000;"> lock.unlock();</span></strong>
            }
    
        }
    }
    


  2. 提示

    ReentrantLock可重入锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,该锁对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显示的调用unlock()方法来释放锁,所以一段被锁的代码可以调用另一个被相同锁保护的方法。


五、死锁

  1.  程序实例

    /*
     * Creation : 2015年10月15日
     */
    package com.tan.thread.bank;
    
    /**
     * 当两个线程相互等待对方释放同步监视器时就会发生死锁
     */
    public class DeadLockTest implements Runnable {
        A a = new A();
        B b = new B();
    
        public void init() {
            Thread.currentThread().setName("主线程");
            a.foo(b);
            System.out.println("进入主线程之后...");
        }
    
        @Override
        public void run() {
            Thread.currentThread().setName("副线程");
            b.bar(a);
            System.out.println("进入副线程之后...");
        }
    
        public static void main(String[] args) {
            DeadLockTest lockTest = new DeadLockTest();
            new Thread(lockTest).start();
            lockTest.init();
        }
    }
    
    class A {
        public synchronized void foo(B b) {
            System.out.println(Thread.currentThread().getName() + "进入A中的foo方法");
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            System.out.println(Thread.currentThread().getName() + "企图调用B示例的last方法...");
            b.last();
        }
    
        public synchronized void last() {
            System.out.println(Thread.currentThread().getName() + "进入A类的last方法中...");
        }
    }
    
    class B {
        public synchronized void bar(A a) {
            System.out.println(Thread.currentThread().getName() + "进入B中的bar方法");
            try {
                Thread.sleep(100);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            System.out.println(Thread.currentThread().getName() + "企图调用A示例的last方法...");
            a.last();
        }
    
        public synchronized void last() {
            System.out.println(Thread.currentThread().getName() + "进入B类的last方法");
        }
    }
    



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值