zoukankan      html  css  js  c++  java
  • Java并发编程02-线程安全性

    一、线程安全

    1. 线程安全

    可以简单的理解为:一个方法或者一个实例可以在多线程环境中使用而不会出现问题。

    2. 线程不安全的原因

    多个线程使用了相同的资源,如同一内存区(变量、数组或对象)、系统(数据库、web服务等)或文件等。更准确的说,是多个线程对同一资源进行了写操作。多个线程只读取相同的资源,是没有线程安全问题的。

    3. 如何保证线程安全

    保证共享内存的原子性、可见性和有序性。

    二、原子性

    对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行。

    1. Java 如何实现原子操作

    在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。

    使用锁很好理解,下面重点说一下循环 CAS 实现的思路。

    (1)Atomic包(使用循环 CAS 实现原子操作)

    Jdk1.5 开始提供了以 Atomic 开头的类,例如 AtomicBoolean(用原子方式更新的 boolean 值)、AtomicInteger(用原子的方式更新的 int 值)等。

    使用 AtomicInteger 实现的线程安全的计数器程序示例:

    public class N18_CAS_AtomicInteger {
        private AtomicInteger atomicI = new AtomicInteger(0);
        private int i = 0;
    
        private void safeCount() {
            atomicI.incrementAndGet();
        }
    
        private void count() {
            i++;
        }
    
        public static void main(String[] args) throws InterruptedException {
            N18_CAS_AtomicInteger counter = new N18_CAS_AtomicInteger();
            ArrayList<Thread> ts = new ArrayList<>(600);
            for (int i = 0; i < 100; ++i) {
                Thread t = new Thread(() -> {
                    for (int j = 0; j < 10000; ++j) {
                        counter.count();
                        counter.safeCount();
                    }
                });
                t.start();
                ts.add(t);
            }
    
            // 等待所有线程执行完成
            for (Thread t: ts)
                t.join();
    
            System.out.println(counter.i);
            System.out.println(counter.atomicI);
        }
    }
    

    运行结果:

    992034
    1000000
    

    AtomicInteger 中的 incrementAndGet 方法就是乐观锁的一个实现,使用自旋 CAS(循环检测更新)的方式来更新内存中的值并通过底层CPU执行来保证是更新操作是原子操作。

    getAndIncrement() 方法的内部:

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    

    getAndAddInt() 方法:

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
        return var5;
    }
    

    此时可以看到 compareAndSwapInt 方法,就是 CAS 缩写的由来。

    其中 var5 是更新后要返回的值。var1 由前面的 this 参数可以看出是 AtomicInteger 实例,var2 是偏移量(AtomicInteger 内部通过改变偏移量记录值)。

    compareAndSwapInt(var1, var2, var5, var5 + var4)其实换成compareAndSwapInt(obj, offset, expect, update)比较清楚,意思就是如果 obj 内的 value 和 expect 相等,就证明没有其他线程改变过这个变量,那么就更新它为 update,如果这一步的 CAS 没有成功,那就采用自旋的方式继续进行 CAS 操作。取出乍一看这也是两个步骤了啊,其实在 JNI 里是借助于一个 CPU 指令完成的。所以还是原子操作。

    (2)CAS 实现原子操作的问题

    • 1)ABA 问题

      • 因为 CAS 需要在操作值的时候,检查值有没有发生变化,如果没有变化则更新值。但是如果一个值原来是 A,变成了 B,有变成了 A,那么使用 CAS 进行检查的时候会发现它的值没有发生变化,但实际上发生了变化。
      • 解决思路就是使用版本号。Atomic 包中提供了 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
    • 2)循环时间开销大

      如果 CAS 不成功,则会原地自旋,如果长时间自旋会给 CPU 带来非常大的执行开销。

    • 3)只能保证一个共享变量的原子操作

    (3)synchronize、 lock、 Atomic 原子性对比

    • synchronize:不可中断锁,适合竞争不激烈,可读性好

    • lock:可中断锁,多样化同步,竞争激烈时能维持常态

    • Atomic:竞争激烈时能维持常态,比 lock 性能好;只能同步一个值

    三、可见性

    多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。

    1. 共享变量在线程间不可见的原因

    共享变量更新后的值没有在工作内存与主内存间及时更新

    2. synchronized

    JMM 的规范中提供了 synchronized 具备的可见性:

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

    3. volatile

    使用 volatile关键字,保证变量可见性(直接从主内存读,而不是从线程cache读)

    注:volatile 变量具有 synchronized 的可见性特性,但是不具备原子性

    四、有序性

    程序的执行顺序按照代码顺序执行,在单线程环境下,程序的执行都是有序的,但是在多线程环境下,JMM 为了性能优化,编译器和处理器会对指令进行重排,程序的执行会变成无序。

    1. volatile/synchronized/lock 可保证有序性

    2. happens-before

    JMM 通过 happens-before 关系向程序员提供跨线程的内存可见性保证。(如果 A 线程的写操作 a 与 B 线程的读操作 b 之间存在 happens-before 关系,尽管 a 操作和 b 操作在不同的线程中执行,但 JMM 向程序员保证 a 操作将对 b 操作可见)

    happens-before 规则:

    • 1)程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
    • 2)监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
    • 3)volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
    • 4)传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
    • 5)start() 规则:如果线程A执行操作 ThreadB.start() (启动线程B),那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。
    • 6)join() 规则:如果线程 A 执行操作 ThreadB.join() 并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回。

    参考:

  • 相关阅读:
    java+opencv实现图像灰度化
    java实现高斯平滑
    hdu 3415 单调队列
    POJ 3368 Frequent values 线段树区间合并
    UVA 11795 Mega Man's Mission 状态DP
    UVA 11552 Fewest Flops DP
    UVA 10534 Wavio Sequence DP LIS
    UVA 1424 uvalive 4256 Salesmen 简单DP
    UVA 1099 uvalive 4794 Sharing Chocolate 状态DP
    UVA 1169uvalive 3983 Robotruck 单调队列优化DP
  • 原文地址:https://www.cnblogs.com/cloudflow/p/13894318.html
Copyright © 2011-2022 走看看