java基础提升篇:synchronized同步块和volatile同步变量

初遇

Java语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。

synchronized同步块

Java中的同步块用synchronized标记。同步块在Java中是同步在某个对象上。所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。

有四种不同的同步块:

  实例方法:一个实例一个线程。

  静态方法:一个类只能由一个线程同时执行。

  实例方法中的同步块

  静态方法中的同步块

在多线程下最好是使用这种:

1public class MyClass {
2 public static void log2(String msg1, String msg2){
3       synchronized(MyClass.class){
4          log.writeln(msg1);
5          log.writeln(msg2);
6       }
7    }
8}

上述同步块都同步在不同对象上。实际需要那种同步块视具体情况而定。

实例方法同步

下面是一个同步的实例方法:

1public synchronized void add(int value){
2    this.count += value;
3}

注意在方法声明中同步(synchronized )关键字。这告诉Java该方法是同步的。

Java实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上,即该方法所属的实例。只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线程一次可以在一个实例同步块中执行操作。一个实例一个线程。

静态方法同步

静态方法同步和实例方法同步方法一样,也使用synchronized 关键字。Java静态方法同步如下示例:

1public static synchronized void add(int value){
2 count += value;
3}

同样,这里synchronized 关键字告诉Java这个方法是同步的。

静态方法的同步是指同步在该方法所在的类对象上。因为在Java虚拟机中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。

对于不同类中的静态同步方法,一个线程可以执行每个类中的静态同步方法而无需等待。不管类中的那个静态同步方法被调用,一个类只能由一个线程同时执行。

实例方法中的同步块

有时你不需要同步整个方法,而是同步方法中的一部分。Java可以对方法的一部分进行同步。

在非同步的Java方法中的同步块的例子如下所示:

1public void add(int value){
2    synchronized(this){
3       this.count += value;
4    }
5  }

示例使用Java同步块构造器来标记一块代码是同步的。该代码在执行时和同步方法一样。

注意Java同步块构造器用括号将对象括起来。在上例中,使用了“this”,即为调用add方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。上述代码使用监视器对象同步,同步实例方法使用调用方法本身的实例作为监视器对象。

一次只有一个线程能够在同步于同一个监视器对象的Java方法内执行。

下面两个例子都同步他们所调用的实例对象上,因此他们在同步的执行效果上是等效的。

 1public class MyClass {
 2   public synchronized void log1(String msg1, String msg2){
 3      log.writeln(msg1);
 4      log.writeln(msg2);
 5   }
 6   public void log2(String msg1, String msg2){
 7      synchronized(this){
 8         log.writeln(msg1);
 9         log.writeln(msg2);
10      }
11   }
12 }

在上例中,每次只有一个线程能够在两个同步块中任意一个方法内执行。

如果第二个同步块不是同步在this实例对象上,那么两个方法可以被线程同时执行。

静态方法中的同步块

和上面类似,下面是两个静态方法同步的例子。这些方法同步在该方法所属的类对象上。

 1public class MyClass {
 2    public static synchronized void log1(String msg1, String msg2){
 3       log.writeln(msg1);
 4       log.writeln(msg2);
 5    }
 6    public static void log2(String msg1, String msg2){
 7       synchronized(MyClass.class){
 8          log.writeln(msg1);
 9          log.writeln(msg2);
10       }
11    }
12  }

这两个方法不允许同时被线程访问。

如果第二个同步块不是同步在MyClass.class这个对象上。那么这两个方法可以同时被线程访问。

Java同步实例

在下面例子中,启动了两个线程,都调用Counter类同一个实例的add方法。因为同步在该方法所属的实例上,所以同时只能有一个线程访问该方法。

 1public class Counter{
 2     long count = 0;
 3     public synchronized void add(long value){
 4       this.count += value;
 5     }
 6  }
 7  public class CounterThread extends Thread{
 8     protected Counter counter = null;
 9     public CounterThread(Counter counter){
10        this.counter = counter;
11     }
12     public void run() {
13    for(int i=0; i<10; i++){
14           counter.add(i);
15        }
16     }
17  }
18  public class Example {
19    public static void main(String[] args){
20      Counter counter = new Counter();
21      Thread  threadA = new CounterThread(counter);
22      Thread  threadB = new CounterThread(counter);
23      threadA.start();
24      threadB.start();
25    }
26  }

