当前位置:   article > 正文

【Java多线程】【线程状态和安全】_java 多线程安全

java 多线程安全

目录

1、线程安全

1.1 线程的所有态

2、 线程安全

2.1 概念

 2.2 线程不安全的原因(重点)

2.3 保证线程安全(重点) 

2.3.1 原子性

2.3.2  可见性

 2.2.3 指令重排序

2.4  synchronized关键字

特性

 synchronized 原子性解决 使用

 synchronized 也能保证内存可见性

2.5 volatile 关键字

 volatile 能保证内存可见性

 volatile 不保证原子性

 volatile禁止指令重排序


1、线程安全

1.1 线程的所有态

线程的状态是一个枚举类型 Thread.State

  1. public static void main(String[] args) {
  2. for (Thread.State state : Thread.State.values()) {
  3. System.out.println(state);
  4. }
  5. }

 状态:

  1. NEW (新建状态):系统中的线程还没创建出来,还只是个对象
  2. RUNNABLE(就绪状态):就绪状态
  • 准备好了,随时可以去CPU运行
  • 调用了start(),正在CPU上运行
  1. BLOCKED(阻塞状态):线程暂停执行,阻塞中
  2. WAITING(等待状态):线程暂停执行(wait()),等待中
  3. TIMED_WAITING(计时等待状态):线程暂停执行(sleep(),join(),wait()),计时等待中
  4. TERMINATED(终止状态):线程执行结束

2、 线程安全

2.1 概念

线程安全:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

线程不安全:多线程同时访问共享资源时,无序的调度,导致执行顺序的变化,结果就出现差错;

 2.2 线程不安全的原因(重点)

本质原因:线程在系统中调度时无序的/随机的 —— 抢占式执行

1.  抢占式执行

2. 多个线程同时修改 共享资源

  • 一个线程修改同一个变量 —>安全
  • 多个线程读取同一个变量 —>安全
  • 多个线程修改同一个变量 —>不安全

3. 修改操作,不是原子的(不可分割的)

  • 修改操作分为了几部分执行(多条CPU指令),加载到系统中,线程调用时无序的,会将这些和其他线程调用的指令进行混淆执行

4. 内存可见性,引起的线程不安全

5. 指令重排序,引起的线程不安全

图演示 

 1.2.3

两个线程各执行1w次,但是共享值依不是 2w,

原因是 同时修改,抢占执行,操作不是原子性

  1. class Counter{
  2. //计数
  3. public int count = 0;
  4. void add(){
  5. count++;
  6. }
  7. }
  8. public class Test {
  9. public static void main(String[] args) throws InterruptedException {
  10. Counter counter = new Counter();
  11. //创建两个线程,分别对count 进行 1w次
  12. Thread thread1 = new Thread(()->{
  13. for (int i = 0; i < 10000; i++) {
  14. counter.add();
  15. }
  16. });
  17. Thread thread2 = new Thread(()->{
  18. for (int i = 0; i < 10000; i++) {
  19. counter.add();
  20. }
  21. });
  22. //启动线程
  23. thread1.start();;
  24. thread2.start();
  25. //等待线程
  26. thread1.join();
  27. thread2.join();
  28. //打印counter
  29. //线程安全/单线程 情况下 应该是2w
  30. System.out.println(counter.count);
  31. //14260、13643、12677、15447....
  32. }
  33. }

 4.5

输入后,依旧运行,造成的原因是 共享值未能修改,指令重排序,优化

  1. public class Test {
  2. public static int falg = 0;
  3. public static void main(String[] args) {
  4. Thread thread1 = new Thread(()->{
  5. //
  6. while (falg == 0){
  7. //空 持续执行
  8. }
  9. System.out.println("thread1 执行结束");
  10. });
  11. Thread thread2 = new Thread(()->{
  12. Scanner scanner = new Scanner(System.in);
  13. //输入一个数,终止 thread1
  14. System.out.println("请输入一个非0整数:");
  15. falg = scanner.nextInt();
  16. });
  17. thread1.start();
  18. thread2.start();
  19. }
  20. }

