【JavaEE】线程安全与线程状态

作者主页:paper jie_博客

本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。

本文于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造,将MySQL基础知识一网打尽,希望可以帮到读者们哦。

其他专栏:《MySQL》《C语言》《javaSE》《数据结构》等

内容分享:本期将会分享线程安全与线程状态~

目录

 线程状态

线程的所有状态

状态的意义

状态图

查看状态

线程安全

什么是线程安全

经典栗子

原因

导致线程不安全的原因

解法方法

加锁 - synchronized

加锁如何操作

加锁后的代码

注意

内存可见性问题

经典栗子

原因

解决方法

Java中锁的特性

互斥性

可重入性

死锁

死锁问题的常见三种情况

解决方法

线程的通知等待 - wait和notify

wait方法

wait的使用

notify方法

注意

wait和sleep的区别

Java标准库中的线程安全类

线程不安全类

线程安全类


 线程状态

线程的所有状态

1. NEW Thread对象创建好了,但还没有调用start()去系统中创建线程

2. RUNNABLE 调用了start(),线程正在执行或者准备就绪随时准备被调度

3. TERMINATED Thread对象还在,但是系统中的线程已经执行完销毁了.

4. TIMED_WAITING 有时间现在的堵塞状态,到达一定时间会解除堵塞

5. WATING 死等的堵塞状态,需要达到一定的条件才会解除堵塞

6. BLOCKED 由于锁竞争引起的堵塞

状态的意义

状态存在的最大用处就是我们去调试多线程出现的bug时会给我们提供很大的参考意义.比如: 程序卡住了,那可能就是一些相关的线程进入了堵塞状态. start()一个Thread对象只能使用一次这是和NEW密切相关的,只有在NEW状态才能使用start(),使用start()后就进入了另一个状态.

状态图

查看状态

我们可以通过JDK的jconsole来去查看进程里的线程的状态和调用栈的情况.我们可以根据这个来观察线程是不是堵塞了,为什么堵塞,执行到哪行堵塞了.

