当前位置: 首页 > news >正文

Java之线程安全

目录

一.上节回顾

1.Thread类常见的属性

2.Thread类中的方法

二.多线程带来的风险

1.观察线程不安全的现象

三.造成线程不安全现象的原因

1.多个线程修改了同一个共享变量

2.线程是抢占式执行的

3.原子性

4.内存可见性

5.有序性

四.解决线程不安全问题 ---synchronized

1.synchronize

2.synchronized解决的线程安全问题

1.解决了原子性问题

2.解决了可见性问题

3.不能解决有序性

3.synchronzied具体的使用方法

1.修饰普通方法

2.修饰静态方法

3.修饰代码块

4.synchronized使用的注意事项

5.synchronized的特性

1.互斥性

2.刷新内存

3.可重入

6.彻底搞懂synchronized锁对象

1.锁对象的记录的信息

2.多线程竞争的锁对象必须为同一个对象

3.锁对象可以为任意对象

五.解决线程不安全问题 ---volatile

1.volatile解决内存可见性

1.重现内存可见性

2.MESI缓存一致性协议

3.内存屏障

2.解决有序性

3.不能解决原子性

六.synchronized和volatile总结

七.wait()和notify()

1.wait()

2.notify()

3.wait()和notify()

八.Java中线程安全的类

1.线程安全的类

2.线程不安全的类


一.上节回顾

上节内容指路:Java之多线程初阶2

1.Thread类常见的属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

2.Thread类中的方法

1.后台进程isDaemon()

2.线程中断  ---interrupt()

3.线程等待  ----join()

4.获取当前线程  ---Thread.currentThread()

5.线程休眠    ---Thread.sleep()

6.start()和run()的区别 

  1. start()方法是真正申请一个系统线程,run()方法是定义线程要执行的任务
  2. 直接调用run()方法,不会去申请一个真正的系统线程(PCB),而是调用对象的方法

7.主动让出CPU   ---yield()

8.线程之间状态的转移图

二.多线程带来的风险

1.观察线程不安全的现象

用两个线程对一个共享的变量做自增操作

public class Demo17_Insecurity {static Counter counter = new Counter();public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; ++i) {counter.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 50000; ++i) {counter.increment();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(counter.count);}
}class Counter {public int count = 0;public void increment() {count++;}}

打印的结果:

按我们的分析来说分别进行5w次的自增操作,最后count的值应该是10w,为什么会产生这种结果呢?

在单线程的情况下进行运行,我们都知道运行的结果为10w,但是多线程的情况下就不对了,当多线程的运行结果与单线程的运行结果不一致的时候,会造成线程不安全的现象

三.造成线程不安全现象的原因

1.多个线程修改了同一个共享变量

多个线程修改同一个共享变量会发生线程不安全的现象

多个线程修改不同的共享变量会发生线程不安全的现象

单线程环境下,也不会出现线程不安全的现象

2.线程是抢占式执行的

多个线程在CPU上的调度是不可知的.

3.原子性

原子性我们在数据库中也学习过,其实都是一样的,指令要么都执行,要么都不执行.但是仅以我个人的理解而言,在多线程中,我们不妨把原子性这样来理解,当发生线程不安全的时候,涉及到了修改了同一个共享变量,修改共享变量的命令可以看成一个整体(原子),也就是这些命令我们要么都一起执行,要么所有的暂时都不先执行,等待一下再执行,这样就保证多线程的安全,确保代码的原子性.

不妨我们就那上面的代码举例,可以有人会疑问,上面就一条代码,这不一定是原子的吗?那不一定,JVM要把这一条代码转换成CPU能看懂的指令,然后让CPU去执行,因此一条代码不一定就是原子性的,可能它由多条指令组成.

比如count++操作由以下操作做成

  1. 从内存中将count的值读到CPU(LOAD)
  2. 进行自增操作(ADD)
  3. 将数据写入到内存中(STORE)

这样我们的一条代码才算是执行完毕.

接下来我们模拟两条线程情况下如何不保证原子性可能会发生的情况(并发)

 

 以上三种情况,除了第一种情况,其他三种都会发生线程不安全的现象,具体原因拿其中一幅图来进行分析.

首先线程1被调入CPU,将count的值加载到自己的工作内存中(LOAD),然后进行自增操作(ADD),此时线程1的工作内存中count=1,但是还没有进行STORE操作,主内存的值还没有改变

