上篇对线程的一些基础知识做了总结,本篇来对多线程编程中最重要,也是最麻烦的一个部分——同步,来做个总结。

  创建线程并不难,难的是如何让多个线程能够良好的协作运行,大部分需要多线程处理的事情都不是完全独立的,大都涉及到数据的共享,本篇是对线程同步的一个总结,如有纰漏的地方,欢迎在评论中指出。

为什么要有同步

  我们来看一个简单的例子,有两个数 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;

该代码不是原子性的,可能会被处理成如下三条指令:

  1. 将 accounts[account1]加载到寄存器
  2. 值减少 num
  3. 结果写回到 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--如何进行线程同步的更多相关文章

  1. Java 多线程基础(五)线程同步

    Java 多线程基础(五)线程同步 当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题. 要解决上述多线程并发访问一个资源的安全性问题,Java中提供了同步机制 ...

  2. 【Java多线程系列三】实现线程同步的方法

    两种实现线程同步的方法 方法 特性 synchronized  不需要显式的加锁,易实现 ReentrantLock 需要显式地加解锁,灵活性更好,性能更优秀,结合Condition可实现多种条件锁  ...

  3. Java 多线程基础(四)线程安全

    Java 多线程基础(四)线程安全 在多线程环境下,如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线 ...

  4. “全栈2019”Java多线程第十六章:同步synchronized关键字详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  5. “全栈2019”Java多线程第十三章:线程组ThreadGroup详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  6. “全栈2019”Java多线程第十一章:线程优先级详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  7. “全栈2019”Java多线程第九章:判断线程是否存活isAlive()详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  8. “全栈2019”Java多线程第五章:线程睡眠sleep()方法详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  9. Java多线程基础:进程和线程之由来

    转载: Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程.当然,Java并发编程涉及到很多方面的内容,不是一朝一夕就能够 ...

随机推荐

  1. 在vs中启动项目,同时给项目传递参数

    问题的引出:项目在startup.cs文件中做了控制,根据读取的控制台的ip 和端口启动项目 : dotnet project --ip 127.0.0.1 --port 8001 这样写的好处是  ...

  2. JAVASCRIPT高程笔记-------第八章 浏览器BOM对象

    8.1 window对象--表示一个浏览器的实例 在全局作用域中声明的任何变量.函数都会变成window对象的属性和方法,与之直接定义window对象的属性的区别是   window.xxx 可以通过 ...

  3. WPF4.0用tablet实现手写输入(更新XP SP3下也能手写输入方法)

    原文:WPF4.0用tablet实现手写输入(更新XP SP3下也能手写输入方法) 由于项目需求一个手写输入的控件,纠结了2天,终于搞定了. 主要是由于本人的英语不过关,一直和ocr混淆在一起,研究了 ...

  4. telerik ChartGrid浅谈

    在最近接触的项目中,有很多都是以Chart图表的方式呈现出来的,关于telerik Chart的使用,有几个小点跟大家分享一下. 1:本例子使用的Chart的命名空间为 xmlns:telerik=h ...

  5. python 识别身份证号码

    # !/usr/bin/python # -*-coding:utf-8-*- import sys import time time1 = time.time() from PIL import I ...

  6. PostMessage和SendMessage有什么区别?(有EnumChildWindowsProc的例子)

    PostMessage只是把消息放入队列,不管其他程序是否处理都返回,然后继续执行;而SendMessage必须等待其他程序处理消息后才返回,继续执行.PostMessage的返回值表示PostMes ...

  7. C# GetFiles

    var path = AppDomain.CurrentDomain.BaseDirectory + "Images\\Rooms\\"; // string[] patterns ...

  8. Android零基础入门第52节:自定义酷炫进度条

    原文:Android零基础入门第52节:自定义酷炫进度条 Android系统默认的ProgressBar往往都不能满足实际开发需要,一般都会开发者自定义ProgressBar. 在Android开发中 ...

  9. 关于DDD领域驱动设计的理论知识收集汇总

    原文:关于DDD领域驱动设计的理论知识收集汇总 最近一直在学习领域驱动设计(DDD)的理论知识,从网上搜集了一些个人认为比较有价值的东西,贴出来和大家分享一下: 我一直觉得不要盲目相信权威,比如不能一 ...

  10. Qt系统对话框中文化及应用程序实现重启及使用QSS样式表文件及使用程序启动界面

    一.应用程序中文化 1).Qt安装目录下有一个目录translations/,在此目录下有qt_zh_CN.ts和 qt_zh_CN.qm把它们拷贝到你的工程目录下. 2).在main函数加入下列代码 ...