【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 }
} 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);
} } }
主方法测试,演示银行卡疯狂存钱,存折疯狂取钱:
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);
}
}
【java 多线程】多线程并发同步问题及解决方法的更多相关文章
- sqlite:多线程操作数据库“database is locked”解决方法(二)
上一篇博客<sqlite:多线程操作数据库“database is locked”解决方法>通过注册延时函数的方法来处理数据库被锁的问题.此方法固然能解决问题,但是在多个线程向数据库写入大 ...
- java开发中遇到的问题及解决方法(持续更新)
摘自 http://blog.csdn.net/pony12/article/details/38456261 java开发中遇到的问题及解决方法(持续更新) 工作中,以C/C++开发为主,难免与其他 ...
- Java 解密错误InvalidKeyException: Illegal key size解决方法
做解密操作,出现如下错误 java.security.InvalidKeyException: Illegal key size // 设置解密模式为AES的CBC模式 Cipher cipher = ...
- 在Eclipse中运行Jboss时出现java.lang.OutOfMemoryError:PermGen space及其解决方法
在Eclipse中运行Jboss时出现java.lang.OutOfMemoryError:PermGen space及其解决方法 在Eclipse中运行Jboss时,时间太长可能有时候会出现java ...
- 访问tomcat出现java.lang.IllegalStateException No output folder错误解决方法
访问tomcat出现java.lang.IllegalStateException: No output folder错误解决方法 问题:tomcat分为安装版和解压缩版,解压缩版如果解压到安装盘,在 ...
- java编程出现的错误对应的解决方法
error: could not open D:\java\jre1.8\lib\amd64\jvm.cfg 解决方法:把java的环境变量%JAVA_HOME%/bin上移到最上面 优化 查看网页源 ...
- java.net.BindException: Address already in use: 解决方法
java.net.BindException: Address already in use: 解决方法 1. 执行cmd 2. cmd命令模式下输入netstat -ano,然后找到占用端口的那 ...
- Delphi 多线程 “尚未调用CoInitialize错误”的解决方法
在Delphi 多线程中出现“尚未调用CoInitialize错误”的解决方法 解决方法如下: function TMyThread.ExecTimer: Boolean;begin Resul ...
- Java中实现线程同步的三种方法
实现同步的三种方法 多线程共享数据时,会发生线程不安全的情况,多线程共享数据必须同步. 实现同步的三种方法: 使用同步代码块 使用同步方法 使用互斥锁ReetrantLock(更灵活的代码控制) 代码 ...
随机推荐
- BZOJ.4151.[AMPPZ2014]The Cave(结论)
BZOJ 不是很懂他们为什么都要DFS三次.于是稳拿Rank1 qwq. (三道题两个Rank1一个Rank3效率是不是有点高qwq?) 记以\(1\)为根DFS时每个点的深度是\(dep_i\).对 ...
- tensorflow 使用 4 非线性回归
# 输入一个 x 会计算出 y 值 y 是预测值,如果与 真的 y 值(y_data)接近就成功了 import tensorflow as tf import numpy as np # py 的画 ...
- lua 语言基础
1.数据类型: string(字符串) ·运算符“+.-.*./”等操作字符串,lua会尝试讲字符串转换为数字后操作: ·字符串连接用“..”运算符 ·用“#”来计算字符串的长度(放在字符串前面) · ...
- 自定义类在PropertyGrid上的展示方法
自定义类在PropertyGrid上的展示方法 零.引言 PropertyGrid用来显示某一对象的属性,但是并不是所有的属性都能编辑,基本数据类型(int, double等)和.Net一些封装的类型 ...
- Angular 2项目的环境配置和项目搭建
AngularJS2 发布于2016年9月份,它是基于ES6来开发的. AngularJS2 是一款开源JavaScript库,由Google维护,用来协助单一页面应用程序运行.AngularJS2 ...
- 2019中山大学程序设计竞赛-Monitor
题目地址 题目大意:给你一个n*m的矩形,在这个矩形内告诉你p个矩形(左下角和右上角坐标),问你q个问题,每次也是给你一个矩形(左下角和右上角坐标),问你每个矩形是否可以被开始给的p个矩形完全覆盖. ...
- C# CreateParams的使用(解决闪屏问题)
<转载自:https://blog.csdn.net/xpwang/article/details/53427479> 窗体和控件的属性CreateParams(这真的是一个属性)很神奇, ...
- javascript基础(Array)
1,join() Array.join(),不改变原数组,将数组中所有元素转换为字符串并连接在一起,返回最后生成的字符串 let a=[1,2,3]; a.join(); // =>" ...
- 20190108C++MFC error 2065 未定义XX原因以及解决方式
今天写界面的时候,明明直接在rc和reourse.h里面加了控件下面是rc和reourse.h照片 编辑的时候一直报错,找了很久发现是新定义的控件有两处定义,定义到其他工程里了所以才会这样,把其他工程 ...
- Floyd算法解决多源最短路问题
说好的写dijkstra 算法堆优化版本的,但是因为,妹子需要,我还是先把Floyd算法写一下吧!啦啦啦! 咳咳,还是说正事吧! ----------------------------------- ...