然后线程2抢占到了CPU,它将主内存count=0加载到自己的工作内存,然后自增操作,然后将工作内存中count=1的值store到主内存中,此时主内存中count=1;

 

最后线程2被调离CPU,线程1被调入CPU,此时线程1将工作内存中的值保存到主内存中,此时主内存的值被修改为1(未发生改变).

 分析了以上我们就可以知道为什么之前我们代码执行的结果和我们预期的结果不一样了.

这就是我们没有确保原子性会带来多线程不安全现象.其最主要的原因还是因为线程是抢占式执行的,CPU调度的随机性

如何上段代码可以保证代码的原子性(也就是修改共享变量的这段代码可以一起执行,或者等待一会再一起执行),就可以解决这个问题了.其实我们可以把这段代码加锁,让之后的线程无法抢占资源,就可以避免这个问题了(具体等到之后演示).

4.内存可见性

在多线程的情况下,一个线程修改了共享变量的值,另一个线程却没有拿到最新的值.

所谓的可见性就是一个线程修改了共享变量的值,能够及时地被其他线程看到.

Java 内存模型 (Java Memory Model)(JMM): Java虚拟机规范中定义了Java内存模型.

1.为什么Java要引入JMM呢?

我们都知道Java是一个跨平台的语言,不同平台的计算设备和操作系统是不同的,JMM对内存管理进行了统一的封装.

下图是一个CPU,不同的平台可能不一样,比如没有缓存.其中寄存器保存代码运行的结果,比如int c=a+b;计算出c的结果保存到寄存器中,下一次CPU需要c的值做其他的运算,我们不必从内存条中进行获取,直接从寄存器中进行获取,这样运行的速度更快.

2.主内存和工作内存

  1. 主内存,指的是硬件中的内存条,进程在启动的时候会申请一些资源,申请到的内存就包括主内存和工作内存,用来保存所有的变量.
  2. 工作内存,指的是每个线程独有的内存,他们之间不能互相访问,起到了线程之间内存隔离的作用.
  3. JMM规定,一个线程在修改某个变量的值的时候,必须要把这个变量的值从主内存中加载到工作内存,修改完成后再刷新到主内存
  4. 每个工作内存之间是相互隔离的.
  5. JMM可以保证原子性,可见性,有序性.

5.有序性

有序性是指,编译过程中,JVM调用本地接口,CPU执行指令过程中,指令的有序性.

现在举一个现实中的例子,一段代码是这样的进行编写的:
1. 去前台取下 U 盘 (到前台)
2. 去教室写 10 分钟作业 (到教室)
3. 去前台取下快递 (到前台)
我们按照这个顺序编写成功并执行了代码,但是JVM在执行的过程中,并不是按照这个顺序进行执行的,因为这样的效率很低,所以CPU进行了指令的重排序,重排序之后代码是按照1-->3-->2的顺序进行执行的,但是这个代码的逻辑不发生改变,也就是重排序前和重排序后的代码运行的结果是一样的,但提高了程序的效率.
这样进行了指令的重排序,在单线程的情况下100%是正确的,但是在多线程的情况下就未必是正确的

四.解决线程不安全问题 ---synchronized

1.synchronize

synchronize关键字相当于给这个方法加了一把锁,当一个线程要执行这个方法的时候,他首先要获取锁,获取到锁之后就执行相应的代码,另一个线程要执行这个方法的时候,他也要先获取锁,但是前一个线程持有锁的时候他就要等待(前面所说的线程的BLOCK状态),直到前一个线程执行完相应的代码释放锁的时候

public class Demo18_Synchronized {static Counter18 counter = new Counter18();public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; ++i) {counter.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 50000; ++i) {counter.increment();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(counter.count);}
}class Counter18 {public int count = 0;public synchronized void increment() {count++;}}

打印结果:

 上面的代码我们可以看出,打印的结果是正确的.

加入synchronize关键字之后,我们可以观察到由多线程操作变为了单线程,虽然效率降低了,但是确保了安全性,所以还是很有必要的.

总结单线程和多线程使用的场景

1.涉及到获取变量的值的时候,考虑使用多线程提高效率.

2.涉及到修改变量值的时候,使用单线程来保证安全性.