2.3 保证线程安全(重点) 

2.3.1 原子性

原子性指一个操作要么完全执行,要么完全不执行,不能被其他操作中断或干扰。

 共享资源中add()中操作可以 大致分为

  1. 变量加载:从内存读取变量到CPU寄存器中
  2. 变量增加:将CPU寄存器中的值+1
  3. 变量保存:将寄存器中的值读取到内存中

这是一个操作,分为三步实现,在多线程中,想要实现线程安全,就要保证原子性;

如果原子性等不到保证,就会造成变量读取、增加、保存的混乱,造成错误结果

2.3.2  可见性

可见性:一个线程对共享变量值的修改,能及时地被其他线程看到

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

主内存:线程之间的共享变量存在

工作内存:线程自己拥有的内存,操作使用内存

读取:先把主内存中共享变量读取拷贝到工作内存,再从工作内存中修改数据

修改:先修改工作内存中的副本, 再同步回主内存.

 可见性:

         当线程A修改类共享变量X,线程B读取的X也会新值 

 2.2.3 指令重排序

代码的执行顺序不一定是 书写时的顺序

书写:

  1. 下楼,拿外卖
  2. 回宿舍,
  3. 下楼,取快递

在单线程下,JVM,CPU指令集会对其进行优化 ,按 1->3->2 执行;这就是指令重排序

但在多线程,执行程度高,优化后的代码不一定是自己想要的值,

2.4  synchronized关键字

  • synchronized 关键字可以用于 方法和代码块中,用于保护共享资源的访问;
  • 可以确保同一时间只有一个线程可以访问共享资源,从而实现原子性操作
  • synchronized用的锁是存在Java对象头里的。

特性

1.  原子性:synchronized关键字可以确保被它修饰的代码块或方法在同一时刻只能由一个线程执行,保证了代码的原子性。

2. 可见性:synchronized关键字可以保证一个线程在获取锁时,它所在的线程工作内存中的变量值是最新的,而不是缓存中的旧值,这保证了线程之间的可见性。

3.  互斥性:synchronized关键字可以保证同一时刻只有一个线程持有锁,其他线程进入阻塞等待,必须等待锁的释放才能获取锁,从而保证了线程之间的互斥性。

 

 4. 重入性synchronized关键字是可重入的,也就是说,如果一个线程已经持有了某个对象的锁,那么它再次进入该对象的synchronized代码块时,仍然可以获得该对象的锁,而不会被自己所持有的锁所阻塞。

 核心操作:

  1. 加锁:就是获取锁,使当前线程可以独占同步资源进行操作,防止其他线程同时修改统一资源,进入synchronized修饰的方法和代码块中,就会触发 加锁
  2. 解锁:就是释放锁,使得其他线程可以获取锁并访问同步资源,出synchronized修饰的方法和代码块中,就触发 解锁

当一个线程 执行到一个synchronized修饰的代码块或者方法时,首先会尝试获取对象的锁,如果该对象的锁已经被其他线程获取,那么当前线程就会被阻塞,直到其他线程释放了该对象的锁;

 synchronized 原子性解决 使用

对于 锁的颗粒不同 分为两种:

加锁主要为了区别是否是同一个对象

1、 对象锁

对象锁:当synchronized 修饰一个对象时,该对象就会成为同步锁

  • 修饰代码块:(this)指定当前对象,锁该类的实例对象
  • 修饰普通方法:锁该类的实例对象
  1. class Counter{
  2. //计数
  3. public int count = 0;
  4. public void add(){
  5. //代码块 对当前对象
  6. synchronized (this){
  7. count++;
  8. }
  9. }
  10. }

