天天看点

并发编程的底层原理-可见性

1.简介

一个线程对主内存的修改能够及时地被其他线程观察到,这种特性被称之为可见性。

2.演示可见性带来的问题

/**
 * 描述:演示可见性带来的问题。
 */
public class FieldVisibility {
    int a = 1;

    int b = 2;

    private void change() {
        a = 3;
        b = a;
    }


    private void print() {
        System.out.println("a=" + a + ";b=" + b + ";");
    }

    public static void main(String[] args) {
        while (true) {
            FieldVisibility fieldVisibility = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    fieldVisibility.change();
                }
            },"thread0").start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    fieldVisibility.print();
                }
            },"thread1").start();
        }
    }
}      

代码运行结果如下所示。

a=3;b=3;
a=1;b=2;
a=3;b=2;
a=1;b=3;      

3.可见性分析

上述结果中,第1个和第2个结果按照正常思路便可以分析出来,但是“a=3;b=2;”和“a=1;b=3;”却不容易得出,这是因为发生了可见性问题。可见性会有四种情况,执行逻辑和结果分别如下所示。

当thread0执行完change()方法后,在打印前,修改的“a=3;b=a(3);”都没有刷进主存,此时会输出“a=1;b=2”的结果。

当thread0执行完change()方法后,在打印前,修改的“a=3;b=a(3);”都刷进了主存,此时会输出“a=3;b=3”的结果。

当thread0执行完change()方法后,在打印a时,修改的“a=3”还没有刷进主存,而在打印b时,修改的“a=3”已经刷进主存了,此时便会输出“a=1;b=3”的结果。

当thread0执行完change()方法后,在打印a时,修改的“a=3”刷进主存,而在打印b时,修改的“b=a(3)”还没有刷进主存了,此时便会输出“a=3;b=2”的结果。

4.可见性问题存在的原因

并发编程的底层原理-可见性

1.所有的共享变量存在于主内存中(Main Memory),每个线程有自己的本地内存(L1-L3)。

2.线程读写共享数据是通过本地内存交换的,线程不能直接读写主内存中的变量,而是只能操作自己本地内存中的变量,然后同步到主内存中。

3.主内存是多个线程共享的,但是线程间不共享本地内存,如果线程间需要通信,必须借助主内存中转来完成,因此读写操作不是实时的,这才导致了可见性问题。

1.高速缓存的速度仅次于寄存器,但是容量比主内存小,所以在CPU和主内存之间就多了三层缓存。

2.线程间对于共享变量的可见性问题不是由多核引起的,而是由多层缓存引起的,如果所有CPU都只用一个缓存,那么就不会存在内存可见性问题。

3.每个核心都会将自己需要的数据读取到独占缓存中,数据修改后需要写入到缓存中,然后等待刷入到主存中,所以会导致有些CPU读取的值是一个过期的值。

5.Happens-before

(1).简介

Happens-before规则是用来解决可见性问题的,在时间上,动作A发生在动作B之前,B保证能看见A,或者说,两个操作可以用Happens-before来确定他们的执行顺序,如果一个操作Happens-before于另一个操作,那第一个操作对于第二个操作就是可见的,这就是Happens-before。

(2).Happens-before规则

  • synchronized和Lock锁操作(线程A在解锁之前的所有操作,对于线程B在加锁之后都是可见的)
  • volatile变量(使用volatitle关键字修饰的变量,只要有一个线程对变量进行修改,那么这个变量就一定对其它线程是可见的)
  • 单线程规则(语句n,一定可以看见语句n之前的所有操作)
  • 线程启动(主线程启动一个子线程,那么子线程一定能看到主线程启动子线程之前的操作)
  • 线程join(一个线程一旦使用join方法,那么这个线程的所有操作,对其它线程一定是可见的)
  • 传递性(如果一个程序有ABC3行代码,A运行完之后对B是可见的,B运行完之后对C是可见的,那么此时,A对C也一定是可见的,这就是传递性原则)
  • 中断(一个线程被其它线程中断interrupt,那么检测中断isInterrupted或者抛出InterruptedException一定能看到这个线程被中断了)
  • 工具类的Happens-before原则
  • 线程安全的容器get一定能看到在此之前的put操作
  • CountDownLatch
  • Semaphore
  • Future
  • CyclicBarrier
  • 线程池

6.volatile

