多线程带来的的风险——线程安全

目录

线程安全

线程不安全问题

1. 观察线程不安全

2.  JMM - Java的内存模型

保证线程安全的条件

原子性 

可见性

第一种可能性

第二种可能性 

现实生活中的超卖现象

问题汇总

防止指令重排

关于synchronized关键字

1. synchronized——监视器锁 monitor lock(对象锁)

2. synchronized代码块刷新内存

3. 可重入

synchronized支持线程的可重入

1. synchronized修饰类中的成员方法,锁的对象就是当前类的对象 

2. synchronized修饰类中的静态方法,锁的是当前这个类的class对象

3. synchronized代码块,明确锁的是哪个对象

练习

Java标准库中的线程安全类

volatile关键字:可见性,内存屏障 

1. volatile关键字可以保证共享变量可见性  强制线程读写主内存的变量值

2. 使用volatile修饰的变量,相当于一个内存屏障


线程安全

线程不安全问题

所谓的线程不安全问题,在多线程并发的场景下,实际运行结果和单线程场景下预期结果不符的问题。

1. 观察线程不安全

/**
 * 观察多线程场景下的线程安全问题
 */
public class ThreadUnSafeDemo {
    private static class Counter {
        int count = 0;

        void increase() {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        // t1将count值 + 5w
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        // t2将count值 + 5w
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 主线程走到此处,说明t1和t2都已经执行结束,理想状态count = 10w
        System.out.println("两个子线程执行结束");
        System.out.println(counter.count);
    }
}


什么是所谓的线程安全:
线程安全指的是代码若是串行执行并行执行,结果完全一致,就称为该代码是线程安全的;多个线程串行执行的结果和并行执行的结果不同,这就称为线程不安全。

更改代码

/**
 * 观察多线程场景下的线程安全问题
 */
public class ThreadUnSafeDemo {
    private static class Counter {
        int count = 0;

