多线程-进阶

news2024/10/5 13:09:24

常见的锁策略

乐观锁和悲观锁
这不是两把具体的锁, 这是两类锁

乐观锁: 预测锁的竞争不是很激烈
悲观锁: 预测锁的竞争会很激烈

乐观和悲观说的都不是绝对的, 唯一的区分就是看预测锁竞争激烈程度的结论, 这两种锁的背后工作是截然不同的,

轻量级锁和重量级锁

轻量级锁加锁解锁开销比较小, 效率更高
重量级锁加锁解锁开销比较大, 效率更低

在大多数情况下,

乐观锁, 是一个轻量级锁(不能完全保证)
悲观锁, 是一个重量级锁(不能完全保证)

自旋锁和挂起等待锁

自旋锁是一种典型的轻量级锁
挂起等待锁是一种典型的重量级锁

自旋锁: 一旦锁释放了,自旋锁能第一时间感知到, 从而有机会获取到锁, 但是会占用大量的系统资源
挂起等待锁: 获取锁的实际可能会迟, 但是它把CPU省下来了

互斥锁和读写锁

互斥锁: 像 synchronized 这样的锁 提供加锁和解锁两个操作如果一个线程被加锁了, 另一个线程尝试加锁就会阻塞等待

读写锁: 提供了三个操作:
1, 针对读加锁,
2. 针对写加锁
3. 解锁

这里的读加锁, 写加锁, 基于一个事实:多线程针对同一个变量并发读,这个是没有线程安全的, 也不需要加锁控制, 读写锁就是针对这种情况锁采取的特殊处理

读锁和读锁之间没有互斥
写锁和写锁之间存在互斥
写锁和读锁之间存在互斥

当前代码中, 如果只是读操作, 加读锁就可以了, 如果有写操作就加写锁.
假设当前有一组线程都去读(加读锁), 这些线程是没有锁竞争的, 也没有安全问题, 又快又准.
假设当前有一组线程既有读又有写, 这才会产生锁竞争, 其实在很多实际开发中, 读操作非常高频, 比写操作多很多. 这样用写锁, 就会提高效率.

公平锁锁和非公平锁

那么什么是公平呢?
此处把公平定义为"先来后到"

公平锁: 当一个资源解锁后, 就由等待队列中最早的来上位
非公平锁: 当一个资源解锁后, 所有等待的都一起来竞争, 各凭本事

操作系统和Java中 synchronized 原生都是"非公平锁"
操作系统这里针对加锁的控制, 本身就是依赖线程调度顺序的, 这个调度是随机的, 不会考虑这个线程等了多长时间.
要实现公平锁, 就得在这个基础上, 引入一些额外的东西(引入一个队列, 让这些加锁的线程去排队)

可冲入锁和不可重入锁

可重入锁: 一个线程对一把锁, 连续加锁多次都不会死锁
不可冲入锁: 一个线程对一把锁, 连续加锁两次出现死锁

synchronized

  1. synchronized 既是一个悲观锁, 也是一个乐观锁
    synchronized 默认是一个乐观锁, 但是如果发现当前锁竞争比较激烈, 就会变成悲观锁

  2. synchronized 既是一个轻量级锁, 也是一个重量级锁
    synchronized默认是一个轻量级锁, 如果发现锁竞争比较激烈, 就会转换成重量级锁

  3. synchronized 这里的轻量级锁是基于自旋锁的方式实现的
    synchronized 这里的重量级锁是基于挂起等待锁的方式实现的

  4. synchronized 不是读写锁

  5. synchronized 是非公平锁

  6. synchronized 是可冲入锁

总结: 上面说的6种锁策略, 可以视为"锁的形容词"

CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V。(交换)
  3. 返回操作是否成功。

那么上述的CAS过程, 就是这样一个简单的交换, 有什么特别之处呢?

这里最特别的地方就是, 这整个 CAS 的过程, 并非是通过一段代码来实现的, 而是通过一条CPU指令来完成的, 也就是说CAS是原子的. 这样就可以在一定程度上回避线程安全问题.

这样的话, 咱们得线程安全问题除了加锁外, 就又有了一个新的方向.

总结:CAS 可以理解是 CPU 给咱们提供的一个特殊指令, 通过这个特殊指令, 就可以一定程度的处理线程安全问题.

**CAS 的应用场景 **

  1. 实现原子类
    Java 标准库中提供的类
		//定义了一个原子的变量count
        AtomicInteger count = new AtomicInteger(0);

