浅谈利用同步机制解决Java中的线程安全问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈利用同步机制解决Java中的线程安全问题相关的知识,希望对你有一定的参考价值。

我们知道大多数程序都不会是单线程程序,单线程程序的功能非常有限,我们假设一下所有的程序都是单线程程序,那么会带来怎样的结果呢?假如淘宝是单线程程序,一直都只能一个一个用户去访问,你要在网上买东西还得等着前面千百万人挑选购买,最后心仪的商品下架或者售空......假如饿了吗是单线程程序,那么一个用户得等前面全国千万个用户点完之后才能进行点餐,那饿了吗就该倒闭了不是吗?以上两个简单的例子,就说明一个程序能进行多线程并发访问的重要性,今天就让我们去了解一下Java中多线程并发访问这个方向吧。
   **第一步:理解多线程的概念**
   很多初学者,并不知道什么是多线程,那么在此我将简单介绍一下多线程。线程是指在一个进程中的一个顺序执行流(也就是一段执行的代码),多线程则是在一个进程中存在多个顺序执行流,它们相互独立,共享进程中的所有资源(进程中的代码段,进程的内存空间等)。
   **第二步:多线程的并发访问**
   那么多个线程又是如何进行并发访问的呢?我们直接上代码:
   public class MyThread extends Thread{
   public void run(){
    for(int i=0;i<5;i++){
    System.out.println(Thread.currentThread().getName()+"  "+i);
    }
}
 public static void main(String[] args) {
    for(int i=0;i<5;i++){
        System.out.println(Thread.currentThread().getName()+"  "+i);
        if(i==3){
            new MyThread().start();
            new MyThread().start();
        }
    }
}
   上面就是一段最简单的多线程并发执行的例子,这段代码中一共有三个线程,main、Thread-0、Thread-1。它们并发执行上面的程序。我们来看看运行的结果:
   main      2
   main      3
   Thread-0  0
   Thread-1  0
   main      4
   Thread-1  1
   Thread-1  2
   Thread-0  1
   Thread-0  2
   以上是我截取的部分运行结果,由运行结果可以看出main、 Thread-0、Thread-1三个线程都对i进行了取值,而且三个线程相对独立,各自取各自的值,相互不影响。同时应该注意到,三个线程取的值是不连续的,这是因为我所创建的i是一个实例变量而不是一个局部变量,每个线程去执行线程执行体的时候都会重新对i进行取值,所以此处对i的取值不是连续的。
   对于上述代码和运行结果可知,多线程并发访问的特点是:线程之间相互独立,不受其他线程的干扰。
   **第三步:多线程并发访问时同步安全问题**
   从第二步的叙述中,我们知道了多个线程可以对一个对象进行同时访问,那么一些问题也随之出现,那就是多线程并发访问一个对象时的安全问题。我们由一个经典题目来慢慢去剖析多线程并发安全问题,并尝试去解决这个问题。
   银行取钱问题:银行取钱的流程我们大概可以分为这么几步:
     *1. 用户输入账户、密码,系统去判断用户的账户密码是否正确。
     *2. 用户输入取款金额。
     *3. 系统判断用户的余额是否大于用户的取款金额。
     *4. 如果用户的余额大于取款金额,则取款成功,如果用户的余额小于取款金额,则取款失败。
   上面的操作结果好像是有理有据的,那么我们就继续上代码去完成上面的需求吧!
        class Account{
        //封装用户的账户、密码
        private String account;
        private double balance;
        public Account(String account,double balance){
            this.balance = balance;
            this.account = account;
        }
        public void setAccount(String account) {
            this.account = account;
        }
        public String getAccount() {
            return account;
        }
        public void setBalance(double balance) {
            this.balance = balance;
        }
        public double getBalance() {
            return balance;
        }
        public int hashcode(){
            return account.hashCode();
        }
        @Override
        public boolean equals(Object obj) {
            if(this == obj){
                return true;
            }
            if(this != obj && obj.getClass()== Account.class){
                Account a = (Account)obj;
                return a.getAccount().equals(account);
            }
            return false;
        }
    }
    class DrawAccount extends Thread{
        //模拟用户的账户
        private Account account;
        //获取当前希望取的钱数
        private double drawAccount;
        private String name;
        public DrawAccount( Account account,double drawAccount) {
            this.account = account;
            this.drawAccount = drawAccount;
        }
        @Override
        public void run() {
            //余额大于取钱的数目
            if(account.getBalance()>=drawAccount){
                System.out.println("您的名字是"+getName()+" "+"您要提取的现金为:"+drawAccount+"元");
                account.setBalance(account.getBalance()-drawAccount);
                System.out.println("您的余额为:"+account.getBalance()+"元");
    }
            else{
        System.out.println("您输入的金额有误,取钱失败");
            }
        }
    }
    public class Drawtext{
        public static void main(String[] args) {
            Account a = new Account("12345", 1500);
            //现在就模拟两个线程去对同一个账户同时取钱
            new DrawAccount(a,1000).start();
            new DrawAccount(a,1000).start();
        }
    }
   上面的代码我开启了两个线程同时取钱,并且完全符合我前面所述的银行取钱流程,那么现在我们运行这个程序:
    您的名字是  Thread-0您要提取的现金为:1000.0元
    您的余额为:500.0
    您的名字是  Thread-1您要提取的现金为:1000.0元
    您的余额为:-500.0

   由上面的运行结果可知,当两个用户(线程)同时取钱的时候,程序就会出现差错,这是与银行系统的需求不匹配的,所以我们要对程序的bug作出分析,并作出相应的修改。
   通过分析可知,我们必须控制在相同的时刻只能有一个用户取钱(也就是说,只能有一个线程对余额进行访问),这个时候,我们就提出了线程安全问题,解决银行多客户对同一账户并发取钱问题,就是要去解决线程安全问题。
   线程安全问题的感念:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
   线程安全问题的常用解决办法:
   1、利用同步机制去解决多线程并发访问而造成的线程安全问题:同步代码块、同步监视器、同步锁。
   2、创建不可变类(对象、方法等)。
   今天我们主要讲解利用同步机制去解决Java中的线程安全问题
   我们还是从概念出发:
   同步:同步指两个或两个以上随时间变化的量在变化过程中保持一定的相对关系。同步(英语:Synchronization),指在一个系统中所发生的事件,之间进行协调,在时间上出现一致性与统一化的现象。在系统中进行同步,也被称为及时、同步化的。
   同步代码块:同步代码块是利用了同步监视器来解决线程同步问题。同步代码块的格式如下:
   Synchronized(obj){
   .......
   //此处就是同步代码块
   }
   上面的格式中:obj是同步监视器,通常由可能被并发访问的共享资源来充当同步监视器。
   那么如果我们要用同步代码快去解决上面银行取钱问题,怎么去做呢?很简单,我们继续上代码:
        class DrawAccount extends Thread{
        //模拟用户的账户
        private Account account;
        //获取当前希望取的钱数
        private double drawAccount;
        private String name;
        public DrawAccount( Account account,double drawAccount) {
            this.account = account;
            this.drawAccount = drawAccount;
        }
        @Override
        public void run() {
            //这里我们必须让account来充当同步监视器,任何线程在执行同步代码快之前都要将同步监视器进行锁定,被锁定的同步监视器,只能由这一个线程去访问,其他线程无法访问,只有当该线程释放了对同步监视器的锁定之后,其他的线程才拥有访问同步监视器的资格。
             Synchronized(account){
            //余额大于取钱的数目
            if(account.getBalance()>=drawAccount){
                System.out.println("您的名字是"+getName()+" "+"您要提取的现金为:"+drawAccount+"元");
                account.setBalance(account.getBalance()-drawAccount);
                System.out.println("您的余额为:"+account.getBalance()+"元");
    }
            else{
        System.out.println("您输入的金额有误,取钱失败");
            }
            }
            //同步代码块结束,线程释放对同步监视器的锁定。
        }
    }
   使用同步代码块去解决问题之后,我们运行上面的代码,看看效果如何?    
    您的名字是  Thread-0您要提取的现金为:1000.0元
    您的余额为:500.0
    您输入的金额有误,取钱失败
   果不其然,再利用同步代码快对程序进行修改之后,我们的问题也迎刃而解!
   下面,我们再用同步方法去解决问题。
   同步方法其实很简单,就是使用synchronized去修饰一个方法,格式如下:
   public synchronized void draw(){}
   上面就是同步方法的标准格式,现在我们用同步方法去解决银行取钱问题。上代码: 
   public synchronized void draw(double drawAmount) {
            //余额大于取钱的数目
            if(account.getBalance()>=drawAccount){
                System.out.println("您的名字是"+getName()+" "+"您要提取的现金为:"+drawAccount+"元");
                account.setBalance(account.getBalance()-drawAccount);
                System.out.println("您的余额为:"+account.getBalance()+"元");
    }
            else{
        System.out.println("您输入的金额有误,取钱失败");
            }
        }   
        }    
        我们将涉及到余额修改的方法改成同步方法,运行程序:
        您的名字是  Thread-0您要提取的现金为:1000.0元
        您的余额为:500.0
        您输入的金额有误,取钱失败     
        我们发现,用同步方法去修改,也能解决问题。那么最后一个方法能否行得通呢?我们来试试吧!
        同步锁:我们这里写的同步锁,是ReentrantLock(可重入锁),使用该锁对象,可以显示的加锁,释放锁。通常使用ReentrantLock的格式如下:
        class A{
        private final ReentrantLock lock = new ReentrantLock();
        //...
        //定义需要保证线程安全的方法
        public void M(){
        //加锁
           lock.lock();
           try{
            //需要保证安全的代码....
          }
          //使用finally块来释放锁
          finally{
            lock.unlock();
          }
        }
        }
        这里出现了finally块,通常我建议大家用finally块来保证锁的释放。
        现在我们用同步锁来修改程序。上代码:  
        public class Account{
         private final ReentrantLock lock = new ReentrantLock(); 
         //模拟用户的账户
        private Account account;
        //获取当前希望取的钱数
        private double drawAccount;
        private String name;
        public DrawAccount( Account account,double drawAccount) {
            this.account = account;
            this.drawAccount = drawAccount;
        }
        public void setAccount(String account) {
               this.account = account;
         }
        public String getAccount() {
               return account;
         }
         //因为不允许余额随便更改,所以我们只设定了balance的get方法
        public double getBalance() {
               return balance;
         }
         //提供一个线程安全的draw()方法去完成取钱的操作
         public void draw(double drawAmount){
         //加锁
         lock.lock();
         try{
         if(balance>=drawAmount){
         System.out.println(Thread.currenThread().getName()+"您要提取的现金为:"+drawAccount+"元");
         //修改余额
         balance-=drawAmount;
         System.out.println("您的余额为:"+balance+"元");
        }
        else{
        System.out.println("您输入的金额有误,取钱失败");
        }
        finally{//使用finally块来释放锁
        lock.unlock();
        }
        //省略hashcode()和equals()方法
        此处我们使用了一个同步锁来对取钱的相关的代码进行锁定,运行结果:
        Thread-0您要提取的现金为:1000.0元
        您的余额为:500.0元
        您输入的金额有误,取钱失败     

        从上面的运行结果可以出,使用同步锁也能防止多线程并发访问而造成的线程安全问题。

        好啦,今天向大伙儿通过银行取钱案例介绍了三种同步方式去解决线程安全问题,相信大家都对三种方法有了以一定的了解,希望我的博客能对大家有所收获。加油啦!
        (备注:因本人能力有限,在写博客的时候难免有所疏漏,如有缺漏之处,恳请各位读者谅解,并欢迎大家给我指出问题,让我能向大家学到更多知识!)
        鸡年第一更!祝大家鸡年快乐!

以上是关于浅谈利用同步机制解决Java中的线程安全问题的主要内容,如果未能解决你的问题,请参考以下文章

阶段1 语言基础+高级_1-3-Java语言高级_05-异常与多线程_第3节 线程同步机制_4_解决线程安全问题_同步代码块

浅谈Java三种实现线程同步的方法

浅谈Java三种实现线程同步的方法

JAVA多线程_线程安全问题

java中多线程安全性和同步的常用方法

浅谈利用PLSQL的多线程处理机制,加快处理大数据表的效率