(1).简介

  • volatile是一种同步机制,相较于synchronized或Lock类是更轻量级的。如果一个变量被volatile修饰,那么JVM就知道这个变量可能会被并发修改。
  • volatile是轻量级的,使用它并不会发生线程上下文切换等开销很大的行为,主要是把值刷到主内存中。因为开销小,与之对应的是能力便小,如volatile做不到synchronized那样的原子保护,volatile仅仅在有限的场景下才能发挥作用。

(2).不适用场景1

/**
 * 描述:不适用场景1
 */
public class NoVolatile implements Runnable {
    volatile int a;

    AtomicInteger realIncrement = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            a++;
            //统计真正计算的次数
            realIncrement.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable r = new NoVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("a的值:"+((NoVolatile) r).a);
        System.out.println("累加的次数:"+((NoVolatile) r).realIncrement.get());
    }
}      

代码运行结果如下所示,其中累计的次数一定是20000,而a的则不定,可以是20000,也可能是其它数值,这说明了volatile不适用于++场景。

a的值:18703
累加的次数:20000      

(3).不适用场景2

/**
 * 描述:不适用场景2
 */
public class NoVolatile implements Runnable {
    volatile boolean done = false;

    AtomicInteger realIncrement = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            flipDone();
            realIncrement.incrementAndGet();
        }
    }

    private void flipDone() {
        done = !done;
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable r = new NoVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("done的值:" + ((NoVolatile) r).done);
        System.out.println("累加的次数:" + ((NoVolatile) r).realIncrement.get());
    }
}      

代码运行结果如下所示,其中累计的次数一定是20000,而done的则不定,可能是true,也可能是false,这说明了volatile不适用于boolean值d的场景。

done的值:true
累加的次数:20000      

(4).适用场景1

/**
 * 描述:适用场景1
 */
public class UseVolatile implements Runnable {
    volatile boolean done = false;

    AtomicInteger realIncrement = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            setDone();
            realIncrement.incrementAndGet();
        }
    }

    private void setDone() {
        //和原来的状态没有关系
        done = true;
    }

    public static void main(String[] args) throws InterruptedException{
        Runnable r = new UseVolatile();
        Thread thread1 = new Thread(r);
        Thread thread2 = new Thread(r);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("done的值:" + ((UseVolatile) r).done);
        System.out.println("累加的次数:" + ((UseVolatile) r).realIncrement.get());
    }
}      

代码运行结果如下所示,其中累计的次数一定是20000,done的值一定是true。

done的值:true
累加的次数:20000      

如果一个共享变量自始至终只被各个线程赋值而没有其它操作,那么就可以用volatile来代替synchronized或者原子变量,因为赋值自身是有原子性的,而volatile又保证了可见行,所以就足以保证线程安全。

(5).适用场景2

/**
 * 描述:演示重排序现象。直到达到某个条件才停止,来测试小概率事件。
 */
public class OutOfOrderExecution {
    private volatile static int a = 0, b = 0;

    private volatile static int x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            CountDownLatch latch = new CountDownLatch(1);

            Thread one = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });
            
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    b = 1;
                    y = a;
                }
            });
            
            two.start();
            one.start();
            latch.countDown();
            one.join();
            two.join();

            String result = "第" + i + "次(" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}      

代码运行结果如下所示,而不会出现“x=0,y=0”的情况。

x=0,y=1
x=1,y=0
x=1,y=1      
  1. volatile修饰符适用于以下场景,某个属性被多个线程共享,其中一个线程修改了此属性,其它线程可以立即得到修改后的值,或者作为触发器实现轻量级同步。
  2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。正因为无锁,不需要花费时间在获取锁和释放锁上,所以说它的代价比较小。
  3. volatile只能作用于属性,这样编译器就不会对这个属性做指令重排序。
  4. 除了volatile可以让变量保证可见行外,synchronized、Lock、并发集合、Thread类的join和start方法等都可以保证可见性,具体参照happens-before原则。
  5. synchronized可见性的理解有两点。一是synchronized不仅保证了原子性,还保证了可见性。

    二是synchronized不仅让被保护的代码安全,让加锁前的代码也具有可见性了。

  6. volatile和synchronized的关系,volatile可以看作是轻量版的synchronized,都是用来保证线程安全的。如果一个共享自始至终只被各个线程赋值而没有其它操作,那么就可以用volatile来代替synchronized或者原子变量,因为赋值自身是有原子性的,而volatile又保证了可见行,所以就足以保证线程安全。