下面举一个例子:
通过这个原子的变量, 来实现两个线程对同一个变量的++操作

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo12 {
    public static void main(String[] args) throws InterruptedException {
        // 这些原子类就是基于 CAS 实现了自增自减等操作, 不用加锁也是线程安全的.
        AtomicInteger count = new AtomicInteger(0);

        // 使用原子类来解决线程安全问题
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // 因为 Java 不支持运算符重载, 所以只能通过普通方法来表示自增自减
                count.getAndIncrement();   // count++
                //count.incrementAndGet();   // ++count
                //count.getAndDecrement();   // count--
                //count.decrementAndGet();   // --count
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();   // count++
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

CAS的典型问题: ABA问题

CAS 在运行中的核心, 检查 value 和 oldValue 的值, 如果一致, 就是为没有被修改过, 就进行下一步操作.

那么这里的一致, 可不可能是被修改过, 又还原回来了呢, 这是有可能的

把 value 的值设置为A,
CAS 判定 value 为 A , 此时可能确实 value 始终是A
但是也可能 value 的值本来是 A , 被改成了 B , 又被还原成了A…

那么这如何应对呢?
那就是加版本号(版本号直接增长, 不能降低)

Synchronized 原理
两个线程针对同一个线程加锁, 就会产生阻塞等待.
synchronized 内部其实还有一些优化机制, 目的就是为了让这个锁更加高效, 更好用.

  1. 锁升级/锁膨胀
  1. 无锁
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

synchronized {
}

当代码执行到这个代码块中, 加锁过程, 就可能会经历这几个阶段

进行加锁的时候, 首先会进入偏向锁 状态, (并不是真正的加锁, 而只是占个位置, 有需要再真正加锁, 没需要就算了)

这个偏向锁的过程就像是"懒汉模式"中提到的"懒加载"一样, “非必要, 不加锁”.
synchronized 的时候, 并不是真的加锁, 而是线偏向锁状态, 做个标记, (这个过程非常轻量), 如果使用锁的整个过程中, 都没有出现锁竞争, 在 synchronized 执行完之后, 取消偏向即可.
但是, 如果使用过程中, 另一个线程也尝试加锁, 在它加锁之前, 迅速的把偏向锁升级为真正的加锁状态, 另一个线程也就只能阻塞等待了.

这样做的好处就是:如果没有锁冲突, 开销就最低了

当 synchronized 发生锁竞争的时候, 就会从偏向锁, 升级为轻量级锁, 此时, synchronized相当于是通过自旋的方式来进行加锁,

如果要是别人很快就释放锁了, 自旋是很划算的, 但是如果迟迟拿不到锁, 一直自旋, 并不划算.
synchronized自旋不是无休止的自旋, 自旋到一定程度之后, 就会再次升级成 重量级锁(挂起等待锁)

重量级锁(挂起等待锁) 则是基于操作系统原生的 API 来加锁了. linux 原生提供了 mutex 一组 API . 操作系统内核提供的加锁功能, 这个锁就会影响到线程的调度了
此时, 如果线程进行了重量级的加锁, 并且发生了锁竞争, 此时线程就会放到阻塞队列中, 暂时不参与 CPU 调度了, 知道锁被释放了, 才有机会被调度到. 并且有机会获取到锁.(一旦当前线程被切换出 CPU 了, 这就是个比较低效的事了)

锁消除

编译器智能的判定, 看当前代码是否需要真的加锁, 如果这个场景不需要加锁, 程序员加锁了, 就自动把锁给消除了.

StringBuffer , 关键方法都带有synchronized
但是在单线程中使用, StringBuffer , 此时就是 synchronized加了也白加, 编辑器会自动把这些加锁操作给干掉.

锁粗化

锁的粒度, synchronized 包含的代码越多, 粒度就越粗, 包含的代码越少, 粒度就越细

通常情况下, 认为锁的粒度细一点比较好, 因为加锁的代码, 是不能并发执行的, 锁的粒度越细, 能并发的代码就越多; 反之就越少, 但是有的情况下, 锁的粒度粗一些, 反而更好.

如果两次加锁之间, 间隙非常小, 这种情况就不如一次大锁直接搞定

JUC(java.util.concurrent) 的常见类

Callable 接口

Callable 是什么?
Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序员借助多线程的方式计算结果.
Callable 和 Runnable 相当, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为
Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
FutureTask 就可以负责这个等待结果出来的工作.

下面举个例子
使用 Callable 计算1+2+3+…+1000

  • 创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
  • 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
  • 把 callable 实例使用 FutureTask 包装一下.
  • 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
  • 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo13 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 使用 Callable 计算1+2+3+...+1000
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        // 想当于一个小票, 使得返回给谁不会出错```
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();

        // get 方法就是获取结果, get会发生阻塞, 直到Callable 执行完毕, get才阻塞完毕, 才获取结果
        Integer result = futureTask.get();
        System.out.println(result);
        
    }
}