public class ThreadDemo5 {public static void main(String[] args) {Thread thread = new Thread(() -> {while(true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});thread.start();while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}

线程安全

什么是线程安全

一段代码不论是在单线程上还是在多线程上都可以通过执行,不会出现bug,这就是"线程安全".

一段代码再单线程上可以通过,但是在多线程上会出现bug,这就是"线程不安全"或者"线程安全问题"

经典栗子

public class ThreadDemo6 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for(int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {for(int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("count: " + count);}
}

上述这个代码,我们预期的结果是100000,但是我们运行后发现结果不是100000.

原因

这就需要我们站在硬件的角度来看软件了. 我们知道count++在cpu中实际是三条指令:

1. load 去内存中拿到count的值放到cpu中的寄存器中

2. add 将寄存器中的值+1

3.将寄存器中的结果放到内存中

这里如果是一个线程执行结果肯定是不会出错的.但多个线程,那线程的调度也是随机的.这些指令的先后顺序就会产出多种情况.有的是正确的,有的是错的:

这里列出了几种情况,但其实这样的情况有无数种:

这里两个 线程是并发还是并行我们都不知道,反正两个线程都有自己的PCB,有各自的上下文,互不干扰(各自一套寄存器里的值,互不干扰).

通过观察我们发现,知道一个线程的save没执行,另一个线程的load执行了的话,那这个结果就不对,使用正确的情况应该是一个线程的save需要先执行完才能执行另一个线程的load.

导致线程不安全的原因

1. 根本原因: 这是因为操作系统线程是被随机调度的,抢占式执行,这可能就是导致指令的执行顺序不同.

2. 代码结构: 多个线程同时改变一个变量. 这里多个线程改变不同变量,多个线程读一个变量,一个线程改变一变量是都不会造成线程安全问题的.

3. 直接原因: 代码没有具有原子性. 这里count++虽然只有一个代码,但其实它有三个指令.在执行到一半的时候可能会被调度走,其他的线程就有机可乘插队进来.这可能就会导致错误.这里我们可以将count++的多个指令理解为一个整体.需要全部执行完才能执行其他的指令.这样才具有原子性.

4. 内存可见

5. 指令重排序

解法方法

知道了这几个方面的原因我们就可以对症下药了:

第一个问题的随机调度是操作系统控制的,我们没法改变操作系统,我们无从下手.

第二个问题我们在写代码的时候需要注意代码的结构,避免出现多个线程同时改变一个变量的问题,但有的时候是无法避免的.

第三个问题我们可以通过加锁的方法来将需要执行的代码指令打包成一个整体,这样就具有原子性了.

加锁 - synchronized

加锁的目的就是为了将需要的代码打包成一个整体,令他们具有原子性.加锁的特点就是排他性,互斥性. 这里就是一个线程在执行加锁操作时,其他的线程是不能执行这个加锁对象里的代码的.

举个栗子:

这就像有一个厕所,多个滑稽需要上厕所,一个滑稽进去后将门关上其他滑稽进不来看不到就叫做加锁,上完厕所出去就叫做解锁.这时其他的滑稽才可以进来.

加锁如何操作

在加锁前,我们需要引入一个类对象,加锁和解锁都是依托这个类进行的.这个类对象可以是Object类或者是它的任意一个子类.加锁在Java中是一个关键字 - synchronized.它的括号里面放所对象,花括号里面就是加锁,花括号后就是解锁.

这里加锁的核心就是一个线程对一个所对象进行加锁了,其他的线程再对这个锁对象进行加锁就会导致堵塞.一直到前面的线程解锁才会解除堵塞.这里就是所谓的锁竞争造成的堵塞.

且我们需要知道原子性这个说法不够准确. 不是说加锁了这里里面的指令就一定会完成或者都不完成.它中途还是会被调度出去的.只是说第一个加锁的线程可以保证后面对这个所对象加锁的线程指令不会插队到第一个线程指令中间执行.并不是说不能调度出CPU.

加锁后的代码

public class ThreadDemo10 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object object = new Object();Thread t1 = new Thread(() -> {for(int i = 0; i < 50000; i++) {synchronized(object) {count++;}}});Thread t2 = new Thread(() -> {for(int i = 0; i < 50000; i++) {synchronized(object) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

我们可以发现这时的结果就是正确的了.

注意

这里需要注意几个点:

一个线程加锁,一个线程不加锁.或者不同的线程加不同的锁这都会造成线程安全问题.

需要加锁的线程的所对象必须是同一个.

这里this和类名.class也是可以作为所对象的.

this:

这里this就是直接指代的test.

class Test {public int count = 0;public void add() {synchronized (this) {count++;}}
}
public class ThreadDemo11 {public static void main(String[] args) throws InterruptedException {Test test = new Test();Thread t1 = new Thread(() -> {for(int i = 0; i < 50000; i++) {test.add();}});Thread t2 = new Thread(() -> {for(int i = 0; i < 50000; i++) {test.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + test.count);}
}

类名.class:

这里因为java进程中的一个类只有一个类对象,这样不同的线程使用的还是同一个对象,锁竞争还是会存在.

class Test {public int count = 0;public void add() {synchronized (Test.class) {count++;}}
}
public class ThreadDemo11 {public static void main(String[] args) throws InterruptedException {Test test = new Test();Thread t1 = new Thread(() -> {for(int i = 0; i < 50000; i++) {test.add();}});Thread t2 = new Thread(() -> {for(int i = 0; i < 50000; i++) {test.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + test.count);}
}

内存可见性问题

内存可见性问题和JVM的代码优化息息相关. 一个线程读,一个线程写也可能导致线程安全问题.

经典栗子

while(flag == 0)

这里我们预期的是通过输入1来跳过t1线程的循环,但是我们发现循环并没有跳过,光标还在闪烁. 

原因

这里我们需要知道while(flag == 0) 这句代码其实有两条指令:

1. lode 将内存的中flag读取到CPU的寄存器中

2. 将寄存器中的值与0比较(条件跳转指令)

整体的情况就是t1线程和main线程启动,mian线程需要等待输入,这之间至少需要几秒.而在这几秒的过程中while()会执行几百亿次.

关键有两点:

1. 在这多次lode中,读取内存中的值都是一样的,没发生改变.

2. 读取内存比条件跳转的开销大很多

这就会导致在等待输入这几秒中,大量的循环比较,其中去读取内存,读到的值却没有改变.读取内存的开销又特别大.这就会让JVM怀疑这样的操作有必要嘛.它就有可能会将读取内存指令删除只用寄存器中的值. 这就导致main中改变了flag,但t1线程却没看到,这就是内存不可见.

解决方法

内存可见性是高度依赖JVM的代码优化的具体实现,代码改变一点,结果可能就不一样.为了保证绝对性,Java中就引入了volatile关键字.它的作用就是保证内存可见. 它可以强制代码不进行优化,就是强制读取内存.

Java中锁的特性

互斥性

互斥性就是一个线程获取了这把锁,另一个线程再尝试获取就需要等待,这里就是锁竞争造成的堵塞.这个特性就是用来解决线程安全问题的.

可重入性

可重入性就是一个线程再使用一把锁的前提下,在嵌套二次使用这把锁.在这种情况下不会让线程卡死.

举个栗子:

public class TreadDemo12 {public static void main(String[] args) {Object object = new Object();Thread thread = new Thread(() -> {synchronized(object) {synchronized (object) {//写代码System.out.println("hello word");}}});thread.start();}
}

在这个代码中,如果不使用可重入锁,就会卡死,进入死锁状态.在C++中就没有可重入锁,就会陷入死锁状态. 这种死锁情况就是: 在一个线程里使用锁的前提下,嵌套第二次再使用这个锁.就会发生第一次这个锁对象已经加锁了,则第二次使用锁对象就需要等待第一次解锁,但第一个解锁在第二次加锁的后面.这就导致线程卡死,进入了死锁状态.

这就是相当于你将钥匙忘在了被锁的房间里.

在Java中就不会发生. 因为Java中的锁是可重入的. 由于是同一个线程,在第二次加锁的时候,就会直接放行,不会造成堵塞. 而Java中的锁可以重入是因为锁里面有两个重要的属性: 加锁线程 和 计数器

加锁线程这个属性会记录加锁的线程是谁. 计数器初始值为0,加锁就会+1,解锁就会-1.

在第一次加锁时,加锁线程就记录这个线程.计数器+1. 第二次就会判断加锁的线程和持有锁线程是不是同一个,是就直接计数器++,不是就堵塞等待. 出第二次加锁的括号,计数器就-1, 出第一次加锁的括号再-1,当计数器为0时,就是解锁成功.

死锁

加锁是对多线程的线程安全问题的解决方式,但是加锁操作不恰当就是会出现死锁问题.

死锁问题的常见三种情况

1. 一个线程一把锁:

一个线程中在持有这把锁的前提下,第二次使用这把锁,这就会导致死锁.但在Java中不会出现.

2. 两个线程两把锁:

一个线程在持有A锁的情况下去尝试获取B锁,同时另一个线程在持有B锁的情况下尝试获取A锁.这就会导致死锁.

栗子:

public class ThreadDemo13 {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized(A) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (B) {System.out.println("在A加锁后,加锁B");}}});Thread t2 = new Thread(() -> {synchronized(B) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized(A) {System.out.println("在B加锁后,加锁A");}}});t1.start();t2.start();}
}

这就相当于车钥匙放到了被锁的房间里,房间钥匙放到了被锁的车里.

N个线程M把锁:

哲学家就餐问题

这里有5个哲学家,5个筷子. 一个哲学家就餐时需要使用两个身边的筷子. 当1滑稽就餐时需要1和5筷子,这是5滑稽和2滑稽想就餐就需要等待了.这样虽然需要等待但是最后还是可以吃上面.但是这里有一个极端的情况就是:所有的滑稽同时拿起左手边的筷子,这时每个人都只有一个筷子,这时需要拿起第二个筷子时发现没有筷子了就需要等待.但是所有人都在等待,就没人吃到面放下筷子.这就是循环等待.

解决方法

在分析解决方法钱我们需要知道发生死锁有4个必要条件:

1. 互斥性: 一个线程使用锁,另一个就需要等待.

2. 不可抢占: 一个线程在使用锁时,另一个线程不能强行抢占,只能等它自动解锁.

3. 请求保持: 一个线程持有一把锁的前提下,尝试获取另一把锁.

4. 循环等待

发生死锁,这4个条件缺一不可.

知道了发生死锁的条件后,我们就可以对症下药.我们只需要破坏其中一个条件就可以解除死锁.

1和2是锁的基本特性,我们不能改变.3我们需要看情况而定,有的情况可以避免,有的情况不可以避免.

4是最容易改变的.我们可以制定规则: 指定获取锁的顺序,为每个锁编号,先获取小的锁,再获取大的锁.这样就不会发生循环等待了.

改变锁的循环等待有多种方式:

1. 增加一把锁

2. 减少一个线程.

3. 引入计数器,限制最多多少个线程同时获取锁

4.制定加锁顺序规则(最常用)

5. 银行家算法

线程的通知等待 - wait和notify

这里是通过引入wait与notify来在应用层面来改变线程执行的先后顺序.

操作系统中线程在内核中的调度是抢占式,随机调度的,这是不可改变的.这里我们就是在应用代码层面来让线程主动放弃CPU的调度,从而影响到线程执行的先后顺序. 也就是让执行条件没达到的线程先放弃CPU的竞争,让其他线程先执行,等到条件达到时再参与竞争.

这里举个栗子:

多个滑稽老哥去ATM上执行一些操作

没有wait和notify时: 1号老哥进去取钱,发现没有钱了,那他就会出来与其他滑稽老哥进行竞争进入ATM的机会,1号老哥可能又会竞争到,再进去取钱发现没有钱,又出来和它们竞争,这样可能会多次1号滑稽进去但又没有进行到有用的操作.

转换成代码:

while(true) {synchronized(....) {if(ATM有钱) {//取钱操作}else {//什么也不做}}}

有wait和notify时: 1号老哥进去取钱,发现没有钱了.那他会出来先不参与和它们老哥竞争进去的机会,而是等待其他老哥把钱存进去后再参与竞争,这样就减少了无效操作.

代码:

while(true) {synchronized(....) {if(ATM有钱) {//取钱操作}else {wait();}}}

画图理解:

对于上面第一种情况还是比较容易发生的, 1号滑稽拿到锁,处于RUNNABLE状态,其他线程处于WAITING 状态. 当1号滑稽解锁后再次竞争时,其他滑稽需要系统先唤醒在竞争,而1号滑稽不用唤醒可以直接竞争. 

wait方法

对于wait方法,它的内部会做三件事情:

1. 解锁.

2. 进入堵塞状态.

3. 等到其他线程执行到notify方法时,解除堵塞,加入到锁竞争中.

wait的使用

1. wait需要在synchronized内部使用,不然会抛出异常.

2. wait的对象需要和synchronized的对象时一致的.

public class ThreadDemo1 {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized(object) {System.out.println("执行wait前");object.wait();System.out.println("执行wait后");}}
}

 

这时我们可以打开jcons观察:

 

通过这里我们观察到main线程在wait这里进入了堵塞状态.需要解除堵塞我们就需要使用notify方法. 

notify方法

notify方法就是用来解除wait造成的堵.

notify是不用在synchronized中使用的.比如在操作系统中也有wait和notify,notify是不用先加锁再使用的.但在Java中notify需要在synchronized中使用,不然会报错.

public class TreadDemo2 {public static void main(String[] args) {Object object = new Object();Thread t1 = new Thread(() -> {synchronized (object) {System.out.println("wait方法前");try {object.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("wait方法后");}});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (object) {System.out.println("notify方法前");object.notify();System.out.println("notify方法后");}});t1.start();t2.start();}
}

 

代码执行过程:

1) t1和t2启动,并发执行.

2) t1拿到锁会进入阻塞等待

3) t2会先休眠一秒,这会让t1先拿到锁

4) 等到t2休眠结束, t1已经进入wait,将锁解除了,这时t2就可以拿到锁

5) 等到t2执行到notify时,t1的堵塞等待结束重新进入到锁竞争中.

6) 虽然t1等待结束但是t2还没释放锁,再等待t2释放锁后,t1才能拿到锁继续执行. 

注意

1. wait方法有三个可以使用:

第一个是死等,这个对代码非常的不利,一但忘记使用notify,线程就会卡死.我们一般常用的是第二种,有时间限制的等待,超过这个时间就不等了.第三个是微秒级的等待.

2. wait和notify的所对象得是一致的,不然会导致wait的堵塞等待解除不了.

3. notifyAll方法可以解除在多个线程使用同一个锁的wait的堵塞,但是这样不利于代码控制,我们还是比较推荐使用notify.

wait和sleep的区别

相同点:

1. sleep是指定时间的,wait也有指定时间的版本.

2. sleep和wait都可以提前唤醒. sleep是interrupt方法,wait是notify方法.

不同点:

1. sleep是在知道要休眠多久的情况下使用,wait是在不知道要等待多久的情况下使用

2. wait需要在synchronized中使用,sleep不需要.

3. wait是Object方法. sleep是Thread的静态方法.

Java标准库中的线程安全类

线程不安全类

ArrayList

LinkedList

HashMap

TreeMap

HashSet

TreeSet

StringBuilder

线程安全类

Vector

HashTable

ConcurrentHashMap

StringBuffer

String

这里前四个为线程安全主要是加了synchronizednized关键字不过这几个类jdk都快弃用了.

String为线程安全是因为它不可改变,就不涉及到线程安全问题了.

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

利用 LD_PRELOAD劫持动态链接库,绕过 disable_function

目录 LD_PRELOAD 简介 程序的链接 动态链接库的搜索路径搜索的先后顺序&#xff1a; 利用LD_PRELOAD 简单的劫持 执行id命令 反弹shell 引申至 PHP 绕过disable_function 方法1&#xff1a;使用蚁剑的扩展工具绕过disable_function 方法2&#xff1a;利用 mail 函数…

时序预测 | Python实现TCN时间卷积神经网络价格预测

时序预测 | Python实现TCN时间卷积神经网络时间序列预测 目录 时序预测 | Python实现TCN时间卷积神经网络时间序列预测预测效果基本介绍模型描述程序设计参考资料预测效果 基本介绍 时间卷积网络,TCN。 利用CNN技术处理时间序列数据。 卷基础层有三种,第一种是一维CNN,用于输…

Chrome显示分享按钮

分享按钮不见了&#xff01; Chrome://flags Chrome Refresh 2023 Disabled 左上角的标签搜索会到右上角。

推荐一款好用的BMP转PNG工具BMP2PNG

推荐一款好用的BMP转PNG工具BMP2PNG 自己写的一个BMP转PNG工具BMP2PNG 写这个工具是因为要使用传奇的部分素材在COCOS2DX使用&#xff0c; 但是COCOS2DX不支持BMP 如果直接将BMP转换到PNG的话&#xff0c;网上找到的工具都不支持透明色转换。难道要用PS一个一个抠图吗&#xf…

对数据页的理解

1.InnoDB 是如何存储数据的&#xff1f; 数据表中的记录是按照行来存储的&#xff0c;但是数据库的读取并不以「行」为单位&#xff0c;否则一次读取&#xff08;也就是一次 I/O 操作&#xff09;只能处理一行数据&#xff0c;效率会非常低。 因此&#xff0c;InnoDB 的数据是按…

TA-Lib学习研究笔记(一)

TA-Lib学习研究笔记&#xff08;一&#xff09; 1.介绍 TA-Lib&#xff0c;英文全称“Technical Analysis Library”,是一个用于金融量化的第三方库&#xff0c;涵盖了150多种交易软件中常用的技术分析指标&#xff0c;如RSI,KDJ,MACD, MACDEXT, MACDFIX, SAR, SAREXT, MA,SM…

外贸B2B自建站怎么建?做海洋建站的方法?

如何搭建外贸B2B自建站&#xff1f;外贸独立站建站方法有哪些&#xff1f; 对于许多初次涉足者来说&#xff0c;搭建一个成功的外贸B2B自建站并不是一件轻松的任务。海洋建站将为您详细介绍如何有效地建设外贸B2B自建站&#xff0c;让您的国际贸易之路更加畅通无阻。 外贸B2B…

2023金盾杯线上赛-AGRT战队-WP

目录 WEB ApeCoin get_source ezupload easyphp MISC 来都来了 芙宁娜 Honor Crypto 我看看谁还不会RSA hakiehs babyrsa PWN sign-format RE Re1 WEB ApeCoin 扫描发现有源码泄露&#xff0c;访问www.tar.gz得到源码。 在源码中发现了冰蝎马。 Md5解码&am…

Java核心知识点整理大全24-笔记

22. 数据结构 22.1.1. 栈&#xff08;stack&#xff09; 栈&#xff08;stack&#xff09;是限制插入和删除只能在一个位置上进行的表&#xff0c;该位置是表的末端&#xff0c;叫做栈顶 &#xff08;top&#xff09;。它是后进先出&#xff08;LIFO&#xff09;的。对栈的基…

redisserver一闪而过 redis闪退解决版本

1.进入Redis根目录 2.输入redis-server 或 redis-server.exe redis.windows.conf 启动redis命令&#xff0c;看是否成功。 执 一闪而过的问题 可能是因为已启动或者其他问题&#xff0c;需要重启 先输入redis-cli.exe再输入shutdown再输入redis-server.exe redis.windows.c…

人大金仓亮相2023信息技术应用创新论坛

11月25日&#xff0c;2023信息技术应用创新论坛在常州开幕。人大金仓受邀分享信息技术应用创新行业应用典型成果&#xff0c;在论坛展览部分集中展示了最具代表性的新产品、应用及解决方案。 江苏省工业和信息化厅副厅长池宇、中国电子工业标准化技术协会理事长胡燕、常州市常务…

重载、重写、重定义的辨析

C重载、重写、重定义 重载、重写、重定义对比一、重载&#xff08;overload&#xff09;二、重写 / 覆盖&#xff08;override&#xff09;三、重定义 / 隐藏&#xff08;redefining&#xff09; * 为什么在虚函数中不能使用 static 关键字&#xff1f;动态绑定&#xff08;Dyn…

外贸行业多人文件共享云盘推荐

Zoho WorkDrive外贸行业解决方案致力于为各类外贸企业客户提供数字化转型的支持&#xff0c;全面覆盖市场调研、客户服务与管理、产品设计与制作、采购、供应商管理、财务对账、单证报关、仓储管理以及物流运输等环节。Zoho WorkDrive企业网盘提供文件资料在线存储、共享、同步…

springboot启动开启热部署

springboot启动开启热部署 手动方式 或者点idea上面的build->build project 自动方式 勾上Build project automatically 然后ctrl alt shift / 选择Registr 勾上就好了 新版idea可以在这里选 热部署范围设置 这是spring-boot-devtools起的作用&#xff0c;所以还…

初识Linux

1、简介 Linux&#xff0c;一般指GNU/Linux&#xff08;单独的Linux内核并不可直接使用&#xff0c;一般搭配GNU套件&#xff0c;故得此称呼&#xff09;&#xff0c;是一种免费使用和自由传播的类UNIX操作系统&#xff0c;其内核由林纳斯本纳第克特托瓦兹&#xff08;Linus Be…

行情分析——加密货币市场大盘走势(11.29)

大饼已经形成了底背离&#xff0c;即MACD往下走&#xff0c;而价格还在往上走&#xff0c;这种后续往往会大跌。继续把空单拿好&#xff0c;已经持仓的无需加仓。多次上涨却一直不能突破&#xff0c;说明多空和空军力量都很强&#xff0c;等待后续出方向。在笔者看来&#xff0…

标题导航点击导航滑动到指定位置滑动到指定位置选中对应导航vue3

菜单导航栏点击导航滑动到指定位置&滑动到指定位置选中对应导航 效果 实现 话不多说直接上代码&#xff0c;有用素质三连(点赞、评论、加关注) import { defineComponent, onBeforeUnmount, onMounted, reactive, ref } from "vue"; import { map } from &quo…

Typora .MD笔记中本地图片批量上传到csdn (.PNG格式)(无需其他任何图床软件)

Typora .MD笔记中本地图片批量上传到csdn &#xff08;.PNG格式&#xff09;(无需其他任何图床软件) 截图软件推荐 qq 截图 快捷键 ctrlshiftA. 步骤一 设置Typora 的图片 点击文件. 点击偏好设置 ->图像 我们可以选择将图片复制到我们的文件夹中。 建议刚写好文件标题就…

推荐6款本周 yyds 的开源项目

&#x1f525;&#x1f525;&#x1f525;本周GitHub项目圈选: 主要包含 链接管理、视频总结、有道音色情感合成、中文文本格式校正、GPT爬虫、深度学习推理 等热点项目。 1、Dub 一个开源的链接管理工具&#xff0c;可自定义域名将繁杂的长链接生成短链接&#xff0c;便于保…

抖音直播招聘报白如何提高求职者体验?

为了提升抖音直播招聘报白中求职者的体验&#xff0c;以下是一些建议&#xff1a; 提供清晰的招聘流程和信息。在直播招聘开始之前&#xff0c;企业或人力资源公司应提供清晰的流程和信息&#xff0c;包括直播时间和直播平台&#xff0c; 职位信息&#xff0c;招聘要求等&…