多线程安全与通信问题

线程安全问题

  • 当多个线程操作(读/写)同一份数据时,可能会出现线程安全问题
  • 进程的内存图:

    如图所示,在代码运行时,每一个线程并不会对堆内存中的变量本身进行操作,而是先复制一个副本放在本地变量表中(加载到自己的工作内存中),随后对这个副本进行操作,操作完毕后再将这个副本的内容赋给堆内存中的对象本体。
  • 出现线程安全问题的原因:CPU在进行操作时,并不是一个线程完全执行完在进行下一个,而是为了提高CPU的计算效率,防止CPU将大部分时间浪费在等待上,采用时间片轮转的方法,即每个线程执行一小段时间就换下一个的方式来计算。在这种情况下执行count++的操作时,线程1和线程2会对堆内存中的数据不停的进行修改,而且会不停的覆盖对方的计算结果,导致计算结果不准确,引发线程安全问题。
  • 代码模拟如下:
java 复制代码
package com.nwu.by0204_ThreadSafe;
class ThreadDemo2 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
         ThreadDemo1.count++;
        }
    }
}
public class ThreadDemo1 extends Thread{
    static int count=0;
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }
}
class Main{
    public static void main(String[] args){
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        threadDemo1.start();
        threadDemo2.start();
        try {
            Thread .sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(ThreadDemo1.count);
    }
}
  • 代码中用到的static关键字
    static意思是静态的,静态变量只在类加载的时候获取一次内存空间,因此代码中的任何对象的修改都会被保留。同时,静态变量的访问需要用类名来访问。
  • 代码中使用sleep方法的解释:
    线程的进行需要时间,在调用线程对象的start方法后,程序会立即打印输出语句,此时的线程还处于就绪状态,因此此时打印出来的count就是0。解决方案:使用sleep方法,让程序等待一秒在打印count的值
  • 运行结果如下:

    结果并非是预期的20000,这就是线程的安全问题。

线程安全问题的解决方案

  • 原子性:一个或多个操作,要么在执行时不会被打断,要么就不执行
  • 原子操作:不会被线程调度所打断的操作
  • 可见性:当多线程访问同一变量时,一个线程修改该变量的值,另一个线程能立刻看见修改的值
  • 为了保证可见性,可以给变量加一个修饰词volatile,加上这个关键字后,这个变量就具备了可见性
  • 为了保证线程安全,Java中有内置锁:synchronized 同步锁
    • synchronized(){ }
    • 参数 :必须是一个当前所有线程都可以访问的唯一对象
    • 当前线程在执行代码块中的内容时,其他所有线程必须等待,直到代码块中的内容执行完毕
    • 等代码块执行完成后会解锁,其他线程继续与该线程进行竞争
  • 代码修改方案:给两个线程的run方法里要执行的代码都加上一把synchronized同步锁即可,同时还需要在new一个静态的对象,让两个线程都可以访问
java 复制代码
package com.nwu.by0204_ThreadSafe;

class ThreadDemo2 extends Thread {
    @Override
    public void run() {
        synchronized (ThreadDemo1.obj) {
            for (int i = 0; i < 10000; i++) {
                ThreadDemo1.count++;
            }
        }

    }
}

public class ThreadDemo1 extends Thread {
    static int count = 0;
    static Object obj = new Object();

    @Override
    public void run() {
        synchronized (obj) {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        }

    }
}

class Main {
    public static void main(String[] args) {
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        threadDemo1.start();
        threadDemo2.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(ThreadDemo1.count);
    }
}
  • 执行结果

代码的最终优化

  • 在源代码中,为了保证线程执行完后在进行打印输出语句,采用的方法是调用Thread类中的sleep方法,让程序停一秒,等待线程执行。更规范的做法是采用join()方法
  • join方法是Thread类中的实例方法,它的作用是让其他线程都要等待这个线程执行完毕后在进行下一步操作,需要在start方法后执行。
  • 采用join方法而不是sleep方法的好处:sleep方法是我们人为的猜出一个等待方法,规定程序需要等待多少ms,而join方法是系统自己判断的,只要该线程执行完毕,就可以开始下一个线程,比认为规定更加精准、规范
  • 代码修改后:
java 复制代码
package com.nwu.by0204_ThreadSafe;

class ThreadDemo2 extends Thread {
    @Override
    public void run() {
        synchronized (ThreadDemo1.obj) {
            for (int i = 0; i < 100000; i++) {
                ThreadDemo1.count++;
            }
        }

    }
}

public class ThreadDemo1 extends Thread {
    static int count = 0;
    static Object obj = new Object();

    @Override
    public void run() {
        synchronized (obj) {
            for (int i = 0; i < 100000; i++) {
                count++;
            }
        }

    }
}

class Main {
    public static void main(String[] args) {
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        threadDemo1.start();
        threadDemo2.start();
        try {
            threadDemo1.join();
            threadDemo2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(ThreadDemo1.count);
    }
}

在修改后的代码中,只有把threadDemo1和threadDemo2两个线程都执行完才会进行下一步(两个线程是并行等待的),也就是执行输出语句。比人为规定sleep时间更精准、更规范

  • 运行截图
相关推荐
Elastic 中国社区官方博客2 分钟前
Kubernetes attributes processor v1:它对 EDOT Collector 意味着什么
java·大数据·elasticsearch·搜索引擎·贪心算法·kubernetes·全文检索
Sarvartha7 分钟前
对象创建与引用
java·开发语言
大侠归来42 分钟前
C语言与C++在基础语法上的区别
java·c语言·c++
小宋10211 小时前
SQL + RAG 混合问答实战:结构化指标与文档证据如何统一路由
java·jvm·人工智能·sql
bro_Java6661 小时前
链表进阶2:双向链表的构建
java·数据结构·链表
DianSan_ERP2 小时前
多平台订单自动下载与回传的技术实现:从消息推送到状态闭环引言
java·linux·服务器·前端·网络·架构·自动化
Joe_Wang52 小时前
【从0到1学习JVM · 13】点进JDK源码只有一个分号?搞懂本地方法栈与JNI机制
java·jvm·学习·本地方法栈
AI 算法大模型备案~当当2 小时前
各地备案数量怎么看:一份属地公告的认读与台账方法
java·数据库·人工智能
智鸟科技GemeOpen开发者智能设备2 小时前
GemeOpen 智能音箱 GSSM0B - 播放控制(Java示例)
java·开发语言·智能音箱
今天的砖头有点烫手啊2 小时前
Java IO/NIO/AIO 演进:从 BIO 到 Netty 的底层逻辑
java·jvm·nio