        void increase() {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        // t1将count值 + 5w
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        // t2将count值 + 5w
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        // 主线程走到此处,说明t1和t2都已经执行结束,理想状态count = 10w
        System.out.println("两个子线程执行结束");
        System.out.println(counter.count);
    }
}

线程不安全:串行执行和并行执行,结果不同。

 

2.  JMM - Java的内存模型

描述多线程场景下Java的线程内存(CPU的高速缓存和寄存器和主内存的关系)

与JVM部分的JVM内存区域划分(JVM将内存划分为6大区域)不是一个概念

每个线程都有自己的工作内存,每次读取变量(共享变量,不是线程的局部变量)都是先从主内存将变量加载到自己的工作内存,之后关于此变量的所有操作都是在自己的工作内存中进行,然后写回主内存。

共享变量:类中成员变量,静态变量,常量都属于共享变量,即在堆和方法区中存储的变量。

保证线程安全的条件

一段代码要保证是线程安全的(多线程场景下和单线程场景下的运行结果保持一致),需要同时满足以下三个特性。

原子性 

原子性:该操作对应CPU的一条指令,这个操作不会被中断,要么全部执行,要么都不执行,不会存在中间状态。这种操作是一个原子性操作。


eg:int a = 10 ——>直接将常量10赋值给a变量原子性,要么没赋值,要么赋值成功。

eg:a+=10 ——> a = a +10 先要读取当前变量a的值,再将a +10计算,最后将计算得出的值重新赋值给a变量(对应三个原子性操作)

可见性

可见性:一个线程对共享变量的修改,能够及时的被其他线程看到,这种特性称为可见性。

(synchronized-上锁,volatile关键字,final关键字可以保证可见性)。

count值是Counter类的一个成员变量,这个属性属于共享属性,多个线程同时访问Count类的同一个对象,这个值属于线程共享变量。count值在主内存中存储(堆上)

这个操作不是一个原子性操作,在某个线程执行count++的时候(分为三步走)其他线程大概率读不到++后的值。

不可见性产生原因:

count变量是共享变量,不同线程都有count的工作内存,工作内存修改不可见;
increase方法内部不是原子性操作;
不可见性导致+不原子性导致的。


问题:为何final能保证可见性?final修饰的常量一定是可见的?
答:常量在定义时就赋值了,且赋值后无法修改。

第一种可能性

不可见性+不原子性导致的线程不安全。

1. 最开始t1和t2在线程启动时,会将主内存中的count值读取到自己的工作内存中,二者count = 0。

2. 开始执行各自的run方法,假设此时t1先执行5253次循环,此时t1.count = 5253这个值,此时t1线程将5253写回主内存

然后t2开始执行,从主内存中加载了这个5253这个初始值运行。同时t1也继续执行,t1先执行结束,最后t1线程的值就是50000,写回主内存。

t2一直读取的自己的工作内存的值,它从5253这个初始值开始运行。在for循环的过程中,一直读取的是自己的工作内存的值(t1在写回主内存的过程中,50000值对t2是不可见的),t2在执行完自己的50000次循环后,将最终值55253写回主内存。


3. 主线程最终去主内存中加载的count值就是55253


简述上述过程

第二种可能性 

t1和t2并行执行,因为原子性的问题,导致的线程不安全。

t1、t2一直在并行执行

t1的工作内存中count = 0,+1操作使count从0变为1,之后将count = 1写回主内存;

t2的工作内存中count = 0,+1操作使count从0变为1,之后将count = 1写回主内存;

分别执行一次后,结果count = 1。本来计划要+2次(即count = 2)最终只是+1次。执行5w次后,count = 50000。

只要执行过程中,t1和t2稍微有一点点时间差,就会得到不是5w的值。

现实生活中的超卖现象

问题汇总

1. 为什么会有这么多内存?
其实只有一个内存——主内存(硬件中的内存条),JMM讲的工作内存实际上是CPU的高速缓存和寄存器

2. 为什么CPU要使用缓存和寄存器,不直接读写内存?
速度:CPU的高速缓存和寄存器的读写速度基本上是内存3到4个数量级(成千上万倍)。
成本:高速缓存和寄存器价格非常昂贵,造成的结果是其空间不可能太大,所以需要内存。

CPU价格最贵,速度最快;内存次之,硬盘最便宜,速度也最慢。

防止指令重排

了解即可,大部分代码的线程安全问题都是因为不满足可见性和原子性


代码的书写顺序不一定就是最终JVM或者CPU的执行顺序


编译器和CPU会对指令优化,这个优化的前提就是保证代码的逻辑没有错误。

在单线程场景下指令重排没问题,但是在多线程场景下就有可能因为指令重排导致错误,一般就是对象还没初始化完成就被别的线程给用了。

关于synchronized关键字

要确保一段代码的线程安全性,需要同时满足可见性,原子性和防止指令重排。
synchronized一个关键字就能同时满足原子性和可见性。

/**
 * 观察多线程场景下的线程安全问题
 */
public class ThreadUnSafeDemo {
    private static class Counter {
        int count = 0;
        synchronized void increase() {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        // t1将count值 + 5w
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        // t2将count值 + 5w
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 主线程走到此处,说明t1和t2都已经执行结束,理想状态count = 10w
        System.out.println("两个子线程执行结束");
        System.out.println(counter.count);
    }
}

1. synchronized——监视器锁 monitor lock(对象锁)

何为锁?锁什么东西?

"互斥" mutex lock
某个线程获取到该对象的锁时,其他线程若也要获取同一个对象的锁,就会处在阻塞等待状态。

当给increase方法加上synchronized关键字,所有进入该对象的线程都需要获取当前counter对象的"锁",获取成功才能进入,获取失败,就会进入阻塞态。

1. 进入synchronized代码块就会尝试执行加锁操作。
2. 退出synchronized代码块,就会释放这个锁。

正因为increase方法上锁处理,多个线程在执行increase方法时,其实是排队进入,同一时刻只可能有一个线程进入increase方法执行对count属性的操作,保证了线程安全性。


在Java内部,每个Java对象都有一块内存,描述当前对象"锁"的信息,锁的信息就表示当前对象被哪个线程持有。

a. 若锁信息没有保存线程,则说明该对象没有被任何线程持有;
b. 若锁信息保存了线程id,其他线程要获取该锁,就处在阻塞状态。

等待队列不是FIFO队列,不满足先来后到的特点。


关于互斥的理解,要理解锁的是谁(对象)

/**
 * 观察多线程场景下的线程安全问题
 */
public class ThreadUnSafeDemo {
    private static class Counter {
        int count = 0;
        // 以下代码操作的属性和操作都是原子性和可见性的
        synchronized void increase() {
            System.out.println(Thread.currentThread().getName() + "获取到锁");
            count++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        // t1将count值 + 5w
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        }, "t1");
        // t2将count值 + 5w
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        }, "t2");
        Counter counter1 = new Counter();
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter1.increase();
            }
        }, "t3");
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        // 主线程走到此处,说明t1、t2、t3都已经执行结束
        System.out.println("两个子线程执行结束");
        System.out.println(counter.count);
    }
}

 


