吃透Java线程安全问题(上)

简介: 吃透Java线程安全问题(上)

一、什么是线程安全

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的


🌰栗子

package Thread;
public class demo77 {
    private static int count;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        t1.start(); // 两个线程在创建好了后,线程所对应的PCB加入到系统链表,参与系统调度
        t2.start();
        // 让主线程main等t1、t2执行完了再接着往下走
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

f35358cba8b1408ab904ad1246d58175.png

想这样,在多线程情况下,程序的运行结果不符合我们的预期,这被称为线程不安全

二、造成线程不安全的原因

根本原因:操作系统的随机调度执行,抢占式执行

还有:我们可以看到我们的count是一个全局变量,我们的线程t1、线程t2对count变量同时都进行了修改——++操作(为什么说是同时呢,因为我们的t1线程、t2线程在创建完了后就参与到系统调度,由系统随机分配线程的执行,可能是t2线程先执行10个指令然后t1再执行10个指令,相当于是同时)


那么我们就改一下代码让t1、t2分批次对count修改不就行了吗?

package Thread;
public class demo77 {
    private static int count;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                t1.join();// 等t1线程执行完了,t2线程再执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        t1.start(); // 两个线程在创建好了后,线程所对应的PCB加入到系统链表,参与系统调度
        t2.start();
        t2.join(); // 等t2线程执行完了,主线程main再接着执行,执行顺序:t1->t2->打印count
        System.out.println(count);
    }
}

0e75d685db6f419586e815248f939b3d.png

大家有没有想过为什么多个线程同时执行count++的时候就会出现BUG呢?

这是因为我们多个线程同时对同一变量修改的所造成的BUG往往和我们操作的原子性有关!!!这时候的操作往往不是一个整体,多个线程并发执行这些操作就可能出现一些问题

如果我们在变量修改过程中,操作是原子的——只是对应一个机器指令,那么即使是多个线程同时对同一个变量修改也不一定会造成BUG,但也可能造成BUG——要看具体的业务场景

总之我们要避免多个线程同时对同一个变量来操作

af5cc27030cf487a81d2cb674906c712.png

🍑 对原子性在多线程并发执行中出现问题的分析

🔔🔔注意:


6749c1d243dd4139ae1d71bb13238fc6.png

当我们执行t1.start()、t2.start()后,t1线程和t2线程就在操作系统内核中创建出来了t1、t2线程就参与到了系统调度当中

而调度是随机的——他可能先让t1执行几个指令,然后t2再执行几个指令、最后再把CPU的控制权交给t1。

于是因为系统的调度是随机的(这是罪魁祸首,但我们无法改变),当我们多个线程同时执行一些不是整体的操作的时候(++或--)由于并发就会产生一些问题:

🌰栗子一


15ac811137144c5abc01c49068c5a363.png

🌰栗子二


2cb8f0784e024604a6c09d38882f0676.png


🌰栗子二

2cb8f0784e024604a6c09d38882f0676.png

为什么会产生上面的BUG呢?

就是因为我们的++操作不是一个整体,是一个由多个指令所组成的操作

解决方案:也是加锁:“synchronized”,意味着把这三条指令打包成了一组指令,然后把这一组指令看出成一条指令了,类似于数学里的“整体代换”思想。

82dc94c8208f479e86508569793a36be.png

首先我们要明白加锁操作都是针对某一个对象来进行的(加锁本质就是给对象头里设置个标记),加锁有以下几种形式

形式一、

6f7db6896d464b26a07d7eccc52b66dd.png

形式二、

package Thread;
class Counter {
    public static int count;
//    public synchronized void increase() {
//        ++count; 这两种写法视为是等价的
//    }
    public void increase() {
        synchronized (this) { // 这里this可以是任意对象,this可以有多个Counter counter1 = new Counter(), Counter counter2 = new Counter();
            ++count;
        }
    }
}
public class demo777 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter1.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter1.increase();
            }
            // 多个线程去调用这个increase方法,其实就是针对这个Counter对象counter1来进行加锁
            // 如果一个线程t1获取到了该对象counter1的锁,那么另一个线程t2就要等到counter1对应的锁开了后(t1线程执行完该锁里的内容——++操作)t2才能执行++操作
            // 此时++操作相当于是成为了一个整体(相当于一个指令,当一个线程再执行这个加锁的整体的指令的时候,另一个线程只能阻塞等待)
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join(); // 确保线程t1和线程t2都执行完了,main主线程再接着执行——输出count
        System.out.println(Counter.count); // 输出10000
    }
}

