内存模型:
主存->高速缓存->cpu
如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。通常称这种被多个线程访问的变量为共享变量。
缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。
1.有序性问题:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
java内存模型
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
java语言:
1.原子性:
Java语言规范规定对任何引用变量和基本变量的赋值都是原子的,除了long和double以外。
在Java中,对任何引用变量和基本变量的读取和赋值操作是原子性操作,(除了long和double以外,32系统读64)即这些操作是不可被中断的,要么执行,要么不执行。
x = 10; //语句1
y = x; //语句2 读取x、y赋值
x++; //语句3 读取x 、x+1、 x赋值
x = x + 1; //语句4 桐3
基础类型除long和double
引用类型
所有Atomic* classes
volatile 修饰的longs 和 doubles
- 可见性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3.有序性 - 单线程有序性,但是会乱序执行
- 锁先放才能再锁
3.传递性
4.volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
volatile:
1.无原子性
2.可见性
3.有序性-内存栅栏
volatile可见性是如何保证的:volatile变量的写操作会导致其他线程的缓存无效,这样只要有变动,就再从主内存读,没变动就走各自的内存缓存。大家的值都一样。
我们知道实例化一个对象经过:
1.分配内存空间
2.初始化对象(构造方法)
3.将内存空间的地址赋值给对应的引用
这三个步骤可能会乱序:
volatile的内存语义:
理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。下面通过具体的示例来说明,示例代码如下。
class VolatileFeaturesExample {
volatile long vl = 0L; // 使用volatile声明64位的long型变量
public void set(long l) {
vl = l; // 单个volatile变量的写
}
public void getAndIncrement() {
vl++; // 复合(多个)volatile变量的读/写
}
public long get() {
return vl; // 单个volatile变量的读
}
}
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
class VolatileFeaturesExample2 {
long vl = 0L; // 64位的long型普通变量
public synchronized void set(long l) { // 对单个的普通变量的写用同一个锁同步
vl = l;
}
public void getAndIncrement() { // 普通方法调用
long temp = get(); // 调用已同步的读方法
temp += 1L; // 普通写操作
set(temp); // 调用已同步的写方法
}
public synchronized long get() { // 对单个的普通变量的读用同一个锁同步
return vl;
}
}
锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。
简而言之,volatile变量自身具有下列特性:
1.可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写
入。
2.原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不
具有原子性。
参考java内存模型volatile和cas
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
使用场景:
- 状态标志:初始化、或者退出线程(内存栅栏起作用)
- DCL双重检查单例:因为写操作happenbefore 读操作
3.安全发布(意思就是对象的set和get都加锁,所有线程才能拿到一致的对象),原因同上
Volatile 适用场景、安全发布、安全共享
内存屏障:1.保证前面的在前面后面的在后面。2.强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
volatile内存屏障
如果你的字段是volatile,Java内存模型将在写操作后
插入一个写屏障指令,在读操作前
插入一个读屏障指令。这意味着写操作:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,比如初始化之类保证是完全初始化了的(解决双重检查锁的问题)
volatile是如何来保证可见性的呢?让我们在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情。Java代码如下:
private valatile Singleton instance = new Singleton();
转成汇编代码如下
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
当有volatile变量修饰时会出现lock addl $0×0,(%esp),Lock前缀的指令在多核处理器下会引发了两件事情 :内存屏障
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。
所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
volatile使用场景
- 一个线程写,多个线程读:
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
- 结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”
volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。
public class CheesyCounter {
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}
单利模式:
1.双重检查失效原因:
instance = new Singleton();
这个语句可能会instance 先赋上值为非null,但其实Singleton还未初始化完毕,这个时候其他线程获取实例,因为instance非null直接返回,但其实返回的是一个未初始化完成的实例。
在instance加volatile关键字可解决。
参考:DCL分析大全
- IODH方式实现单例:Initialization on Demand Holder
1.调用getInstance方法的时候,才会加载SingletonHolder类;
2.这个加载类的过程是线程安全的
public class Singleton {
static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
1.对引用(包括对象引用和数组引用)的非同步访问(即set加锁,而get没加锁),即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。
线程调度:
阻塞方法:调用wait ,比如生产者消费者
也可参考阻塞队列的实现,增删数据时候的阻塞
通常公平性会减少吞吐量但是却减少了可变性以及避免了线程饥饿。
wait的时候阻塞并放弃锁,当唤醒后就是拿到锁的状态了。
锁得释放:
1.执行完代码 2.异常 3.wait或者notify
2.sleep、yield不回放弃锁.
wait\notify必须放synchronized代码内。
wait()方法必须放在一个循环中,因为在多线程环境中,共享对象的状态随时可能改变。当一个在对象等待池中的线程被唤醒后,并不一定立即恢复运行,等到这个线程获得了锁及CPU才能继续运行,又可能此时对象的状态已经发生了变化。
wait()/sleep()的区别
1.wait方法依赖于同步,而sleep方法可以直接调用。
2.sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。
3.obj.wait会使线程进入obj对象的等待集合中并等待唤醒。
4.wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。
interrupt方法:
1.如果线程正在wait/sleep/join,则线程会立刻抛出InterruptedException
2.如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException。
join:当前线程暂停、让join的线程运行,运行完成后自己再接着跑。
yield:让出cpu、大家重新抢
Lock接口与synchronized的区别
1.非阻塞地获取锁:trylock
2.能被中断地获取锁:lockInterruptibly,synchronized获取锁的时候是一直阻塞的
3.超时获取锁:带指定时间的
4.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
多个Condition的强大之处,假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程,那么假设只有一个Condition会有什么效果呢,缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。
DCL(双重检测)问题:
如下代码:调用LazySingleton.getInstance().getSomeField()有可能返回someField的默认值0,
public class LazySingleton {
private static LazySingleton instance;
private int someField;
private LazySingleton() {
this.someField = new Random().nextInt(200)+1; // (1)
}
public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
}
public int getSomeField() {
return this.someField; // (7)
}
}