创建了两个线程。他们的构造器引用同一个Counter实例。Counter.add方法是同步在实例上,是因为add方法是实例方法并且被标记上synchronized关键字。因此每次只允许一个线程调用该方法。另外一个线程必须要等到第一个线程退出add()方法时,才能继续执行方法。

如果两个线程引用了两个不同的Counter实例,那么他们可以同时调用add()方法。这些方法调用了不同的对象,因此这些方法也就同步在不同的对象上。这些方法调用将不会被阻塞。如下面这个例子所示:

 1public class Example {
 2   public static void main(String[] args){
 3     Counter counterA = new Counter();
 4     Counter counterB = new Counter();
 5     Thread  threadA = new CounterThread(counterA);
 6     Thread  threadB = new CounterThread(counterB);
 7     threadA.start();
 8     threadB.start();
 9   }
10 }

注意这两个线程,threadA和threadB,不再引用同一个counter实例。CounterA和counterB的add方法同步在他们所属的对象上。调用counterA的add方法将不会阻塞调用counterB的add方法。

volatile同步变量

线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步。因此存在A和B不一致的情况。volatile就是用来避免这种情况的。volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的(也就是上面说的A) 。

理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步。下面我们通过具体的示例来说明。

 1class VolatileFeaturesExample {
 2    volatile long vl = 0L;  //使用volatile声明64位的long型变量
 3    public void set(long l) {
 4        vl = l;   //单个volatile变量的写
 5    }
 6    public void getAndIncrement () {
 7        vl++;    //复合(多个)volatile变量的读/写
 8    }
 9    public long get() {
10        return vl;   //单个volatile变量的读
11    }
12}

假设有多个线程分别调用上面程序的三个方法,这个程序在语意上和下面程序等价:

 1class VolatileFeaturesExample {
 2    long vl = 0L;               // 64位的long型普通变量
 3    public synchronized void set(long l) {     //对单个的普通 变量的写用同一个监视器同步
 4        vl = l;
 5    }
 6    public void getAndIncrement () { //普通方法调用
 7        long temp = get();           //调用已同步的读方法
 8        temp += 1L;                  //普通写操作
 9        set(temp);                   //调用已同步的写方法
10    }
11    public synchronized long get() { 
12    //对单个的普通变量的读用同一个监视器同步
13        return vl;
14    }
15}

如上面示例程序所示,对一个volatile变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个监视器锁来同步,它们之间的执行效果相同。

监视器锁的happens-before规则保证释放监视器和获取监视器的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

监视器锁的语义决定了临界区代码的执行具有原子性。这意味着即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读写就将具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。

简而言之,volatile变量自身具有下列特性:

可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

参考文章:
  • http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
  • http://www.cnblogs.com/Qian123/p/5691705.html

原文发布于微信公众号 - 好好学java(SIHAIloveJAVA)

原文发表时间:2018-05-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏云霄雨霁

设计线程安全的类

1444
来自专栏Android先生

Java多线程-带你认识Java内存模型,内存分区,从原理剖析Volatile关键字

地址:https://juejin.im/post/59f8231a5188252946503294

763
来自专栏我爱编程

Day26异步IO

协程 协程,又称微线程,纤程。英文名Coroutine。 协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来...

2484
来自专栏向治洪

volatile和Synchronized区别

volatile和Synchronized区别 volatile Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进...

1698
来自专栏javathings

volatile 解决了什么问题?

volatile 关键字是最常问到的问题,关于这个关键字的作用解释,网上的文章已经多如牛毛了。

1113
来自专栏pangguoming

C#异步编程

1173
来自专栏ImportSource

一分钟告诉你synchronized和volatile的区别

先从一道笔试题开始: 9. volatile关键字是否能保证线程安全?() 答案:不能 ---- synchronized和volatile的区别 sync...

34512
来自专栏云霄雨霁

Java虚拟机--内存区域划分

1384
来自专栏chenssy

【死磕Java并发】-----Java内存模型之重排序

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件: 1. 在单线程环境...

1142
来自专栏芋道源码1024

【死磕Java并发】—– Java内存模型之重排序

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件: 在单线程环境下不能改...

3527

扫码关注云+社区