指标:理解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种:
-
修饰普通方法,一个对象中加锁的方法只允许是一个线程访问的.这种情况锁的是访问该方法的实例对象,如果是多个线程不同的对象访问该方法,则无法保证同步.
-
修饰静态方法,静态方法是类方法,所以这种情况下锁的是包含这个方法的类,也就是类对象,这种情况下,多个线程的不同对象也是可以保证同步的
-
修饰代码块,如果是synchronized (obj),这个同步效果等同于修饰普通方法, 如果是synchronized (obj.class)同步效果等同于修饰静态方法.
问题1:不能同步 问题2:能同步
多线程三要素
- 原子性 (指的是一个或多个不能再被分割的操作,某系列的操作步骤要么全部执行,要么都不执行)
- 可见性 (一个线程对主内存的修改可以及时的被其他线程观察到。)
- 有序性 (防止指令重排)
区别
volatile 具有可见性和有序性
synchronized 具有 原子性、有序性 和可见性
其他区别:
- volatile 对于i++ 就会出现失效,因为不具有原子性
- volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。
- 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前缀的指令在多核处理器下会引发了两件事情。
将当前处理器缓存行的数据会写回到系统内存。
这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
volatile 是如何实现有序性?
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
- 在每个volatile写操作前插入StoreStore屏障
- 在每个volatile写操作后插入StoreLoad屏障
- 在每个volatile读操作前插入LoadLoad屏障
- 在每个volatile读操作后插入LoadStore屏障
synchronized 是如何实现可见性?
- 线程解锁前,必须把共享变量的最新值刷新到主内存中
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值
synchronized 是如何实现有序性?
Java虚拟机会在 MonitorEnter( 它包含了读操作 ) 对应的机器码指令之后临界区开始之前的地方插入一个获取屏障,并在临界区结束之后 MonitorExit ( 它包含了写操作 ) 对应的机器码指令之前的地方插入一个释放屏障
由于获取屏障禁止了临界区中的任何读、写操作被重排序到临界区之前的可能性。而释放屏障又禁止了临界区中的任何读、写操作被重排序到临界区之后的可能性。因此临界区内的任何读、写操作都无法被重排序到临界区之外。从而保证了有序性
synchronized 是如何实现原子性?
因为锁的原因,锁定的区域为 临界区 , 一旦一个线程要访问这个临界区首先就要对这个临界区加锁,当然临界区只允许存在一把锁,如果有人已经在临界区上加了锁时,其他线程就无法对临界区再次加锁,当线程走出临界区时就需要对齐进行解锁。锁是通过互斥保障原子性的,保证了``临界区代码一次只能够被一个线程执行`