Java多线程之synchronized和volatile的比较

概述

在做多线程并发处理时,经常需要对资源进行可见性访问和互斥同步操作。有时候,我们可能从前辈那里得知我们需要对资源进行 volatile 或是 synchronized 关键字修饰处理。可是,我们却不知道这两者之间的区别,我们无法分辨在什么时候应该使用哪一个关键字。本文就针对这个问题,展开讨论。


版权说明

著作权归作者所有。

商业转载请联系作者获得授权,非商业转载请注明出处。

本文作者:Coding-Naga

发表日期: 2016年4月5日

本文链接:http://blog.csdn.net/lemon_tree12138/article/details/51062421

来源:CSDN

更多内容:分类 >> 并发与多线程


内存语义分析

happens-before 模型简介

如果你单从字面上的意思来理解 happens-before 模型,你可能会觉得这是在说某一个操作在另一个操作之前执行。不过,学习完 happens-before 之后,你就不会还这样理解了。以下是《Java 并发编程的艺术》书上对 happens-before 的定义:

在 JMM(Java Memory Model) 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以在一个线程之内,也可以是在不同的线程之间。

volatile 的内存语义

对于多线程编程来说,每个线程是可以拥有共享内存中变量的一个拷贝,这一点在后面还是会讲到,这里就不作过多说明。如果一个变量被 volatile 关键字修饰时,那么对这的变量的写是将本地内存中的拷贝刷新到共享内存中;对这个变量的读会有一些不同,读的时候是无视他的本地内存的拷贝的,只是从共享变量中去读取数据。

synchronized 的内存语义

我们说 synchronized 实际上是对变量进行加锁处理。那么不管是读也好,写也好都是基于对这个变量的加锁操作。如果一个变量被 synchronized 关键字修饰,那么对这的变量的写是将本地内存中的拷贝刷新到共享内存中;对这个变量的读就是将共享内存中的值刷新到本地内存,再从本地内存中读取数据。因为全过程中变量是加锁的,其他线程无法对这个变量进行读写操作。所以可以理解成对这个变量的任何操作具有原子性,即线程是安全的。


实例论证

上面的一些说明或是定义可能会有一些乏味枯燥,也不太好理解。这里我们就列举一些例子来说明,这样比较具体和形象一些。

volatile 可见性测试

RunThread.java

public class RunThread extends Thread {
    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunFlag(boolean flag) {
        isRunning = flag;
    }

    @Override
    public void run() {
        System.out.println("I‘m come in...");
        boolean first = true;
        while(isRunning) {
            if (first) {
                System.out.println("I‘m in while...");
                first = false;
            }
        }
        System.out.println("I‘ll go out.");
    }
}

MyRun.java

public class MyRun {
    public static void main(String[] args) throws InterruptedException {
        RunThread thread = new RunThread();
        thread.start();
        Thread.sleep(100);
        thread.setRunFlag(false);
        System.out.println("flag is reseted: " + thread.isRunning());
    }
}

对于上面的例子只是一个很普通的多线程操作,这里我们很容易就得到了 RunThread 线程在 while 中进入了死循环。

我们可以在 main() 方法里看到一句 Thread.sleep(100) ,结合前面说到的 happens-before 内存模型,可知下面的 thread.setRunFlag(false) 并不会 happens-before 子线程中的 while 。这样一来,虽然主线程中对 isRunning 进行了修改,然而对子线程中的 while 来说,并没有改变,所以这就会引发在 while 中的死循环。

在这种情况下,线程工作时的内存模型像下面这样

在这里,可能你会奇怪,为什么会有两个“内存块”?这是出于多线程的性能考虑的。虽然对象以及成员变量分配的内存是在共享内存中的,不过对于每个线程而言,还是可以拥有这个对象的拷贝,这样做的目的是为了加快程序的执行,这也是现代多核处理器的一个显著特征。从上面的内存模型可以看出,Java的线程是直接与它自身的工作内存(本地内存)交互,工作内存再与共享内存交互。这样就形成了一个非原子的操作,在Java里多线程的环境下非原子的操作是很危险的。这个我们都已经知道了,因为这可能会被异步的读写操作所破坏。

这里工作内存被 while 占用,无法去更新主线程对共享内存 isRunning 变量的修改。所以,如果我们想要打破这种限制,可以通过 volatile 关键字来处理。通过 volatile 关键字修饰 while 的条件变量,即 isRunning。就像下面这样修改 RunThread.java 代码:

private volatile boolean isRunning = true;