ReentrantLock

ReentrantLock 也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”
synchronized 是直接基于代码块来加锁解锁的
ReentrantLock 更传统, 使用 lock 和 unlock 方法加锁解锁

ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();

        reentrantLock.unlock();

这样就带来一个问题, reentrantLock 可能执行不到
比如说, 这加锁的代码块中, 存在 return, 或者 异常, 就可能导致 unlock 执行不到

那么如何解决这个问题呢?
那就是使用把 unlock放到 finely 中

lock.lock(); 
try {  
// working  
} finally {  
lock.unlock()  
} 

那这个锁有什么优势呢?

  1. ReentrantLock 提供了公平锁版本的实现
 ReentrantLock reentrantLock = new ReentrantLock(true);
  1. 对于 synchronized 来说, 提供的加锁方式就是死等, 只要获取不到锁, 就一直阻塞等待.
    而 ReentrantLock 提供了更灵活的 等待方式.
        reentrantLock.tryLock();

这个是无参数版本, 能加上锁就加, 不能加上就放弃.

        reentrantLock.tryLock(5, TimeUnit.SECONDS);

这是有参数版本, 制定了超时时间, 加不上锁就等待一会, 如果时间到了也没等到就放弃

  1. ReentrantLock 提供了一个更强大, 更方便的等待通知机制
    synchronized 搭配的是 wait, notify , notify 的时候是随机唤醒一个wait 的线程
    ReentrantLock 搭配一个Condition 类. 进行唤醒的时候可以唤醒指定的线程

结论: 虽然 ReentrantLock 有一定的优势, 但是在实际开发中大部分还是使用 synchronized.

原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个

  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

以 AtomicInteger 举例,常见方法有

方法操作
addAndGet(int delta);i += delta;
decrementAndGet();–i;
getAndDecrement();i–;
incrementAndGet();++i;
getAndIncrement();i++;

基于CAS, 确实更高效的解决了线程安全, 但是CAS不能代替锁, CAS适用范围是有限的, 不像锁适用范围那么广

信号量 Semaphore

信号量本质是就是一个 “计数器” ,描述了可用资源的个数

p操作: 申请一个可用资源, 计数器就要 -1;
v操作: 释放一个可用资源, 计数器就要 +1;

p操作如果要是计数器为0了, 继续p操作, 就会阻塞等待

锁可以视为计数器为1的 信号量, 二元信号量, 锁是信号量的一种特殊情况

/**
 * @describe
 * @author chenhongfei
 * @version 1.0
 * @date 2023/10/21
 */
package thread;

import java.util.concurrent.Semaphore;

public class ThreadDemo15 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        semaphore.acquire();
        System.out.println("执行一次p操作");
        semaphore.acquire();
        System.out.println("执行一次p操作");
        semaphore.acquire();
        System.out.println("执行一次p操作");
        semaphore.acquire();
        System.out.println("执行一次p操作");
    }
}

最后一次p操作就阻塞等待了

代码中也是可以Semaphone 来实现类似锁的效果, 来保证线程安全

多线程使用哈希表

HashMap 是线程不安全的
HashTable 是线程安全的(给一些关键方法加了 synchronized)

更推荐使用 ConcurrentHashMap 更优化的线程安全哈希表

ConcurrentHashMap 进行了哪些优化, 比HashTable 好在哪里? 和 HashTable 的区别是啥

1. 最大的优化之处: ConcurrentHashMap 相比于 HashTable 大大缩小了锁冲突的概率. (把一把大锁, 转化为了多把小锁)
HashTable 做法是直接在方法上加 synchronized, 相当于是给 this 加锁,
只要操作哈希表上的任意元素, 都会产生加锁, 也就都可能发生锁冲突

ConcurrentHashMap 做法是, 每个链表有各自的的锁, (不是一起共用一个锁了)
具体来说, 就是使用每个链表的头结点, 作为锁对象, (两个线程针对同一个锁对象进行加锁, 才会产生锁竞争, 才会阻塞等待, 针对不同的对象, 就没有锁竞争了)(JDK1.8 及其以后版本是这样, 在JDK1.7和之前是使用"分段锁")
在这里插入图片描述
在这里插入图片描述

