Java并发编程之可见性,有序性,原子性

1.原子性

问题引入

public class T01_Volatile {
    private boolean running = true;
    private void m(){
        System.out.println("m start");
        while (running){

        }
        System.out.println("m end");
    }
    public static void main(String[] args) throws InterruptedException, IOException {
        T01_Volatile t = new T01_Volatile();
        new Thread(t::m,"T01").start();
        Thread.sleep(1000);
        t.running = false;
    }
}

上面是一段小程序,逻辑很简单,开启一个子线程运行m方法,方法开始打印m start,之后是个死循环,主线程沉睡一秒后将running设置为false,死循环结束,接着打印m end,运行程序:

m start

奇怪的现象发生了,主线程将running设置为了false,只打印了m start ,m end 并没有执行,说明子线程的循环并没有结束,这是因为主线程和子线程在不同的cpu内核上运行,程序运行的时候主线程和子线程会把runing拷贝一份放在自己的缓存区,程序运行后他们都各自操作的是自己缓存区的runing,主线程对running的修改对子线程来说是不可见的。

接下来我们对变量runing通过添加volatile关键字进行修饰,如下所示:

private volatile boolean runing = true;

再次运行,log打印:

m start
m end

程序结束了,m end被打印了,通过volatile修饰的变量能保证线程之间数据的同步,主线程将runing设置为false之后,子线程是及时可见的,这就是volatile的主要作用。

有的同学可能有过这样的测试,将runing的volatile关键字设置remove掉,在while循环中打印一句话,即:

    private boolean running = true;
    private void m(){
        System.out.println("m start");
        while (running){
            System.out.println("ok");
        }
        System.out.println("m end");
    }

此时运行程序发现m end也打印了,这是为什么呢?这是因为System.out.println方法是一个同步的方法,源码如下:

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

在多线程编程模型中某些语句会触发数据同步机制,此时synchronized语法,synchronized关键字不但保证了原子性,还保证的数据的可见性。

什么是程序?什么是进程?什么是线程

wechat.exe点击运行发生了什么?

  • 1.分配内存空间,拷贝相关资源到内存中,进程被创建

  • 2.找到的主线程入口函数开始执行

  • 3.第一条指令被传递到cpu的pc区,由他来记录(程序计数器)

  • 4.第一条指令的数据传递到cpu的Registers(寄存器)

  • 5.cpu的ALU开始计算 (计算单元)

  • 6.cpu计算完毕将数据回传给内存

存指令,存数据,计算,cpu的速度比内存高的多,cup计算单元访问寄存器的速度是它访问内存速度的100倍

image

计算机缓存架构,在cpu和内存之前存在很多缓存区

running没有更新的本质

上面我们主要了解一个程序运行之后的工作流程,下面我们主要来看看多线程情况下她到底是怎么运行的,在cpu和主内存存在三个缓冲区,如下图所示的L1,L2,L3,还是以之前我们所讲的那个实例为例,当子线程启动的时候,会把running从内存依次读到L3,L2,L1,然后ALU计算的时候,先从L1查找running,如果L1有就直接用了,没有L1没有回继续往下的L2查找,如果在L2找到了就直接用了,否则继续往L3找,L3没有找到会在主内存找,至于主内存在什么时候进行数据同步,这个时间不确定,正是由于这个原因就导致了running的值一直是ture,而主线程设置的false,只是在主线程所在的缓存中更新了,主内存中的running并没有更新。

volatile bytecodeInterpreter.cpp,volatile的最终实现是一条汇编指令 lock addL lock锁住了cpu到内存的联通线,lock指令不能单独存在

image

一核一线程

volite本质是汇编指令lock和addl ,bytecode.cpp ,本质就是一把锁,锁缓存或锁总线,lock指令不能但是存在,她的本质意思是当我执行后面的这条指令的时候,锁缓存或者锁总线。

缓存行

MESI缓存一致性协议,时不时的会触发一下,来保持数据的一致性,把两个x拆开,他们位于不同的内存空间,一个缓存行,读的是一块数据

MESI协议,没有上锁是一种不及时的同步,不管加不加v,s底层永远存在,慢:同一个缓存行,数据同步有消耗,快:在不同的缓存行,不需要同步数据。

public class T02_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 p11,p22,p33,p44,p55,p66,p77;
    }
    public static T[] arr = new  T[2];
    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws InterruptedException {
        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);
    }
}
image
image

2.有序性

