2026/1/2 3:57:08
网站建设
项目流程
二手房交易网站排行,网络工程师是什么专业,互联科技行业网站,做环保网站案例分析目录
一、体会线程安全问题
二、线程安全的概念
三、线程安全问题的原因
四、解决线程安全问题的方法
4.1 synchronized 关键字 一、体会线程安全问题
当我们编写一个多线程程序#xff0c;要求两个线程对同一个变量#xff08;共享变量#xff09;进行修改#xff0…目录一、体会线程安全问题二、线程安全的概念三、线程安全问题的原因四、解决线程安全问题的方法4.1 synchronized 关键字一、体会线程安全问题当我们编写一个多线程程序要求两个线程对同一个变量共享变量进行修改得到的结果是否与预期一致创建两个线程分别对共享变量count进行自增5万次操作最后输出的结果理论上应为10万但是实际上输出的结果是一个小于10万且不确定的数。读者可以自行实现一下该多线程程序运行后看看结果是否符合预期。public class Demo14_threadSafety { private static int count 0; public static void main1(String[] args) { Thread t1 new Thread(() - { for (int i 0; i 50000; i) { count; } System.out.println(t1-结束); }); Thread t2 new Thread(() - { for (int i 0; i 50000; i) { count; } System.out.println(t2-结束); }); t1.start(); t2.start(); // 理论上输出的结果应是100000实际输出的结果是0 // 原因是主线程 main 运行太快了当 t1 和 t2 线程还在计算时主线程已经打印结果、运行完毕了 System.out.println(count); } // 让主线程等待 t1 和 t2 线程等到它们两个都执行完成再打印故使用 join 方法 public static void main2(String[] args) throws InterruptedException { Thread t1 new Thread(() - { for (int i 0; i 50000; i) { count; } System.out.println(t1-结束); }); Thread t2 new Thread(() - { for (int i 0; i 50000; i) { count; } System.out.println(t2-结束); }); t1.start(); t2.start(); // 在主线程中通过 t1 和 t2 对象调用 join 方法 // 表示让主线程 main 等待 t1 线程和 t2 线程 t1.join(); t2.join(); // 当两个线程都执行完毕后主线程再继续执行打印操作 System.out.println(count); // 实际输出的结果小于100000仍不符合预期 } }二、线程安全的概念通过上面的一个例子想必读者已经体会到线程安全问题了吧那究竟什么是线程安全问题呢其原因是什么如何解决线程安全问题呢不要急且听小编慢慢道来如果在多线程环境下运行的程序其结果符合预期或与在单线程环境下运行的结果一致就说这个程序是线程安全的否则是线程不安全的。上面的例子在单线程环境下运行——比如来两个循环对共享变量进行自增操作那么结果是符合预期的但是在多线程环境下运行就不符合预期。因此该程序是线程不安全的也可以说该程序存在线程安全问题。三、线程安全问题的原因究竟是哪里出问题导致程序出现线程安全问题呢究其根本罪魁祸首是操作系统的线程调度有随机性/抢占式执行。由于操作系统的线程调度是有随机性的这就会存在这种情况某一个线程还没执行完呢就调度到其他线程去执行了从而导致数据不正确。当然了一个巴掌拍不响还有以下三个导致线程不安全的原因原子性指 Java 语句一条 Java 语句可能对应不止一条指令若对应一条指令就是原子的。可见性一个线程对主内存共享变量的修改可以及时被其他线程看到。有序性一个线程观察其他线程中指令的执行顺序由于 JVM 对指令进行了重排序观察到的顺序一般比较杂乱。因其原理与 CPU 及编译器的底层原理有关暂不讨论。之前的例子就是由于原子性没有得到保障而出现线程安全问题public static void main2(String[] args) throws InterruptedException { Thread t1 new Thread(() - { for (int i 0; i 50000; i) { count; } System.out.println(t1-结束); }); Thread t2 new Thread(() - { for (int i 0; i 50000; i) { count; } System.out.println(t2-结束); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); }1. “count ” 这条语句对应多条指令读取数据、计算结果、存储数据。2. t1 线程和 t2 线程分别执行一次“count”语句期望的结果是“count 2”其过程如下初始情况当线程执行“count ”时总共分三个步骤load、update、save由于线程调度的随机性/抢占式执行可能会出现以下情况可能出现的情况有很多种这里只是其中一种这时候 t1 正在执行“count ”这条语句执行了“load 和 update”指令后t1 的工作内存寄存器存着更新后的值但是还未被写回内存中接着调度到 t2 线程并开始执行“count ”语句并且语句中包含的三条指令都执行。此时由于 t1 更新后的 count 的值还未写回内存因此 t2 执行 load 操作所获取到的 count 仍是 0。接着 t2 执行 update 和 save 指令当 t2 执行完成内存的 count 已被修改为 1 。此时调度回 t1 线程并继续执行 save 指令但是 t1 线程寄存器中 count 的值也是 1 此时写回内存更新后 count 的值依然是 1 。结果 count 1与预期的 count 2 不符因此存在线程安全问题其原因是操作系统的随机线程调度和 count 语句存在非原子性。四、解决线程安全问题的方法从上面的例子我们知道当一条语句的指令被拆开来执行的话是存在线程安全问题的但是当我们将“count ”这条语句的三个指令都放在一起执行怎么样当线程调度的情况如下此时 t1 线程开始执行“count ”语句的 load、update 和 save 指令。内存中的 count 为 0t1 读取到内存中的 count 之后更新至 1 并写回内存中。当 t1 执行完成后内存的 count 由 0 更新至 1接着调度至 t2 线程开始执行“count ”语句的 load、update 和 save 指令。经过更新后内存中的 count 为 1此时 t2 读取 count 并更新为 2然后写回内存中。当 t2 执行完成内存中的 count 就更新成 2 了可以发现结果与预期相符说明这个方法可行。可以将操作顺序改成先让 t1 线程完成“count ”操作再让 t2 线程完成该操作——即串行执行。现在我们对之前的例子进行优化// 可以试着让 t1 线程先执行完后再让 t2 线程执行改成串行执行 public static void main3(String[] args) throws InterruptedException { Thread t1 new Thread(() - { for (int i 0; i 50000; i) { count; } System.out.println(t1-结束); }); Thread t2 new Thread(() - { for (int i 0; i 50000; i) { count; } System.out.println(t2-结束); }); t1.start(); t1.join(); t2.start(); t2.join(); System.out.println(count); }刚刚是让一个线程一次性执行“count ”这条语句的三个指令也就是说我们是通过这样操作将原本是非原子的三条指令打包成了一个原子指令即执行过程中不可被打断——调度走。这样就有效的解决了线程安全问题。而上述的操作其实就是 Java 中的加锁操作。当一个线程执行一个非原子的语句时通过加锁操作可以防止在执行过程中被调度走或被其他线程打断若其他线程想要执行该语句则要进入阻塞等待的状态当线程执行完毕并将锁释放操作系统这时唤醒等待中的线程才可以执行该语句。就相当于上厕所当厕所内没有人时没有线程加锁就可以使用当厕所内有人时已经有线程加锁了那么就必须等里面的人出来后才能使用。注意前一个线程解锁之后并不是后一个线程立刻获取到锁。而是需要靠操作系统唤醒阻塞等待中的线程的。若 t1、t2 和 t3 三个线程竞争同一个锁当 t1 线程获取到锁t2 线程再尝试获取锁接着 t3 线程尝试获取锁此时 t2 和 t3 线程都因获取锁失败而处于阻塞等待状态。当 t1 线程释放锁之后t2 线程并不会因为先进入阻塞状态在被唤醒后比 t3 先拿到锁而是和 t3 进行公平竞争。不遵循先来后到原则4.1 synchronized 关键字加锁 / 解锁这些操作本身是在操作系统所提供的 API 中的很多编程语言对其进行了封装Java 中使用 synchronized 关键字来进行加锁 / 解锁操作其底层是使用操作系统的mutex lock来实现的。Java 中的任何一个对象都可以用作“锁”。synchronized 锁对象{—— 进入代码块相当于加锁操作// 一些需要保护的逻辑}—— 出了代码块相当于解锁操作当多个线程针对同一个锁对象竞争的时候加锁操作才有意义。对之前的例子进行加锁操作public class Demo15_synchronized { private static int count 0; public static void main1(String[] args) throws InterruptedException { Object locker new Object(); Thread t1 new Thread(() - { for (int i 0; i 50000; i) { synchronized (locker) { count; } } System.out.println(t1-结束); }); Thread t2 new Thread(() - { for (int i 0; i 50000; i) { synchronized (locker) { count; } } System.out.println(t2-结束); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }synchronized 关键字用来修饰普通方法时相当于给this加锁synchronized 关键字用来修饰静态方法时相当于给类对象加锁。于是可以使用另一种写法// 写法二 // 将 count 所包含的三个操作封装成一个 add 方法 // 使用 synchronized 修饰 add 方法 class Counter { private int count 0; synchronized public void add () { // synchronized 修饰普通方法相当于给 this 加锁 count; } // 相当于 // public void add () { // synchronized (this) { // count; // } // } public int get () { return count; } } public static void main(String[] args) throws InterruptedException { Counter counter new Counter(); Thread t1 new Thread(() - { for (int i 0; i 50000; i) { counter.add(); } }); Thread t2 new Thread(() - { for (int i 0; i 50000; i) { counter.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.get()); }这样一来就成功解决了多线程程序中的线程安全问题。今天暂且到这吧~完