线程1获取到锁之后执行了对应的代码,线程2也要执行这个方法,但是检查锁的状态已经被持有,所以它处在堵塞(BLOCK)的状态,当线程1执行完方法之后,线程2才有可能获得到锁(并不一定),因为线程是抢占式执行的,可能线程1再次执行这个方法,再次先获得到了锁.

现在举一个形象点的例子,小人1去上厕所,它获取到了锁,进入到上厕所,其他的小人要想上厕所,因为现在已经上了锁,所以他们只能处在等待堵塞的状态,当小人1出来之后,他们又在一起竞争(小人1也参与竞争),竞争到锁的就可以先上厕所,其它的小人又需要等待,

注意: 只要线程没有释放锁(即使被调离CPU),其它线程还是无法执行这个方法(代码块)的.

2.synchronized解决的线程安全问题

1.解决了原子性问题

通过以上分析不难看出来,通过加锁解决了原子性的问题.保证synchronized圈住的代码块一定是作为一个整体一起执行的,即使被调离CPU,其他的线程也不会打破线程1代码执行的原子性,其他线程一直会等待线程1释放完锁之后才会执行.

2.解决了可见性问题

synchronized通过加锁,将多线程变为单线程,并行变为串行的操作,可以保证线程1修改到最新的值一定会被其他的线程获取到.

3.不能解决有序性

sychronized不能解决有序性,后面有其他的关键字可以解决.

3.synchronzied具体的使用方法

1.修饰普通方法

修饰方法直接在方法命名前加入synchronzied关键字即可.上面已经演示,这里不做过多的叙述,此时的锁对象为Counter18对象,也就是this对象

class Counter18 {public int count = 0;public synchronized void increment() {count++;}}

2.修饰静态方法

这里的锁对象是Counter18类对象,可以就是Counter18.class

class Counter18 {public static int count = 0;public static synchronized void increment() {count++;}}

3.修饰代码块

当一个方法内不仅涉及到变量的修改操作的时候,还涉及到获取变量的值的操作的时候,我们没有必要将所有的代码都加上锁,只需要把修改变量的代码块加上锁即可,因为获取变量值的操作不涉及线程安全的问题,这样既可以保证安全性,也可以保证效率.

class Counter19 {public int count = 0;public void increment() {//获取到数据的操作   ---这部分的操作没必要加锁//getUserName()//get...()//修改数据的操作  ---这部分代码一定要加锁synchronized (this){count++;}}}

synchronized加到代码块上去需要一个对象(锁对象),作用是当多个线程竞争时候判断竞争的是否为同一把锁,如果是就参与竞争,不是就各自执行各自的内容.

4.synchronized使用的注意事项

1.从并行到串行:首先要保证正确,才是效率
2.加锁与CPU调度:加锁后对一个方法加锁并不说是这个线程一直把这个方法执行完才被调度走,而是被调度走时,不释放这个锁,别的线程需要一直等待,就好比图书馆占座
3.加锁的范围(粒度):加在for循环外面就和串行是一模一样的了,但是加在外面两个for 循环之间就是并发执行的,这个写仍然比两个for循环分别执行要快很多,加锁的范围,这个要根据实际情况考虑,加锁的范围越大称为锁的粒度大,加锁的范围小称为锁的粒度小
4.只给一个线程加锁:不会产生竞争,结果还是错的                                                                        比如一共有两个方法,代码的操作都是一样的,但是一个方法加了synchronized,一个没有加,两个线程一个调用synchronized方法,另一个调用没有加的方法,此时结果还是错的

5.给代码块加锁:如果要加锁的代码只是一段代码怎么办? synchronized中以修饰方法也可以修饰代码块,修饰代码块时要传入一个参数,这个参数就是要锁哪个对象
6.锁对象:竞争的锁都必须是针对同一个对象的,可以自定义一个单独的对象表示锁,也可以使用this,通常使用this就可以了,我们写多线程代码时不关心锁对象到底是谁,只关心他们竞争的是不是同一把锁(同一个对象),如果是同一把锁就可以产生锁竞争,不是同一个对象就不会产生锁竞争,锁对象可以使Java中任意的对象.

5.synchronized的特性

1.互斥性

synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到同一个对象 synchronized 就会 阻塞等待 .
  • 进入到synchronized代码块相当于加锁(LOCK)
  • 离开synchronized代码块相当于解锁(UNLOCK)

理解阻塞等待

针对每一把锁都维护一个堵塞等待队列,当A拿到锁执行的时候,其他线程(比如B,C线程)尝试获取到锁,因为A已经获取到了锁,B,C进入堵塞等待队列,当线程A执行完释放锁的时候,操作系统唤醒其他的线程(B,C出列),竞争同一把锁,不一定B先出列一定B就先竞争到锁.

2.刷新内存

synchronized内的代码执行完毕直接将新的值刷新到主内存中,解决了多线程的可见性的问题.

3.可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
    理解把自己锁死
指的是一个线程加了一把锁之后,又加了同一把锁(因为上一把锁没有释放,所以这一把锁一之处在堵塞的状态,而且是不可能释放上一把锁的),这时候就会 死锁                    ----这样的锁成为不可重入锁
而synchronized是可重入锁,不会出现死锁的现象.

6.彻底搞懂synchronized锁对象

1.锁对象的记录的信息

Java虚拟机中,对象在内存中结构可以分为4个区域

markword和类型指针统称为对象头