2. ConcurrentHashMap 做了一个激进的操作

针对读操作不加锁, 只针对写加锁

读和读 之间没有冲突
写和写 之间有冲突
读和写 之间也没有冲突

这样就会出现一个问题, 很多场景下, 读写之间不加锁控制, 可能会读到一个写了一半的结果, 这里ConcurrentHashMap 做的优化是通过 volatile + 原子的写操作 来控制

3. ConcurrentHashMap 内部也充分的使用了CAS , 通过这个进一步来削减加锁的数目

4. 针对扩容, 采取了"化整为零"的方式

HashMap 和 HashTable 扩容是创建一个更大的数组空间, 把旧的数组上的链表上的每个元素搬运到新的数组上(删除+插入), 这个操作会在某次的put的时候触发
如果元素特别多, 就会导致这个搬运操作就会非常耗时, 就会出现某次put 比平时put 卡很多倍

ConcurrentHashMap 中, 采取的是每次搬运一小部分元素的方式,
创建一个新的数组, 旧的数组也保留
每次put操作, 都往新数组上添加, 同时搬运一小部分(把一部分旧的元素搬运到新的数组上)
每次get操作, 旧数组和新数组都查询
每次remove操作, 删了就行

经过一定时间后, 所有元素都搬运好了, 最终在释放旧元素

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1118048.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浅谈人工智能视频分析技术的原理及行业场景应用

人工智能视频分析技术是利用计算机视觉、模式识别和深度学习算法等技术&#xff0c;对视频数据进行自动化处理和分析的过程。其基本工作原理包括以下几个步骤&#xff1a; 视频采集&#xff1a;通过摄像头或其他视频设备获取源视频数据。 视频预处理&#xff1a;对视频进行去噪…

如何更改eclipse的JDK版本

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、有时候导入一些网上的资源需要更换JDK二、使用步骤1. 总结 一、有时候导入一些网上的资源需要更换JDK 具体操作如下 二、使用步骤 1. 在eclipse上方工具栏找…

使用列表实现向量运算

