专栏首页yukong的小专栏【java并发编程实战1】何为线程安全性线程安全性

【java并发编程实战1】何为线程安全性线程安全性

多线程问题,一直是我们老生常谈的一个问题,在面试中也会被经常问到,如何去学习理解多线程,何为线程安全性,那么大家跟我的脚步一起来学习一下。

线程安全性

定义:

当多个线程访问某个类时,不管运行时环境采用何种调度方式 或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现正确的行为,那么称这个类时线程安全的。

线程的安全性主要体现在三个方法

  • 原子性:即不可分割,提供互斥访问,同一时刻只能有一个线程对它进行操作
  • 可见性:一个线程对共享变量的修改,可以及时被其他线程观察到
  • 有序性:序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。

1、原子性

1、访问(读/写)某个共享变量的操作从其执行线程以外的线程来看,该操作要么已经执行结果,有么尚未执行,也就是说其他线程不会看到“该操作执行了部分的效果”。

2、访问同一组共享变量的原子操作 不能够被交错的。

在java中实现原子性的两种方式:

  • 使用CAS也是atomic包下的类。
  • 使用锁

在java语言中,除long/double之外的任何类型的变量的写操作都是原子操作。 java语言中任何变量的读操作都是原子操作。 需要注意的是 原子操作 + 原子操作 != 原子操作 例如 i++ 先读后写 读跟写都是原子操作,但是 i++并不是原子操作

下面用代码讲一下实现的两种方式

例子

/**
 * @author yukong
 * @date 2018/8/29
 * @description 线程不安全
 */
public class CountExample {

    /**
     * 并发线程数目
     */
    private static int threadNum = 1000;

    /**
     * 闭锁
     */
    private static CountDownLatch countDownLatch  = new CountDownLatch(threadNum);

    private static Integer i = 0;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int j = 0; j < threadNum; j++) {
            executorService.execute(() -> {
                add();
            });
        }
        // 使用闭锁保证当所有统计线程完成后,主线程输出统计结果。 其实这里也可以使用Thread.sleep() 让主线程阻塞等待一会儿实现
        countDownLatch.await();
        System.out.println(i);
    }

    private static void add() {
        countDownLatch.countDown();
        i++;
    }

}

上面这段代码很明显因为i++不是原子性操作,所以不是线程安全的。

那么根据上面讲的,我们可以使用锁,或者atomic包下的类实现。

2、可见性

一个线程对共享变量的修改能够及时被其他线程所观察。

这句话怎么理解呢?

在JMM(Java Memory Model)的定义中,所有的变量都需要存储在主体内存中,主内存是共享内存区域,所有的线程都能访问的,但是线程对变量的操作(读、写)必须在工作内存中完成。

1、首先将变量从主内存中拷贝到自己的工作内存。

2、对变量进行读写操作。

3、操作完成,将变量回写到主内存中。

从上面可以得知,线程不能直接操作主内存的变量,必须要在工作内存中操作。

简单了解一下JMM的规定,那么我们就可以很容易的理解可见性了。

1535527111889.png

由上图可知 ,在多线程情况下,线程对共享变量的的操作都是拷贝一份副本到自己的工作内存中操作的,然后才写回到主内存中,这就可能存在一个问题,线程1修改了共享变量X的值,但是还未写回主内存,另外一个线程2又对主内存中的同一个共享变量x进行操作,但此时线程1工作内存中的变量x对线程n并不可以,这种工作内存与主内存同步延迟的问题就造成了可见性问题,另外指令重排序也会导致可见性问题。

那么对于可见性问题,使用什么解决方法呢?

  • synchronized关键字
  • volatile关键词

为什么synchronized能保证可见性呢?根据JMM关于synchronized的规定

  • 线程解锁前,必须把共享变量的最新刷新到主内存。
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要重新从主内存中读取最新值。

那么volatile又是怎么实现可见性的呢?

其实volatile是通过加入内存屏障和禁止指令重排序优化来实现的。

  • 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新到主内存中
  • 对于volatile变量读操作时,会在读操作前加入一条load屏障指定,从主内存读取共享变量最新的值到工作内存中。

那大家可能就会想问了,我把上面的代码的i变量用volatile修饰一下,是不是就保证线程安全,输出的结果就是1000呢,答案是否定的,volatile保证的是可见性,并不能保证原子性。但是利用volatile可见性这个特点,我们可以利用它完全一些线程中的通信

volatile boolean flag = false; 
// thread a
{
    flag = true;
    // do somethings
}

// thread b
{
    while (flag) {
        // do somethings
    }
}

这样就完全一个线程中通信的案例。

3、有序性

在JMM(java 内存 模型)中,运行编译器和处理器对指令就行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响多线程并发执行的正确性。

在java中,可以通过volatile关键字来保证一定的有序性。另外也可以通过synchronizedLock来保证有序性。很显然,synchronized跟lock保证每个时刻是只有一个线程执行同步代码,相当于让线程属性执行同步代码,自然保证了有序性。

另外java内存模型也具备一些先天的有序性,即不需要通过任何手段就能够保证的有序性,这个通常也称为Happen-Before原则。如果两个操作的资源无法从Happen-Before原则推导出来,那么他们就不能保证它的有序性,虚拟机就可以随机对他们进行重排序。

