聊聊Java多线程之内存可见性

可见性(Visibility)

线程可见性简介

线程之间的可见性是指当一个线程修改一个变量,另外一个线程可以马上得到这个修改值。

假设我们有2个线程:A为读线程,读取一个共享变量的值,并根据读取到的值来判断下一步执行逻辑;B为写线程,对一个共享变量进行写入。很有可能B线程写入的值对于A线程是不可见的。

两个线程间的不可见性

我们用一个例子来表示这种线程间变量不可见的情况。Nonvisibility中的示例包含两个共享数据的线程。Cancel线程将更新标志,Work线程将一直循环直到读取到Cancel线程更新标志:



public class NonVisibilityDemo1 {
    public static void main(String[] args) throws InterruptedException {
        new WorkThread().start();
        Thread.sleep(1000);
        new CancelThread().start();
    }
}


class WorkThread extends Thread {

    @Override
    public void run() {
        System.out.println("WorkThread start");
        while (true) {
            if (ShareData.flag == 1) {
                break;
            }
        }
        System.out.println("WorkThread end");
    }
}

class ShareData {
    public static int flag = -1;
}


class CancelThread extends Thread {
    @Override
    public void run() {
        ShareData.flag = 1;
        System.out.println("CancelThread set flag=" + ShareData.flag);
    }
}


/**
Output:
WorkThread start
CancelThread set flag=1
...
...
...
程序一直运行不退出
**/

这个程序可能会一直循环下去,因为Work线程可能读取不到Cancel线程对于flag的写入而永远等待。

线程间的不可见性是怎样产生的

要理解线程间对共享变量的不可见性,需要大概理解CPU的工作流程。

先了解我们使用的程序变量可能存储的位置:

  • 每个CPU有寄存器,CPU对变量的运算需要从寄存器中读写变量
  • 除寄存器(Register)外还有高速缓存子系统(Cache),写缓冲器(Store Buffer),无效化队列 (Invalidate Queue)
  • 计算机的主内存

也就是说,我们对一个变量的读写操作,可能要途径:主内存->Cache->Store Buffer->寄存器->CPU。

那么可能产生变量不可见的情况就会有:

  1. 每个处理器都有自己的寄存器,不同的线程可能运行在不同的CPU上,例如线程A在CPU-1中运行更改了变量V的值从0到1,于此同时(瞬时),线程2在CPU-2中读取变量V的值仍然是0,这时对线程A对变量的操作则对线程B体现了不可见性。

  2. CPU对变量操作之后,需要将对该变量的更新写入到主内存中,而处理器对主内存并不是直接访问,而是通过该CPU的写缓冲器(Store Buffer)中,还没到达该处理器的Cache中,这时一个CPU的Store Buffer是无法于另一个CPU共享该变量的更新的,这样也产生了不可见性。

虽然一个CPU的Cache是不可以被另一个CPU直接读取的,但是处理器可以通过缓存一致性协议(Cache Coherence Protocol)来读取其他处理器的Cache中的数据,并且将数据同步该处理器的Cache中,这个过程称之为缓存同步。并且为了保证可见性,需要将CPU对变量做的更新最终写入到该CPU的高速缓存或者主内存中,这个过程称为冲刷处理器缓存
即通过冲刷处理器缓存来保证CPU对变量的更新冲刷到Cache中,通过缓存同步将对变量的更新同步到其他的处理器。

如何解决线程间不可见性

为了保证线程间可见性我们必须要保证对共享数据的写操作和读操作都是同步的,也就是写操作线程和读操作线程都需要在同一个锁上进行同步。我们一般有3种方式去保持同步:

  • volatile:只保证可见性
  • Atomic相关类:保证可见性和原子性
  • Lock: 保证可见性和原子性

使用volatile关键字来解决可见性问题

Java提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

我们尝试更改上一个示例,使用volatile关键字来修饰共享数据ShareData.flag

public class VisibilityByVolatileDemo {
    public static void main(String[] args) throws InterruptedException {
        new WorkThread().start();
        Thread.sleep(1000);
        new CancelThread().start();
    }
}


class WorkThread extends Thread {

    @Override
    public void run() {
        System.out.println("WorkThread start");
        while (true) {
            if (ShareData.flag == 1) {
                break;
            }
        }
        System.out.println("WorkThread end");
    }
}

class ShareData {
    public static volatile  int flag = -1;
}


class CancelThread extends Thread {
    @Override
    public void run() {
        ShareData.flag = 1;
        System.out.println("CancelThread set flag=" + ShareData.flag);
    }
}

/**
Output:
WorkThread start
WorkThread end
CancelThread set flag=1
程序运行结束
**/

由于对ShareData.flag使用了volatile关键字进行了修饰,程序可以正常结束,并且读线程可以正常的访问到写线程对共享数据flag的修改从而正常结束。

使用AtomicInteger类来解决可见性问题

我们再尝试更改上一个示例,使用AtomicInteger类来包装共享数据ShareData.flag:


public class VisibilityByAtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        new WorkThread().start();
        Thread.sleep(1000);
        new CancelThread().start();
    }
}


class WorkThread extends Thread {

    @Override
    public void run() {
        System.out.println("WorkThread start");
        while (true) {
            if (ShareData.flag.get() == 1) {
                break;
            }
        }
        System.out.println("WorkThread end");
    }
}

class ShareData {
    public static AtomicInteger flag = new AtomicInteger(-1);
}


class CancelThread extends Thread {
    @Override
    public void run() {
        ShareData.flag.set(1);
        System.out.println("CancelThread set flag=" + ShareData.flag);
    }
}

/**
Output:
WorkThread start
WorkThread end
CancelThread setFlag flag=1
程序运行结束
**/

由于ShareData.flag使用的类型是AtomicInteger,写线程对flag的修改对于读线程是可见的,这样写线程可以读取到flag被更新为1并正常退出。

使用synchronized来解决可见性问题

使用synchronized关键字对操作加锁也可以保证线程间的可见性,并且保证操作的原子性。内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。加锁的含义不仅仅局限于互斥行为,还包括内存可见性。
我们再构造一个示例来说明synchronized关键字所起的作用。首先我们还是需要2个线程,一个读线程,一个写线程,然后把读写操作封装到ShareData中,然后观察在没有synchronized关键字修饰时程序
的运行情况。


public class VisibilityBySynchronizedDemo {
    public static void main(String[] args) throws InterruptedException {
        new WorkThread().start();
        Thread.sleep(1000);
        new CancelThread().start();
    }
}


class WorkThread extends Thread {

    @Override
    public void run() {
        System.out.println("WorkThread start");
        while (true) {
            if (ShareData.getFlag() == 1) {
                break;
            }
        }
        System.out.println("WorkThread end");
    }
}

class ShareData {
    private static  int flag = -1;

    public static synchronized int getFlag() {
        return flag;
    }

    public static synchronized void setFlag(int value) {
        flag = value;
    }
}


class CancelThread extends Thread {
    @Override
    public void run() {
        ShareData.setFlag(1);
        System.out.println("CancelThread setFlag flag=" + ShareData.getFlag());
    }
}

/**
Output:
WorkThread start
WorkThread end
CancelThread setFlag flag=1
程序结束
**/

由于getFlag()setFlag()方法都使用了synchronized关键字修饰,保证了原子性和可见性,程序正常结束。

小结

在Java平台中,如何保证可见性呢?也就是我们上面提到的

  • volatile:只保证可见性
  • Atomic相关类:保证可见性和原子性
  • Lock: 保证可见性和原子性

其实无论对于上述的何种方式,其本质都是会使相应的CPU进行刷新处理器缓存动作,来保证共享变量的可见性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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