文档章节

Java并发编程之线程安全

12叔
 12叔
发布于 2017/08/26 13:42
字数 1807
阅读 52
收藏 2

引子

上文讲到我们使用线程来提高系统的资源利用率和吞吐量,同时也会带来线程安全的问题

今天我们就来探讨一下引发线程安全的原因

首先我们定义一下什么是线程安全 简单来说线程安全就是正确性,指的是程序在并发情况下执行的结果和预期一致.

安全性问题是首要解决的问题,而引起线程安全的问题本质是线程之间的通信方式的问题.

操作系统里面定义了几种通信的方式

  1. 管道 pipeline
  2. 信号 signal
  3. 消息队列 messsage queue
  4. 共享内存 shared memory
  5. 信号量 semaphore
  6. Socket

我们主要讨论共享内存的方式

共享内存

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。

线程之间的共享变量存储在主内存中,每个线程都有自己的本地内存,存储了该线程的共享变量副本,所以线程A和线程B之前需要通信的话,必须经过以下两个步骤

  • 线程A把本地内存中更新过的共享变量刷新到主内存中
  • 线程B到主内存中读取线程A之前更新过的共享变量

输入图片说明

由于共享内存的这种通信方式,在多线程并发的情况下从而会引发可见性和有序性问题, 从而导致线程安全的问题.

我们线程安全问题主要关注三个核心的概念 可见性,有序性和原子性

可见性

可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。

可见性的关键还是在对变量的写操作之后能够在理解写回到主内存, 这样其他线程就能从主内存中看到最新的写的值,

但是现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。 此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据.

下面这个例子说明了这个问题

public class Visibility {

    private static boolean status;
    private static int i;

    public static void exec() {

        while (!status) {
            Thread.yield();
        }

        System.out.println(i);

    }

    public static void main(String[] args) {

        new Thread(() -> exec()).start();

        status = true;
        i = 99;
    }
}

这个例子可能会打印i=0的情况 就是因为线程无法保证对共享内存的可见性

我们可以用 volatile,synchronized, 显式锁,原子变量这些同步手段都可以保证可见性

但是注意 仅仅满足可见性并不一定能保证线程安全,因为还可能会存在有序性问题

有序性

有序性保证共享变量在并发的情况下,实际执行的结果和单线程的执行结果是一样的.

有序性的语意注意有2种,

  1. 最常见的就是保证多线程执行的串行顺序

下面这个例子说明了这个问题

当多个线程访问共享变量的情况下,就会发生有序性问题

public class Order {

    private volatile int c = 1  //我们用volatile保证了共享变量的可见性

    public void incr() {
        c++;
    }

    public int value() {
        return c;
    }


}


我们分析一下程序执行的步骤

A线程执行

  1. 读取c 的量1
  2. c+1

A线程挂起 B线程执行

  1. 读取c 的量1
  2. c+1
  3. 写入c=2

A线程执行

  1. 写入c=2

输入图片说明

这样看到incr()方法执行了2次但是结果还是c=2

为什么会出现这种情况,主要原因还是在于线程之间执行顺序的随机性,导致内存的不一致

我们把这种如果对资源的访问顺序敏感的现象,称做竞态条件。 导致竞态条件发生的代码区称作临界区。 上例中incr()方法就是一个临界区,它会产生竞态条件。

  1. 防止重排序引起的问题
boolean started = false; // 语句1
long counter = 0L; // 语句2
counter = 1; // 语句3
started = true; // 语句4

从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行

处理器或编译器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码,这种方式叫指令重排.

volatile, final, synchronized,显式锁都可以保证有序性.

除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式地保证有序性。两个操作的执行顺序只要可以通过happens-before推导出来,则JVM会保证其顺序性,反之JVM对其顺序性不作任何保证,可对其进行任意必要的重新排序以获取高效率

只有同时满足了有序性和可见性的才能保证线程安全.

原子性

先来了解一下原子性的概念 原子性是指某些操作在语意上是原子的,即一个操作(可能包含有多个子操作)要么全部执行,要么全部都不执行 对基本类型的读/写操作,CAS操作等在机器指令级别都是原子的.

那么如果能保证我们保证一个线程执行的所有方法都具有原子性,那么不存在有序性问题了.

加锁可以保证复合语句的原子性,synchronized可以保证多条语句在synchronized块中语意上是原子的,显式锁保证临界区的原子性。

public synchronized void testLock () {
    int j = i;
    i = j + 1;
}

原子变量(如AtomInteger)使用CAS封装了对变量的原子操作。

AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
  new Thread(() -> {
    for(int a = 0; a < iteration; a++) {
      atomicInteger.incrementAndGet();
    }
  }).start();
}

总结

本文我们主要论述了引发线程安全的原因,根据共享内存的引出线程安全的三个核心概念可见性和有序性和原子性的问题 想要解决对共享变量访问的引起的线程安全问题的方法是使用同步,接下来我们将探讨同步原理和使用方式,和如何设计线程安全的并发代码

© 著作权归作者所有

共有 人打赏支持
12叔

12叔

粉丝 148
博文 26
码字总数 54281
作品 3
杭州
程序员
读书笔记之《Java并发编程的艺术》-并发编程基础

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
0
8
读书笔记之《Java并发编程的艺术》-并发编程容器和框架(重要)

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
0
1
(三)Java并发学习笔记--线程封闭

线程封闭 实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并发最简单的方法就是线程封闭。什么是线程封闭呢? 就是把对象封装到一个线程里,只有这一个线程能看到此对象。...

潘天涯
08/14
0
0
Java 使用 happen-before 规则实现共享变量的同步操作

前言 熟悉 Java 并发编程的都知道,JMM(Java 内存模型) 中的 happen-before(简称 hb)规则,该规则定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。按照官方的...

stateIs0
01/20
0
0
跳槽时,这些Java面试题99%会被问到

我在 Oracle 已经工作了近 7 年,面试过从初级到非常资深的Java工程师,且由于 Java 组工作任务的特点,我非常注重面试者的计算机科学基础和编程语言的理解深度,可以不要求面试者非要精通 ...

Java小铺
08/15
0
0

没有更多内容

加载失败,请刷新页面

加载更多

deepin系统使用deepin-wine安装exe程序

deepin自带原生deepin-wine使用命令如下: deepin-wine QQMusicSetup.exe deepin-wine的程序位置: /root/.wine 默认安装的QQ浏览器快捷方式位置: /root/.wine/drive_c/'Program Files'/Te...

临江仙卜算子
36分钟前
1
0
快速get到学习Linux操作系统的点

快速get到学习Linux操作系统的点 Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。Linux能够运行主要的UNIX工具软件...

linuxCool
42分钟前
2
0
聊聊:Linux分区的那些方案

安装linux的整体步骤其实比较简单,唯一可能值得说明的地方,大概就是linux的分区了。 下面来给大家推荐一些分区方案。 1 分两个区 实际上,很多时候我们只需要分两个区:`/`和交换分区,日常...

Linux就该这么学
53分钟前
1
0
适配器模式和外观模式

适配器模式: 将一个类的接口,转换成客户期望的另一个接口。适配器让原本不兼容的类可以合作无间。 例子: //将Enumeration转换成Iteratorpublic class EnumerationIterator implements Iter...

王怀楼
55分钟前
2
0
7-CXF与Spring整合发布webservice

Spring+CXF整合来管理webservice 实现步骤: 1. 添加cxf.jar 包(集成了Spring.jar、servlet.jar ),spring.jar包 ,servlet.jar 包 2. 编写业务类,通过CXF来发布webservice 员工管理: 方法...

江戸川
57分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部