a. t2线程在等待t1线程持有的counter对象的锁——t1和t2处在互斥关系;

b. t3线程获取的是counter1这个对象的锁。没线程和他竞争,所以每次都能成功获取,t3和t1、t2不构成互斥关系。

到底是不是互斥关系,就要看锁的是不是一个对象。

2. synchronized代码块刷新内存

线程执行synchronized代码块的流程

a. 获取对象锁
b. 从主内存拷贝变量值到工作内存
c. 执行代码
d. 将更改后的值写回主内存

e. 释放对象锁

因为synchronized保证互斥,同一时刻只有一个线程获取到这个对象的锁,就保证了可见性和原子性。

从a到e只有一个线程能执行其他线程都在等待,bcd三步对于其他线程就是天然的可见性和原子性。

问题:加锁操作与单线程的区别?

只是在牵扯到变量更改的时候进行上锁操作,对于该类中其他方法,仍然是并发执行。


此时类中新增getCount方法,方法中没有变量更改操作,所以不存在互斥关系,多个线程可以同时读取。


所谓的单线程,指的是一个类的所有代码同一时刻只有一个线程在操作。
上述指的是,只有increase方法(上锁的方法)是单线程操作,其他方法仍然可以多线程并行。

3. 可重入

获取到对象锁的线程可以再次加锁,这个操作就称为可重入。

Java中的线程安全锁都是可重入的(包括java.concurrent.lock)

public class Reentrant {
    private class Counter {
        int val;
        // 锁的是当前Counter对象
        synchronized void increase() {
            val++;
        }
        // 也是锁counter对象
        synchronized void increase1() {
            // 就相当于对同一个Counter对象加了两次锁
            increase();
        }
    }
}

如果不支持可重入,线程进入了increase1方法,之后进入increase需要再次获取锁,无法进入increase。线程1拿到当前Counter对象锁的线程阻塞在这里,等待线程自己释放锁之后再进入increase()——>这个程序永远不会停止。线程1一直阻塞在这里——死锁。

synchronized支持线程的可重入

Java中每个对象都有一个"对象头",其中包括:

a. 描述当前对象的锁信息,即当前对象被哪个线程持有;

b. 以及一个"计数器",即当前对象被上锁的次数。


情况Ⅰ. 若线程1需要进入当前对象的同步代码块(synchronized),此时当前对象的对象头没有锁信息,线程1是第一个获取锁的线程,进入同步代码块,对象头修改持有线程为线程1, 计数器的值由0 + 1 =1。当线程1在同步代码块中再次调用当前对象的其他同步方法可以进入,计数器的值再次+1,说明此时对象锁被线程1获取了两次。

问题:为什么加两次锁?

答:

synchronized代码块语法没问题,考虑的是多线程的安全问题。之所以在increase1上也加锁,说明increase1需要保证线程安全。


情况Ⅱ. 若线程2需要进入当前对象的同步块,此时当前对象的对象头持有线程为线程1,且计数器值不为0,线程2就会进入阻塞状态,一直等到线程1释放锁为止(直到计数器值为0,才是真正释放锁)。

1. synchronized修饰类中的成员方法,锁的对象就是当前类的对象 

synchronized是对象锁,必须得有个具体的对象让他锁

问题:t1和t2是否互斥?

答:

当前t1通过counter1对象调用的increase,则锁的是counter1这个对象,t1获取到的是counter1这个对象的锁;

当前t2通过counter2这个对象调用的increase,锁的是counter2这个对象,t2获取到的是counter2这个对象的锁;
t1和t2不互斥,各回各家,各锁各门。

下面的代码中t1、t2才互斥

2. synchronized修饰类中的静态方法,锁的是当前这个类的class对象

class对象全局唯一,相当于我把这个类给锁了,同一时刻只能有一个线程访问这个方法(无论是几个对象)。