  • markword    ---------主要描述了当前是哪个线程获取到锁资源,记录的是线程对象信息

  • 类型指针(_class)

  • 实例数据(instance_data)  类中的属性

  • 对齐填充(padding)  每一个类对象占用的字节必须为8字节的整数倍

  1. 当线程来争抢锁资源时,会检查锁对象的对象头
  2. 如果锁对象,对象头中的信息为空,那么直接获取到锁资源
  3. 如果对象头中的信息不为空,那么先判断一下记录的是不是当前线程,如果不是就阻塞等待,如果是那么就直接拿到锁

2.多线程竞争的锁对象必须为同一个对象

当把count定义为类属性(static),并且将increment加上synchronized关键字.定义两个不同的Counter20对象,然后调用各自的increment方法,他们对同一个count进行自增操作(因为count是类属性).

public class Demo20_Synchronized {static Counter20 counter = new Counter20();static Counter20 counter2 = new Counter20();public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; ++i) {counter.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 50000; ++i) {counter2.increment();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(counter.count);}
}class Counter20 {public static int count = 0;public synchronized void increment() {count++;}}

打印的结果:

 由打印的结果可以看出并不符合我们预期的结果,但是我们明明都加了synchronized关键字了,为什么会出现这种结果呢?因为他们竞争的不是一个锁对象,前面我们已经提过了加在方法上的锁对象是this,也就是new出来的count和count2对象,他们竞争的不是一个锁,运行出来的肯定无法是预期结果.

这样和上面运行出来的结果都不是100000,他们的锁对象都是一样的

class Counter20 {public static int count = 0;public void increment() {synchronized (this) {count++;}}}

如何要运行出预期的结果,可以做以下修改

class Counter20 {public static int count = 0;public void increment() {synchronized (Counter20.class) {count++;}}}

我们将锁对象换成了Counter20 .class,Counter20在内存中只有一份,因此两个线程竞争的锁对象是一样的,结果自然也是预期的.

其实我们做如下修改也是一样的

class Counter20 {public static int count = 0;public synchronized static void increment() {count++;}}

这样锁对象也是Counter20.class,因此竞争的也是一个锁对象.

3.锁对象可以为任意对象

这里可以为Object类对象,当然可以为其他类对象

    public void increment() {synchronized (Object.class) {count++;}}

也可以是new出来的普通对象.

class Counter20 {public static int count = 0;public Object locker=new Object();public void increment() {synchronized (locker) {count++;}}}

