• 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() 操作成功返回。

    参考:

  • 相关阅读:
    AT&T不能访问公司网络
    尝鲜:windows 7 来了
    .net控件编程 资料篇
    Annual part 2009
    从Visual studio 2005移出Visual Assist
    不能在IIS 5.1增加应用程序扩展的BUG
    The problem of the user is not associated with a trusted sql server connection 混合登录选项设置的问题
    让我们难忘的那些歌曲
    分享利用VPC防止病毒软件的进入你的windows电脑
    杂读 May 12,2008
  • 原文地址:https://www.cnblogs.com/cloudflow/p/13894318.html
Copyright © 2020-2023  润新知