java 多线程并发同步问题及解决方法
一、线程并发同步概念
线程同步其核心就在于一个“同”。所谓“同”就是协同、协助、配合,“同步”就是协同步调昨,也就是按照预定的先后顺序进行运行,即“你先,我等, 你做完,我再做”。
线程同步,就是当线程发出一个功能调用时,在没有得到结果之前,该调用就不会返回,其他线程也不能调用该方法。
就一般而言,我们在说同步、异步的时候,特指那些需要其他组件来配合或者需要一定时间来完成的任务。在多线程编程里面,一些较为敏感的数据时不允许被多个线程同时访问的,使用线程同步技术,确保数据在任何时刻最多只有一个线程访问,保证数据的完整性。
二、线程同步中可能存在安全隐患
用生活中的场景来举例:小生去银行开个银行账户,银行给 me 一张银行卡和一张存折,小生用银行卡和存折来搞事情:
银行卡疯狂存钱,存完一次就看一下余额;同时用存折子不停地取钱,取一次钱就看一下余额;
具体代码实现如下:
先弄一个银行账户对象,封装了存取插钱的方法:
package com.test.threadDemo2; /** * 银行账户 * @author Administrator * */ public class Acount { private int count=0; /** * 存钱 * @param money */ public void addAcount(String name,int money) { // 存钱 count += money; System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName()); SelectAcount(name); } /** * 取钱 * @param money */ public void subAcount(String name,int money) { // 先判断账户现在的余额是否够取钱金额 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); } }
编写银行卡对象:
package com.test.threadDemo2; /** * 银行卡负责存钱 * @author Administrator * */ public class Card implements Runnable{ private String name; private Account account = new Account(); public Card(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.addAccount(name,100);26 } } }
编写存折对象(和银行卡方法几乎一模一样,就是名字不同而已):
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); } } }
主方法测试,演示银行卡疯狂存钱,存折疯狂取钱:
package com.test.threadDemo2; public class ThreadDemo2 { public static void main(String[] args) { // 开个银行帐号 Account account = new Account(); // 开银行帐号之后银行给张银行卡 Card card = new Card("card",account); // 开银行帐号之后银行给张存折 Paper paper = new Paper("存折",account); Thread thread1 = new Thread(card); Thread thread2 = new Thread(paper); thread1.start(); thread2.start(); } }
结果显示:从中可以看出 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 即可:
package com.test.threadDemo2; /** * 银行账户 * @author Administrator * */ public class Acount { private int count; /** * 存钱 * @param money */ public synchronized void addAcount(String name,int money) { // 存钱 count += money; System.out.println(name+"...存入:"+money); } /** * 取钱 * @param money */ public synchronized void subAcount(String name,int money) { // 先判断账户现在的余额是否够取钱金额 if(count-money < 0){ System.out.println("账户余额不足!"); return; } // 取钱 count -= money; System.out.println(name+"...取出:"+money); } /** * 查询余额 */ public void SelectAcount(String name) { System.out.println(name+"...余额:"+count); } }
运行效果:
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 的释放。
二者都在等待对方所持有锁的释放,而二者却又都没释放自己所持有的锁,这时二者便会一直阻塞下去。这种情形称为死锁。
package com.testDeadLockDemo; public class LockA { private LockA(){} public static final LockA lockA = new LockA(); }
package com.testDeadLockDemo; public class LockB { private LockB(){} public static final LockB lockB = new LockB(); }
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++; } } }
测试:
package com.testDeadLockDemo; public class Test { public static void main(String[] args) { DeadLock deadLock = new DeadLock(); Thread t1 = new Thread(deadLock); Thread t2 = new Thread(deadLock); t1.start(); t2.start(); } }
运行结果:
五、线程通信
在共享资源中增加镖旗,当镖旗为真的时候才可以存钱,存完了就把镖旗设置成假,当取款的时候发现镖旗为假的时候,可以取款,取完款就把镖旗设置为真。
只需修改 Account 类 和 测试类 即可
package com.test.threadDemo2; /** * 银行账户 * @author Administrator * */ public class Acount { private boolean flag=false; // 默认flag 为false,要求必须先存款再取款 private int count=0; /** * 存钱 * @param money */ public void addAcount(String name,int money) { synchronized(this) { // flag 为true 表示可以存款,否则不可以存款 if(flag) { try { this.wait(); } 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; this.notifyAll(); } } } /** * 取钱 * @param money */ public void subAcount(String name,int money) { synchronized(this) { if(!flag) { try { this.wait(); } 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; this.notifyAll(); } } } /** * 查询余额 */ public void SelectAcount(String name) { System.out.println(name+"...余额:"+count); } }
package com.test.threadDemo2; public class ThreadDemo2 { public static void main(String[] args) { // 开个银行帐号 Acount acount = new Acount(); // 开银行帐号之后银行给张银行卡 Card card1 = new Card("card1",acount); Card card2 = new Card("card2",acount); Card card3 = new Card("card3",acount); // 开银行帐号之后银行给张存折 Paper paper1 = new Paper("paper1",acount); Paper paper2 = new Paper("paper2",acount); // 创建三个银行卡 Thread thread1 = new Thread(card1,"card1"); Thread thread2 = new Thread(card2,"card2"); Thread thread3 = new Thread(card3,"card3"); // 创建两个存折 Thread thread4 = new Thread(paper1,"paper1"); Thread thread5 = new Thread(paper2,"paper2"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); thread5.start(); } }
运行结果:
使用同步锁也可以达到相同的目的:
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); } }