java---线程安全详解

目录

前言

一、线程不安全产生的原因

1.多个线程同时修改一个变量

2.非原子性操作

3.内存可见性问题

4.指令重排序问题

 二、线程安全的解决

1.加锁排队执行

1. 同步锁synchronized

2.可重入锁ReentrantLock

2.原子类AtomicInteger

总结


前言

线程安全是指某个方法或某段代码,在多线程中能够正确的执行,不会出现数据不一致或数据污染的情况,我们把这样的程序称之为线程安全的,反之则为非线程安全的。

一、线程不安全产生的原因

1.多个线程同时修改一个变量

如果是多线程同时修改不同的变量(每个线程只修改自己的变量),也是不会出现非线程安全的问题了,比如以下代码,线程 1 修改 number1 变量,而线程 2 修改 number2 变量,最终两个线程执行完之后的结果如下:

public class test2{
    // 全局变量
    private static int number = 0;
    // 循环次数(100W)
    private static final int COUNT = 1_000_000;
    // 线程 1 操作的变量 number1
    private static int number1 = 0;
    // 线程 2 操作的变量 number2
    private static int number2 = 0;

    public static void main(String[] args) throws InterruptedException {
        // 线程1:执行 100W 次 number+1 操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                number1++;
            }
        });
        t1.start();

        // 线程2:执行 100W 次 number-1 操作
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                number2--;
            }
        });
        t2.start();

        // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
        t1.join();
        t2.join();
        number = number1 + number2;
        System.out.println("number=number1+number2 最终结果:" + number);
    }
}

 程序的运行结果:

java---线程安全详解_第1张图片

 从上述结果可以看出,多线程只要不是同时修改同一个变量,也不会出现线程安全问题

2.非原子性操作

原子性操作是指操作不能再被分隔就叫原子性操作。首先,要明确java代码中的一条语句可能代表多个指令,例如 r++ 、r--都是对应多条指令,所以在这里他们的原子性没有得到保证。

java---线程安全详解_第2张图片

以上就是一个经典的错误,r 原本等于 1,线程 1 进行 -1 操作,而线程 2 进行加 1,最终的结果 n,r 应该还等于 1 才对,但通过上面的执行,number 最终被修改成了 0,这就是非原子性导致的问题。

3.内存可见性问题

在 Java 编程中内存分为两种类型:工作内存和主内存,而工作内存使用的是 CPU 寄存器实现的,而主内存是指电脑中的内存,我们知道 CPU 寄存器的操作速度是远大于内存的操作速度的,它们的性能差异如下图所示:

java---线程安全详解_第3张图片

 在 Java 语言中,为了提高程序的执行速度,所以在操作变量时,会将变量从主内存中复制一份到工作内存,而主内存是所有线程共用的,工作内存是每个线程私有的,这就会导致一个线程已经把主内存中的公共变量修改了,而另一个线程不知道,依旧使用自己工作内存中的变量,这样就导致了问题的产生,也就导致了线程安全问题。

4.指令重排序问题

例如在实例化对象的时候:

SomeObeject  so = new SomeObject();

上述代码可以分为三步:

1.根据类计算对象的大小:在堆内存中分配内存空间给该对象。

2.对对象进行初始化(构造代码块、构造方法)。

3.把引用交给 so。

但是由于代码重排序的原因,将本来的顺序变为 1 -> 3 ->2 ;假设在执行到 3的时候,发生了线程调度,假如此时新来的线程需要 so对象,所以就使用了,但是,因为代码重排序的问题,上一个线程还没有进行对象的初始化,所以导致发生错误。

 二、线程安全的解决

1.加锁排队执行

Java 中有两种锁:synchronized 同步锁和 ReentrantLock 可重入锁。

1. 同步锁synchronized

synchronized 是 JVM 层面实现的自动加锁和自动释放锁的同步锁,它的实现代码如下:

public class Test2 {
    // 全局变量
    private static int number = 0;
    // 循环次数(100W)
    private static final int COUNT = 1_000_000;

    public static void main(String[] args) throws InterruptedException {
        // 线程1:执行 100W 次 ++ 操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                // 加锁排队执行
                synchronized (Test2.class) {
                    number++;
                }
            }
        });
        t1.start();

        // 线程2:执行 100W 次 -- 操作
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                // 加锁排队执行
                synchronized (Test2.class) {
                    number--;
                }
            }
        });
        t2.start();

        // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
        t1.join();
        t2.join();
        System.out.println("number 最终结果:" + number);
    }
}

 运行结果如下:

java---线程安全详解_第4张图片

2.可重入锁ReentrantLock

ReentrantLock 可重入锁需要程序员自己加锁和释放锁,它的实现代码如下: 

public class Test2{
    // 全局变量
    private static int number = 0;
    // 循环次数(100W)
    private static final int COUNT = 1_000_000;
    // 创建 ReentrantLock
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        // 线程1:执行 100W 次 ++ 操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                lock.lock();    // 手动加锁
                number++;       // ++ 操作
                lock.unlock();  // 手动释放锁
            }
        });
        t1.start();

        // 线程2:执行 100W 次 -- 操作
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                lock.lock();    // 手动加锁
                number--;       // -- 操作
                lock.unlock();  // 手动释放锁
            }
        });
        t2.start();

        // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
        t1.join();
        t2.join();
        System.out.println("number 最终结果:" + number);
    }
    
}


 程序运行结果如下:

java---线程安全详解_第5张图片

 

2.原子类AtomicInteger

AtomicInteger 是线程安全的类,使用它可以将 ++ 操作和 -- 操作,变成一个原子性操作,这样就能解决非线程安全的问题了,如下代码所示:

public class Test2 {
    // 创建 AtomicInteger
    private static AtomicInteger number = new AtomicInteger(0);
    // 循环次数
    private static final int COUNT = 1_000_000;

    public static void main(String[] args) throws InterruptedException {
        // 线程1:执行 100W 次 ++ 操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                // ++ 操作
                number.incrementAndGet();
            }
        });
        t1.start();

        // 线程2:执行 100W 次 -- 操作
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                // -- 操作
                number.decrementAndGet();
            }
        });
        t2.start();

        // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
        t1.join();
        t2.join();
        System.out.println("number 最终结果:" + number.get());
    }
}

程序的运行结果:

java---线程安全详解_第6张图片


 

总结

加油偶~~

你可能感兴趣的:(java)