文章目录
synchronized、volatile
1 Java内存模型JMM
所有共享变量都存储在主存中,每个线程都保存了一份该变量的副本
2 伪共享
Cache内部是按行存储的,Cache行大小一般为2的幂次数字节
由于每次从主存中复制变量到Cache中的是内存块(局部性原理),而不是单个变量,就存在一个Cache行中有多个变量,当多线程操作的时候同一个Cache行里的不同变量时,性能会下将,这就是伪共享
通过填充字段来填充该变量所在的Cache行
3 指令重排
每个指令包含多个步骤,每个步骤可能使用不同的硬件,指令重排对于提高CPU处理性能十分必要,虽然由此带来乱序的问题,但是这点牺牲是值得的
指令重排一般分类:
- 编译器优化重排:编译器在不改变单线程程序语义的前提下,重新安排
- 指令并行重排:现代处理器采用了指令级并行技术。在不存在数据依赖性前提下,重新安排
- 内存系统重排:由于处理器使用缓存和读写缓冲区,导致内存与缓存的数据同步存在时间差
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义一致
JMM提供了happens-before规则,只要不改变程序的执行结果(单线程或者正确同步的多线程),编译器和处理器怎么优化都行
如果操作A happens-before 操作B,那么操作A在内存上所作的操作对操作B都是可见的,不管他们在不在一个线程
4 synchronized
synchronized块是Java 提供的一种原子性内置锁、同步锁、监视器锁
线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起
拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁
synchronized的内存语义:将synchronized块使用到的变量从线程的工作内存中删除,直接从主存中获取;退出synchronized块时,将变量的修改刷新到主存
构造方法不能修饰synchronized
synchronized可用来标记
- 静态方法,使用当前类作为锁
- 普通方法,使用当前实例作为锁
- 代码块,使用某个变量作为锁
synchronized void method() {
//业务代码
}
synchronized static void method() {
//业务代码
}
synchronized(this) {
//业务代码
}
// 双重校验的单例模式
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
5 volatile
当变量被声明为volatile时,则不会从线程的工作内存获取该变量,会直接从主存中获取
保证了并发的可见性,但并不保证操作的原子性
标记为volatile也可以避免指令重排
6 synchronized和Lock
synchronized | Lock |
---|---|
关键字 | 类 |
依赖于JVM | 靠JDK实现的 |
执行完或发生异常,自动释放锁 | 需要在finally中手动释放锁 |
可重入锁 | 可重入锁 |
等待不可中断 | 等待可中断 |
非公平 | 公平、非公平 |
默认使用Object的wait/notify方法 | 可以使用Condition的await/signal方法,唤醒指定线程 |
7 synchronized和volatile
synchronized | volatile |
---|---|
修饰方法、代码块 | 只能作用于变量 |
可见性、原子性 | 只能保证可见性 |
主要解决资源访问的同步性 | 主要解决变量访问的可见性 |