public class T03_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread one = new Thread(new Runnable() {
                public void run() {
                    //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                    //shortWait(100000);
                    a = 1;
                    x = b;
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            one.start();
            other.start();
            one.join();
            other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}

image
image

乱序执行,不是两个线程之间的乱序执行,而是同一个线程中代码的乱序执行,乱序执行是为了提供程序的运行效率,烧水泡茶的例子,线程的执行结果能保证最终的一致性,此时就可能发生乱序来提升效率

不能重排:

x++
x = x + 1

可以重排:

 a = 1;
 x = b;
public class T04_NoVisbllity {
    private static boolean ready = false;
    private static int number;
    private static class ReaderThread extends Thread{
        @Override
        public void run() {
            while (!ready){
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new ReaderThread();
        t.start();
        number = 42;
        ready = true;
        t.join();
    }
}

可能一直处于没有准备好,ready的是不可见的,虽然ready被主线程修改,另外可能打印的结果为0,因为:

number = 42;
ready = true;

可能会出现指令重排

public class T05_ThisEscape {
    private int num = 8;

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

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

num的打印结果可能为0,this是一个半初始化状态

public class T06_SingleInstance {
    private static T06_SingleInstance instance;
    private T06_SingleInstance() {
    }
    public static T06_SingleInstance getInstance() {
        if(instance == null){
            synchronized (T06_SingleInstance.class){
                if(instance == null){
                    instance = new T06_SingleInstance();
                }
            }
        }
        return instance;
    }
}

instance 可能由于指令重排而导致的处于半初始化状态,加上volatile关键字即可,禁止指令重排。

3.原子性

synchronized的实现过程:

  • 1.Java代码:synchronized
  • 2.字节码:monitorenter moniterexit
  • 3.jvm:自动升级
  • 4.汇编:lock comxchg

工作原理

synchronized互斥锁的本质是原子性,把下面的整体操作当成一个原子,一个线程执行完毕,另外一个线程才能执行,把原来并发执行的程序,变成序列化执行。

在Java SE 1.5之前,多线程并发中,synchronized一直都是一个元老级关键字,而且给人的一贯印象就是一个比较重的锁。为此,在Java SE 1.6之后,这个关键字被做了很多的优化,从而让以往的“重量级锁”变得不再那么重。

synchronized主要有两种使用方法,一种是代码块,一种关键字写在方法上。这两种用法底层究竟是怎么实现的呢?在1.6之前是怎么实现的呢?

字节码实现原理 在java语言中存在两种内建的synchronized语法:1、synchronized语句;2、synchronized方法。对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit字节码指令。而synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。

那么monitorenter和monitorexit以及access_flags底层又是通过什么底层技术来实现的原子操作呢?

Mutex Lock

简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。

那么我们来看看mutex lock又是一个什么鬼?

我们来看看常见的linux的内核互斥锁长什么样:

/linux/include/linux/mutex.h

struct mutex {
         /* 1: unlocked, 0: locked, negative: locked, possible waiters */
         atomic_t                count;
         spinlock_t              wait_lock;
         struct list_head        wait_list;
  #ifdef CONFIG_DEBUG_MUTEXES
          struct thread_info      *owner;
          const char              *name;
         void                    *magic;
  #endif
  #ifdef CONFIG_DEBUG_LOCK_ALLOC
          struct lockdep_map      dep_map;
 #endif
 };

mutex lock的作用及访问规则:

mutex lock互斥锁主要用于实现内核中的互斥访问功能。mutex lock内核互斥锁是在原子 API 之上实现的,但这对于内核用户是不可见的。对它的访问必须遵循一些规则:同一时间只能有一个任务持有互斥锁,而且只有这个任务可以对互斥锁进行解锁。互斥锁不能进行递归锁定或解锁。一个互斥锁对象必须通过其API初始化,而不能使用memset或复制初始化。一个任务在持有互斥锁的时候是不能结束的。互斥锁所使用的内存区域是不能被释放的。使用中的互斥锁是不能被重新初始化的。并且互斥锁不能用于中断上下文。但是互斥锁比当前的内核信号量选项更快,并且更加紧凑,因此如果它们满足您的需求,那么它们将是您明智的选择。

对象在内存中的布局

Java Object Layout

image

Object obj = new Object(); 16个字节

makword 8个字节
classpointer 8个字节
成员变量:0个字节

User :int id String main
makword 8个字节
classpointer 4个字节 (压缩)
int 4
String 4
共20个字节 24

锁升级

image

CAS工作原理

image

CAS底层:lock cmpxchg 本质是锁定北桥芯片的一个电信号

总结

在java6之前,synchronized关键字就是那个很重的互斥锁。我们之所以说它重,是因为底层需要进行用户态到内核态的切换。于是在java6中对synchronized进行了优化。减少底层的切换,于是有了轻量级锁,只是进行基本的CAS原子操作;然而CAS还是会涉及的底层,于是又有了偏向锁,通过mark word对象头中的thread ID进行置换,如果下次还是同样的线程,那么就直接进入,而不再需要进行CAS底层原子操作。另外我们也介绍了锁消除这种机制,从而减少了一些根本不必要的同步。我们还简单的讨论了锁粗化,这是一种减少我们频繁的获取和释放锁的不错的做法

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容