这样一来, volatile 修改了 isRunning 的可见性,使得主线程的 thread.setRunFlag(false) 将会 happens-before 子线程中的 while 。最终,使得子线程从 while 的循环中跳出,问题解决。

下面我们来看看 volatile 是如何修改了 isRunning 的可见性的吧。

这里,因为 isRunning 被 volatile 修饰,那么当子线程想要访问工作内存中的 inRunning 时,被强制地直接从共享内存中获取。而共享内存中的 isRunning 被主线程修改过了,已经被修改成了 false ,while 被打破,这样子线程就从 while 的循环中跳出来了。

volatile 原子性测试

volatile 确实有很多优点,可是它却有一个致命的缺点,那就是 volatile 并不是原子操作。也就是在多线程的情况,仍然是不安全的。

可能,这个时候你会发问说,既然 volatile 保证了它在线程间的可见性,那么在什么时候修改它,怎么修改它,对于其他线程是可见的,某一个线程读到的都会是修改过的值,为什么还要说它还是不安全的呢?

我们通过一个例子来说明吧,这样更形象一些。大家看下面这样一段代码:

public class DemoNoProtected {

    static class MyThread extends Thread {
        static int count = 0;

        private static void addCount() {
            for (int i = 0; i < 100; i++) {
                count++;
            }
            System.out.println("count = " + count);
        }

        @Override
        public void run() {
            addCount();
        }
    }

    public static void main(String[] args) {
        MyThread[] threads = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            threads[i] = new MyThread();
        }

        for (int i = 0; i < 100; i++) {
            threads[i].start();
        }
    }
}
count = 300
count = 300
count = 300
count = 400
... ...
count = 7618
count = 7518
count = 9918

这是一个未经任何处理的,很直白的过程。可是它的结果,也很直白。其实这个结果并不让人意外,从我们学习Java的时候,就知道Java的多线程并不安全。是不是从上面的学习中,你感觉这个可以通过 volatile 关键字解决?既然你这么说,那么我们就来试一试,给 count 变量添加 volatile 关键字,如下:

public class DemoVolatile {
    static class MyThread extends Thread {
        static volatile int count = 0;
        ... ...
    }

    public static void main(String[] args) {
        ... ...
    }
}
count = 100
count = 300
count = 400
count = 200
... ...
count = 9852
count = 9752
count = 9652
... ...
count = 8154
count = 8054

不知道这个结果是不是会让你感觉到意外。对于 count 的混乱的数字倒是好理解一些,应该多个线程同时修改时就发生这样的事情。可是我们在结果为根本找不到逻辑上的最大值“10000”,这就有一些奇怪了。因为从逻辑上来说, volatile修改了 count 的可见性,对于线程 A 来说,它是可见线程 B 对 count 的修改的。只是从结果中并没有体现这一点。

我们说,volatile并没有保证线程安全。在上面子线程中的 addCount() 方法里,执行的是 count++ 这样一句代码。而像 count++ 这样一句代码从学习Java变量自增的第一堂课上,老师就应该强调过它的执行过程。count++ 可以类比成以下的过程:

int tmp = count;
tmp = tmp + 1;
count = tmp;

可见,count++ 并非原子操作。任何两个线程都有可能将上面的代码分离进行,安全性便无从谈起了。

所以,到这里我们知道了 volatile 可以改变变量在线程之间的可见性,却不能改变线程之间的同步。而同步操作则需要其他的操作来保证。

synchronized 同步测试

上面说到 volatile 不能解决线程的安全性问题,这是因为 volatile 不能构建原子操作。而在多线程编程中有一个很方便的同步处理,就是 synchronized 关键字。下面来看看 synchronized 是如何处理多线程同步的吧,代码如下:

public class DemoSynchronized {

    static class MyThread extends Thread {
        static int count = 0;

        private synchronized static void addCount() {
            for (int i = 0; i < 100; i++) {
                count++;
            }
            System.out.println("count = " + count);
        }

        @Override
        public void run() {
            addCount();
        }
    }

    public static void main(String[] args) {
        MyThread[] threads = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            threads[i] = new MyThread();
        }

        for (int i = 0; i < 100; i++) {
            threads[i].start();
        }
    }
}
count = 100
count = 200
count = 300
... ...
count = 9800
count = 9900
count = 10000

通过 synchronized 我们可以很容易就获得了理想的结果。而关于 synchronized 关键字的内存模型可以这样来表示:

某一个线程在访问一个被 synchronized 修饰的变量时,会对此变量的共享内存进行加锁,那么这个时候其他线程对其的访问就会被互斥。 synchronized 的内部实现其实也是锁的概念。


Ref

  • 《Java多线程编程核心技术》
  • 《Java并发编程的艺术》

时间: 2024-10-01 05:23:18

Java多线程之synchronized和volatile的比较的相关文章

JAVA多线程之Synchronized关键字--对象锁的特点

一,介绍 本文介绍JAVA多线程中的synchronized关键字作为对象锁的特点. 二,分析 synchronized可以修饰实例方法,如下形式: 1 public class MyObject { 2 3 synchronized public void methodA() { 4 //do something.... 5 } 这里,synchronized 关键字锁住的是当前对象.这也是称为对象锁的原因. 为啥锁住当前对象?因为 methodA()是个实例方法,要想执行methodA(),

Java多线程之synchronized关键字

一.synchronized锁住的不是代码块,是对象. 1 /** 2 * synchronized 对某个对象加锁 3 */ 4 public class SynchronizedTest { 5 6 private int count = 10; 7 private Object o = new Object(); 8 9 private void method() { 10 synchronized (o) { //任何线程想执行下面这段代码都需要拿到o这把锁 11 count--; 12

Java多线程之synchronized及其优化

Synchronized和同步阻塞synchronized是jvm提供的同步和锁机制,与之对应的是jdk层面的J.U.C提供的基于AbstractQueuedSynchronizer的并发组件.synchronized提供的是互斥同步,互斥同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只有一个线程访问. 在jvm中,被synchronized修饰的代码块经javac编译之后,会在代码块前后分别生成一条monitorenter和moniterexit字节码指令,这两个字节码都需要一个

java多线程之synchronized

首先来看下一个场景,某电影院某个时间4个窗口同时在卖票,本场电影总共票只有100张,卖完为止.看下实际代码. package cn.com.thread; public class TestThread { public static void main(String[] args) { SellTicketThread t=new SellTicketThread(); new Thread(t,"窗口1").start(); new Thread(t,"窗口2"

Java多线程之synchronized(一)

在上节中已经说过了“非线程安全”是如何出现的,链接如下:http://www.cnblogs.com/chentong/p/5650137.html,那么怎么解决“非线程安全”问题呢,只需要在两个线程都需要同时访问的方法前面加上synchronized关键字即可,我只贴出需要修改的这个方法的代码,具体修改如下: public static class GetNum { private int num = 0; //两个线程访问同一个对象中的同步方法时一定是线程安全的 synchronized p

Java多线程之synchronized(二)

为了解决“非线程安全”带来的问题,上一节中使用的办法是用关键字synchronized修饰多个线程可能同时访问到的方法,但是这样写是存在一定的弊端的,比如线程A调用一个用synchronized修饰的同步方法,这个方法要执行很长时间,那么其它的线程必须无条件的等线程A执行完释放掉对象锁,当然前提是其他的线程也要访问这个同步方法.这种情况就可以用synchronized代码块来解决.在解决之前我先附上一段没优化之前的方法,这样就可以直观的看到效果差异. 证明synchronized方法的弊端,代码

Java多线程之synchronized(五)

上篇介绍了用synchronized修饰static方式来实现“Class 锁”,今天要介绍另一种实现方式,synchronized(class)代码块,写法不一样但是作用是一样的.下面我附上一段代码来看一下synchronized(class)代码块的基本用法,如下: public static void main(String[] args) { Service4 s1 = new Service4(); Service4 s2 = new Service4(); ThreadA a = n

Java多线程之synchronized线程锁

1 package org.study2.javabase.ThreadsDemo.sync; 2 3 /** 4 * @Auther:GongXingRui 5 * @Date:2018/9/18 6 * @Description:synchronized线程锁 7 **/ 8 public class TicketApp { 9 public static void main(String args[]) { 10 Ticket ticket = new Ticket(); 11 Threa

JAVA多线程之volatile 与 synchronized 的比较

一,volatile关键字的可见性 要想理解volatile关键字,得先了解下JAVA的内存模型,Java内存模型的抽象示意图如下: 从图中可以看出: ①每个线程都有一个自己的本地内存空间--线程栈空间???线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作 ②对该变量操作完后,在某个时间再把变量刷新回主内存 关于JAVA内存模型,更详细的可参考: 深入理解Java内存模型(一)——基础 因此,就存在内存可见性问题,看一个示例程序:(摘自书上) 1 public c