向量内积 from operator import mul x [4, 6, 9] y [2, 2, 7] print(sum(map(mul, x, y)))相当于 x [4, 6, 9] y [2, 2, 7] print(sum((i*j for i, j in zip(x, y))))两个等长的向量对应元素相加 from operator import add x [4, 6, 9] y [2, 2, 7] print(list(map(ad…

一些经典的神经网络(第20天)

1. 经典神经网络&#xff08;LeNet&#xff09; LeNet是早期成功的神经网络&#xff1b; 先使用卷积层来学习图片空间信息 然后使用全连接层来转到到类别空间 【通过在卷积层后加入激活函数&#xff0c;可以引入非线性、增加模型的表达能力、增强稀疏性和解决梯度消失等问题…

基于RSSI的室内wifi定位系统 计算机竞赛

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; wifi室内定位系统 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;…

【经历】在职8个月->丰富且珍贵

在职8个月->丰富且珍贵 2021-3~2021-11&#xff1a;面试进入一家做400电话的公司&#xff0c;我进入公司时&#xff0c;加上我只有四个人(老板、人事、业务)&#xff0c;开发只有我&#xff0c;所以&#xff1a;产品~设计~前端~后端~测试~上线~维护~培训&#xff0c;只有我自…

并发性Socket通信源码(基于linux环境下多线程)

服务器端&#xff1a;server.c 1 #include <stdio.h>2 #include <stdlib.h>3 #include <unistd.h>4 #include <string.h>5 #include <arpa/inet.h>6 #include <pthread.h>7 void* working(void *arg);8 //信息结构体9 struct sockinfo10 …

烘焙蛋糕外卖小程序商城的作用是什么

随着经营成本上升及电商业态的冲击&#xff0c;传统烘焙蛋糕门店商家经营止步不前&#xff0c;加之口罩原因更是雪上加霜&#xff0c;引流拓客、经营转化复购大幅度降低&#xff0c;而线上又因外卖平台间的激烈竞争&#xff0c;导致中小烘焙蛋糕商家进退两难。 烘焙蛋糕店经营…

PO模式在selenium自动化测试框架的优势

大家都知道po模式可以提高代码的可读性和减少了代码的重复&#xff0c;但是相对的缺点还有&#xff0c;今天通过本文一起学习下PO模式在selenium自动化测试框架的优势&#xff0c;需要的朋友可以参考下 PO模式简介 1.什么是PO模式 PO模型是:Page Object Model的简写 页面对象…

使用 Sealos 一键私有化部署 Serverless 框架 Laf

太长不看&#xff1a;Laf 上架了 Sealos 的模板市场&#xff0c;通过 Laf 应用模板即可一键部署&#xff01; Laf 是一个完全开源的项目&#xff0c;除了使用公有云之外&#xff0c;还有大量的用户选择私有化部署 Laf。然而&#xff0c;私有化部署通常伴随着更多的复杂性和门槛…

java 实现定时任务

1、EnableScheduling spring自带的定时任务功能&#xff0c;使用比较简单方便&#xff1a; 1、需要定时执行的方法上加上Scheduled注解&#xff0c;这个注解中可以指定定时执行的规则&#xff0c;稍后详细介绍。 2、Spring容器中使用EnableScheduling开启定时任务的执行&…

解决找不到VCRUNTIME140_ 1.dll问题的5个方法分享

近日&#xff0c;许多用户在运行某些软件时遇到了“由于找不到VCRUNTIME140_1.dll无法继续执行此代码”的问题。这个错误通常出现在运行某些软件或游戏时&#xff0c;提示找不到必要的动态链接库文件。本文将详细VCRUNTIME140_ 1.dll文件是什么&#xff0c;并分享如何解决这个问…

1658.将x减到0的最小操作数(滑动窗口)

目录 一、题目 二、分析代码 一、题目 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 二、分析代码 class Solution { public:int minOperations(vector<int>& nums, int x) {int _MaxLength INT_MIN;int _sum 0;int tmp 0;for (aut…

数据回归算法 | Python逻辑回归

逻辑回归是一种经典的机器学习算法,用于解决二分类问题。 它常被用于预测某个事件发生的概率,通过将输入特征映射到一个概率值来进行分类。 简单聊聊 逻辑回归就像一位智能的侦探,根据一些线索(输入特征)判断某件事情是否会发生。 它将这些线索组合起来,计算出一个关于…

洛谷 P1216 [USACO1.5] [IOI1994]数字三角形题解

观察题目我们发现从前往后推会有条件判断&#xff0c;不容易写出来。所以就从后往前推。 也就是说后面的状态已经是推出来了&#xff0c;保证是最大值。 //数字三角形 #include<iostream> using namespace std; const int N 510; int f[N][N], n;int main() {ios::sync…

利用Bootstrap的面包屑组件实现面包屑层次分级导航效果

目录 01-相关基础知识02-一个简单的示例03-改变面包悄中的层级分隔符 01-相关基础知识 可以用类breadcrumb实现面包屑层次导航效果。 当使用 Bootstrap 构建网页时&#xff0c;breadcrumb 类用于创建面包屑导航&#xff08;breadcrumb navigation&#xff09;&#xff0c;这是…

在 RN 中构建自适应 UI

移动开发的世界在不断变化&#xff0c;随之而来的是对能够适应任何设备或方向的用户界面的需求。React Native 提供了一套丰富的工具来构建这样的需求。 在本文中&#xff0c;我们将探讨如何在 React Native 中设计响应式和自适应 UI&#xff0c;重点关注不同的设备尺寸、方向…

yakit使用爆破编码明文_dnslog使用

yakit使用爆破编码密码 文章目录 yakit使用爆破编码密码yakit使用1 yakit编码密码进行爆破2 准备eval.php文件放入web3 访问http://192.168.225.206/eval.php,使用bp抓包,测试后环境准本好4 使用yakit4.1 进入页面&#xff0c;点击这里进行配置默认端口80834.2 发送到模糊测试4…

Qt 官方文档及阅读方法

文章目录 选择 All Qt Modules 查找模块选择 C Classes 查看该模块的所有的类当前类说明文档 QT 官方文档参考&#xff1a;https://doc.qt.io/qt-5 选择 All Qt Modules 查找模块 选择 C Classes 查看该模块的所有的类 当前类说明文档 包括 属性公共函数重新实现的公共功能公…

如何正确停止线程?为什么 volatile 标记位的停止方法是错误的?

Java全能学习面试指南&#xff1a;https://javaxiaobear.cn 今天我们主要学习如何正确停止一个线程&#xff1f;以及为什么用 volatile 标记位的停止方法是错误的&#xff1f; 首先&#xff0c;我们来复习如何启动一个线程&#xff0c;想要启动线程需要调用 Thread 类的 start…