【java多线程】多线程并发同步问题及解决方法

一、线程并发同步概念

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

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

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

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

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

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

具体代码实现如下:

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

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

编写银行卡对象:

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

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

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

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

 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 类中的存钱取钱方法就行了:

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

3.2 同步方法

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

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

运行效果:

3.3 使用同步锁:

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

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

运行效果:

四、死锁

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

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

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

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

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

 1package com.testDeadLockDemo;
 2
 3public class LockA {
 4
 5     private LockA(){}
 6
 7     public static final LockA lockA = new LockA();
 8 }
 9
10
11package com.testDeadLockDemo;
12
13 public class LockB {
14
15 private LockB(){}
16
17     public static final LockB lockB = new LockB();
18 }
19
20
21package com.testDeadLockDemo;
22
23public class DeadLock implements Runnable{
24    private int i=0;
25
26    @Override
27    public void run() {
28        while(true) {
29            if(i%2==0){
30                synchronized(LockA.lockA) {
31                    System.out.println("if...lockA");
32                    synchronized(LockB.lockB) {
33                        System.out.println("if...lockB");
34                    }
35                }
36            }else {
37                synchronized(LockB.lockB) {
38                    System.out.println("else...lockB");
39                    synchronized(LockA.lockA) {
40                        System.out.println("else...lockA");
41                    }
42                }
43            }
44            i++;
45        }
46
47    }
48}

测试:

 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 public class Acount { 
  8     private boolean flag=false;
  9    // 默认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 }
 72
 73
 74 package com.test.threadDemo2; 
 75
 76 public class ThreadDemo2 { 
 77     public static void main(String[] args) { 
 78
 79         // 开个银行帐号 
 80         Acount acount = new Acount(); 
 81
 82         // 开银行帐号之后银行给张银行卡
 83         Card card1 = new Card("card1",acount);
 84         Card card2 = new Card("card2",acount);
 85         Card card3 = new Card("card3",acount);
 86
 87         // 开银行帐号之后银行给张存折
 88         Paper paper1 = new Paper("paper1",acount);
 89         Paper paper2 = new Paper("paper2",acount);
 90
 91         // 创建三个银行卡
 92         Thread thread1 = new Thread(card1,"card1");
 93         Thread thread2 = new Thread(card2,"card2");
 94         Thread thread3 = new Thread(card3,"card3");
 95         // 创建两个存折
 96         Thread thread4 = new Thread(paper1,"paper1");
 97         Thread thread5 = new Thread(paper2,"paper2");
 98
 99         thread1.start();
100        thread2.start();
101         thread3.start();
102
103         thread4.start();
104         thread5.start();
105     }
106 }

运行结果:

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

 1package com.test.threadDemo2;
 2
 3import java.util.concurrent.locks.Condition;
 4import java.util.concurrent.locks.ReentrantLock;
 5/**
 6   * 银行账户
 7   * @author Administrator
 8   **/
 9public class Acount2 {
10    private boolean flag=false;
11    // 默认flag 为false,要求必须先存款再取款
12    private int count=0;
13    private final ReentrantLock lock = new ReentrantLock();
14    private final Condition condition = lock.newCondition();    
15    /**
16       * 存钱
17       * @param money
18       */
19    public void addAcount(String name,int money) {
20        lock.lock();
21        try {
22            // flag 为true 表示可以存款,否则不可以存款
23            if(flag) {
24                try {
25                    condition.await();
26                } catch (InterruptedException e) {
27                    // TODO Auto-generated catch block e.printStackTrace();
28                }
29            }else {
30                // 存钱
31                count += money;
32                System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
33                SelectAcount(name);
34                flag = true;
35                condition.signalAll();
36            }
37        }finally {
38            lock.unlock();
39        }
40    }    
41    /**
42       * 取钱
43       * @param money
44       */
45    public void subAcount(String name,int money) {
46        lock.lock();
47        try {
48            if(!flag) {
49                try {
50                    condition.await();
51                } catch (InterruptedException e) {
52                    e.printStackTrace();
53                }
54            }else {
55            // 先判断账户现在的余额是否够取钱金额
56            if(count-money < 0){  
57                System.out.println("账户余额不足!"); 
58                return;  
59            } 
60                // 取钱
61                count -= money;
62                System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
63                SelectAcount(name);
64                flag = false;
65                condition.signalAll();
66            }
67        }finally {
68            lock.unlock();
69        }
70    }    
71    /**
72       * 查询余额
73       */
74    public void SelectAcount(String name) {
75        System.out.println(name+"...余额:"+count);
76    }
77}

文章来源:https://www.cnblogs.com/mujingyu/p/7856388.html

原文发布于微信公众号 - 高性能服务器开发(easyserverdev)

原文发表时间:2018-04-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java成长之路

八、java对象和方法区的垃圾回收

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性...

1652
来自专栏月牙寂

[以太坊源代码分析] I.区块和交易,合约和虚拟机

本文转载来源自:http://blog.csdn.net/teaspring/article/details/75389151 感谢原作者teaspring...

5925
来自专栏玩转JavaEE

MongoDB中MapReduce使用

玩过Hadoop的小伙伴对MapReduce应该不陌生,MapReduce的强大且灵活,它可以将一个大问题拆分为多个小问题,将各个小问题发送到不同的机器上去处理...

3524
来自专栏我杨某人的青春满是悔恨

走进 RxSwift 之观察者模式

RxSwift 是 ReactiveX 系列的 Swift 版本,如果你之前用过 ReactiveCocoa(RAC) 的话,想必对 Functional Re...

2092
来自专栏生信小驿站

数据处理第3部分:选择行的基本和高级的方法

原文地址:https://suzan.rbind.io/2018/02/dplyr-tutorial-3/ 作者:Suzan Baert 这是系列dplyr...

981
来自专栏黄Java的地盘

[翻译]WebSocket协议第二章——Conformance Requirements

本文为WebSocket协议的第二章,本文翻译的主要内容为WebSocket协议中相关术语的介绍。

941
来自专栏一个会写诗的程序员的博客

《Kotlin 程序设计》第七章 Kotlin 编译过程分析第七章 Kotlin 编译过程分析

http://mp.weixin.qq.com/s/lEFRH523W7aNWUO1QE6ULQ

1892
来自专栏MasiMaro 的技术博文

windows 异常处理

为了程序的健壮性,windows 中提供了异常处理机制,称为结构化异常,异常一般分为硬件异常和软件异常,硬件异常一般是指在执行机器指令时发生的异常,比如试图向一...

1752
来自专栏FreeBuf

浅析ReDoS的原理与实践

*本文原创作者:MyKings,本文属FreeBuf原创奖励计划,未经许可禁止转载 ReDoS(Regular expression Denial of Ser...

8155
来自专栏令仔很忙

观察者模式和Spring的结合

这周给分了一个任务,就是对查询回来的数据进行各种各样的过滤,有七种不同的过滤条件。过滤条件是在数据库中存着的。在我们项目中有一个热发,就是定时的从数据库中把数...

1452

扫码关注云+社区

领取腾讯云代金券