【JavaEE】——线程的安全问题和解决方式

阿华代码,不是逆风,就是我疯,你们的点赞收藏是我前进最大的动力!!希望本文内容能够帮助到你!

目录

一:问题引入

二:问题深入

1:举例说明

2:图解双线程计算

​编辑

3:线程不安全原因的总结

(1)根本原因

(2)代码结构

(3)直接原因

(4)内存可见性问题

(5)指令重排序问题

5:解决问题的思路

(1)针对根本原因解决

(2)针对代码结构的原因解决

(3)针对直接原因------加锁

三:synchronized关键字(加锁)

1:synchronized

2:核心内容

(1)含义

(2)代码解释:

①"锁竞争"

②"加锁"

③"同一对象"

④"都要加锁"

3:变式

变式①:this

[变式②: 类名.class](#变式②: 类名.class)

变式③:public前加synchronized

变式④:static方法前加synchronized(少见)


一:问题引入

用多线程,让计数器自增到1_0000

java 复制代码
package thread;


public class ThreadDemon19 {
    private static long count = 0;

    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();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("双线程的计算结果是:"+count);
    }

}
java 复制代码
package thread;


public class ThreadDemon20 {
    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 1_0000; i++) {
                count ++;
            }
        });



        t1.start();
        t1.join();
        System.out.println("单线程的计算结果是:"+count);
    }

}

通过上述代码的举例,我们发现解决同一个问题,怎么最后的结果会不一样呢,真是奇了怪了。

二:问题深入

结果不一样,猜测是循环自增代码这一块出现问题

1:举例说明

我们知道cpu可以读取指令,解析指令,执行指令此处我们重点关注执行指令

count++,实际由三个指令构成的

(1)load:从内存中读取数据到cpu的寄存器当中

(2)add:把寄存器当中的值+1

(3)save:把寄存器当中的值写回到内存中

2:图解双线程计算

t1,t2双线程的运行下,可能同一次读取操作中,t1和t2都读取到的是没有自增的数

可以通俗的理解,本来t1由数字1自增后到2,t2读取的应该是2,然后自增到3.

但是如果t2 在 t1把自增后的2 save回寄存器中 之前 读取的话 t2读到的就是1,最后只能自增到2

(可以理解成被覆盖了)

所以这就出现了矛盾(计算的数据越小矛盾越小,因为cpu运行速度很快,可能第一个线程运行结束了,第二个线程还没有开始运行)

(以上可以画出无数种情况,比如t1线程自增了2次,3次,t2线程才执行了1次。)(这就是线程的随机调度和抢占式执行)

3:线程不安全原因的总结

(1)根本原因

是线程的"抢占式执行和随机调度"

(2)代码结构

多个线程可以修改同一变量

(3)直接原因

是上述多线程修改变量这一操作并不是"原子性"的,而是可拆分的(就像我们上面画的图),这里就是操作系统底层结构的问题了

(4)内存可见性问题

(5)指令重排序问题

(4)(5)条上述代码没有涉及,我们后续再详细引入

5:解决问题的思路

为了确保结果的正确,**我们得确保第一个线程save了过后,第二个线程再去load。**这时第二个线程load到的数据才是自增过后正确的数据

(1)针对根本原因解决

不可行。如果要修改线程的"抢占式执行和随机调度"这一机制的话,就得修改操作系统中的内核,相当于是重新写了一个"新的系统"

(2)针对代码结构的原因解决

有些地方,代码结构可以进行修改,但是有些地方不可以,视情况而论

(3)针对直接原因------加锁

count++,由三个指令完成,我们如果给这三个指令打包成一个整体的话就可以避免线程出现问题了,也就是"加锁"

三:synchronized关键字(加锁)

1:synchronized

关键字:synchronized(对象){加锁内容};

注:synchronized的加锁依赖于对象

2:核心内容

(1)含义

如果第一个线程对某个对象上锁之后,第二个线程要想对同一个对象上锁,就必须等第一个线程释放锁,此时第二个线程是处于BLOCKED阻塞状态

java 复制代码
package thread;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Hua YY
 * Date: 2024-09-21
 * Time: 15:27
 */
public class ThreadDemon21 {
    private static long count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();//创建一个object对象

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (locker){  //锁到object这个对象上
                    count++;
                }
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized(locker){   //锁到object这个对象上
                    count++;
                }

            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("计算结果是:" + count);
    }

}

(2)代码解释:

①"锁竞争"

通过"锁竞争"让让第二个线程无法插入到第一个线程的执行指令当中。

②"加锁"

"加锁"就是把count++中三个指令(load,add,save)打包成一个"原子性"的操作(最小单位的操作,再也不可分割了)

③"同一对象"

加锁的对象不同,"锁竞争"就不会发生,线程安全问题依旧存在

④"都要加锁"

如果第一个线程加锁,第二个线程不加锁,"锁竞争"也不会发生,线程安全问题依旧存在

3:变式

变式①:给this加锁

this指向的还是同一个对象,t1和t2依旧会产生"锁竞争"

java 复制代码
package thread;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Hua YY
 * Date: 2024-09-21
 * Time: 15:27
 */
class Test{
    public long count = 0;
    public void add(){
        synchronized (this){
            count++;
        }
    }

}
public class ThreadDemon22 {


    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                    test.add();

            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                    test.add();
            }
        });
        
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("计算结果是:" + test.count);
    }
}
变式②: 类名.class

获取到当前类的对象,类对象包含了这个类的各种信息(类名字,属性,方法.......)

变式③:public前加synchronized

等价写法

变式④:给类对象加锁(static)
static方法前加synchronized(少见)

如果synchronized加到static方法上,就相当于给类对象加锁了

相关推荐
捕鲸叉23 分钟前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer27 分钟前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq30 分钟前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml41 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~1 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616881 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7891 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java2 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山2 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
睡觉谁叫~~~2 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust