并发编程三大特性

并发编程三大特性

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;
    }

}
相关推荐
阿在在2 小时前
Spring 系列(二):加载 BeanDefinition 的几种方式
java·后端·spring
颜酱2 小时前
前端算法必备:双指针从入门到很熟练(快慢指针+相向指针+滑动窗口)
前端·后端·算法
小当家.1052 小时前
Maven与Gradle完整对比指南:构建工具深度解析
java·gradle·maven
p***s912 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
AI架构师之家2 小时前
一文分清机器学习、深度学习和各类 AI 工具的关系:心法与招式的区别
后端·ai编程
neoooo2 小时前
🍃Spring Boot 多模块项目中 Parent / BOM / Starter 的正确分工
java·后端·架构
黄金贼贼2 小时前
2026最新java单元测试json校验器
java·单元测试·json
菜鸟的迷茫2 小时前
为了防雪崩加了限流,结果入口先挂了
java·后端·架构
荒诞硬汉2 小时前
数组常见算法
java·数据结构·算法