多线程之线程安全

写在前面

本文一起看下线程安全相关内容。

1:重要的概念

多线程之线程安全_第1张图片

1.1:竞态条件

多个线程竞争同一资源,如果是对多个线程访问资源的顺序敏感(即导致非预期结果),则该资源就是竞态条件。

1.2:临界区

会导致竞态条件发生的区域叫做临界区。

如果是出现了竞态条件,则需要对临界区的代码通过锁做好并发控制,否则会导致程序的错误。

2:并发相关的性质

并发相关的性质有,原子性,可见性,有序性,分别看下。如果是能够满足这3个特性,我们就可以认为程序时多线程安全的。

2.1:原子性

原子性的意思是,一组不可被终中断的操作,要么执行,要么不执行。如下:

x = 1 
    赋值操作,是原子的
x++ 
    读取变量,然后+1,然后赋值,其中读取变量是原子的,赋值也是原子的,但整体不是原子的
y=x 
    读取x的值,是原子的,然后赋值给y是原子的,但是整体不是原子的
x=x+1
    读取x的值,是原子的,对x值+1,将结果赋值给x是原子的,但是整体不是原子的

对于基础数据类型的读取,和赋值操作是原子,一旦一个操作有这两个操作组成则就不是原子的了,一般实现原子的方式是synchronized关键字,Lock上锁。

2.2:可见性

默认情况下每个线程都是读取自己内存的数据副本,而不会从主内存中读取数据,所以默认的其他线程的修改,本线程是读不到的,想要读到最新的修改,只要不读取线程内存的副本而改为读取主内存就行了,java提供了volatile关键字来实现这个要求。即通过volatile关键字就可以实现可见性。

除了volatile关键字之外,通过synchronized关键字和Lock也可以实现可见性,此时线程只能串行执行,并且在释放锁之前,会将所有的修改都刷新到主内存中,这样当其他线程获取锁时,第一次肯定是从主内存读取数据的,就能实现可见性,但是这种串行化的方式会损失性能。

注意:volatile关键字无法实现原子性,即只能保证每次都从主存中读取数据。

2.3:有序性

happen-before有以下两个层次的含义:

1:如果是A happen-before B,则A肯定在B之前执行
2:如果A happen-before B,则B能够看到A的修改,即数据是可见的,这由JMM机制保证

因此happen-before一组保证了某些条件下保证有序性的规则,也是一种在特定的条件下满足数据可见性的规则。happen-before规则一共有8中分别看下。

2.3.1:程序次序规则

一个线程内,前面的程序在后面的程序执行执行,如下图:

多线程之线程安全_第2张图片

含义:

1:一个线程,前面的操作A happen-before 后面的操作B,即 前面的操作A 肯定在 后面的操作B 之前执行
2:一个线程,前面的操作A happen-before 后面的操作B,即 前面的操作A 产生的修改 肯定在 后面的操作B 中可见

描述的是一个线程内的先后顺序。

2.3.2:锁定规则

对于一个锁的unlock操作先行发生于对同一个锁的的lock操作,即unlock发生在后续的lock之前,注意这里的锁是synchronized,如下:

多线程之线程安全_第3张图片

含义:

1:单个或多个线程内,某线程对Lock的unlock操作A happen-before 后续的某个线程对Lock的lock操作B,这里其实已经强调了先后关系,所以happen-before就有点废话了
2:单个或多个线程内,某线程对Lock的unlock操作A happen-before 后续的某个线程对Lock的lock操作B,则 某线程对Lock的unlock操作A 产生的修改 肯定在 后续的某个线程对Lock的lock操作B 中可见

2.3.3:volatile规则

对一个volatile变量的写操作A 先行发生于 后续对这个volatile变量的读操作B。

含义:

1:单个或多个线程内,对一个volatile变量的写操作A hanppen-before 后续对这个volatile变量的读操作B,这里已经指明后续了,所以happen-before就有点废话了
2:单个或多个线程内,对一个volatile变量的写操作A hanppen-before 后续对这个volatile变量的读操作B,则 对一个volatile变量的写操作A 

2.3.4:传递规则

A 先行于 B ,B 先行于 C,则 先行于 C。该规则的最大作用是可以实现灵活的数据可见性,如下代码:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
    }
  }
}

假定如下的线程执行顺序:

多线程之线程安全_第4张图片

首先根据程序次序规则,线程A的x = 42 happen-before 线程A的v = true;,其次根据volatile规则,线程A的写volatile变量v=true happen before 线程B的读volatile变量v,因此线程A的x = 42 happen before 线程B的读volatile变量v,因此线程B能够读取到线程A修改的变量值,即线程B能够读取到v=true,x=42,所以线程B执行最终会输出42

2.3.5:线程启动规则

Thread对象的start方法先行于Thread的run方法的内容。

含义:

1:Thread对象的start方法 happen-before Thread的run方法的内容,即Thread run方法在Thread start方法之后执行
1:Thread对象的start方法 happen-before Thread的run方法的内容,即Thread run方法可以看到Thread start方法线程产生的修改

2.3.6:线程中断规则

对线程interrupt方法的调用,先行于线程本身对interrupt异常的捕获逻辑。

多线程之线程安全_第5张图片

含义:

1:对线程的interrupt方法调用 happen before 线程的InterrupttedException的捕获逻辑,即线程的interrupt方法调用 在 线程的InterrupttedException的捕获逻辑 之前执行
1:对线程的interrupt方法调用 happen before 线程的InterrupttedException的捕获逻辑,即 线程的InterrupttedException的捕获逻辑 可以看到 线程的interrupt方法调用 的线程产生的修改

2.3.7:线程终结检测规则

线程中的所有操作 先行发生于 线程状态的检测,如下图:

多线程之线程安全_第6张图片

考虑这样的场景,线程B的执行需要依赖于线程A的操作产生的修改,则可以在线程B中执行A.join等待线程B执行完毕,然后在执行A.isAlive检测线程A的状态,使之发生happen-before,则能保证在线程B中看到线程A的修改,如下:

多线程之线程安全_第7张图片

含义:

1:线程A中的所有操作 happen-before 对于线程A的状态检测,即对线程A的所有操作,发生在对于线程A的状态检测之前
2:线程A中的所有操作 happen-before 对于线程A的状态检测,即对线程A的状态检测后的操作,能够看到线程A所有操作已经产生的修改

2.3.8:对象终结规则

一个对象的初始化先行发生于一个对象finalize()方法的调用,如下:

多线程之线程安全_第8张图片

含义:

1:对象A的初始化 happen-before 对象A的finalize方法调用,即对象A的finalize方法调用发生在对象A的初始化操作之前
2:对象A的初始化 happen-before 对象A的finalize方法调用,即对象A的finalize方法中可以看到对象A的初始化产生的修改

3:一个例子

如下两个线程修改同一个变量产生数据错误的例子,代码:

public class Counter {
    
    private int sum = 0;

    public void incr() {
        sum = sum + 1;
    }
    public int getSum() {
        return sum;
    }
    
    public static void main(String[] args) throws InterruptedException {
        int loop = 10_0000;
        
        // test single thread
        Counter counter = new Counter();
        for (int i = 0; i < loop; i++) {
            counter.incr();
        }

        System.out.println("single thread: " + counter.getSum());
    
        // test multiple threads
        final Counter counter2 = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < loop / 2; i++) {
                counter2.incr();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < loop / 2; i++) {
                counter2.incr();
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("multiple threads: " + counter2.getSum());
    }
}

正常情况下,单线程+1和多线程+1的最终的都应该是100000,但事实不是,因为多线程因为存在可见性的问题,会小于该值,运行如下:

single thread: 100000
multiple threads: 54491

当然这个小于100000的值具体是多少,是不确定,但肯定小于100000,原因是存在从线程本地读取副本,而无法读到最新修改的情况。根本原因是存在竞态条件private int sum = 0,因此我们只需要让竞态条件所形成的临界区加锁,就行了,串行访问,这样,每次sum+1后都会将最新的修改刷到主存,每次读取也都主存中读取最新的值,修改incr方法如下:

public synchronized void incr() {
    sum = sum + 1;
}

再次运行:

single thread: 100000
multiple threads: 100000

在方法上加synchronized关键字其实就是在this对象上的修改标记字对应的锁状态字节对应的值,不同的加锁方式和对应的加锁方式参考下图:

多线程之线程安全_第9张图片

4:其他相关知识点

4.1:volatile

特点如下:

1:每次读都强制从主存中读
2:适用于单线程写,多线程读的场景
3:能不用就不用,不确定也不用
4:替代方案Atomic原子类(实现最终的一致性)
6:内存屏障,组织指令重排序

对于6,可参考如下代码:

int a = 0;
int b = 9;
volatile boolean isRight = false;

a = 999; // 语句1
b = 888; // 语句2
isRight = true;  // 语句3
b = a - 1; // 语句4
a = a + b; // 语句5

这里语句3有以下几个语义:

1:语句4,语句5不会排到语句1,语句2的前面
2:语句1,语句2的修改对语句3,语句4,语句5是可见的

4.2:final

多线程之线程安全_第10张图片

final本身能够提供最大程度的数据安全,因此,在程序中最大限度的使用final是个好习惯。

写在后面

参考文章列表

Java 对象结构 。

happen-before原则 。

happens-before是什么?JMM最最核心的概念,看完你就懂了 。

你可能感兴趣的:(Java高级开发进阶教程,java,jvm,开发语言)