zoukankan      html  css  js  c++  java
  • 【java 多线程】多线程并发同步问题及解决方法

    一、线程并发同步概念

    线程同步其核心就在于一个“同”。所谓“同”就是协同、协助、配合,“同步”就是协同步调昨,也就是按照预定的先后顺序进行运行,即“你先,我等, 你做完,我再做”。

    线程同步,就是当线程发出一个功能调用时,在没有得到结果之前,该调用就不会返回,其他线程也不能调用该方法。

    就一般而言,我们在说同步、异步的时候,特指那些需要其他组件来配合或者需要一定时间来完成的任务。在多线程编程里面,一些较为敏感的数据时不允许被多个线程同时访问的,使用线程同步技术,确保数据在任何时刻最多只有一个线程访问,保证数据的完整性。

    二、线程同步中可能存在安全隐患

    用生活中的场景来举例:小生去银行开个银行账户,银行给 me 一张银行卡和一张存折,小生用银行卡和存折来搞事情:

    银行卡疯狂存钱,存完一次就看一下余额;同时用存折子不停地取钱,取一次钱就看一下余额;

    具体代码实现如下:

    先弄一个银行账户对象,封装了存取插钱的方法:

     1 package com.test.threadDemo2;
     2 
     3 /**
     4  * 银行账户
     5  * @author Administrator
     6  *
     7  */
     8 public class Acount {
     9     private int count=0;
    10     
    11     /**
    12      * 存钱
    13      * @param money
    14      */
    15     public void addAcount(String name,int money) {
    16         17             // 存钱
    18             count += money;
    19             System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
    20             SelectAcount(name);
    21 22     }
    23     
    24     /**
    25      * 取钱
    26      * @param money
    27      */
    28     public void subAcount(String name,int money) {
    29         30             // 先判断账户现在的余额是否够取钱金额
    31             if(count-money < 0){  
    32                 System.out.println("账户余额不足!"); 
    33                 return;  
    34             } 
    35             // 取钱
    36             count -= money;
    37             System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
    38             SelectAcount(name);
    39 40     }
    41     
    42     /**
    43      * 查询余额
    44      */
    45     public void SelectAcount(String name) {
    46         System.out.println(name+"...余额:"+count);
    47     }
    48 }

    编写银行卡对象:

     1 package com.test.threadDemo2;
     2 /**
     3  * 银行卡负责存钱
     4  * @author Administrator
     5  *
     6  */
     7 public class Card implements Runnable{
     8     private String name;
     9     private Account account = new Account();
    10     
    11     public Card(String name,Account account) {
    12         this.account = account;
    13         this.name = name;
    14     }
    15     
    16     @Override
    17     public void run() {
    18         
    19         while(true) {
    20             try {
    21                 Thread.sleep(1000);
    22             } catch (InterruptedException e) {
    23                 e.printStackTrace();
    24             }
    25             account.addAccount(name,100);26         }
    27     }
    28     
    29 }

    编写存折对象(和银行卡方法几乎一模一样,就是名字不同而已):

    package com.test.threadDemo2;
    /**
     * 存折负责取钱
     * @author Administrator
     *
     */
    public class Paper implements Runnable{
        private String name;
        private Account account = new Account();
        
        public Paper(String name,Account account) {
            this.account = account;
            this.name = name;
        }
        
        @Override
        public void run() {
            while(true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.subAccount(name,50);
            }
            
        }
    
    }

    主方法测试,演示银行卡疯狂存钱,存折疯狂取钱:

     1 package com.test.threadDemo2;
     2 
     3 public class ThreadDemo2 {
     4     public static void main(String[] args) {
     5         
     6         // 开个银行帐号
     7         Account account = new Account();
     8         // 开银行帐号之后银行给张银行卡
     9         Card card = new Card("card",account);
    10         // 开银行帐号之后银行给张存折
    11         Paper paper = new Paper("存折",account);
    12         
    13         Thread thread1 = new Thread(card);
    14         Thread thread2 = new Thread(paper);
    15         
    16         thread1.start();
    17         thread2.start();            
    18     }
    19 }

    结果显示:从中可以看出 bug

     

    从上面的例子里就可以看出,银行卡存钱和存折取钱的过程中使用了 sleep() 方法,这只不过是小生模拟“系统卡顿”现象:银行卡存钱之后,还没来得及查余额,存折就在取钱,刚取完钱,银行卡这边“卡顿”又好了,查询一下余额,发现钱存的数量不对!当然还有“卡顿”时间比较长,存折在卡顿的过程中,把钱全取了,等银行卡这边“卡顿”好了,一查发现钱全没了的情况可能。

    因此多个线程一起访问共享的数据的时候,就会可能出现数据不同步的问题,本来一个存钱的时候不允许别人打断我(当然实际中可以存在刚存就被取了,有交易记录在,无论怎么动这个帐号,都是自己的银行卡和存折在动钱。小生这个例子里,要求的是存钱和查钱是一个完整过程,不可以拆分开),但从结果来看,并没有实现小生想要出现的效果,这破坏了线程“原子性”。

    三、线程同步中可能存在安全隐患的解决方法

      从上面的例子中可以看出线程同步中存在安全隐患,我们必须不能忽略,所以要引入“锁”(术语叫监听器)的概念:

    3.1 同步代码块:

      使用 synchronized() 对需要完整执行的语句进行“包裹”,synchronized(Obj obj) 构造方法里是可以传入任何类的对象,

      但是既然是监听器就传一个唯一的对象来保证“锁”的唯一性,因此一般使用共享资源的对象来作为 obj 传入 synchronized(Obj obj) 里:

      只需要锁 Account 类中的存钱取钱方法就行了:

    package com.test.threadDemo2;
    
    /**
     * 银行账户
     * @author Administrator
     *
     */
    public class Acount {
        private int count=0;
        
        /**
         * 存钱
         * @param money
         */
        public void addAcount(String name,int money) {
            synchronized(this) {
                // 存钱
                count += money;
                System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
                SelectAcount(name);
            }
        }
        
        /**
         * 取钱
         * @param money
         */
        public void subAcount(String name,int money) {
            synchronized(this) {
                // 先判断账户现在的余额是否够取钱金额
                if(count-money < 0){  
                    System.out.println("账户余额不足!"); 
                    return;  
                } 
                // 取钱
                count -= money;
                System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
                SelectAcount(name);
            }
        }
        
        /**
         * 查询余额
         */
        public void SelectAcount(String name) {
            System.out.println(name+"...余额:"+count);
        }
    }

    3.2 同步方法

    者在方法的申明里申明 synchronized 即可:

     1 package com.test.threadDemo2;
     2 /**
     3  * 银行账户
     4  * @author Administrator
     5  *
     6  */
     7 public class Acount {
     8     private int count;
     9     
    10     /**
    11      * 存钱
    12      * @param money
    13      */
    14     public synchronized void addAcount(String name,int money) {
    15             // 存钱
    16             count += money;
    17             System.out.println(name+"...存入:"+money);
    18     }
    19     
    20     /**
    21      * 取钱
    22      * @param money
    23      */
    24     public synchronized void subAcount(String name,int money) {
    25             // 先判断账户现在的余额是否够取钱金额
    26             if(count-money < 0){  
    27                 System.out.println("账户余额不足!");  
    28                 return;  
    29             } 
    30             // 取钱
    31             count -= money;
    32             System.out.println(name+"...取出:"+money);
    33     }
    34     
    35     /**
    36      * 查询余额
    37      */
    38     public void SelectAcount(String name) {
    39         System.out.println(name+"...余额:"+count);
    40     }
    41 }

    运行效果:

    3.3 使用同步锁:

    account 类创建私有的 ReetrantLock 对象,调用 lock() 方法,同步执行体执行完毕之后,需要用 unlock() 释放锁。

    package com.test.threadDemo2;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 银行账户
     * @author Administrator
     *
     */
    public class Acount {
        private int count;
        private ReentrantLock lock = new ReentrantLock();
        
        /**
         * 存钱
         * @param money
         */
        public void addAcount(String name,int money) {
            lock.lock();
            try{
                // 存钱
                count += money;
                System.out.println(name+"...存入:"+money);
            }finally {
                lock.unlock();
            }
        }
        
        /**
         * 取钱
         * @param money
         */
        public void subAcount(String name,int money) {
            lock.lock();
            try{
                // 先判断账户现在的余额是否够取钱金额
                if(count-money < 0){  
                    System.out.println("账户余额不足!");  
                    return;  
                } 
                // 取钱
                count -= money;
                System.out.println(name+"...取出:"+money);
            }finally {
                lock.unlock();
            }
        }
        
        /**
         * 查询余额
         */
        public void SelectAcount(String name) {
            System.out.println(name+"...余额:"+count);
        }
    }

    运行效果: 

     

     四、死锁

    当线程需要同时持有多个锁时,有可能产生死锁。考虑如下情形:

    线程 A 当前持有互斥所锁 lock1,线程 B 当前持有互斥锁 lock2。

    接下来,当线程 A 仍然持有 lock1 时,它试图获取 lock2,因为线程 B 正持有 lock2,因此线程 A 会阻塞等待线程 B 对 lock2 的释放。

    如果此时线程 B 在持有 lock2 的时候,也在试图获取 lock1,因为线程 A 正持有 lock1,因此线程 B 会阻塞等待 A 对 lock1 的释放。

    二者都在等待对方所持有锁的释放,而二者却又都没释放自己所持有的锁,这时二者便会一直阻塞下去。这种情形称为死锁。

    1 package com.testDeadLockDemo;
    2 
    3 public class LockA {
    4     
    5     private LockA(){}
    6     
    7     public static final LockA lockA = new LockA();
    8 }
    1 package com.testDeadLockDemo;
    2 
    3 public class LockB {
    4     private LockB(){}
    5     
    6     public static final LockB lockB = new LockB();
    7 }
    package com.testDeadLockDemo;
    
    public class DeadLock implements Runnable{
        private int i=0;
        
        @Override
        public void run() {
            while(true) {
                if(i%2==0){
                    synchronized(LockA.lockA) {
                        System.out.println("if...lockA");
                        synchronized(LockB.lockB) {
                            System.out.println("if...lockB");
                        }
                    }
                }else {
                    synchronized(LockB.lockB) {
                        System.out.println("else...lockB");
                        synchronized(LockA.lockA) {
                            System.out.println("else...lockA");
                        }
                    }
                }
                i++;
            }
            
        }
    }

    测试:

     1 package com.testDeadLockDemo;
     2 
     3 public class Test {
     4     public static void main(String[] args) {
     5         DeadLock deadLock = new DeadLock();
     6         
     7         Thread t1 = new Thread(deadLock);
     8         Thread t2 = new Thread(deadLock);
     9         t1.start();
    10         t2.start();
    11         
    12     }
    13 }

    运行结果:

     五、线程通信

    在共享资源中增加镖旗,当镖旗为真的时候才可以存钱,存完了就把镖旗设置成假,当取款的时候发现镖旗为假的时候,可以取款,取完款就把镖旗设置为真。

    只需修改 Account 类 和 测试类 即可

     1 package com.test.threadDemo2;
     2 
     3 /**
     4  * 银行账户
     5  * @author Administrator
     6  *
     7  */
     8 public class Acount {
     9     private boolean flag=false;    // 默认flag 为false,要求必须先存款再取款
    10     private int count=0;
    11     
    12     /**
    13      * 存钱
    14      * @param money
    15      */
    16     public void addAcount(String name,int money) {
    17         synchronized(this) {
    18             // flag 为true 表示可以存款,否则不可以存款
    19             if(flag) {
    20                 try {
    21                     this.wait();
    22                 } catch (InterruptedException e) {
    23                     // TODO Auto-generated catch block
    24                     e.printStackTrace();
    25                 }
    26             }else {
    27                 // 存钱
    28                 count += money;
    29                 System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
    30                 SelectAcount(name);
    31                 flag = true;
    32                 this.notifyAll();
    33             }
    34         }
    35     }
    36     
    37     /**
    38      * 取钱
    39      * @param money
    40      */
    41     public void subAcount(String name,int money) {
    42         synchronized(this) {
    43             if(!flag) {
    44                 try {
    45                     this.wait();
    46                 } catch (InterruptedException e) {
    47                     e.printStackTrace();
    48                 }
    49             }else {
    50             // 先判断账户现在的余额是否够取钱金额
    51             if(count-money < 0){  
    52                 System.out.println("账户余额不足!"); 
    53                 return;  
    54             } 
    55                 // 取钱
    56                 count -= money;
    57                 System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
    58                 SelectAcount(name);
    59                 flag = false;
    60                 this.notifyAll();
    61             }
    62         }
    63     }
    64     
    65     /**
    66      * 查询余额
    67      */
    68     public void SelectAcount(String name) {
    69         System.out.println(name+"...余额:"+count);
    70     }
    71 }
     1 package com.test.threadDemo2;
     2 
     3 public class ThreadDemo2 {
     4     public static void main(String[] args) {
     5         
     6         // 开个银行帐号
     7         Acount acount = new Acount();
     8         
     9         // 开银行帐号之后银行给张银行卡
    10         Card card1 = new Card("card1",acount);
    11         Card card2 = new Card("card2",acount);
    12         Card card3 = new Card("card3",acount);
    13         
    14         // 开银行帐号之后银行给张存折
    15         Paper paper1 = new Paper("paper1",acount);
    16         Paper paper2 = new Paper("paper2",acount);
    17         
    18         // 创建三个银行卡
    19         Thread thread1 = new Thread(card1,"card1");
    20         Thread thread2 = new Thread(card2,"card2");
    21         Thread thread3 = new Thread(card3,"card3");
    22         // 创建两个存折
    23         Thread thread4 = new Thread(paper1,"paper1");
    24         Thread thread5 = new Thread(paper2,"paper2");
    25         
    26         thread1.start();
    27         thread2.start();
    28         thread3.start();
    29         
    30         thread4.start();
    31         thread5.start();
    32     }
    33 }

    运行结果:

     使用同步锁也可以达到相同的目的:

    package com.test.threadDemo2;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 银行账户
     * @author Administrator
     *
     */
    public class Acount2 {
        private boolean flag=false;    // 默认flag 为false,要求必须先存款再取款
        private int count=0;
        private final ReentrantLock lock = new ReentrantLock();
        private final Condition condition = lock.newCondition();
        
        /**
         * 存钱
         * @param money
         */
        public void addAcount(String name,int money) {
            lock.lock();
            try {
                // flag 为false 表示可以存款,否则不可以存款
                if(flag) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }else {
                    // 存钱
                    count += money;
                    System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
                    SelectAcount(name);
                    flag = true;
                    condition.signalAll();
                }
            }finally {
                lock.unlock();
            }
        }
        
        /**
         * 取钱
         * @param money
         */
        public void subAcount(String name,int money) {
            lock.lock();
            try {
                if(!flag) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                // 先判断账户现在的余额是否够取钱金额
                if(count-money < 0){  
                    System.out.println("账户余额不足!"); 
                    return;  
                } 
                    // 取钱
                    count -= money;
                    System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
                    SelectAcount(name);
                    flag = false;
                    condition.signalAll();
                }
            }finally {
                lock.unlock();
            }
        }
        
        /**
         * 查询余额
         */
        public void SelectAcount(String name) {
            System.out.println(name+"...余额:"+count);
        }
    }
  • 相关阅读:
    图片处理
    RBAC打造通用web管理权限
    决定人生的三种成本:机会成本,沉没成本,边际成本
    实验楼可以做各种程序实验
    .net2.0 C# Json反序列化
    不装mono,你的.NET程序照样可以在Linux上运行!
    .net framework4与其client profile版本的区别
    spark transform系列__sortByKey
    自己主动化开发測试的一些理论根据及经验总结(2015)
    Toast分析——实现自己的Toast
  • 原文地址:https://www.cnblogs.com/mujingyu/p/7856388.html
Copyright © 2011-2022 走看看