 当然这样不会产生预期的结果

也可以是new出来的类对象(static)

class Counter20 {public static int count = 0;public static Object locker=new Object();public void increment() {synchronized (locker) {count++;}}}

这样结果就是正确的.

总结:synchronized可以解决原子性,可见性,但不能解决有序性

五.解决线程不安全问题 ---volatile

synchronized解决了原子性,内存可见性,但是没有解决有序性问题..

synchronized没有通过线程间通信真正解决内存可见性.只是并行变串行粗暴的解决.

1.volatile解决内存可见性

1.重现内存可见性

public class Demo21_volatile {private static int flag = 0;public static void main(String[] args) throws InterruptedException {// 定义第一个线程Thread t1 = new Thread(() -> {System.out.println("t1线程已启动.");// 循环判断标识位while (flag == 0) {}System.out.println("t1线程已退出.");});// 启动线程t1.start();// 定义第二个线程,来修改flag的值Thread t2 = new Thread(() -> {System.out.println("t2线程已启动");System.out.println("请输入一个整数:");Scanner scanner = new Scanner(System.in);// 接收用户输入并修改flag的值flag = scanner.nextInt();System.out.println("t2线程已退出");});// 确保让t1先启动TimeUnit.SECONDS.sleep(1);// 启动t2线程t2.start();}}

当我们输入一个非0值的时候,预期结果应该是t1线程退出循环,0值的时候,不退出循环

但是实际输出却不一样,等待很长时间都没有t1线程已退出的信息

1.在执行的过程中,线程1将变量的值从主内从中加载到自己的工作内存,也就是寄存器和缓存器中

2.CPU对执行过程中做了一定优化:既然线程没有对变量进行修改,而从工作内存中读取的速度是从主内存中读取速度的1万倍以上,所以每次变量的值就从工作内存中读取

3.此时线程2修改了变量的值,但是没有一种机制来通知线程1来获取最新的值.

此时volatile就出现了,我们使用volatile来修饰flag变量

private static volatile int flag = 0;

可以看到线程1成功退出,这样就成功的保证了内存可见性.

接下来分析实现内存可见性的原因,

2.MESI缓存一致性协议

 当某个线程对共享变量进行修改时,通知其他CPU对该变量的缓存值置为失效状态

当其他CPU从缓存中获取该共享变量值的时候,发现这个值被置为失效状态,那么就需要重新从主内存中加载最新的值

3.内存屏障

Load 表示读操作,Store表示写操作

1.在每个volatile写操作前插入StoreStore屏障,这样就能让其他线程修改A变量后,把修改的值对当前线程可见
2.在写操作后插入StoreLoad屏障,这样就能让其他线程获取A变量的时候,能够获取到已经被当前线程修改的值
3.在每个volatile读操作前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也都能获取到相同的值,这样所有的线程读取的数据就一样了
4.在读操作后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量的值之前,获取到主内存里面A变量的的值。

注意:volatile关键字只能对变量添加

2.解决有序性

有序性是指在保证程序运行正确的前提下,编译期CPU对指令进行重排序优化的过程.

用volatile修饰的变量,就是告诉编译器不要对这个变量涉及的操作进行重排序,从而解决了有序性的问题.

3.不能解决原子性

用以下代码进行验证

public class Demo22_Volatile2 {static Counter22 counter = new Counter22();public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; ++i) {counter.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 50000; ++i) {counter.increment();}});thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(counter.count);}
}class Counter22 {public volatile int count = 0;public void increment() {count++;}}

打印结果:

 因此我们可以总结出volatile不可以解决原子性.

六.synchronized和volatile总结

下表展示了synchronized和volatile可以解决和不可以解决的问题,Y=可以解决,N=不可以解决

原子性可见性有序性
synchronizedYYN
volatileNYY

七.wait()和notify()

wait()和notify()方法是Object类中定义的方法,每个对象都会默认继承Object,所以每个类都可以使用wait()和notify()方法

1.wait()

wait()是让线程死等,此时线程的状态为WAITING

wait(long)是让线程等待一段时间,过了时间就不等了,过时不候,此时线程的状态为TIMED_WAITING

之前我们学习过join()方法也是等待一段时间,但是wait和join不一样,join是让调用方去等,wait是让执行方去等.

2.notify()

notify()和notifyAll()都是唤醒等待的线程

notify()只唤醒一个线程,并参与锁竞争.

notifyAll()一次性唤醒所有的进程,线程共同去参与锁竞争

3.wait()和notify()

我们创建两个线程,一个线程等待,让另一个线程唤醒前一个线程.

public class Demo23_Wait_Notify {private static Object locker = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {while (true) {System.out.println(Thread.currentThread() + ":wait方法之前");try {//等待资源,线程堵塞locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread() + ":wait方法之后");System.out.println("============================");// 等待一会try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}, "t1");thread1.start();Thread thread2 = new Thread(() -> {while (true) {System.out.println(Thread.currentThread() + ":notify方法之前");locker.notify();System.out.println(Thread.currentThread() + ":notify方法之后");}}, "t2");thread2.start();}
}

打印结果:

