volatile
在jdk1.2之前,java的内存模型实现总是从主存(即共享内存)中读取变量,而在当今的Java版本中,可以将变量保存到本地内存(比如说寄存器)中,而不是从主存中读取,所以就等于将变量做了份拷贝,A线程改了值B线程仍用原值就会造成数据的不一致。
因此就有了volatile,在变量前声明,告诉jvm这个变量是不稳定的,每次读取都要从主存中读取。
volatile使得每个线程每次都是从主存中读取数据并将数据写回主存,这样两个不同的线程总是看到同一变量的同一个值,保证了同步数据的可见性。
要使volatile变量提供理想的线程安全,必须同时满足下面两个条件:
对变量的写操作不依赖于当前值
该变量没有包含在具有其它变量的不变式中
比如说x++,就涉及读取–修改–写入操作序列组成的组合操作,必须与原子方式执行,而volatile不能提供必须的原子特性,实现正确的操作需要使x的值在操作期间保持不变,而volatile无法实现该点
synchronized
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它锁获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记才能访问这个资源,否则就进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,所以称为互斥锁。
- 如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝。
- 类的每个实例都有自己的对象级别锁。当一个线程访问实例对象中的 synchronized 同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其他线程这时如果要访问 synchronized 同步代码块或同步方法,便需要阻塞等待,直到前面的线程从同步代码块或方法中退出,释放掉了该对象级别锁。
- 访问同一个类的不同实例对象中的同步代码块,不存在阻塞等待获取对象锁的问题,因为它们获取的是各自实例的对象级别锁,相互之间没有影响。
- 持有一个对象级别锁不会阻止该线程被交换出来,也不会阻塞其他线程访问同一示例对象中的非 synchronized 代码。当一个线程 A 持有一个对象级别锁(即进入了 synchronized 修饰的代码块或方法中)时,线程也有可能被交换出去,此时线程 B 有可能获取执行该对象中代码的时间,但它只能执行非同步代码(没有用 synchronized 修饰),当执行到同步代码时,便会被阻塞,此时可能线程规划器又让 A 线程运行,A 线程继续持有对象级别锁,当 A 线程退出同步代码时(即释放了对象级别锁),如果 B 线程此时再运行,便会获得该对象级别锁,从而执行 synchronized 中的代码。
- 持有对象级别锁的线程会让其他线程阻塞在所有的 synchronized 代码外。例如,在一个类中有三个synchronized 方法 a,b,c,当线程 A 正在执行一个实例对象 M 中的方法 a 时,它便获得了该对象级别锁,那么其他的线程在执行同一实例对象(即对象 M)中的代码时,便会在所有的 synchronized 方法处阻塞,即在方法 a,b,c 处都要被阻塞,等线程 A 释放掉对象级别锁时,其他的线程才可以去执行方法 a,b 或者 c 中的代码,从而获得该对象级别锁。
- 使用 synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁。obj 为对象的引用,如果获取了 obj 对象上的对象级别锁,在并发访问 obj 对象时时,便会在其 synchronized 代码处阻塞等待,直到获取到该 obj对象的对象级别锁。当 obj 为 this 时,便是获取当前对象的对象级别锁。
- 类级别锁被特定类的所有示例共享,它用于控制对 static 成员变量以及 static 方法的并发访问。具体用法与对象级别锁相似。
- 互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。synchronized 关键字经过编译后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令。根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要尝试获取对象的锁,如果获得了锁,把锁的计数器加 1,相应地,在执行 monitorexit 指令时会将锁计数器减 1,当计数器为 0 时,锁便被释放了。由于 synchronized 同步块对同一个线程是可重入的,因此一个线程可以多次获得同一个对象的互斥锁,同样,要释放相应次数的该互斥锁,才能最终释放掉该锁。