在Java内存模型上

不同的平台,内存模型不同,但是jvm内存模型规范是统一的. 实际上,Java的多线程并发问题最终将反映在Java的内存模型中. 所谓的线程安全无非是控制多个线程对多个资源的有序访问或修改. 总结Java的内存模型,必须解决两个主要问题: 可见性和顺序. 我们都知道计算机具有缓存,并且处理器在每次处理数据时都不会占用内存. JVM定义了自己的内存模型,从而屏蔽了底层平台内存管理的细节. 对于Java开发人员,如果您解决了多线程的可见性和有序性,则必须根据jvm内存模型进行明确说明.

那么,可见度是什么?多个线程无法相互通信,它们之间的通信只能通过共享变量来完成. Java内存模型(JMM)指定jvm具有主内存,该内存由多个线程共享. 当对象是新对象时,它也会分配到主存储器中. 每个线程都有其自己的工作内存. 工作存储器将一些对象的副本存储在主存储器中. 当然,线程的工作内存大小是有限的. 当线程对对象进行操作时,执行顺序如下:

(1)将变量从主内存复制到当前工作内存(读取和加载)

(2)执行代码并更改共享变量值(使用和分配)

(3)使用工作存储器数据刷新与主存储器有关的内容(存储和写入)

JVM规范定义了到主内存的线程的操作指令: 读取线程安全问题代码,加载,使用,分配,存储,写入. 当一个共享变量在多个线程的工作内存中具有一个副本时,如果一个线程修改了共享变量,则其他线程应该能够看到修改后的值,这就是多线程可见性的问题.

那么,什么是订单?当线程引用变量时,无法直接从主内存中引用该变量. 如果线程在工作内存中没有该变量,则会将副本从主内存复制到工作内存中. 加载,线程将在完成后引用该副本. . 当同一线程再次引用该字段时,可以从主内存中获取变量的副本(读取加载使用),也可以直接引用原始副本(使用),即顺序读取,加载和使用可以由JVM实施系统决定来确定.

线程无法直接将值分配给主内存中的字段. 它将值分配给工作存储器中的变量副本(分配). 完成后,变量副本将同步到主存储区(store-write). 过去根据JVM实现系统的决定进行同步. 如果存在此字段,它将从主存储器分配给工作存储器. 此过程为读取加载. 完成后,线程将引用变量副本. 当同一线程重复多次时为字段分配值,例如:

Java代码

for(int i=0;i<10;i++)

a++;

线程可能仅向工作内存中的副本分配值,并且仅在最后一次分配后才同步到主存储区域,因此JVM实现系统可以确定分配,存储和更改的顺序. 假设有一个共享变量x,线程a执行x = x + 1. 从上面的描述可以知道,x = x + 1不是原子操作,其执行过程如下:

1从主存储器到工作存储器读取变量x的副本

2将1加到x

3将x加1的值写回主存储器

如果另一个线程b执行x = x-1,则执行过程如下:

1从主存储器到工作存储器读取变量x的副本

2从x减去1

3将x减1的值写回到主存储器中

因此,显然,x的最终值不可靠. 假设x现在为10,则线程a增加1,线程b减少1. 从表面上看,最终的x仍为10,但是在多线程情况下会发生这种情况:

1: 线程a从主内存到工作内存读取x的副本,并且工作内存中x的值为10

2: 线程b从主内存读取x的副本到工作内存,并且工作内存中的x值为10

3: 线程a将工作内存中的x加1,而工作内存中的x为11

4: 线程a将x提交到主内存,其中x为11

5: 线程b将工作内存中的x值减1,而工作内存中的x值为9

6: 线程b将x提交到主内存,其中x为9

类似地,x可能为11,如果x是一个银行帐户,线程存款,线程b扣除,显然这是一个严重的问题,要解决此问题,必须确保线程a和线程b顺序正确执行后,每个线程执行的递增或递减1是原子操作. 看下面的代码:

Java代码

public class Account {

private int balance;

public Account(int balance) {

this.balance = balance;

}

public int getBalance() {

return balance;

}

public void add(int num) {

balance = balance + num;

}

public void withdraw(int num) {

balance = balance - num;

}

public static void main(String[] args) throws InterruptedException {

Account account = new Account(1000);

Thread a = new Thread(new AddThread(account, 20), "add");

Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");

a.start();

b.start();

a.join();

b.join();

System.out.println(account.getBalance());

}

static class AddThread implements Runnable {

Account account;

int amount;

public AddThread(Account account, int amount) {

this.account = account;

this.amount = amount;

}

public void run() {

for (int i = 0; i < 200000; i++) {

account.add(amount);

}

}

}

static class WithdrawThread implements Runnable {

Account account;

int amount;

public WithdrawThread(Account account, int amount) {

this.account = account;

this.amount = amount;

}

public void run() {

for (int i = 0; i < 100000; i++) {

account.withdraw(amount);

}

}

}

}