 可以看到报了非法的监视器状态异常,一般是与synchronized相关.

做如下修改之后可以观察到正确的结果

public class Demo23_Wait_Notify {private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {while (true) {System.out.println(Thread.currentThread().getName() + ":wait方法之前");try {synchronized (locker) {//等待资源,线程堵塞locker.wait();}} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + ":wait方法之后");System.out.println("============================");}}, "t1");thread1.start();Thread thread2 = new Thread(() -> {while (true) {System.out.println(Thread.currentThread().getName() + ":notify方法之前");synchronized (locker) {//等待资源,线程堵塞locker.notify();}System.out.println(Thread.currentThread().getName() + ":notify方法之后");// 等待一会try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}, "t2");thread2.start();}
}

打印的结果:

根据这个代码的内容和打印的结果我们不难看出线程1的synchronized在执行wait方法等待的时候,为什么线程2能够进入并且进行notify操作呢?synchronized方法在没有执行完代码块的时候不释放锁资源,其他的线程是不可以进入执行代码的,这我们上面看到的内容不一样,因此我们可以大胆的猜测,在wait和notify方法执行的时候会释放锁资源.

总结:从执行的结果可以看出,wait()和notify()方法执行后会释放锁资源.wait()和notify()要对于同一个锁搭配使用.

 面试题:说一下wait()和sleep()的区别

1.本质上都是让线程等待,但是两个方法没什么关系

2.wait()是Object类中定义的方法,sleep()是Thread类中定义的方法

3.wait()必须与synchronized搭配使用,调用之后会释放锁.sleep()只是让线程进入堵塞等待,和锁没有什么区别

八.Java中线程安全的类

1.线程安全的类