或  创建个 私有Object类锁对象,用于锁的同步控制;

  1. class Counter{
  2. //计数
  3. public int count = 0;
  4. //私有锁对象
  5. private Object lock = new Object();
  6. public void add(){
  7. //代码块 对当前锁对象
  8. synchronized (lock){
  9. count++;
  10. }
  11. }
  12. }

 方法;

  1. class Counter{
  2. //计数
  3. public int count = 0;
  4. //修饰普通方法 当前对象
  5. public synchronized void add(){
  6. count++;
  7. }
  8. }

2、 类锁

 类锁:当synchronized 修饰一个静态方法或代码块时,该类就成为同步锁;

  • 修饰代码块:(类名.class)指定类,锁该类对象
  • 修饰静态方法:锁该类对象
  1. class Counter{
  2. //计数
  3. public int count = 0;
  4. public void add(){
  5. //代码块 对该类加锁
  6. synchronized (Counter.class){
  7. count++;
  8. }
  9. }
  10. }

方法

  1. class Counter{
  2. //计数
  3. public int count = 0;
  4. //修饰静态方法 加锁该类
  5. public synchronized static void add(){
  6. count++;
  7. }
  8. }

 synchronized 也能保证内存可见性

  1. class Quit{
  2. public int falg = 0;
  3. }
  4. public class Test {
  5. public static void main(String[] args) {
  6. Quit qu = new Quit();
  7. Thread thread1 = new Thread(()->{
  8. //持续执行
  9. while (true){
  10. //
  11. synchronized (qu){
  12. if (qu.falg != 0){
  13. System.out.println("thread1 执行结束");
  14. break;
  15. }
  16. }
  17. }
  18. });
  19. Thread thread2 = new Thread(()->{
  20. Scanner scanner = new Scanner(System.in);
  21. //输入一个数,终止 thread1
  22. System.out.println("请输入一个非0整数:");
  23. qu.falg = scanner.nextInt();
  24. });
  25. thread1.start();
  26. thread2.start();
  27. }
  28. }

注意:无论如何,加锁都可能导致阻塞,对于代码的效率肯定会有影响

2.5 volatile 关键字

特性:

  1. 可见性:当一个线程修改了volatile变量的值,其他线程能够立即看到该变量的最新值。
  1. 原子性:对于单个volatile变量的读/写操作是具有原子性的,但是对于多个操作的复合操作则不具有原子性。
  1. 顺序性:对于volatile变量的读/写操作会按照程序的顺序执行,不能被重排序。

 volatile 能保证内存可见性

volatile 修饰的共享变量, 能够保证 "内存可见性".

  1. 未加 volatile 修饰共享变量,线程1对代码进行了优化,不再读取操作,线程2修改共享变量未被线程1读取
  2. 加上volatile 修饰共享变量,线程2修改共享变量会被线程1及时读取,终止线程
  1. public class Test {
  2. //保证 共享变量falg 内存可见性
  3. public volatile static int falg = 0;
  4. public static void main(String[] args) {
  5. Thread thread1 = new Thread(()->{
  6. //
  7. while (falg == 0){
  8. //空 持续执行
  9. }
  10. System.out.println("thread1 执行结束");
  11. });
  12. Thread thread2 = new Thread(()->{
  13. Scanner scanner = new Scanner(System.in);
  14. //输入一个数,终止 thread1
  15. System.out.println("请输入一个非0整数:");
  16. falg = scanner.nextInt();
  17. });
  18. thread1.start();
  19. thread2.start();
  20. }
  21. }
  22. ***************
  23. 请输入一个非0整数:
  24. 1
  25. thread1 执行结束
  26. ***************

 volatile 不保证原子性

 volatile 使用 针对单个变量被一个线程读,一个线程写;对于多个线程的符合操作不具有原子性

 volatile禁止指令重排序

  1. volatile 禁止对 修饰变量的读/写操作 指令的重排序;
  2. 任何对 volatile 变量的写入操作都会先于后续的读取操作执行。这样就保证了在多线程环境下,对共享变量的修改能够及时被其他线程看到,从而保证了程序的正确性。

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/650080
推荐阅读
相关标签
  

闽ICP备14008679号