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

 程序的运行结果:

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

2.非原子性操作

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

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

3.内存可见性问题

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

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

 运行结果如下:

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


 程序运行结果如下:

 

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

程序的运行结果:


 

总结

加油偶~~

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java高并发线程安全集合是指在多线程环境下能够保证数据一致性和线程安全的数据结构。Java提供了许多高并发线程安全集合,包括ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList、CopyOnWriteArraySet等。 ConcurrentHashMap是一个线程安全的哈希表,它允许多个线程同时读取并修改其中的元素。它使用分段锁的方式来实现并发访问,不同的线程可以同时访问不同的分段,从而提高了并发性能。 ConcurrentSkipListMap是一个基于跳表的并发有序映射,它可以提供较好的并发性能,且支持按照键的顺序进行遍历。它的实现是通过通过多层链表实现的,每一层链表中的节点按照键的顺序排列。 ConcurrentSkipListSet是一个基于ConcurrentSkipListMap的并发有序集合,它实现了Set接口,并且保证元素的有序性和线程安全性。 CopyOnWriteArrayList是一个线程安全的ArrayList,它通过每次修改时创建一个新的副本来实现线程安全。虽然在插入和删除操作时需要复制整个数组,但读取操作非常高效,适用于读操作远多于写操作的场景。 CopyOnWriteArraySet是一个线程安全的Set,它是基于CopyOnWriteArrayList实现的。它通过复制整个数组来实现线程安全,保证了元素的唯一性和线程安全。 这些高并发线程安全集合在多线程环境中保证了数据的一致性和线程安全性,能够提高并发性能和效率,适用于高度并发和需要频繁读写的场景。但需要注意的是,并发集合在某些操作上可能会损失一些性能,因此在选择使用时需根据具体需求进行权衡和选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值