  • Vector (不推荐使用)
  • Stack
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

2.线程不安全的类

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

相关文章:

Java之线程安全

目录 一.上节回顾 1.Thread类常见的属性 2.Thread类中的方法 二.多线程带来的风险 1.观察线程不安全的现象 三.造成线程不安全现象的原因 1.多个线程修改了同一个共享变量 2.线程是抢占式执行的 3.原子性 4.内存可见性 5.有序性 四.解决线程不安全问题 ---synchroni…...

我有一个方法判断你有没有编程天赋

我有一个方法判断你有没有编程天赋 一 前言 基于知识的诅咒的原理 做一个敲击者很难。问题在于敲击者已拥有的知识&#xff08;歌曲题目&#xff09;让 他们想象不到缺乏这种知识会是什么情形。当他们敲击的时候&#xff0c;他 们不能想象听众听到的是那些独立的敲击声而不是…...

python 生成chart 并以附件形式发送邮件

import requests import json import pandas as pd import numpy as np import matplotlib.pyplot as plt data np.random.randn(5, 3)#生成chart def generate_line_chart(data):df pd.DataFrame(np.abs(data),index[Mon, Tue, Wen, Thir, Fri],columns[A, B, C])df.plot()…...

leetcode-035-搜索插入位置

题目及测试 package pid035; /*35. 搜索插入位置 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。示例 1:输入: nums …...

读书笔记--数据治理之法

继续延续上一篇文章&#xff0c;对数据治理之法进行学习。数据治理之法是战术层面的方法&#xff0c;是一套涵盖8项举措的数据治理实施方法论&#xff0c;包括梳理现状与确定目标、能力成熟度评估、治理路线图规划、保障体系建设、技术体系建设、治理策略执行与监控、绩效考核与…...

送了老弟一台 Linux 服务器,它又懵了!

大家好&#xff0c;我是鱼皮。 前两天我学编程的老弟小阿巴过生日&#xff0c;我问他想要什么礼物。 本来以为他会要什么游戏机、Q 币卡、鼠标键盘啥的&#xff0c;结果小阿巴说&#xff1a;我想要一台服务器。 鱼皮听了&#xff0c;不禁称赞道&#xff1a;真是个学编程的好苗…...

CentOS 7(2009) 升级 GCC 版本

1. 前言 CentOS 7 默认安装的 gcc 版本为 4.8&#xff0c;但是很多时候都会需要用到更高版本的 gcc 来编译源码&#xff0c;那么本文将会介绍如何在线升级 CentOS 的 gcc 版本。 2. 升级 GCC (1). 安装 centos-release-scl&#xff1b; [imaginemiraclecentos7 ~]$ sudo yum…...

java非静态代码块和静态代码块介绍

代码块 SE.10.0…02.28 非静态普通代码块&#xff1a;定义在方法内部的代码块&#xff0c;不用任何关键字修饰&#xff0c;又名构造代码块、实例代码块 静态代码块&#xff1a;用static修饰的代码块 非静态代码块 public class Test {public static void main(String[] args…...

Golang中接口类型详解与最佳实践(二)

之前的文章《Golang中的interface(接口)详解与最佳实践》详细介绍了接口类型的定义、使用方法和最佳实践。接口类型使得编写可扩展、可维护和可复用的高质量代码变得更加容易。 如何判断是否实现了某个接口&#xff1f; 还是使用之前文章的例子&#xff0c;例如声明了如下一个…...

ChatGPT 探讨内存屏障的意内存

一、与 ChatGPT 探讨内存屏障的意内存 轻松的氛围&#xff0c;跟 ChatGPT 从内存屏障问题一直扯到CAP原理 我&#xff1a; 2023/4/14 17:48:09 那我可以理解为{ shared_var 1; asm volatile ("sfence" ::: "memory"); asm volatile ("lfence" …...

P1039 [NOIP2003 提高组] 侦探推理

此题难度为&#xff1a;提高/省选- 作者为&#xff1a;CCF_NOI 题目描述 明明同学最近迷上了侦探漫画《柯南》并沉醉于推理游戏之中&#xff0c;于是他召集了一群同学玩推理游戏。游戏的内容是这样的&#xff0c;明明的同学们先商量好由其中的一个人充当罪犯&#xff08;在明…...

模拟电路学习笔记 - 概念与结论

真空二极管&#xff0c;电子管ENIAC发源地&#xff0c;基础方法二极管双极管三极管场向管学习特性&#xff0c;最终运放运方的目的是运用&#xff0c;射频&#xff0c;计算…放大电路大功率元器件和微元器件学习他们的特性分粒 集成设计的角度&#xff0c;不要仅仅分析设计的前…...

Linux驱动开发:I2C子系统

目录 1、I2C简介 1.1 两根线 1.2 信号 1.3 写时序 1.4 读时序 1.5 I2C速率 1.6 I2C驱动框架简介 2、I2C设备驱动 2.1 I2C相关API 2.1.1 i2c_driver 2.1.2 注册&#xff1a;i2c_add_driver 2.1.3 注销&#xff1a;i2c_del_driver 2.1.4 module_i2c_driver&#xff…...

[C++] 动态内存与智能指针

众所周知&#xff0c;C五大内存区&#xff1a;全局数据区(静态区)、代码区、栈区、堆区、常量区。 全局数据区(静态区)&#xff1a;存放全局变量&#xff0c;静态数据和常量&#xff1b; 代码区&#xff1a;存放所有类成员函数和非成员函数代码&#xff0c;函数体的二进制代码。…...

多态的原理

有了虚函数&#xff0c;会在类的对象增加一个指针&#xff0c;该指针就是虚函数表指针_vfptr;虚表本质就是函数指针数组,虚表里面存放着该对象的虚函数的地址&#xff1b; 派生类继承有虚函数基类的对象模型 子类继承父类的虚表指针时&#xff0c;是对父类的虚表指针进行了拷…...

RK3588平台开发系列讲解(内存篇)Linux 伙伴系统数据结构

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、 页二、区三、内存节点沉淀、分享、成长,让自己和他人都能有所收获!😄 📢Linux 系统中,用来管理物理内存页面的伙伴系统,以及负责分配比页更小的内存对象的 SLAB 分配器了。 本篇将介绍伙伴系统相关数据结…...

Windows(MFC/C++)上进程间通讯的几种简单又实用的方法

前段时间&#xff0c;做了一个项目&#xff0c;涉及数据传输。项目实现方式有很多种&#xff0c;但不同的实现方式&#xff0c;对数据的传输方法不同&#xff0c;且各有优缺点。 下文就不同情况来如何选择数据传输(通讯)方式。 先说说需求&#xff0c;模块A获取测试数据&#…...

嘉兴桐乡会计考证培训-备考中级职称有必要报班吗?

备考中级会计职称有必要报班吗&#xff1f;其实&#xff0c;备考报班不能说是必需的&#xff0c;但听课学习确实是节省时间的一种方式&#xff0c;根据同学们的反馈&#xff0c;自学所花费的时间远远多于跟着老师学。上元教育就整理了一些学员报班之前走过的弯路 报班之前 在2…...

java元注解和自定义注解的区别

Java的元注解和自定义注解是两个不同的概念。 元注解是Java内置的一组用于修饰其他注解的注解&#xff0c;包括Retention、Target、Inherited和Documented。它们可以控制被修饰的注解的保留策略、目标范围、是否继承等属性&#xff0c;并且可以在编写自定义注解时使用。 Retent…...

技术到底是什么

背景 我发了朋友圈&#xff1a;做了个奇怪的梦&#xff0c;梦见被离职了&#xff0c;理由竟然是&#xff1a;你技术太菜了 我补充评论&#xff1a;我还没想明白怎么回事&#xff0c;就醒了。有点遗憾的是&#xff1a;想再努力反驳两句&#xff0c;结果没机会了… 很多人评论…...

什么CRM客户管理系统最好?

产业互联网背景下&#xff0c;企业数字化转型日渐深化。毋庸置疑&#xff0c;客户是企业的命脉&#xff0c;企业发展的关键便是以客户为中心&#xff0c;为客户创造价值&#xff0c;并不断实现企业的可持续性增长&#xff0c;而这也是每个企业永不落幕的主题。 一套优秀的CRM客…...

吴军《计算之魂》读后感

前言 断断续续&#xff0c;终于完成了这本书的第一次通读&#xff0c;记录下自己的一些想法。 先说一个小故事。前段时间家里买了一个小鱼缸&#xff0c;问我有没有办法让水自动循环&#xff0c;但不想用电。没有好的想法&#xff0c;去小某书上搜了下&#xff0c;好多案例教…...

CSS进阶

01-复合选择器 定义&#xff1a;由两个或多个基础选择器&#xff0c;通过不同的方式组合而成。 作用&#xff1a;更准确、更高效的选择目标元素&#xff08;标签&#xff09;。 后代选择器 后代选择器&#xff1a;选中某元素的后代元素。 选择器写法&#xff1a;父选择器 …...

金兰组织 | 2023金兰解决方案集经营管理篇正式发布

为助力企业创新管理、提质增效&#xff0c;人大金仓携手金兰组织成员单位&#xff0c;于近期发布多项经营管理领域的联合解决方案&#xff0c;共享创新应用成果。 /人大金仓高级副总裁宋瑞/ 人大金仓高级副总裁宋瑞在致辞中表示&#xff1a;“联合解决方案创新是指通过把不同领…...

【python】pytorch包:深度学习(序章)

今日听闻师姐说pytorch实现深度学习要比keras更好用一些&#xff0c;特此记录 Part 0. 机器学习 与 深度学习 的联系与区别 参考B站视频链接 联系 深度学习是机器学习的分支&#xff0c;人工神经网络为基础&#xff0c;对数据的特征进行学习的方法 区别 特征抽取 机器学…...

HTML <acronym> 标签

HTML5 中不支持 <acronym> 标签在 HTML 4 中用于定义首字母缩写词。 实例 标记一个首字母缩写&#xff1a; <acronym title"World Wide Web">WWW</acronym> 浏览器支持 IEFirefoxChromeSafariOpera 所有主流的浏览器均支持 <acronym> …...

python基本数据类型 - 字典集合

引入 在内存中存储的数据可以是不同的数据类型。比如名字可以使用字符串存储&#xff0c;年龄可以使用数字存储&#xff0c;python有6种基本数据类型&#xff0c;用于各种数据的存储&#xff0c;分别是&#xff1a;numbers(数字类型)、string(字符串)、List(列表)、Tuple(元组…...

python数据类型总结

标准数据类型 Python 有以下几种标准数据类型&#xff1a; 整数&#xff08;int&#xff09;&#xff1a;表示整数值&#xff0c;如 1, -5, 0 等。浮点数&#xff08;float&#xff09;&#xff1a;表示小数值&#xff0c;如 3.14, -0.01, 1.0 等。字符串&#xff08;str&…...

TS内置类型总结

typeof 取对象身上的类型 const person {name: ,job: ,age:18 } type p typeof person ->> type p {name: string;job: string;age: number; }keyof取一个类型的属性明作为一个联合类型 const person {name: ,job: ,age: 18 } type p typeof person type k keyof p…...

Spring Cloud Alibaba: Gateway 网关过滤器 GatewayGatewayFilter factory (记录)

目录 AddRequestHeader GatewayFilter factory AddRequestHeadersIfNotPresent GatewayFilter factory AddRequestParameter GatewayFilter Factory AddResponseHeader GatewayFilter Factory CircuitBreaker GatewayFilter factory circuit breaker based on the status…...