最全java多线程总结2--如何进行线程同步
上篇对线程的一些基础知识做了总结,本篇来对多线程编程中最重要,也是最麻烦的一个部分——同步,来做个总结。
创建线程并不难,难的是如何让多个线程能够良好的协作运行,大部分需要多线程处理的事情都不是完全独立的,大都涉及到数据的共享,本篇是对线程同步的一个总结,如有纰漏的地方,欢迎在评论中指出。
为什么要有同步
我们来看一个简单的例子,有两个数 num1,num2,现在用 10 个线程来做这样一件事--每次从 num1 中减去一个随机的数 a,加到 num2 上。
public class Demo1 {
public static void main(String[] args) {
Bank bank = new Bank();
//创建10个线程,不停的将一个账号资金转移到另一个账号上
for (int i = 0; i < 100; i++) {
new Thread(() -> {
while (true) {
int account1 = ((Double) Math.floor(Math.random() * 10)).intValue();
int account2 = ((Double) Math.floor(Math.random() * 10)).intValue();
int num = ((Long) Math.round(Math.random() * 100)).intValue();
bank.transfer(account1, account2, num);
try {
Thread.sleep(((Double) (Math.random() * 10)).intValue());
} catch (Exception e) {
}
}
}).start();
}
}
}
class Bank {
/**
* 10个资金账户
*/
public int[] accounts = new int[10];
public Bank() {
Arrays.fill(accounts, 1000);
}
public void transfer(int from, int to, int num) {
accounts[from] -= num;
accounts[to] += num;
//计算和
int sum = 0;
for (int j = 0; j < 10; j++) {
sum += accounts[j];
}
System.out.println(sum);
}
}
正常情况下,无论什么时候资金账号的和应该都是 10000.然而真的会这样吗?运行程序一段时间后会发现和不等于 10000 了,可能变大也可能变小了。
竞争
上面的代码中有多个程序同时更新账户信息,因此出现了竞争关系。假设两个线程同时执行下面的一句代码:
accounts[account1] -= num;
该代码不是原子性的,可能会被处理成如下三条指令:
- 将 accounts[account1]加载到寄存器
- 值减少 num
- 结果写回到 accounts[account1]
这里仅说明单核心情况下的问题(多核一样会有问题),单核心是不能同时运行两个线程的,如果一个线程 A 执行到第三步时,被剥夺了运行权,线程 B 开始执行完成了整个过程,然后线程 A 继续运行第三步,这就产生了错误,线程 A 的结果覆盖了线程 B 的结果,总金额不再正确。如下图所示:
如何同步
锁对象
为了防止并发导致数据错乱,Java 语言提供了 synchronized 关键字,并且在 Java SE 5 的时候加入了 ReentrantLock 类。synchronized 关键字自动提供了一个锁以及相关的条件,这个后面再说。ReentrantLock 的基本使用如下:
myLock.lock()//myLock是一个ReetrantLock对象示例
try{
//要保护的代码块
}finally{
//一定要在finally中释放锁
myLock.unlock();
}
上述结构保证任意时刻只有一个线程进入临界区,一旦一个线程调用 lock 方法获取了锁,其他所有线程都会阻塞在 lock 方法处,直到有锁线程调用 unlock 方法。
将 ban 类中的 transfer 方法加锁,代码如下:
class Bank {
/**
* 10个资金账户
*/
public int[] accounts = new int[10];
private ReentrantLock lock = new ReentrantLock();
public Bank() {
Arrays.fill(accounts, 1000);
}
public void transfer(int from, int to, int num) {
try {
lock.lock();
accounts[from] -= num;
accounts[to] += num;
//计算和
int sum = 0;
for (int j = 0; j < 10; j++) {
sum += accounts[j];
}
System.out.println(sum);
} finally {
lock.unlock();
}
}
}
经过加锁,无论多少线程同时运行,都不会导致数据错乱。
锁是可以重入的,已经持有锁的线程可以重复获取已经持有的锁。锁有一个持有计数(hold count)来跟踪 lock 方法的嵌套调用。每 lock 一次计数+1,unlock 一次计数-1,当 lock 为 0 时锁释放掉。
可以通过带 boolean 参数构造一个带有公平策略的锁--new ReentrantLock(true)
。公平锁偏爱等待时间最长的线程。但是会导致性能大幅降低,而且即使使用公平锁,也不能确保线程调度器是公平的。
条件对象
通常我们会遇到这样的问题,当一个线程获取锁后,发现需要满足某个条件才能继续往后执行,这就需要一个条件对象来管理已经获取锁但是却不能做有用工作的线程。
现在来考虑给转账加一个限制,只有资金充足的账户才能作为转出账户,也就是不能出现负值。注意下面的代码是不可行的:
if(bank.accounts[from]>=num){
bank.transfer(from,to,num);
}
因为多线程下极有可能 if 判断成功后,刚好数据被其他线程修改了。
可以通过条件对象来这样实现判断:
class Bank {
/**
* 10个资金账户
*/
public int[] accounts = new int[10];
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public Bank() {
Arrays.fill(accounts, 1000);
}
public void transfer(int from, int to, int num) {
try {
lock.lock();
while (accounts[from] < num) {
//进入阻塞状态
condition.await();
}
accounts[from] -= num;
accounts[to] += num;
//计算和
int sum = 0;
for (int j = 0; j < 10; j++) {
sum += accounts[j];
}
System.out.println(sum);
//通知解除阻塞
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
在 while 循环中判断是否满足,如果不满足条件,调用await
方法进入阻塞状态,同时放弃锁。这样让其他线程有机会给转出账户转入资金也满足判断条件。
当某一个线程完成转账工作后,应该调用signalAll
方法让所有阻塞线程接触阻塞状态,因为此时可能会满足判断条件,可以继续转账操作。
注意:调用signalAll
不会立即激活一个等待线程,仅仅只是接触阻塞状态,以便这些线程可以通过竞争获取锁,继续进行 while 判断。
还有一个方法signal
随机解除一个线程的阻塞状态。这里可能会导致死锁的产生。
synchronized 关键词
上一节中的 Lock 和 Condition 为开发人员提供了强大的同步控制。但是大多数情况并不需要那么复杂的控制。从 java 1.0 版本开始,Java 中的每个对象都有一个内部锁。如果一个方法用synchronized
声明,那么对象的锁将保护整个方法,也就是调用方法时自动获取内部锁,方法结束时自动解除内部锁。
同 ReentrantLock 锁一样,内部锁也有 wait/notifyAll/notify 方法,对应关系如下:
- wait 对应 await
- notifyAll 对应 signalAll
- notify 对应 signal
之所以方法名不同是因为 wait 这几个方法是 Object 类的 final 方法,为了不发生冲突,ReentrantLock
类中方法需要重命名。
用 synchronized 实现的 ban 类如下:
class Bank {
/**
* 10个资金账户
*/
public int[] accounts = new int[10];
private ReentrantLock lock = new ReentrantLock();
// private Condition condition = lock.newCondition();
public Bank() {
Arrays.fill(accounts, 1000);
}
synchronized public void transfer(int from, int to, int num) {
try {
// lock.lock();
while (accounts[from] < num) {
//进入阻塞状态
// condition.await();
this.wait();
}
accounts[from] -= num;
accounts[to] += num;
//计算和
int sum = 0;
for (int j = 0; j < 10; j++) {
sum += accounts[j];
}
System.out.println(sum);
//通知解除阻塞
// condition.signalAll();
this.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
// finally {
// lock.unlock();
// }
}
}
静态方法也可以声明为 synchronized,调用这中方法,获取到的是对应类的类对象的内部锁。
代码中怎么用
最好既不使用 Lock/Condition 也不使用 synchronized 关键字,大多是情况下都可以用 java.util.concurrent 包中的类来完成数据同步,该包中的类都是线程安全的。会在下一篇中讲到。
如果能用 synchronized 的,尽量用它,这样既可以减少代码数量,减少出错的几率。
如果上面都不能解决问题,那就只能使用 Lock/Condition 了。
本篇所用全部代码:github
本文原创发布于:https://www.tapme.top/blog/detail/2019-04-10-20-52
最全java多线程总结2--如何进行线程同步的更多相关文章
- Java 多线程基础(五)线程同步
Java 多线程基础(五)线程同步 当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题. 要解决上述多线程并发访问一个资源的安全性问题,Java中提供了同步机制 ...
- 【Java多线程系列三】实现线程同步的方法
两种实现线程同步的方法 方法 特性 synchronized 不需要显式的加锁,易实现 ReentrantLock 需要显式地加解锁,灵活性更好,性能更优秀,结合Condition可实现多种条件锁 ...
- Java 多线程基础(四)线程安全
Java 多线程基础(四)线程安全 在多线程环境下,如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线 ...
- “全栈2019”Java多线程第十六章:同步synchronized关键字详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- “全栈2019”Java多线程第十三章:线程组ThreadGroup详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- “全栈2019”Java多线程第十一章:线程优先级详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- “全栈2019”Java多线程第九章:判断线程是否存活isAlive()详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- “全栈2019”Java多线程第五章:线程睡眠sleep()方法详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- Java多线程基础:进程和线程之由来
转载: Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程.当然,Java并发编程涉及到很多方面的内容,不是一朝一夕就能够 ...
随机推荐
- Smali相关的基础知识点
通过本篇博客的学习,相信你可以无压力的读懂Smali语言文件,并可以将Smali还原成java!!! 其实Smali语言并不是很难,如果你有一些汇编指令的基础,学习Smali就更加简单了,有兴趣的可以 ...
- [C++] 反编译器
各种开源的decompiler都不太好用,眼下最好的反编译器是IDA pro. 尽管是收费的,只是破解版非常好找. 我试过5.5版本号的,还不错. 我把windows notepad进行了反编译,多少 ...
- 机器学习: t-Stochastic Neighbor Embedding 降维算法 (二)
上一篇文章,我们介绍了SNE降维算法,SNE算法可以很好地保持数据的局部结构,该算法利用条件概率来衡量数据点之间的相似性,通过最小化条件概率 pj|i 与 pi|j 之间的 KL-divergence ...
- 将 WPF、UWP 以及其他各种类型的旧 csproj 迁移成基于 Microsoft.NET.Sdk 的新 csproj
原文 将 WPF.UWP 以及其他各种类型的旧 csproj 迁移成基于 Microsoft.NET.Sdk 的新 csproj 写过 .NET Standard 类库或者 .NET Core 程序的 ...
- matlab 矢量化编程(四)—— 标量函数转化为能够处理矢量的函数
1. 组合的矢量实现 nchoosek(n, k) 的第二个参数在 matlab 下是不支持矢量化的,必须是标量形式.但 matlab 下的 gamma 函数,却可支持,矢量形式,又因为,gamma ...
- zedboard之GPIO驱动器(离FPGA直到LINUX申请书)
笔者:xiabodan 资源: http://blog.csdn.net/xiabodan/article/details/24308373 1 EDK 大家知道我们在EDK中建立GPIO然后倒出 ...
- 三:Java之Applet
首先我要说的是Applet是一种应用程序,它是一种由JAVA编写的小应用程序,通常这样的应用程序都像他的名字一样,是一个非常小的程序,或许有些朋友就会问了,那么它是用来干什么的呢?JAVA程序就是JA ...
- python 教程 第二十一章、 扩展Python
第二十一章. 扩展Python /* D:\Python27\Lib\Extest-1.0\Extest2.c */ #include <stdio.h> #include <std ...
- WPF中Style文件的引用——使用xaml代码或者C#代码动态加载
原文:WPF中Style文件的引用--使用xaml代码或者C#代码动态加载 WPF中控件拥有很多依赖属性(Dependency Property),我们可以通过编写自定义Style文件来控制控件的外观 ...
- Layui 2.0.0 正式发布:潜心之作,开箱即用的前端UI框架(确实很多内容)
Hi,久违了.处暑逼近之际,潜水半年的 layui 是时候出来透透气了.我们带来的是全新的 2.0 版本,一次被我们定义为“破茧重生”的倾情之作.如果你已曾用过 layui,你将真正感受到一次因小而大 ...