二、多线程安全

1.多线程安全问题

当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题,但是做读操作是不会发生数据冲突问题。
经典实例:两个窗口同时出售100张票

public class TicketThread —— Runnable {
	private int ticketCount = 100;
	@override run():
		while(ticketCount > 0) {
			try : Thread.sleep(50);
			sale();	
		}
	public void sale():
			if(ticketCount > 0) {
				sysout(Thread.currentThread().getName() + "正在出售" + (100 - ticketCount + 1) + 								"张票");
				ticketCount--;
			}
}

main(): TicketThread ticketThread = new TicketThread();
		Thread t1 = new Thread(ticketThread, "窗口1--");
		Thread t2 = new Thread(ticketThread, "窗口2--");
		t1.start();
		t2.start();

如何解决多线程之间线程安全问题:同步synchronized或锁lock。

2.synchronized使用及原理

2.1 synchronized 使用

synchronized(内置锁、互斥锁、可重入锁)

  • 1.synchronized方法:Java中的每个对象都有一个锁(Lock)或者叫做监听器(Monitor),当访问某个对象的synchronized方法时,表示将该对象上锁,此时其他任何线程都无法再去访问该对象的任何synchronized方法,直到之前的那个线程执行方法完毕后(或者抛出了异常),那么将该对象的锁释放掉,其他线程才有可能访问对象的synchronized方法。
  • 2.synchronized static : 如果某个synchronized 方法是static的,那么锁住的是synchronized 方法所在的对象对应的Class对象。同理其他线程无法访问synchronized static 方法。
synchronized : this.synchronized 
synchronized static : class.synchronized 
  • 3.synchronized 块:
synchronized (object) {
}

表示线程执行的时候会对object对象上锁

synchronized 方法是一种粗粒度的并发控制。在某一时刻,只能有一个线程执行该synchronized 方法。
synchronized 块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内,synchronized 块之外的代码是可以被多个线程同时访问到的。

synchronized 块锁住的对象和synchronized 方法锁住的对象一样,则不能同时访问synchronized 方法。

2.2 synchronized 原理

synchronized 代码块原理:

public void method(){
	synchronized(this) {
	}
}
// 反编译:
public void method();
code:
0: ---
1: dup
2: 
3:monitorenter
...
13:monitorexit
monitorenter:
// 每个对象都有一个监视器锁monitor。当monitor被占用时就会处于锁定状态。
// 线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
// 1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
// 2.如果线程已经占有该monitor,只是重新进入,则进入monitor的数加1
// 3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
// 1.执行monitorexit的线程必须是objectref所对应的monitor的所有者。
// 2.指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

synchronized方法原理:

public synchronized void method() {

}
// 反编译
public synchronized void method();
descriptor:()V
flags:ACC_SYNCHRONIZED
code:
相对于普通方法,常量池多了ACC_SYNCHRONIZED标识符
当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标识是否被设置,如果设置了,执行线程将会
先获取monitor,获取成功之后,才能执行方法体,方法执行完后再释放monitor。
在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

3.多线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
经典事例:synchronized 嵌套

final Object lockA = new Object();
final Object lockB = new Object();

class ThreadA —— Runnable:
    @override run():
        synchronized(lockA):
            try:Thread.sleep(2000);
            synchronized(lockB):
                try:Thread.sleep(2000);
class ThreadB —— Runnable:
    @override run():
        synchronized(lockB):
            try:Thread.sleep(2000);
            synchronized(lockA):
                try:Thread.sleep(2000);
main():
    ThreadA A = new ThreadA();
    ThreadB B = new ThreadB();
    Thread AThread = new Thread(A);
    Thread BThread = new Thread(B);
    AThread.start();
    BThread.start();

4.ThreadLocal:提供线程内的局部变量

4.1 ThreadLocal介绍

很多文章在对比ThreadLocal与synchronized异同,既然是作比较,那应该是认为这两者解决相同或者类似的问题,上面的描述,问题在于,ThreadLocal并不解决多线程共享变量的问题。
正确理解:

  • 1.ThreadLocal提供了线程安全的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本
  • 2.ThreadLocal变量通常被private static 修饰.

简单示例:

class MyRunnable —— Runnable:
    public int count; // 共用同一份变量
    @override run():
        for(int i = 0; i < 3; i++)
            count++;
class MyRunnable —— Runnable:
    private static ThreadLocal<Integer> countLocal = new ThreadLocal<Integer>(){
        protected Integer initialVal(){
            return 1;
        }
    }
    @override run():
        for(int i = 0; i < 3; i++){
            int count = countLocal.get();
            countLocal.set(count);
        }

4.2 ThreadLocal原理

