吃透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


目录
打赏
0
0
0
0
11
分享
相关文章
Java智慧工地(源码):数字化管理提升施工安全与质量
随着科技的发展,智慧工地已成为建筑行业转型升级的重要手段。依托智能感知设备和云物互联技术,智慧工地为工程管理带来了革命性的变革,实现了项目管理的简单化、远程化和智能化。
55 5
Java并发编程实战:使用synchronized关键字实现线程安全
Java并发编程实战:使用synchronized关键字实现线程安全
104 0
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
116 3
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
121 2
java 中 i++ 到底是否线程安全?
Java 异常处理:筑牢程序稳定性的 “安全网”
本文深入探讨Java异常处理,涵盖异常的基础分类、处理机制及最佳实践。从`Error`与`Exception`的区分,到`try-catch-finally`和`throws`的运用,再到自定义异常的设计,全面解析如何有效管理程序中的异常情况,提升代码的健壮性和可维护性。通过实例代码,帮助开发者掌握异常处理技巧,确保程序稳定运行。
88 2
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
【JavaEE】——线程的安全问题和解决方式
【JavaEE】——线程的安全问题和解决方式。为什么多线程运行会有安全问题,解决线程安全问题的思路,synchronized关键字的运用,加锁机制,“锁竞争”,几个变式
|
5月前
|
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
87 4
Java 泛型深入解析:类型安全与灵活性的平衡
Java 泛型通过参数化类型实现了代码重用和类型安全,提升了代码的可读性和灵活性。本文深入探讨了泛型的基本原理、常见用法及局限性,包括泛型类、方法和接口的使用,以及上界和下界通配符等高级特性。通过理解和运用这些技巧,开发者可以编写更健壮和通用的代码。