那么下面就详细介绍Happen-Before(先行发生原则):

  1. 线程次序规则: 在一个线程内,按照代码顺序,书写在前的代码先行发生于书写在后的代码操作。
  2. 锁定原则:一个unlock操作先行发生于后面的对同一个锁的lock操作。
  3. volatile变量原则,对同一个变量的写操作先行发生于后面对这个变量的读操作。
  4. 传递原则:如果操作A先行发生于操作B,而且操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
  5. 线程启动原则:Thread对象的start()方法先行发生于此线程的每一个操作。
  6. 线程中断原则:Thread对象的interrupt()方法先行发生于被中断线程检测到中断事件的发生
  7. 线程终结原则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测线程是否已经终止。
  8. 线程终结原则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

4、总结

如果一个操作具有以上的三种特性,那么我们称它为线程安全的。

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 并发编程(二):线程安全性

    线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题。这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的...

    沉默王二
  • 并发实战 之「 线程安全性」

    在早期的计算机中不包含操作系统,它们从头到尾只能执行一个程序,并且这个程序能访问计算机中的所有资源。在这种环境中,不仅程序难以编写和运行,而且对于昂贵且稀有的计...

    CG国斌
  • 【Java并发编程】线程安全与性能

    类的线程安全表现为: 操作的原子性,类似数据库事务。 内存的可见性,当前线程修改后其他线程立马可看到。 不做正确的同步,在多个线...

    Java深度编程
  • Java并发编程(2)- 线程安全性详解

    说到原子性,就不得不提及JDK里的atomic包,该包中提供了很多Atomic的类,本小节主要介绍该包中常用的几个类。这些类都是通过CAS来实现原子性的,ato...

    端碗吹水
  • Java 并发编程(四):如何保证对象的线程安全性

    先让我吐一句肺腑之言吧,不说出来会憋出内伤的。《Java 并发编程实战》这本书太特么枯燥了,尽管它被奉为并发编程当中的经典之作,但我还是忍不住。因为第四章“对象...

    沉默王二
  • 并发编程-06线程安全性之可见性 (synchronized + volatile)

    并发编程-06线程安全性之可见性 (synchronized + volatile)

    小小工匠
  • 慕课网高并发实战(四)- 线程安全性

    当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要额外的同步或协同,这个类都能表现出正确行为,那么就称这个...

    Meet相识
  • Java并发编程与高并发之线程安全性(原子性、可见性、有序性)

    1、并发的基本概念:同时拥有两个或者多个线程,如果程序在单核处理器上运行,多个线程将交替地换入或者换出内存,这些线程是同时存在的,每个线程都处于执行过程中的某个...

    别先生
  • 并发编程之多线程线程安全

    当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。

    蒋老湿
  • Java并发编程(4)- 线程安全策略

    有一种对象发布了就是安全的,这就是不可变对象,本小节简单介绍一下不可变对象。不可变对象可以在多线程在保证线程安全,不可变对象需要满足的条件:

    端碗吹水
  • 并发编程-05线程安全性之原子性【锁之synchronized】

    并发编程-06线程安全性之可见性 (synchronized + volatile)

    小小工匠
  • 线程的安全性 - 并发基础篇

    官人们好啊,我是汤圆,今天给大家带来的是《线程的安全性 - 并发基础篇》,希望有所帮助,谢谢

    汤圆学Java
  • 并发编程-什么是线程安全?

    定义“线程安全”这个概念是一个非常复杂的事情。越是正式而严肃的描述它越是复杂难懂,不仅没办法提供一些实际的指导,而且还没法有一个直观的理解。还有一些不太正式的描...

    ImportSource
  • Java并发编程与高并发之线程安全策略

    1、安全的发布对象,有一种对象只要发布了,就是安全的,就是不可变对象。一个类的对象是不可变的对象,不可变对象必须满足三个条件。

    别先生
  • Java并发编程的艺术(十二)——线程安全

    1. 什么是『线程安全』? 如果一个对象构造完成后,调用者无需额外的操作,就可以在多线程环境下随意地使用,并且不发生错误,那么这个对象就是线程安全的。 2. ...

    大闲人柴毛毛
  • 并发编程-12线程安全策略之常见的线程不安全类

    所谓线程不安全的类,是指一个类的实例对象可以同时被多个线程访问,如果不做同步或线程安全的处理,就会表现出线程不安全的行为,比如逻辑处理错误、抛出异常等。

    小小工匠
  • Java多线程编程-(1)-线程安全和锁Synchronized概念

    (1)在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单位都是进程。

    Java后端技术
  • 并发编程-11线程安全策略之线程封闭

    在上篇博文并发编程-10线程安全策略之不可变对象 ,我们通过介绍使用线程安全的不可变对象可以保证线程安全。

    小小工匠
  • 秒懂Java并发和线程安全

    在平时写代码的时候我们经常会说“这会不会有线程安全问题,是不是得加把锁呢?”,细细的品一下这句话,是包涵很多知识点在里面。线程?,线程安全?,什么时候才会出现线...

    居士

扫码关注云+社区

领取腾讯云代金券