JUC_2 并发三大特性

244 阅读3分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

并发时的三大特性:可见性 有序性 原子性

可见性 (Visibility)

volatile

/**
 * volatile 关键字,使一个变量在多个线程间可见
 * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
 * 使用volatile关键字,会让所有线程都会读到变量的修改值
 * 
 * 在下面的代码中,running是存在于堆内存的t对象中
 * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
 * 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
 * 
 * 使用volatile,将会强制所有线程都去堆内存中读取running的值
 * 
 * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
 */
package com.mashibing.juc.c_001_00_Visibility;

import com.mashibing.util.SleepHelper;

public class T01_HelloVolatile {
    private static /*volatile*/ boolean running = true;

    private static void m() {
        System.out.println("m start");
        while (running) {
            //System.out.println("hello");
        }
        System.out.println("m end!");
    }

    public static void main(String[] args) {

        new Thread(T01_HelloVolatile::m, "t1").start();

        SleepHelper.sleepSeconds(1);

        running = false;
    }
}

解析 System.out.println("hello") 底层代码

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

可以看出,println()里面使用了synchronized,在一定程度上也可以实现可见性,但是缺少volatile的即时性,会执行多次循环后可能才会达到主内存和工作内存中数据的一致。

volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性

public class T02_VolatileReference {
    private static class A {
        boolean running = true;

        void m() {
            System.out.println("m start");
            while (running) {
            }
            System.out.println("m end!");
        }
    }

    private volatile static A a = new A();

    public static void main(String[] args) {
        new Thread(a::m, "t1").start();
        SleepHelper.sleepSeconds(1);
        a.running = false;
    }
}

缓存

从CPU的计算单元(ALU)到:

1629101922.jpg 多级缓存

1629102076.jpg

缓存行

按块读取,64个字节

程序局部性原理,可以提高效率,充分发挥总线CPU针脚等一次性读取更多数据的能力

为什么缓存一行是64个字节?

缓存行越大,局部性空间效率越高,但读取时间慢

缓存行越小,局部性空间效率越低,但读取时间快

工业实践后取一个折中值,目前多用:64字节

缓存行对齐

  • 缓存行对齐

    缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高

    Disruptor

  • 认识缓存行对齐的编程技巧

package com.mashibing.juc.c_001_02_FalseSharing;

import java.util.concurrent.CountDownLatch;

public class T01_CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class T {
        //private long p1, p2, p3, p4, p5, p6, p7;
        public long x = 0L;
        //private long p9, p10, p11, p12, p13, p14, p15;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(()->{
            for (long i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }

            latch.countDown();
        });

        Thread t2 = new Thread(()->{
            for (long i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }

            latch.countDown();
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

缓存一致性协议MESI

1629104588.jpg

伪共享

A、B两个变量在同一个缓存行内,A变量修改同步数据的同时B也被同步。

有序性

CPU的乱序执行

为什么会乱序?

简单来说,主要是为了提高效率。

单线程的as-if-serial

单个线程,两条语句,没有依赖关系,未必是按顺序执行

单线程的重排序,必须保证最终一致性

as-if-serial:看上去像是序列化(单线程)

举例

对象的创建过程会出现乱序 this对象逸出

public class T03_ThisEscape {

    private int num = 8;

    public T03_ThisEscape() {
        new Thread(() -> System.out.println(this.num)
        ).start();
    }

    public static void main(String[] args) throws Exception {
        new T03_ThisEscape();
        System.in.read();
    }
}

this可能在num半初始化的时候指向了T03_ThisEscape对象,num输出为0。

不要在构造方法里面new完线程直接启动,可以在其它方法进行线程的启动。

原子性

线程的原子性

从一个简单的小程序谈起:

import java.util.concurrent.CountDownLatch;

public class T00_IPlusPlus {
    private static long n = 0L;

    public static void main(String[] args) throws Exception {

        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    //synchronized (T00_IPlusPlus.class) {
                    n++;
                    //}
                }
                latch.countDown();
            });
        }

        for (Thread t : threads) {
            t.start();
        }

        latch.await();
        System.out.println(n);
    }
}

上述程序由于多线程之间的竞争,导致数据没有达到预期。

可以通过上锁解决问题,但注意synchronized可以保证数据的可见性、原子性,不能保证有序性,如果有许多业务需要处理,并且之间没有依赖关系,会产生重排序。

上锁的本质

上锁的本质是把并发编程序列化

注意序列化并非其他程序一直没机会执行,而是有可能会被调度,但是抢不到锁,又回到Blocked或者Waiting状态(sync锁升级)

一定是锁定同一把锁(抢一个坑位)

import com.mashibing.util.SleepHelper;

public class T00_01_WhatIsLock {
    private static Object o = new Object();

    public static void main(String[] args) {
        Runnable r = () -> {
            //synchronized (o) { //打开注释试试看,对比结果
                System.out.println(Thread.currentThread().getName() + " start!");
                SleepHelper.sleepSeconds(2);
                System.out.println(Thread.currentThread().getName() + " end!");
            //}
        };

        for (int i = 0; i < 3; i++) {
            new Thread(r).start();
        }
    }
}

什么样的语句(指令)具备原子性?

CPU级别汇编,需要查询汇编手册!

Java中的8大原子操作:(了解即可)

  1. lock:主内存,标识变量为线程独占
  2. unlock:主内存,解锁线程独占变量
  3. read:主内存,读取内存到线程缓存(工作内存)
  4. load:工作内存,read后的值放入线程本地变量副本
  5. use:工作内存,传值给执行引擎
  6. assign:工作内存,执行引擎结果赋值给线程本地变量
  7. store:工作内存,存值到主内存给write备用
  8. write:主内存,写变量值

一些基本概念

race condition => 竞争条件 , 指的是多个线程访问共享数据的时候产生竞争

数据的不一致(unconsistency),并发访问之下产生的不期望出现的结果

如何保障数据一致呢?--> 线程同步(线程执行的顺序安排好)

monitor (管程) ---> 锁

critical section -> 临界区

如果临界区执行时间长,语句多,叫做 锁的粒度比较粗,反之,就是锁的粒度比较细