理解 Java中的synchronized关键字 、synchronized 与 volatile 区别

1,982 阅读5分钟

指标:理解synchronized的含义、明确synchronized关键字修饰普通方法、 静态方法和代码块时锁对象的差异.

有如下一个类A

   class A{
        public synchronized  void a(){}
        public synchronized  void b(){}
    }

两个对象

A a1 = new A();
A a2 = new A();

Thread1         Thread2
a1.a();         a2.a();

请问二者能否构成线程同步?

如果A的定义是这样的呢?

    class A{
        public static synchronized  void a(){}
        public static synchronized  void b(){}
    }

synchronized 修饰对象为以下3种:

  1. 修饰普通方法,一个对象中加锁的方法只允许是一个线程访问的.这种情况锁的是访问该方法的实例对象,如果是多个线程不同的对象访问该方法,则无法保证同步.

  2. 修饰静态方法,静态方法是类方法,所以这种情况下锁的是包含这个方法的类,也就是类对象,这种情况下,多个线程的不同对象也是可以保证同步的

  3. 修饰代码块,如果是synchronized (obj),这个同步效果等同于修饰普通方法, 如果是synchronized (obj.class)同步效果等同于修饰静态方法.

    问题1:不能同步 问题2:能同步

多线程三要素

  1. 原子性 (指的是一个或多个不能再被分割的操作,某系列的操作步骤要么全部执行,要么都不执行)
  2. 可见性 (一个线程对主内存的修改可以及时的被其他线程观察到。)
  3. 有序性 (防止指令重排)

区别

volatile 具有可见性和有序性

synchronized 具有 原子性、有序性 和可见性

其他区别:

  1. volatile 对于i++ 就会出现失效,因为不具有原子性
  2. volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。
  3. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞

内存模型定义的8种操作

lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;

unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;

load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;

use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;

assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;

store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;

write(写入):作用于主内存,它把store传送值放到主内存中的变量中。

进阶知识

volatile 是如何实现可见性?

Java代码:
instance = new Singleton();  //instance是volatile变量

汇编代码:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock  addl $0x0,(%esp);

有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。

  1. 将当前处理器缓存行的数据会写回到系统内存。

  2. 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

JMM内存模型原理参考

volatile 是如何实现有序性?

编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

  1. 在每个volatile写操作前插入StoreStore屏障
  2. 在每个volatile写操作后插入StoreLoad屏障
  3. 在每个volatile读操作前插入LoadLoad屏障
  4. 在每个volatile读操作后插入LoadStore屏障

synchronized 是如何实现可见性?

  1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
  2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

synchronized 是如何实现有序性?

Java虚拟机会在 MonitorEnter( 它包含了读操作 ) 对应的机器码指令之后临界区开始之前的地方插入一个获取屏障,并在临界区结束之后 MonitorExit ( 它包含了写操作 ) 对应的机器码指令之前的地方插入一个释放屏障

由于获取屏障禁止了临界区中的任何读、写操作被重排序到临界区之前的可能性。而释放屏障又禁止了临界区中的任何读、写操作被重排序到临界区之后的可能性。因此临界区内的任何读、写操作都无法被重排序到临界区之外。从而保证了有序性

synchronized 是如何实现原子性?

因为锁的原因,锁定的区域为 临界区 , 一旦一个线程要访问这个临界区首先就要对这个临界区加锁,当然临界区只允许存在一把锁,如果有人已经在临界区上加了锁时,其他线程就无法对临界区再次加锁,当线程走出临界区时就需要对齐进行解锁。锁是通过互斥保障原子性的,保证了``临界区代码一次只能够被一个线程执行`