文档章节

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叔

粉丝 147
博文 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 使用 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
【转】Java线程面试题Top50

目录(?)[-] 50道Java线程面试题 1 什么是线程 2 线程和进程有什么区别 3 如何在Java中实现线程 4 用Runnable还是Thread 6 Thread 类中的start 和 run 方法有什么区别 7 Java中Runnable和Cal...

gehui
2015/08/14
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

JS三元运算示例

1. topFlag=topFlag ==0?1:0; 等于 if(topFlag=00){ topFlag=1; }else if(topFlag == 1){ topFlag=0; } 2. 5>3?alert('5大'):alert('3大'); 即 if(5>3){alert('5大')}else{alert('3大')}; 注......

森火
今天
0
0
利用Slf4j的MDC跟踪方法调用链

why? 一个web项目通常提供很多URL访问地址, 项目一般都是分层处理,例如Controller——>Service——>DAO。 如果想根据日志查看用户一次请求都走了哪些方法(多数是查错误)。 如果系统是多人...

杨春炼
今天
7
0
Maven介绍及安装

Maven介绍及安装 以下内容是本人早期学习时的笔记,可能比较详实繁琐,现在复习一下Maven,顺便将内容抛出来,供大家一起学习进步。 一、Maven简介 Maven是Apache旗下的一款项目管理工具,是...

星汉
今天
0
0
小程序Aes解密

主要步骤: 1、下载AES源码(JS版) 2、在小程序中新建一个公共的文件夹,把AES源码拷贝进去(注意:需要暴露接口 module.exports = CryptoJS;) 3、添加一个用于加密解密的公共JS,可取名为...

Mr_Tea伯奕
今天
0
0
Go实现文件传输(基本传输可用)

发送端 package mainimport ("fmt""os""net""io")func SendFile(path string, connect net.Conn){file, oerr :=os.Open(path)if oerr !=nil{fmt.Println("Open", oerr)......

CHONGCHEN
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部