1)最常见的使用方式:

    public static ThreadLocal<Session> session = ThreadLocal.withInitial(() -> new Session());

(注意)虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程只能访问到自己调用ThreadLocal的set设置的值。

2)实现原理:

/**
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */
public class ThreadLocal<T> {
    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        // 1.获取当前线程
        Thread t = Thread.currentThread();
        // 2.获取该线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 3.若该线程的ThreadLocalMap对象已存在,则替换该map里的值  
        // 否则创建1个ThreadLocalMap对象。
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        // 1.获取当前线程
        Thread t = Thread.currentThread();
        // 2.获取该线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

/*
 *
 * @author  unascribed
 * @see     Runnable
 * @see     Runtime#exit(int)
 * @see     #run()
 * @see     #stop()
 * @since   JDK1.0
 */
public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

(思考)ThreadLocalMap的键的key = 当前ThreadLocal实例,为什么不是当前Thread本身。
(参考)获取ThreadLocal变量里的值是通过Thread里的ThreadLocalMap获得的,而每个Thread可能存有多个ThreadLocal实例的变量,所以通过this对象去得到当前ThreadLocal存储的变量。

4.3 必读文章

ThreadLocal的使用以及实现原理初步解析

ThreadLocal的原理深度解析

Thread使用场景

ThreadLocal内存泄露

全面理解ThreadLocal

5.ThreadLocal适用场景

对于JavaWeb,Session保存了很多信息,很多时候需要通过Session来获取信息,有些时候又需要修改Session的信息。
一方面,需要保证每个线程都有自己单独的Session实例。另一方面,由于很多地方都需要操作Session,存在多方法共享Session的需求。
如果不使用ThreadLocal,可以在每个线程内构建一个Session实例,并将该实例在多个方法间传递。
如下所示:

public class SessionHandler{
    @Data
    public static class Session{
        private String id;
        private String user;
        private String status;
    }

    public Session createSession(){
        return new Session();
    }

    public String getUser(Session session){
        return session.getUser();
    }
}

main():
new Thread(() -> {
    SessionHandler handler = new SessionHandler();
    Session session = handler.createSession();
    handler.getUser(session);
})

// 可以实现需求,但是每个需要使用Session的地方都需要
// 显示传递Session对象,方法耦合度高。

使用ThreadLocal重新实现该功能:

public class SessionHandler {

    public static ThreadLocal<Session> session = ThreadLocal.withInitial(() -> new Session());

    @Data
    public static class Session{
        private String id;
        private String user;
        private String status;
    }

    public String getUser(Session session){
        return session.get().getUser();
    }

    public String getStatus(Session session){
        return session.get().getStatus();
    }    
}

main():
    new Thread(() -> {
        SessionHandler handler = new SessionHandler();
        handler.getUser();
        handler.getStatus();
    })

6.多线程的三大特性

6.1 原子性

一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就不执行。

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

请分析一下哪些操作是原子性操作:即这些操作是不可被中断的,要么执行,要么不执行。

x = 10; // 语句1
y = x; // 语句2
x++; // 语句3
x = x + 1; //语句4

只有语句1是原子性操作,其他三个语句都不是原子性操作。
语句1:赋值语句,线程执行这个语句会直接将数值10写入到工作内存中。
语句2:实际上包含2个操作,它先要读取x的值,再将x的值,写入到工作内存,虽然读取x的值以及将x的值写入工作内存这两个操作是原子性操作,但是放在一起就不是原子性操作了。

同样的,x++和x = x + 1 包含3个操作,读取x的值,进行加1,写入新值。

所以上面4个语句只有语句1的操作具有原子性。

从上面可以看出,Java内存模型,只保证了基本读取和赋值操作是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和lock来实现,由于synchronized和lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了。

6.2 可见性

当多线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
示例:

class VolatileDemo extends Thread {
    public boolean flag = true;
    @override run(){
        sysout("开始执行子线程...");
        while(flag){

        }
        sysout("线程停止");
    }
    void setRunning(boolean flag){
        this.flag = flag;
    }
}

main(){
    VolatileDemo demo = new VolatileDemo();
    demo.start();
    Thread.sleep(3000);
    demo.setRunning(false);
    sysout("flag已经设置成false")
    Thread.sleep(1000);
    sysout(demo.flag); // false
    // 主内存已经设置为false,但是demo并没有去读。
}

分析结果:虽然已经将flag设置为false,但是线程之间是不可见得,读取的是副本,没有及时读取到主内存结果。

volatile 变量:

  • 1.当线程对volatile变量进行写操作时,会将修改后的值刷新回主内存。
  • 2.当线程对volatile变量进行读操作时,会将自己工作内存中的变量置为无效,之后再通过主内存拷贝新值到工作内存中使用。

普通的共享变量不能保证可见性,另外通过synchronized和lock也能够保证可见性,synchronized和lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主内存中,因此可以保证可见性。

6.3 有序性

程序的执行结果按照代码的先后顺序执行

int i = 0;
boolean falg = false;
i = 1; // 语句1
flag = true; // 语句2

从这段代码顺序上来看,语句1是在语句2前面的,那么JVM真正在执行这段代码的时候会保证语句1一定在语句2之前吗?
不一定。为什么呢?这里可能会发生指令重排序。

指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入的代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证最终执行结果和代码顺序的结果是一致的。

在上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同。

int a = 10; // 语句1
int r = 2; // 语句2
a = a + 3; // 语句3
r = a * a; // 语句4

可不可能是这个执行顺序:2 1 4 3
不可能,因为处理器在进行指令重排序时会考虑指令之间的数据依赖性,如果一个指令instruction2必须用到instruction1的结果,那么处理器会保证instruction1会在instruction2之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程内:

// 线程1:
context = loadContext(); // 语句1
inited = true; // 语句2
// 线程2:
while(!inited){
    sleep();
}
doSomethingWithContext(context);

语句1和语句2没有数据依赖性,假如发生了重排序,
在线程1执行过程中先执行语句2,而此时线程2会认为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingWithContext(context)方法,而此时context没有被初始化,就会导致程序出错。

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

要想并发程序正确的执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能导致程序运行不正确。

  • 1.保证原子性:
    1)使用j.u.c.atomic下的工具类,如AtomicBoolean、AtomicIntege、AtomicLong和AtomicReference等。
    2)使用synchronized
    3) 使用lock
  • 2.保证可见性和有序性的方法是使用volatile关键字修饰变量。

volatile 禁止指令重排序

在java里面,可以通过volatile关键字来保证一定的“有序性”。另外,可以通过synchronized和lock来保证有序性,
很显然synchronized和lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性。这个通常也成为happens-before原则。

如果两个操作的执行次序无法从happens-before原则推导出来,那么就不能保证它们的有序性,虚拟机可以随意的对它们进行重排序。

happens-before原则(优先发生原则):

  • 程序次序原则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 锁定原则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量原则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。(在不违背程序结果的情况下,注意这些规则针对的是单线程)
  • 传递原则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动原则:Thread对象的start()方法先行发生于此线程的每一个操作。
  • 线程中断原则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测。
  • 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。

解析:

  • 程序次序规则:一段代码在单线程中执行的结果是有序的。
  • 锁定规则:无论在单线程,还是在多线程环境,一个锁处于锁定状态,那么必须先执行unlock操作才能进行lock操作。

7.多线程内存模型JMM

JMM(Java Memory Model)是JVM定义的内存模型,用来屏蔽各种硬件和操作系统的内存访问差异。

  • 主内存:所有的变量都存储在主内存中。
  • 工作内存:每条线程都有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。

不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

8.深入剖析volatile关键字

1)volatile关键字的两层含义:

  • 1.一个线程修改了某个变量的值,这新值对其他线程是立即可见的。
  • 2.禁止进行指令重排序。

2)volatile保证原子性:

public class Test{
    public volatile int inc = 0;

    public void increase(){
        inc++;
    }

    public static void main(String[] args){
        final Test test = new Test();
        for(int i = 0; i < 10; i++){
            new Thread(){
                public void run(){
                    for(int j = 0; j < 1000; j++){
                        test.increase();
                    }
                }
            }.start();
        }
        while(Thread.activeCount > 1){
            Thread.yield();
        }
        sysout(test.inc);
    }
}

运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

假设某个时刻变量inc的值为10.

线程1对变量进行自增操作,线程1先读取了inc的原始值,然后线程1被阻塞。

然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存无效,所以线程2会直接去主存读取inc的值,10,加1,11,写入工作内存,写入主存。

然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,加1,11写入工作内存,写入主存。

那么两个线程分别进行了一次自增操作后,inc只增加1。

根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

  • 解决方法1:synchronized
public synchronized void increase(){
    inc++;
}
  • 解决办法2:lock
Lock lock = new ReentrantLock();

public void increase(){
    lock.lock();
    try: inc++;
    finally: lock.unlock();
}
  • 解决办法3:AutomicInteger
public AutomicInteger inc = new AutomicInteger();

public void increase(){
    inc.getAndIncrement();
}

j.u.c.atomic包下提供了一些院子操作类,即对基本数据类型的自增、自减、加法、减法等进行了封装,保证这些操作是原子性操作,atomic是利用CAS(Compare And Swap)来实现原子性操作的。
CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是原子性的。