第一个执行结果是10200,第二个执行结果是1060. 每个执行的结果都是不确定的,因为线程的执行顺序是不可预测的. 这是Java同步的根本原因. sync关键字可确保多个线程对于同步块是互斥的. 作为同步方法进行同步解决了Java多线程的执行顺序和内存可见性. volatile关键字“解决多线程内存可见性问题”. 稍后将详细描述.

同步关键字

如上所述,java使用synced关键字作为多线程并发环境的执行顺序的保证之一. 当一段代码将修改共享变量时,这段代码将成为一个互斥或关键的部分. 为了确保共享变量的正确性,synchronized指示关键部分. 典型用法如下:

Java代码

synchronized(锁){

临界区代码

}

为了确保银行帐户的安全,该帐户的操作方法如下:

Java代码

public synchronized void add(int num) {

balance = balance + num;

}

public synchronized void withdraw(int num) {

balance = balance - num;

}

您只是说过sync的用法是这样的:

Java代码

synchronized(锁){

临界区代码

}

那么这对于公共同步的void add(int num)意味着什么?实际上,在这种情况下,锁定是此方法的对象. 同样,如果该方法是公共静态同步的void add(int num),则锁是该方法所在的类.

理论上,每个对象都可以用作锁,但是当一个对象用作锁时线程安全问题代码,应该由多个线程共享,这样才有意义. 在并发环境中,未共享的对象不是锁. 有意义的. 如果有这样的代码:

Java代码

public class ThreadTest{

public void test(){

Object lock=new Object();

synchronized (lock){

//do something

}

}

}

将lock变量作为锁存在根本没有任何意义,因为它根本不是共享对象,并且每个线程都将执行Object lock = new Object();. 每个线程都有自己的锁,并且没有锁竞争.

每个锁对象都有两个队列,一个是就绪队列,另一个是阻塞队列. 就绪队列存储将获取锁的线程. 阻塞队列存储被阻塞的线程. 唤醒线程(通知)后,它将进入就绪队列并等待CPU调度. 当线程a首次执行account.add方法时,jvm将检查锁定对象帐户的就绪队列是否已经在等待线程. 如果存在,则表明帐户锁已被占用. 由于这是第一次运行,因此该帐户的就绪队列为空,因此线程a获取锁并执行account.add方法. 如果这时发生,线程b必须执行account.withdraw方法,因为线程a已经获得了锁并且还没有释放它,所以线程b必须进入帐户的就绪队列并等待锁被执行.

线程执行关键部分代码的过程如下:

1获取同步锁

2清除工作记忆

3将变量副本从主内存复制到工作内存

4计算这些变量

5将变量从工作存储器写入主存储器

6解除锁定

可以看出,同步不仅可以保证多个线程的并发排序,而且可以确保多个线程的内存可见性.

生产者/消费者模型

生产者/消费者模型实际上是一个非常经典的线程同步模型. 在许多情况下,仅在多个线程之间保证共享资源操作上多个线程的相互排斥是不够的. 有合作.

假设存在这样一种情况,即桌子上有一块盘子,桌子上只能放一个鸡蛋. 擅长将鸡蛋放在盘子上. 如果盘子里有鸡蛋,请等到没有鸡蛋,B专门从盘子里取出鸡蛋. 如果板上没有鸡蛋,请等待直到板上有鸡蛋. 实际上,印版是相互排斥的区域. 每次将鸡蛋放在盘子上时,它应该是互斥的. A的等待实际上是在主动放弃锁,B也必须提醒A在等待时放鸡蛋.

如何让线程主动释放锁

很简单,只需调用锁的wait()方法即可. wait方法来自Object,因此任何对象都具有此方法. 看下面的代码片段:

Java代码

Object lock=new Object();//声明了一个对象作为锁

synchronized (lock) {

balance = balance - num;

//这里放弃了同步锁,好不容易得到,又放弃了

lock.wait();

}

如果线程获取锁,进入同步块并执行lock.wait(),则该线程将进入锁的阻塞队列. 如果调用lock.notify(),则将通知阻塞队列中的线程进入就绪队列.

本文来自电脑杂谈,转载请注明本文网址:

http://www.pc-fly.com/a/jisuanjixue/article-264852-1.html

