java并发编程知识体系详解篇二(并发编程理论基础篇二)

并发编程理论基础篇一总结

在这里插入图片描述

并发的三大根源性问题

首先模拟一个并发编程的经典问题,下面程序执行的结果是死循环的,首先执行的是线程1,然后整个线程睡眠1000毫秒(也就是一秒)。然后线程2修改了flag的值,这个时候线程1读取不到flag的值,所以也就没有下文了。
在这里插入图片描述
实例代码:

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/25 22:14
 */
public class ThreadVisibility {
    
    

    private int flag = 1;

    public void refreshCount(){
    
    
        flag = 0;
        System.out.println(Thread.currentThread().getName() + "线程修改了flag的值,flag: " + flag);
    }

    public void load(){
    
    
        System.out.println(Thread.currentThread().getName() + "线程开始执行了");
        int i = 0;
        while (flag == 1){
    
    
            i ++;
        }
        System.out.println(Thread.currentThread().getName() + "线程跳出循环 i:" + i);
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        ThreadVisibility visibility1 = new ThreadVisibility();
        Thread thread1 = new Thread(() -> visibility1.load(), "线程1");
        thread1.start();
        Thread.sleep(1000);
        Thread thread2 = new Thread(() -> visibility1.refreshCount(), "线程2");
        thread2.start();
    }
}

结果:
在这里插入图片描述

原子性

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。将整个操作视作一个整体是原子性的核心特征。在Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。

实例代码:

 		int i = 0; //1
        int j = i; //2

        i ++; // 3

        i = j + 1; // 4

        System.out.println(i);
        System.out.println(j);

这里就有一个有趣的问题了,上面的四个操作,有那几个是原子性操作?在单线程的环境下,可以默认上面的四个操作都是原子性操作,但是在多线程的环境下则不同,在java中,只有基本数据类型的赋值才是原子性操作。

编译后的底层指令集文件:

// 取常量0,保存
 0 iconst_0
 1 istore_1
// 创建变量j 并赋值为i。
 2 iload_1
 3 istore_2
// 执行i ++;
 4 iinc 1 by 1
 7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
// 执行 i = j + 1;
14 iload_2
15 iconst_1
16 iadd
17 istore_1

18 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
21 iload_1
22 invokevirtual #3 <java/io/PrintStream.println : (I)V>
25 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
28 iload_2
29 invokevirtual #3 <java/io/PrintStream.println : (I)V>
32 return

显而易见,操作1是原子性操作,操作2中包含了两个操作:读取 i ,将 i 赋值给 j ,操作3中包含了三个操作: 读取 i 值,将 i + 1, 将 改变后的 i 值赋值给 i,操作四中包含了三个操作,过程跟操作三一样。

示例代码:

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/03/26 0:04
 */
public class AtomThread {
    
    
    private int a = 0;

    public void add(){
    
    
        a ++;
    }

    public static void main(String[] args) {
    
    
        AtomThread thread = new AtomThread();
        // 结果输出不到1000
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(){
    
    
                public void run(){
    
    
                    for (int j = 0; j < 100; j++) {
    
    
                        thread.add();
                    }
                }
            }.start();
        }
        // 结果输出1000
//        for (int i = 0; i < 10; i++) {
    
    
//            for (int j = 0; j < 100; j++) {
    
    
//                thread.add();
//            }
//        }
        System.out.println(thread.a);
    }
}

结果:
在这里插入图片描述
代码分析:
上面也提到了什么叫原子性,一个操作步骤可以划分多个,但是每一个分割的操作步骤都必须按照顺序执行,而且原子性就是不可分割的操作。如图所示,当然,程序中有10个线程,图中只是展示了两个线程,基本理论差不多的。当线程1执行到0+1=1这个步骤时,线程2读取了a,那么这个时候线程1没有完全执行完毕a的赋值的增加的过程,所有内存中a的值仍为0,这个时候线程2读取到的值为错误的0,当线程1和线程2执行完毕,这个时候a的值都为1,而不是需要的2,这就产生了错乱了。
在这里插入图片描述

保证原子性

在这里提供一个经典问题:假如某储户从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。 试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。这个时候就体现出了保证原子性的必要性了。

保证原子性的方法:
在这里插入图片描述

有序性

即程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题

实例代码:

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

代码分析:
上面定义了一个int型的变量 i 和一个boolean 类型的 flag 变量,下面先给变量 i 赋值,然后再给变量 flag赋值,那么就有一个问题了,这些代码在java中执行的顺序是跟代码的先后顺序一致吗?不一定,因为在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。当发生了指令重排序后,这个执行顺序就不一定跟代码的先后顺序一致了。
在这里插入图片描述
重排序类型:

1、编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3、内存系统的重排序:由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

可见性

可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

什么是可见性问题呢?
对于如今的多核处理器,每颗CPU都有自己的缓存,缓存读取的速度非常快,但是缓存仅仅对它所在的处理器可见,CPU缓存与内存的数据不容易保证一致。因此为了避免处理器停顿下来等待向内存写入数据而产生的延迟,处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区合并对同一内存地址的多次写,并以批处理的方式刷新,也就是说写缓冲区不会即时将数据刷新到主内存中。这个时候缓存不能及时刷新向内存中写入数据,这就导致了可见性问题。

猜你喜欢

转载自blog.csdn.net/m0_46198325/article/details/123746713