形式三、

018a085ce216489fbb08810cdaf4749a.png

5b3d92c51476453ba56468f8a16bf76d.png

当我们给不同的对象上锁后,如果用住房来比喻

不同的房间相当于是不同的对象,不同的线程相当于是不同的客人

如果房间1住了客人A,那么房间1就上了锁,客人B就需要等客人A不再住房间1(开了锁)然后客人B才能住房间1;或者客人B住其他的房间(其他的对象,没上锁的)

package Thread;
// 测试线程竞争,对锁的竞争
public class demo7777 {
    public static Object object1 = new Object();
    public static Object object2 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 针对object1对象进行加锁,加锁操作是针对某一个对象来进行的
            synchronized (object1) {
                System.out.println("t1线程start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1线程finish");
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            synchronized (object1) {  // 针对object1对象来进行加锁操作
                System.out.println("t2线程start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2线程finish");
            }
        });
        t2.start();
    }
}

c5a282fd9ba745d9baa072a4834e4339.png


3b5b44a9a8894c2c84acd50f70e1571d.png

我们上面就是两个线程t1和t2同时对object1这个对象进行了加锁,然后t1与t2直接就产生了竞争。从上述代码的实现过程中我们也可以看到,等到t1线程执行完了后,t2线程才开始执行。


但如果是两个线程对不同的对象进行加锁,则没有竞争(就像两个客人(两个线程)住不同的房间(不同的对象)当然不会发生竞争。

package Thread;
// 测试线程竞争,对锁的竞争
public class demo7777 {
    public static Object object1 = new Object();
    public static Object object2 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (object1) { // 针对object1对象来进行加锁,加锁操作是针对一个对象来进行的
                System.out.println("t1线程start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1线程finish");
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            synchronized (object2) { // 针对object2对象进行加锁
                System.out.println("t2线程start");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2线程finish");
            }
        });
        t2.start();
    }
}

1c2e26aa617846a49be3f3028124b543.png

对上面补充一下:

eebd49d636f44fffb0555a40c2614ef9.png


相关文章
|
3月前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
3月前
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
Java并发编程实战:使用synchronized关键字实现线程安全
60 0
|
25天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
16天前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
34 4
|
1月前
|
存储 安全 Java
Java-如何保证线程安全?
【10月更文挑战第10天】
|
1月前
|
安全 Java 编译器
Java 泛型深入解析:类型安全与灵活性的平衡
Java 泛型通过参数化类型实现了代码重用和类型安全,提升了代码的可读性和灵活性。本文深入探讨了泛型的基本原理、常见用法及局限性,包括泛型类、方法和接口的使用,以及上界和下界通配符等高级特性。通过理解和运用这些技巧,开发者可以编写更健壮和通用的代码。
|
2月前
|
安全 Java API
java安全特性
java安全特性
29 8
|
2月前
|
安全 Java 调度
Java 并发编程中的线程安全和性能优化
本文将深入探讨Java并发编程中的关键概念,包括线程安全、同步机制以及性能优化。我们将从基础入手,逐步解析高级技术,并通过实例展示如何在实际开发中应用这些知识。阅读完本文后,读者将对如何在多线程环境中编写高效且安全的Java代码有一个全面的了解。
|
2月前
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
68 11
|
2月前
|
安全 Java API
【本地与Java无缝对接】JDK 22外部函数和内存API:JNI终结者,性能与安全双提升!
【9月更文挑战第6天】JDK 22的外部函数和内存API无疑是Java编程语言发展史上的一个重要里程碑。它不仅解决了JNI的诸多局限和挑战,还为Java与本地代码的互操作提供了更加高效、安全和简洁的解决方案。随着FFM API的逐渐成熟和完善,我们有理由相信,Java将在更多领域展现出其强大的生命力和竞争力。让我们共同期待Java编程新纪元的到来!
99 11
下一篇
无影云桌面