Java多线程(下)

学习一时爽,一直学习一直爽

  Hello,大家好,我是 もうり,一个从无到有的技术+语言小白。

多线程混乱

在 Java 中多线程永远是一个重要的话题

多线程混乱造成 线程不安全

线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

回顾 synchronized 关键字

  • DateHolder,java
/** * @author: 毛利 */public class DateHolder {    private long number = 0;    public void change(long delta) {        number += delta;    }    public void print() {        System.out.println("Number=" + number);    }}
  • ChangeData,java
/** * @author: 毛利 */public class ChangeData implements Runnable{    // 私有对象DataHolder    private DataHolder dataHolder;    private long loopCount;    private long delta;    // 传递变量    public ChangeData(long delta, long loopCount, DataHolder dataHolder) {        this.delta = delta;        this.loopCount = loopCount;        this.dataHolder = dataHolder;    }    // 重载run方法    @Override    public void run() {        for (int i = 0; i < loopCount; i++) {            dataHolder.change(delta);        }        dataHolder.print();    }}
  • 使用单线程来跑,结果没有溢出
/** * @author: 毛利 */public class SingleThreadSimple {    public static void main(String[] args) {        // TODO 对一个数据进行相同次数的加减,而且也没有溢出,最后的结果应该是0        DataHolder dataHolder = new DataHolder();        // 4294967294 Integer.MAX_VALUE        ChangeData increase = new ChangeData(2, Integer.MAX_VALUE, dataHolder);        increase.run();        ChangeData decrease = new ChangeData(-2, Integer.MAX_VALUE, dataHolder);        decrease.run();        // 0    }}

但是对于多线程,结果就不一样了

public class MultiThreadChaos {    public static void main(String[] args) {        // TODO 同样的运算,安排在两个线程里做,结果就不一样了        DataHolder dataHolder = new DataHolder();        // TODO 创建两个线程 increaseThread 和 decreaseThread        Thread increaseThread = new Thread(new ChangeData(2, Integer.MAX_VALUE, dataHolder));        Thread decreaseThread = new Thread(new ChangeData(-2, Integer.MAX_VALUE, dataHolder));        System.out.println("执行开始");        increaseThread.start();        decreaseThread.start();        /*        执行开始        Number=-4219868950        Number=-4294408024         */    }}

解决方法:一个 synchronized 解决问题

  public synchronized static void changeStatic(long delta) {        numberStatic += delta;    }

除了 synchronized,还有另一个 volatile 关键字,不仅仅在 Java 语言中有,在很多语言中都有的,而且其用法和语义也都是不尽相同的。

volatile 通常被比喻成 "轻量级的 synchronized",也是 Java 并发编程中比较重要的一个关键字。

和 synchronized 不同,volatile 是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。

相比 Sychronized(重量级锁,对系统性能影响较大),volatile 提供了另一种解决 原子性,可见性和有序性

补充几个概念

  • 共享变量:共享变量是指可以同时被多个线程访问的变量,共享变量是被存放在堆里面,所有的方法内临时变量都不是共享变量。(共享变量是被存放在堆内存中的)
  • 可见性:内存的可见性是指线程之间的可见性,一个线程的修改状态对另外一个线程是可见的,用通俗的话说,就是假如一个线程 A 修改一个共享变量 flag 之后,则线程 B 去读取,一定能读取到最新修改的 flag。
  • 原子性:Java 中,对基本数据类型的读取和赋值操作是原子性操作,所谓原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行。比如:
  • 有序性:Java 内存模型中,允许编辑器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

volatile 都是在 cpu 指令级别进行操作。volatile 是无法保证变量安全的:

volatile 变量只能确保可见性。

Java 内存模型(JMM)

java 内存模型 (Java Memory Model,JMM) 是 java 虚拟机规范定义的,用来屏蔽掉 java 程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现 java 程序在各种不同的平台上都能达到内存访问的一致性。

大家都知道计算机都是多核的,在多核中,不仅面临如上问题,还有如果多个核用到了同一个数据,如何保证数据的一致性、正确性等问题,也是必须要解决的。所以协调 cpu 和各个硬件之间的速度差异是非常重要的,要不然 cpu 就一直在等待,浪费资源。

JMM 涉及到的概念有:

  • 主内存:java 虚拟机规定所有的变量 (不是程序中的变量) 都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
  • 工作内存:java 虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。

指令重排 demo

来源: 极客时间

public class DataHolder {    int a, b, c, d, f, g;    long e;    // TODO 有 volatile 修饰就会影响之前的指令重排    // volatile long e;    public void operateData() {        // TODO 按照这个顺序执行,g 的值是肯定小于等于 e 的。但是实际执行在执行的时候,可能会为了优化的目的重排        a += 1;        b += 1;        c += 1;        d += 1;        e += 1;        f += 1;        g += 1;    }    int counter;    // 计算触发重排多少次    public void check() {        // TODO 看似不可能的条件,实际可能被触发到        if (g > e) {            System.out.println("got it " + (counter++));        }    }}
public class VolatileAppMain {    public static void main(String[] args) {        DataHolder dataHolder = new DataHolder();        Thread operator = new Thread(() -> {            while (true) {                dataHolder.operateData();            }        });        operator.start();        Thread checker = new Thread(() -> {            while (true) {                dataHolder.check();            }        });        checker.start();    }}

从结果来看一直存在指令重排

public class VolatileAppMain {    public static void main(String[] args) {        DataHolder dataHolder = new DataHolder();        Thread operator = new Thread(() -> {            while (true) {                dataHolder.operateData();            }        });        operator.start();        Thread checker = new Thread(() -> {            while (true) {                dataHolder.check();            }        });        checker.start();    }}

加上 volatile关键字

volatile long e;

就不存在所谓的指令重排

volatile 的可见性

volatile 变量只能确保可见性。

/** * @author: 毛利 */public class Test {//    public static final int num = 1;    private /*volatile*/ boolean flag = true;//    private volatile boolean flag = true;    public void testvolatile(){        System.out.println("开始读取……");        while (flag){//            System.out.println(num);        }        System.out.println("读取完成……");    }    public static void main(String[] args) {        Test test = new Test();        new Thread(test::testvolatile).start();        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        test.flag = false;    }}

一直卡死在哪里

如果加上了 volatile,那么 flag 就是可见了

如果可以读取静态变量 num,flag 也可以可见了

果然读取完内存,然后读取完成

参考: https://www.jianshu.com/p/15106e9c4bf3 https://www.cnblogs.com/laowen-zjw/p/6735790.html

本文分享自微信公众号 - 毛利学Python(sen13717378202)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏芋道源码1024

一个 Java 对象到底有多大?

编写Java代码的时候,大多数情况下,我们很少关注一个Java对象究竟有多大(占据多少内存),更多的是关注业务与逻辑。但是殊不知,在我们不经意间,大量的内存被无...

10410
来自专栏攻城狮的那点事

Java 8中集合优雅快速的处理方式

相信现在大多数的伙伴们,都在使用Java 8了,而 Java 8相比以前的版本,是作出了革命性的改变。Java8的特性大致可总结为,开发速度更快,代码更少,增加...

14280
来自专栏葫芦

java 序列化Serializable

张工是一名java程序员,工作5年了,一直从事java开发。最近到某互联网公司面试,做了笔试题后,有一道笔试题是这样子的:Serializable有什么作用,张...

11360
来自专栏用代码征服天下

一文读懂Spring MVC执行流程

说到Spring MVC执行流程,网上有很多这方面的文章介绍,但是都不太详细,作为一个初学者去读会有许多不理解的地方,今天这篇文章记录一下我学习Spring M...

13070
来自专栏JAVA杂谈

Java日志Log4j或者Logback的NDC和MDC功能

Java中使用的日志的实现框架有很多种,常用的log4j和logback以及java.util.logging,而log4j是apache实现的一个开源日志组件...

15120
来自专栏Java识堂

帮你少写一大半参数校验代码的小技巧

几乎每个web网站都会对用户提交的参数进行校验,前端要做,后端也要做。防止用户直接通过接口调用的方式来请求或保存数据,从而导致产生脏数据等其他严重的后果。

10420
来自专栏Crossin的编程教室

【Python 第75课】可迭代对象和迭代器

for 循环是我们在 Python 里非常常用的一个语法,但你有没有思考过 for 循环是怎样实现的?

10520
来自专栏大数据成神之路

SparkRDD转DataSet/DataFrame的一个深坑

原需求:希望在map函数中将每一个rdd转为DataSet或者DataFrame。

12920
来自专栏D·技术专栏

Spring组成

百科上的一段对于spring的定义,从中可以提取到Spring是开源、面向接口编程的轻量级框架,解决了业务逻辑和其他层之间的耦合问题。 Spring如何实现解藕...

9910
来自专栏Java3y

lambda与函数式

前面两篇文章介绍了什么是响应式编程?和响应式流的特性,一味讲概念终是枯燥,还是上手敲一敲代码实在感受一下响应式编程的“手感”吧。

8610

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励