3)volatile能保证有序性吗?
volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

volatile关键字禁止重排序有两层意思:

  • 1.当程序执行到volatile变量的读操作或写操作时,在其前面的操作的更改肯定已经全部进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。

  • 2.在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

// x、y为非volatile变量
// flag为volatile变量
x = 2; // 语句1
y = 0; // 语句2
flag = true; // 语句3
x = 4; // 语句4
y = -1; // 语句5

flag变量为volatile变量,那么在指令进行重排序的时候不会将3放到1,2之前,也不会将3放到4,5后面
但是1,2,4,5不做任何顺序保证。并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕的,且语句1和语句2的执行结果对语句3、语句4、5是可见的。

9.volatile关键字实现原理

9.1 volatile保证可见性

cpu缓存:
cpu缓存的出现主要是为了解决cpu运算速度与内存读取(写)速度不匹配的矛盾,因为cpu运算速度要比内存读写速度快得多。举个例子:

  • 一次主内存的访问速度通常在几十到几百个时钟周期。
  • 一次L1高速缓存的读写只需要1-2个时钟周期。
  • 一次L2高速缓存的读写也只需要数十个时钟周期。

这种访问速度的显著差异,导致CPU可能花费很长时间等待数据或把数据写入内存。

基于此,现代CPU大多数情况下读写都不会直接访问内存,取而代之的是CPU缓存,CPU缓存是位于CPU与内存之间的临时数据,它的容量比内存小得多但是交换速度却比内存快得多。

缓存中的数据是内存的一小部分数据,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先从缓存中读取,从而加快读取速度。

按照读取顺序与CPU结合的紧密程度,CPU缓存可分为:

  • 一级缓存,L1 Cache,位于CPU内核的旁边,是与CPU最为紧密的CPU缓存。
  • 二级缓存,L2 Cache
  • 三级缓存,L3 Cache

每一级缓存中所存储的数据全部都是下一级缓存中的一部分。这三种缓存的技术难度和制造成本是相对递减,所以其容量也相对递增。

当CPU要读取一个数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存或内存中查找。

当系统运行时,cpu执行计算过程如下:

  • 1.程序以及数据被加载到内存。
  • 2.指令和数据被加载到CPU缓存。
  • 3.CPU执行指令,把结果写到高速缓存。
  • 4.高速缓存中的数据写回主内存

如果服务器是单核CPU,那么这些步骤不会有任何问题,如果是多核CPU。
试想下面一种情况:
cpu_cache

  • 1.核0读取了1个字节,根据局部性原理,相邻的字节同样被读入核0缓存
  • 2.核3做了同样的操作,这样核0与核3的缓存拥有相同的数据
  • 3.核0修改了那个字节,被修改后,那个字节被写回核0的缓存,但是并没有写入主存。
  • 4.核3访问该字节,由于核0并未将数据写回主存,数据不同步。

为了解决这一问题,CPU制造商规定了缓存一致性协议(MESI)

9.2 总线锁(Bus Locking)

一种处理一致性问题的办法就是使用Bus Locking(总线锁)。当一个CPU对其缓存中的数据进行操作的时候,往总线发送一个Lock信号。这个时候,所有CPU收到这个信号之后就不操作自己缓存中对应的数据了,当操作结束后,释放锁以后,所有的CPU就去内存中获取最新数据更新。——性能问题

9.3 MESI协议

MESI 是保持一致性的协议,它的方法是在CPU缓存中保存一个标志位,这个标志位有4中状态:

  • M:Modify,修改缓存,当前CPU的缓存已经被修改了,即与内存中的数据不一致了
  • E:Exclusive,独占缓存,当前CPU的缓存和内存中的一致,而且其他处理器并没有可使用的缓存数据。
  • S:Share,共享缓存,和内存保持一致的内存拷贝,多组缓存可以同时拥有针对同一内存地址的共享缓存段。
  • I:Invalid,失效缓存,这个说明CPU中的缓存已经不能被使用了。

CPU读取遵循以下几点:

  • 如果缓存状态是I,那么就从内存中读取。否则就从缓存中直接读取。
  • 如果缓存处于M或E的CPU读取到其他CPU有读操作,就把自己的缓存写入到内存中,并将自己的状态设为S
  • 只有缓存状态是M或E的状态,CPU才可以修改缓存中的数据,修改后,缓存状态为M。

9.4 volatile保证可见性的底层原理:

instance = new Singleton(); // instance是volatile变量
// 反编译
movb $0x0
lock add1 $0x0

lock前缀的指令在多核处理器下会引发两件事情:

  • 1.将当前处理器缓存行的数据写回到系统内存。
  • 2.这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值