public class Reentrant {
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Counter counter3 = new Counter();
        Thread t1 = new Thread(() -> {
            counter1.increase2();
        }, "t1");
        Thread t2 = new Thread(() -> {
            counter2.increase2();
        }, "t2");
        Thread t3 = new Thread(() -> {
            counter3.increase2();
        }, "t3");
        t1.start();
        t2.start();
        t3.start();
    }

    private static class Counter {
        // 当synchronized修饰静态方法,则相当于将Counter这个类的所有对象都给锁了
        // 其实锁的Counter类的class对象,全局唯一
        synchronized static void increase2() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + "获取到锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

 

 


这是一个静态方法,锁的Counter.class对象,全局唯一。

无论通过哪个Counter对象调用increase2(),同一时刻只能有一个线程获取到这个锁,其他线程都在等待,synchronized修饰静态方法需要谨慎。

Counter.class对象是什么?

任何一个类的.class对象都是全局唯一的,当JVM加载这个类的时候产生,描述该类的核心信息(具备哪些属性和方法),这个对象是反射的核心对象。

3. synchronized代码块,明确锁的是哪个对象

锁的粒度更细,只有在需要同步的若干代码才加上synchronized关键字。

public class Reentrant {
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Thread t1 = new Thread(() -> {
            counter1.increase4();
        }, "t1");
        Thread t2 = new Thread(() -> {
            counter1.increase4();
        }, "t2");
        Thread t3 = new Thread(() -> {
            counter1.increase4();
        }, "t3");
        t1.start();
        t2.start();
        t3.start();
    }

    private static class Counter {
        int val;
        // 方法上没加synchronized关键字,因此所有线程都可以并发进入increase4方法
        void increase4() {
            System.out.println(val);
            System.out.println("准备进入同步代码块");
            // 同步代码块,进入同步代码块,必须获取到指定的锁
            // this表示当前对象引用,锁的就是当前对象counter1
            synchronized (this) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "获取到当前对象的锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

public class Reentrant {
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Counter counter3 = new Counter();
        Thread t1 = new Thread(() -> {
            counter1.increase4();
        }, "t1");
        Thread t2 = new Thread(() -> {
            counter2.increase4();
        }, "t2");
        Thread t3 = new Thread(() -> {
            counter3.increase4();
        }, "t3");
        t1.start();
        t2.start();
        t3.start();
    }

    private static class Counter {
        int val;
        // 方法上没加synchronized关键字,因此所有线程都可以并发进入increase4方法
        void increase4() {
            System.out.println(val);
            System.out.println("准备进入同步代码块");
            // 同步代码块,进入同步代码块,必须获取到指定的锁
            // this表示当前对象引用,锁的就是当前对象
            synchronized (this) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "获取到当前对象的锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

public class Reentrant {
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Counter counter3 = new Counter();
        Thread t1 = new Thread(() -> {
            counter1.increase4();
        }, "t1");
        Thread t2 = new Thread(() -> {
            counter2.increase4();
        }, "t2");
        Thread t3 = new Thread(() -> {
            counter3.increase4();
        }, "t3");
        t1.start();
        t2.start();
        t3.start();
    }

    private static class Counter {
        int val;
        // 方法上没加synchronized关键字,因此所有线程都可以并发进入increase4方法
        void increase4() {
            System.out.println(val);
            System.out.println("准备进入同步代码块");
            // 同步代码块,进入同步代码块,必须获取到指定的锁
            // 若锁的是class对象,全局唯一
            synchronized (Reentrant.class) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "获取到当前对象的锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }

        // 当synchronized修饰静态方法,则相当于将Counter这个类的所有对象都给锁了
        // 其实锁的Counter类的class对象,全局唯一
        synchronized static void increase2() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + "获取到锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        // 锁的是当前Counter对象
        synchronized void increase() {
            val++;
        }

        // 也是锁counter对象
        synchronized void increase1() {
            // 就相当于对同一个Counter对象加了两次锁
            increase();
        }
    }
}

练习

到底互斥与否,就看多个线程锁的是什么?只有锁的是同一个对象才互斥,不同对象就不互斥。

public class LockNormal {
    public static void main(String[] args) {
        Object lock = new Object();
        Counter c1 = new Counter();
        c1.lock = lock;
        Counter c2 = new Counter();
        c2.lock = lock;
        Counter c3 = new Counter();
        c3.lock = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                c1.increase();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                c2.increase();
            }
        }, "t2");
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                c3.increase();
            }
        }, "t3");
        t1.start();
        t2.start();
        t3.start();
    }

    private static class Counter {
        int val;
        Object lock;
        void increase() {
            // 不需要同步的代码
            // synchronized锁任意对象,传什么锁什么
            synchronized (lock) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "获取了锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

问题:这三个线程谁和谁互斥?

A. 都是互斥的t1,t2,t3互斥

B. 都是并发的
C. t1和t2互斥,和t3并发

D. t2和t3互斥,和t1并发

E. t1和t3互斥,和t2并发

答:C

t3和t1,t2不构成互斥,t3的lock是一个新的对象。

两次运行结果如下

Java标准库中的线程安全类

线程不安全:多线程发修改同一个集合的内容,就有数据安全问题。

只要多线程访问同一个vector对象,都是互斥的。

volatile关键字:可见性,内存屏障 

1. volatile关键字可以保证共享变量可见性  强制线程读写主内存的变量值

相较于普通的共享变量,使用volatile关键字可以保证共享变量的可见性
a. 当线程读取的是volatile关键字时,线程直接从主内存中读取该值到工作内存中(无论当前工作内存中是否已经有该值)。
b. 当线程的是volatle关键字的变量,将当前修改后的变量值(工作内存中)立即刷新到主内存中,且其他正在读此变量的线程会等待(不是阻塞),直到写回主内存操作完成,保证读的一定是刷新后的主内存值。

对于同一个volatile变量,他的写操作一定发生在他的读操作之前,保证读到的数据一定是主内存中刷新后的数据。

未添加volatile关键字

import java.util.Scanner;
public class NonVolatile {
    private static class Counter {
        int flag = 0; // 未添加volatile关键字
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            // t1将flag加载到工作内存后一直读取的是当前工作内存的值
            // t2的修改对t1是不可见的
            while (counter.flag == 0) {
                // 一直循环
            }
            System.out.println(counter.flag + "退出循环");
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请改变flag的值");
            counter.flag = scanner.nextInt();
        });
        t2.start();
    }
}

