并发编程三大特性

并发编程三大特性

1.1 原子性

原子性的定义:原子性是指一个操作(多条指令)是不可分割的。 在一个线程在执行某一段指令时,其他的线程如果也想执行,需要等待前一个线程执行完毕后才能执行。

原子性可以解决线程安全问题。在多个线程在同时对一个共享资源(共享变量)进行操作时,出现的问题。

在Java端保证原子性一般有三种方式:

CAS、synchronized、ReentrantLock

代码实操

java 复制代码
public class CompanyTest {

    private static int count;

    // 如果方法不追加synchronized,会导致200次++操作结束后,结果不是200
    // 如果方法追加上了synchronized,200次++的操作结束后,结果就是预期的200了。
    @SneakyThrows
    public static synchronized void increment() {
        TimeUnit.MILLISECONDS.sleep(100);
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

1.2 可见性

可见性的问题

可见性问题如何解决。需要提到JMM(Java内存模型)

JMM,用于屏蔽掉硬件和各个操作系统之间内存访问的差异。

而在Java代码层面上,如果要实现这种可见性,有几种方式:

volatile关键字,synchronized,Lock锁(本质也是volatile)

代码实现,认证可见性问题的存在

ini 复制代码
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while(flag){

        }
        System.out.println("t1线程结束!");
    });
    t1.start();
    Thread.sleep(100);
    flag = false;
    System.out.println("main线程将flag改为false");
}

基于volatile的方式,来实现可见性的效果

ini 复制代码
private static volatile boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while(flag){

        }
        System.out.println("t1线程结束!");
    });
    t1.start();
    Thread.sleep(100);
    flag = false;
    System.out.println("main线程将flag改为false");
}

基于synchronized实现内存可见性

ini 复制代码
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while(flag){
	    // 这里的println操作中,涉及到了synchronized操作,间接实现了可见性
            System.out.println(1);
        }
        System.out.println("t1线程结束!");
    });
    t1.start();
    Thread.sleep(100);
    flag = false;
    System.out.println("main线程将flag改为false");
}

基于lock锁的方式,实现内存可见性,本质其实是修改volatile修饰的数据实现的

java 复制代码
private static boolean flag = true;

static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while(flag){
            lock.lock();
            lock.unlock();
        }
        System.out.println("t1线程结束!");
    });
    t1.start();
    Thread.sleep(100);
    flag = false;
    System.out.println("main线程将flag改为false");
}

lock锁的本质是基于对volatile修饰的变量做读写实现的,咱们可以自己来实现这个效果

ini 复制代码
private static boolean flag = true;

private static volatile int count = 0;


public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while(flag){
            count++;
        }
        System.out.println("t1线程结束!");
    });
    t1.start();
    Thread.sleep(100);
    flag = false;
    System.out.println("main线程将flag改为false");
}

1.3 有序性

在Java中,.java文件在被编译后,会生成多条指令,这些指令需要CPU去执行。CPU在执行这些指令时,就会在一定程度上对这些指令做重新排序,在不影响最终结果的前提下,对指令做一些重新排序。

在Java做编译时,JVM内部也提供了一个优化,JIT,在JIT优化时,也会在一定程度上对指令做重新排序。

搞个Java程序,验证一下指令重排序的存在。

ini 复制代码
static int a, b, x, y;
/**
    正常情况下,x和y应该是有三种结果,11,10,01这种情况
    但是咱们判断的是x和y同时都是0的情况,如果出现这种情况,说明t1或者t2的两个操作,可能出现了指令重排序
*/
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
        a = 0;
        b = 0;
        x = 0;
        y = 0;

        Thread t1 = new Thread(() -> {
            a = 1;
            x = b;
        });

        Thread t2 = new Thread(() -> {
           b = 1;
           y = a;
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        if(x == 0 && y == 0){
            System.out.println("第" + i + "次循环中,x = " + x + ",y = " + y);
        }
    }
}

指令重排序出现的问题。单例模式。懒汉式。

如下代码,这种单纯基于DCL实现线程安全的懒汉模式时,会出现一个问题。

new对象时,存在三个操作

  • 开辟空间
  • 初始化属性
  • 引用赋值

这三个操作是可能出现指令重排序的情况,可能就会造成,test != null,但是还没用执行第二步的初始化属性,导致其他线程拿着一个还未初始化完成的,或者说一个半成品对象去操作,这会带来一些线程安全的问题。

csharp 复制代码
public class CompanyTest {

    private static CompanyTest test;

    private CompanyTest(){}

    // DCL  Double Check Lock
    public static CompanyTest getInstance(){
        if(test == null) {
            synchronized (CompanyTest.class) {
                if(test == null) {
                    test = new CompanyTest();
                }
            }
        }
        return test;
    }

}

在Java中,解决指令重排的方式很简单,可以给涉及到指令重排的属性追加上一个关键字 volatile

csharp 复制代码
public class CompanyTest {
    // 追加volatile关键字,确保操作test属性时,不会出现指令重排的问题,保证了有序性。
    private static volatile CompanyTest test;

    private CompanyTest(){}

    // DCL  Double Check Lock
    public static CompanyTest getInstance(){
        if(test == null) {
            synchronized (CompanyTest.class) {
                if(test == null) {
                    test = new CompanyTest();
                }
            }
        }
        return test;
    }

}
相关推荐
野犬寒鸦14 分钟前
从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用
java·服务器·数据库·后端·学习·算法
wenzhangli714 分钟前
ooderA2UI BridgeCode 深度解析:从设计原理到 Trae Solo Skill 实践
java·开发语言·人工智能·开源
HalvmånEver16 分钟前
Linux:线程互斥
java·linux·运维
rainbow688924 分钟前
深入解析C++STL:map与set底层奥秘
java·数据结构·算法
灵感菇_28 分钟前
Java 锁机制全面解析
java·开发语言
indexsunny28 分钟前
互联网大厂Java面试实战:Spring Boot微服务在电商场景中的应用与挑战
java·spring boot·redis·微服务·kafka·spring security·电商
娇娇乔木41 分钟前
模块十一--接口/抽象方法/多态--尚硅谷Javase笔记总结
java·开发语言
saber_andlibert1 小时前
TCMalloc底层实现
java·前端·网络
逍遥德1 小时前
如何学编程之01.理论篇.如何通过阅读代码来提高自己的编程能力?
前端·后端·程序人生·重构·软件构建·代码规范
wangjialelele1 小时前
平衡二叉搜索树:AVL树和红黑树
java·c语言·开发语言·数据结构·c++·算法·深度优先