java 线程安全性_Java线程安全性问题摘要_Power Node Java Academy的组织相关推荐

  1. java线程触发_java线程

    线程. 状态 新建状态(New): 当用 new 操作符创建一个线程时, 例如 new Thread(r),线程还没有开始运行,此时 线程处在新建状态. 当一个线程处于新生状态时,程序还没有开始运行线 ...

  2. java多线程抽奖_java 线程池、多线程并发实战(生产者消费者模型 1 vs 10) 附案例源码...

    导读 前二天写了一篇<Java 多线程并发编程>点我直达,放国庆,在家闲着没事,继续写剩下的东西,开干! 线程池 为什么要使用线程池 例如web服务器.数据库服务器.文件服务器或邮件服务器 ...

  3. 线程池 java 新建方式_Java线程池的四种创建方式

    Java通过Executors提供四种线程池,分别为: newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程. newFi ...

  4. java中我爱你_Java线程学习(转)

    编写具有多线程能力的程序经常会用到的方法有: run(),start(),wait(),notify(),notifyAll(),sleep(),yield(),join() 还有一个重要的关键字:s ...

  5. java 线程状态_JAVA线程漫谈:线程状态与状态转换解析

    线程使用方式 JDK线程的顶层抽象是Runnable接口,本质上,线程相关的类都是基于Runnable和Thread实现类实现. JDK API级别有不同的创建线程的方式,但本质是还是基于Runnab ...

  6. java 线程状态_Java线程为何没有Running状态?我猜你不知道。

    作者:国栋原文:https://my.oschina.net/goldenshaw/blog/705397 Java虚拟机层面所暴露给我们的状态,与操作系统底层的线程状态是两个不同层面的事.具体而言, ...

  7. java线程分类_Java 线程类别

    Java 线程类别 守护线程和非守护线程 守护线程和非守护线程之前的唯一区别在于:是否阻止JVM的正常退出. JVM正常退出是与异常退出相对的概念,异常退出如调用System.exit(status) ...

  8. java runnable 异常_JAVA 线程中的异常捕获

    在java多线程程序中,所有线程都不允许抛出未捕获的checked exception(比如sleep时的InterruptedException),也就是说各个线程需要自己把自己的checked e ...

  9. idea 线程内存_Java线程池系列之-Java线程池底层源码分析系列(一)

    课程简介: 课程目标:通过本课程学习,深入理解Java线程池,提升自身技术能力与价值. 适用人群:具有Java多线程基础的人群,希望深入理解线程池底层原理的人群. 课程概述:多线程的异步执行方式,虽然 ...

最新文章

  1. jenkins 邮件配置 二 ***
  2. Keil编译报错:Cannot open include file: 'stdbool.h': No such file or directory问题解决
  3. App in Scala
  4. js 下拉底部加载|滑轮滚动到页面底部ajax加载数据的实例
  5. python邮件的图片放在哪里_用python保存电子邮件中的嵌入图像
  6. php内核介绍及扩展开发指南 pdf vp进,PHP内核介绍及扩展开发指南—Extensions 的编写...
  7. 【今日CV 视觉论文速览】16 Nov 2018
  8. java流数据base64,Base64数据的流解码
  9. [luogu2680] 运输计划 (lca+二分+树上差分)
  10. 《只管去做》-如何做靠谱的年度计划
  11. Pytorch——神经网络训练方法的演进
  12. PHP实现微信网页登陆授权开发
  13. github 下载慢问题 - 代理 - 汇总
  14. PDF文件进行在线分割如何去操作
  15. SpringBoot实现发送QQ邮箱验证码
  16. 大型剪纸艺术——美到惊奇
  17. LeetCode:838. 推多米诺————中等
  18. 数据分析实战之超市零售分析(附python代码)
  19. docker部署开发环境
  20. 往年二本计算机分数线,全国一本、二本院校历年录取分数线汇总

热门文章

  1. 从0到1 | 滴滴DB自动化运维实践了解一下
  2. 快来一起玩转LiteOS组件:Curl
  3. 谈谈有什么方法可以快捷实现多场景下的线程安全
  4. 真香!全场景AI计算开源框架MindSpore,我爱了
  5. 【华为云技术分享】Spark如何与深度学习框架协作,处理非结构化数据
  6. 【华为云技术分享】自动网络搜索(NAS)在语义分割上的应用(二)
  7. PyTorch最佳实践,怎样才能写出一手风格优美的代码
  8. 关于redis,学会这8点就够了
  9. AOS编排语言系列教程(二):初识AOS编排语言,创建你的第一个AOS模板
  10. java中 与%_java中“/”与“%”的区别