t1将flag加载到工作内存后一直读取的是当前工作内存的值,t2的修改对t1是不可见的。

添加了volatile关键字

import java.util.Scanner;
public class NonVolatile {
    private static class Counter {
        volatile int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            // volatile变量每次都读写主内存
            while (counter.flag == 0) {
                // 一直循环
            }
            System.out.println(counter.flag + "退出循环");
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请改变flag的值");
            counter.flag = scanner.nextInt();
        });
        t2.start();
    }
}

使用synchronized保证可见性

import java.util.Scanner;
public class Volatile {
    private static class Counter {
         int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while (true) {
                synchronized (counter) {
                   if (counter.flag == 0) {
                       continue;
                   }else {
                       break;
                   }
                }
            }
            System.out.println(counter.flag + "退出循环");
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请改变flag的值");
            counter.flag = scanner.nextInt();
        });
        t2.start();
    }
}

volatile只能保证可见性,无法保证原子性,因此若代码不是原子性操作,仍然不是线程安全的,volatile != synchronized。 

/**
 * 观察多线程场景下的线程安全问题
 */
public class ThreadUnSafeDemo {
    private static class Counter {
        // 使用关键字volatile,保证可见性
        volatile int count = 0;

       // 以下代码操作的属性和操作只是可见性的,不保证原子性
       void increase() {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        // t1将count值 + 5w
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        }, "t1");
        // t2将count值 + 5w
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        }, "t2");
        Counter counter1 = new Counter();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 主线程走到此处,说明t1和t2都已经执行结束,理想状态count = 10w
        System.out.println("两个子线程执行结束");
        System.out.println(counter.count);
    }
}

2. 使用volatile修饰的变量,相当于一个内存屏障

未使用volatile时

class {
    int x = 1;       // 1
    int y = 2;       // 2
    boolean z = true;// 3
    x = × + 1;       // 4
    y = y + 2;       // 5
}

指令重排:CPU会在不影响结果的前提下、执行时不一定按照书写顺序执行。

a. 1 2 3 4 5
b. 1 3 4 2 5
c. 2 3 5 1 4
d. 1 4 2 5 3

这四个排法最终结果都一样。


使用volatile修饰时

class {
    int x = 1;       // 1
    int y = 2;       // 2
    volatile boolean z = true;// 3
    x = × + 1;       // 4
    y = y + 2;       // 5
}

CPU在执行到第3行时,一定保证1和2已经执行结束(1和2的顺序可以打乱,1、2必须在3之前),且1和2的结果对后面的结果可见;
此时执行到第三行,4和5一定还没开始;
当把第三行执行结束,4和5顺序也能打乱。

a. 1 2 3 4 5 √
b. 1 3 4 2 5 ×
c. 2 3 5 1 4 ×
d. 1 4 2 5 3 ×

e. 2 1 3 4 5 √

f.  2 1 3 5 4 √

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瘦皮猴117

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值