• 线程安全问题


    目录

    什么是线程安全

    为什么会存在线程安全问题 

    JAVA内存模型——JMM

    如何保证线程安全

    synchronized

    互斥性

    刷新内存

     可重入

    synchronized的使用

    volatile

    保证共享变量的可见性

    volatile作为内存屏障

    Java标准库的线程安全类 

    什么是线程安全

    线程安全就是在多线程并发情况下执行的结果跟单线程下顺序执行的预期结果不符的问题,这种现象就是线程不安全问题

    1. package thread;
    2. public class ThreadUnsafe {
    3. private static class count{
    4. int count=0;
    5. void increase(){
    6. count++;
    7. }
    8. }
    9. public static void main(String[] args) throws InterruptedException {
    10. count c1=new count();
    11. Thread t1=new Thread(new Runnable() {
    12. @Override
    13. public void run() {
    14. for (int i = 0; i < 50000; i++) {
    15. c1.increase();
    16. }
    17. }
    18. });
    19. Thread t2=new Thread(new Runnable() {
    20. @Override
    21. public void run() {
    22. for (int i = 0; i < 50000; i++) {
    23. c1.increase();
    24. }
    25. }
    26. });
    27. t1.start();
    28. t2.start();
    29. t1.join();
    30. t2.join();
    31. System.out.println(t1.getState());
    32. System.out.println(t2.getState());
    33. System.out.println("两个搬砖人已经工作结束,一共板砖"+c1.count+"块");
    34. }
    35. }

    为什么会存在线程安全问题 

    JAVA内存模型——JMM

    JMM 和 JVM部分讲到JVM内存区域划分(JVM实实在在将内存划分为6大区域),而JVM只是用来描述线程工作内存和主内存的关系(就是多线程下的JAVA线程内存(CPU的高速内存和寄存器)和主内存的关系)

    • 每个线程都有自己的工作内存,每次读取变量(共享变量,不是线程的局部变量,比如方法的局部变量和run方法的局部变量是线程私有),共享变量都是存在主内存中
    • 共享变量(类中成员变量,静态变量,常量都属于共享变量,在堆和方法区中存储的变量)
    • 关于修改共享变量,每次都是先从主内存将变量加载到自己的工作内存,之后关于此变量的所有操作都是在自己的工作内存中进行,然后写回主内存
    • 如果t1线程已经将主内存的变量的写入工作内存,如果此时其他线程t2已经将主内存的变量写入内存,那么对于t2来说,t1修改后的值就是不可见的,或者如果没有将主内存的变量写入内存,此时从主内存读的值是还没有更新的值(脏读)

    因为操作的非原子性和属性的不可见性和防止指令重排导致的线程不安全

    保证三大特性 原子性 可见性 防止指令重排

    原子性

    该操作对应CPU的一条指令,这个操作不会被中断,要么全部执行,要么都都不执行,不会存在中间状态

     

    原子性的超卖现象

     客户端A先卖了一张票,然后去判断B的,由于A没将卖掉的结果写入主内存,所以客户端B的显示还有票,然后又卖了一次,那么执行A的将卖票的结果写入主内存,也执行B的卖票的结果,所以一张票就被卖了两次,称为超卖现象

    可见性

    一个线程对共享变量的的修改,能够及时的被其他线程看到,这种特性称为可见性,(可见性可以用final,volatile和synchronized上锁来保证)

     

    • 在多线程的执行下,就存在各种各样的执行的可能性,执行的结果充满了各种的可能性,这种可能性是不确定,是可能是由原子性和可见性的原因导致每次的执行结果不一样

    指令重排

    代码的书写顺序不一定就是JVM或者CPU最终的执行顺序

    • 在单线程的上述的重排没有关系,但是在多线程下就会有很大的问题 
    • 指令重排了解即可,因为很少是因为指令重排导致的线程安全问题

    如何保证线程安全

    我们只需要保证线程的原子性,防止指令重排,和可见就可以防止线程安全的发生

    synchronized

    • 用synchronized可以保证线程的原子性和可见性

    定义:就是监视器锁,monitor lock(对象锁),锁的是资源

    互斥性

    synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到 同一个对象 synchronized 就会 阻塞等待 .,处于阻塞等待状态
    • 进入 synchronized 修饰的代码块, 相当于 加锁
    • 退出 synchronized 修饰的代码块, 相当于 解锁
    •  synchronized用的锁是存在Java对象头里的。

     synchronized的上锁就拿上厕所举例子,四个滑稽想上厕所,厕所是一种资源,我们怎么才能大胆安心的去上厕所呢?要保证厕所是安全的,我们就给厕所上锁,让厕所是安全的,每次只能一个人占用厕所,别人进不来,在上厕所的时候上锁(进入synchronized代码块)(此时其他三个人就处于该资源的阻塞等待状态),上完厕所解锁(执行完synchroized代码块)

    1. public class ThreadUnsafe {
    2. private static class count{
    3. int count=0;
    4. synchronized void increase(){
    5. count++;
    6. }
    7. }
    8. public static void main(String[] args) throws InterruptedException {
    9. count c1=new count();
    10. Thread t1=new Thread(new Runnable() {
    11. @Override
    12. public void run() {
    13. for (int i = 0; i < 50000; i++) {
    14. c1.increase();
    15. }
    16. }
    17. });
    18. Thread t2=new Thread(new Runnable() {
    19. @Override
    20. public void run() {
    21. for (int i = 0; i < 50000; i++) {
    22. c1.increase();
    23. }
    24. }
    25. });
    26. t1.start();
    27. t2.start();
    28. t1.join();
    29. t2.join();
    30. System.out.println(t1.getState());
    31. System.out.println(t2.getState());
    32. System.out.println("两个搬砖人已经工作结束,一共板砖"+c1.count+"块");
    33. }
    34. }

    •  正因为increase方法上锁处理,多个线程在执行increase方法时候其实是排队进入,同一个时刻只可能有进入一个线程进入increase方法执行对count属性的操作
    • 比如t1先执行了increase方法,那么t1就会获取counter这个对象的锁,然后执行increase方法,t2如果想执行这个increase方法会处于阻塞态,就必须等到t1释放锁之后,才能执行

    锁的数据结构

    在JAVA内部,每个JAVA对象都有一块内存,描述当前对象“锁”的信息,锁的信息就表示当前对象被哪个线程持有

    • 若锁信息没有保存线程,说明该对象没有被任何线程持有
    • 若锁的信息保存了线程id,其他线程要获取该锁,就处在阻塞状态

     要注意锁的对象不同

    1. package thread;
    2. public class ThreadUnsafe {
    3. private static class count{
    4. int count=0;
    5. synchronized void increase(){
    6. System.out.println(Thread.currentThread().getName()+"获得锁");
    7. count++;
    8. try {
    9. Thread.sleep(1000);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. }
    15. public static void main(String[] args) throws InterruptedException {
    16. count c1=new count();
    17. Thread t1=new Thread(new Runnable() {
    18. @Override
    19. public void run() {
    20. for (int i = 0; i < 50000; i++) {
    21. c1.increase();
    22. }
    23. }
    24. },"t1");
    25. Thread t2=new Thread(new Runnable() {
    26. @Override
    27. public void run() {
    28. for (int i = 0; i < 50000; i++) {
    29. c1.increase();
    30. }
    31. }
    32. },"t2");
    33. count c2=new count();
    34. Thread t3=new Thread(new Runnable() {
    35. @Override
    36. public void run() {
    37. for (int i = 0; i < 50000; i++) {
    38. c2.increase();
    39. }
    40. }
    41. },"t3");
    42. t1.start();
    43. t2.start();
    44. t3.start();
    45. t1.join();
    46. t2.join();
    47. t3.join();
    48. System.out.println("两个搬砖人已经工作结束,一共板砖"+c1.count+"块");
    49. }
    50. }

     

    •  因为t1和t2有竞争关系,其t1和t2与t3没有竞争关系

    刷新内存

    synchronized的工作流程

    1. 获取对象锁
    2. 从主内存拷贝变量到工作内存
    3. 执行代码
    4. 将更改后的值写入主内存
    5. 释放对象锁

    因为其1-5都是只有一个线程能执行,所以其2-4对于其他线程就是天然的可见性和原子性

     可重入

    • JAVA的线程安全锁都是可重入的(包括java.concurrent.lock)
    • 可重入的意思就是获取的对象锁的线程可以再次加锁

    •  按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁.
    • 可重入锁可以防止这种死锁问题

    synchronized支持线程的可重入

    1. Java中每个对象都有一个对象头(描述当前对象的锁信息,当前对象被哪个线程所拥有,以及一个计数器——当前对象被上锁的次数)
    2. 若线程1需要进入当前对象的同步代码块(sychronized),此时当前对象的对象头没有锁信息

    synchronized的使用

    synchronized是对象锁,所以必须要有具体的对象让他锁

    直接修饰类的成员方法

    • 当前这个方法是通过谁调用的,synchronized就锁哪个对象 

    直接修饰类的静态方法

    •  用来修饰类的静态方法,锁的是当前这个类的class对象(任何一个类的class对象全局唯一),描述该类的核心信息(具备那些属性和方法),这个对象是反射的核心对象

    直接修饰代码块

    • 明确锁的对象
    • 锁的粒度更细,只有再需要同步的若干代码块上加上synchronized关键字

    • this表示当前对象的引用 ,如果不同的线程调用对象是不同的,那么不同线程之间是不互斥的(所争的资源是不一样的),如果不同线程调用对象是相同的,那么不同线程之间是互斥的

    • Reentrant.class表示这个类的唯一的class对象,只要是调用这个类的对象,都会对着个代码块进行互斥访问 

    互斥与否,就看多个线程锁的到底是什么?

    1. package thread;
    2. public class ThreadLock {
    3. private static class Counter {
    4. int val;
    5. Object lock;
    6. void increase() {
    7. //不需要同步的代码
    8. //需要同步的代码 synchronized(任意对象,传啥锁啥)
    9. synchronized (lock) {
    10. while (true) {
    11. System.out.println(Thread.currentThread().getName());
    12. try {
    13. Thread.sleep(1000);
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. }
    18. }
    19. }
    20. }
    21. public static void main(String[] args) {
    22. Object lock=new Object();
    23. Counter c1=new Counter();
    24. c1.lock=lock;
    25. Counter c2=new Counter();
    26. c2.lock=lock;
    27. Counter c3=new Counter();
    28. c3.lock=new Object();
    29. Thread t1=new Thread(()->{
    30. c1.increase();
    31. },"t1");
    32. Thread t2=new Thread(()->{
    33. c2.increase();
    34. },"t2");
    35. Thread t3=new Thread(()->{
    36. c3.increase();
    37. },"t3");
    38. t1.start();
    39. t2.start();
    40. t3.start();
    41. }
    42. }
    • t1和t1构成互斥,因为锁的lock是同一个对象
    • t1和t3 t2和t3都不互斥,因为锁的不是同一个对象

    volatile

    保证共享变量的可见性

    volatile关键字可以保证共享变量可见性,相较于普通的共享变量,使用volatile关键字可以保证共享变量的可见性

    • 当线程读取的是volatile关键字时,线程直接从主内存中读取该值到工作内存中(无论当前工作内存中是否已经有该值)
    • 当线程写的是volatile关键字变量,将当前修改后的变量值(工作内存中)立即刷新到主内存,且其他正在读此变量的线程会等待(不是阻塞),直到写回主内存操作完成,保证读的一定是刷新后的主内存值
    1. package thread;
    2. import java.util.Scanner;
    3. public class ThreadVolatile {
    4. private static class Counter{
    5. volatile int flag=0;
    6. }
    7. public static void main(String[] args) {
    8. Counter counter=new Counter();
    9. Thread t1=new Thread(new Runnable() {
    10. @Override
    11. public void run() {
    12. while (counter.flag==0){
    13. };
    14. System.out.println(counter.flag+"退出循环");
    15. }
    16. });
    17. t1.start();
    18. Thread t2=new Thread(new Runnable() {
    19. @Override
    20. public void run() {
    21. Scanner scanner=new Scanner(System.in);
    22. System.out.println("请改变flag的值");
    23. counter.flag=scanner.nextInt();
    24. }
    25. });
    26. t2.start();
    27. }
    28. }

    •  volatile保证其可见性,无法保证原子性,因此若代码不是原子性操作,任然不是线程安全的

    volatile作为内存屏障

     

    Java标准库的线程安全类 

    不安全的类

    • 多线程并发修改同一个集合的内容,就有数据安全问题

     安全的类

    • Vector和HashTable的读写操作都是单线程的
    • ConcurrentHashMap锁的是哈希桶对象
    • CopyOnWriteArrayList:线程安全的List集合,采用读写分离的机制,多线程并发读,互斥写 
  • 相关阅读:
    xargs如何保留文本中的引号
    Vue入门介绍
    一文说透kafka底层架构
    【WSN定位】基于matlab灰狼算法优化无线传感器非测距定位【含Matlab源码 2008期】
    基于桶的排序之计数排序
    zookeeper + kafka集群搭建详解
    【JS高级】ES5标准规范之创建子对象以及替换this_10
    redis 常见的命令
    【递归、搜索与回溯算法】第三节.21. 合并两个有序链表和206. 反转链表和24. 两两交换链表中的节点
    linux 下 java环境安装
  • 原文地址:https://blog.csdn.net/qq_50985215/article/details/125466665