文档章节

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叔

粉丝 149
博文 27
码字总数 55075
作品 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
基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

许多以Java多线程开发为主题的技术书籍,都会把对Java虚拟机和Java内存模型的讲解,作为讲授Java并发编程开发的主要内容,有的还深入到计算机系统的内存、CPU、缓存等予以说明。实际上,在实...

leoliu168
2018/11/08
0
0
基于JVM原理JMM模型和CPU缓存模型深入理解Java并发编程

许多以Java多线程开发为主题的技术书籍,都会把对Java虚拟机和Java内存模型的讲解,作为讲授Java并发编程开发的主要内容,有的还深入到计算机系统的内存、CPU、缓存等予以说明。实际上,在实...

Java高级技术
2018/11/21
0
0
Java线程面试题 Top 50

不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎。大多数待遇丰厚的Java开发职位都要求开发者...

loda0128
2015/05/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Java生成二维码图片

maven配置jar包 <dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.3.3</version></dependency><dependency><groupId>com.google.z......

骑羊放狼灬
2分钟前
0
0
oracle 修改字段类型

1.varchar2 类型修改 例子:alter table T_Node modify (NODE_CONTEXT varchar2(4000)); 2.varchar2 修改为clob 例子: alter table T_Node add hehe clob; update T_Node set hehe=NODE_CO......

qimh
5分钟前
0
0
别再写 bug 了,避免空指针的 5 个案例!

空指针是我们 Java 开发人员经常遇到的一个基本异常,这是一个极其普遍但似乎又无法根治的问题。 本文,栈长将带你了解什么是空指针,还有如何有效的避免空指针。 什么是空指针? 当一个变量...

Java技术栈
9分钟前
0
0
FastJson对BigDecimal保留两位小数(valueFilter)

实现ValueFilter public class BigDecimalValueFilter implements ValueFilter { @Override public Object process(Object o, String name, Object value) {//o是待转换的对象,n......

石日天
11分钟前
0
0
android 颜色透明度参照比

##透明度参照表: 00%=FF(不透明) 5%=F2 10%=E5 15%=D8 20%=CC 25%=BF 30%=B2 35%=A5 40%=99 45%=8c 50%=7F 55%=72 60%=66 65%=59 70%=4c 75%=3F 80%=33 85%=21 90%=19 95%=0c 100%=00(全透......

东街小霸王
12分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部