什么是线程安全问题,出现了你会解决吗?

一、线程安全问题

多线程虽然能够提高程序运行的效率,但如果出现程序在多线程的执行结果不符合预期,就属于线程安全问题。
比如以下示例,我们定义一个类、一个静态变量num和两个方法,一个方法使num++100次,一个方法使num–一百次,我们使用两个线程分别调用这两个方法,那最终的结果会是0吗?

package safeProblemThread;

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Safe safe=new Safe();
        Thread thread1=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                safe.add();
            }
        });
        Thread thread2=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                safe.sub();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(safe.num);
    }
}
class Safe{
    public static int num=0;
    public void add(){
        num++;
    }
    public void sub(){
        num--;
    }
}

最终结果:在这里插入图片描述
结果显然不是0;而且每次运行的结果都不一样,不相信的小伙伴可以尝试一下。那这种情况就属于线程安全问题,怎么解决呢?我们首先就得先知道线程不安全元素有哪些:

1.1线程安全问题导致的原因

1.抢占式执行:CPU层面,不可解决
2.多个线程修改同一个变量:文章开头的代码就有这个原因导致线程安全问题,由于多个线程修改同一个变量,线程间不能及时的返回和获取修改后的值,这就导致了你修改你的,我修改我的,某一个时间点更新,这就导致每一次的结果都不符合预期且不相同。
3.非原子性操作:这里的原子性类似于数据库中事务的原子性,
如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。在上面的代码里,num++或者num–实际是非原子性操作,因为在底层实际执行了三步操作,获取num的值,加1或者减1,更新num的值。
4.内存可见性问题:可⻅性指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到。在多线程的情况下,当一个线程进行到第二步,第二个个线程就把数据更新了,当第一个线程进行更新时,就会导致第二个线程刚才做的操作实际是无效的。(这里指num++或num–的步骤)
5.指令重排序:指令重排序是编译器为了优化代码而打乱代码的执行顺序,这种在单线程的情况下是不会出错的,但是在多线程的情况下就会出现线程安全问题。
线程安全问题并不是这5个原因单独造成,而是其中的多个共同造成,如何解决呢?

二、volatile解决内存可见性和指令重排序

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

内存可见性实际是访问工作内存,所以速度非常快,但是volatile强制内存读写操作,虽然能解决问题,但是速度变慢了。

/**
 * volatile解决线程安全问题
 */
public class Demo2 {
    public static volatile boolean flag=false;

    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            System.out.println("线程1开始执行");
            while (!flag) {
            }
            System.out.println("线程1执行结束");
        });
        Thread thread2=new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag=true;
            System.out.println("变量值已经修改");
        });
        thread1.start();
        thread2.start();
    }
}

这个代码就是用volatile解决了线程安全问题(小伙伴可以试一下,如果不不使用volatile修饰flag的话,代码将会一直处于死循环),但是由于volatile只能解决内存可见性和指令重排序,文章开头的代码问题还是不行。

三、锁

我们可以使用锁来结局线程安全问题,而Java中可以使用synchronized和lock两种方式。

3.1synchronized

1.基本使用

  • 修饰静态方法
    private static int num=0;
    public static synchronized void add(){
        num++;
    }
  • 修饰普通方法
    public synchronized int add(int a){
        return a++;
    }
  • 修饰代码块
        synchronized (this){
            ret=a+b;
        }

2.特性

  1. 互斥
    synchronized会起到互斥作用,当某个线程执行到synchronized的代码中时,其他线程若也要进入到synchronized的代码中,则要等上一个线程执行完成。

synchronized用的锁是存储在对象头,当进入synchronized相当于加锁,退出synchronized相当于解锁。

  1. 刷新内存
    synchronized的工作过程:
    (1)获得互斥锁
    (2)从主内存中拷贝变量的最新副本到工作内存中
    (3)执行代码
    (4)将更改后的共享变量的值刷新到主内存中
    (5)释放互斥锁
  2. 可重入
    当一个线程获得了锁后,当再次进入后不会出现锁死自己
    public static void main(String[] args) {
        Object lock=new Object();
        synchronized (lock){
            System.out.println("进来");
            synchronized (lock){
                System.out.println("再进来");
            }
        }
    }

可重入性就使代码会执行第二个输出,而不会等到释放锁后才能执行第二个输出,造成卡死。
这样我们就可以解决文章开头出现的线程安全问题了。

    public static void main(String[] args) throws InterruptedException {
        Safe safe=new Safe();
        Thread thread1=new Thread(()->{
            synchronized (safe){
                for (int i = 0; i < 10000; i++) {
                    safe.add();
                }
            }
        });
        Thread thread2=new Thread(()->{
            synchronized (safe){
                for (int i = 0; i < 10000; i++) {
                    safe.sub();
                }
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(safe.num);
    }

这样的代码,运行多少次结果都为0,就不存在线程安全问题。
3.synchronized实现原理
synchronized实现原理
https://blog.csdn.net/m0_49622667/article/details/124266741
synchronized发展:无锁->偏向锁->轻量级锁->重量级锁

3.2Lock

和synchronized不一样,lock我们称为手动锁,因为它不像synchronized一样只需要修饰就行,我们在使用lock时还需要手动的创建锁,手动的释放锁。
我们先来看一下lock的基本使用:

        Object object=new Object();
        Lock lock=new ReentrantLock();
        lock.lock();
        try {
			//业务代码
        }finally {
            lock.unlock();
        }

这里的ReentrantLock()就是可重入锁,那为什么会这样写代码呢?原因有两个:

  1. 当创建了锁之后,在执行任务代码后如果出现某些异常导致会导致锁未释放。所以在finally中关闭锁。
  2. 如果将创建锁放在try中,当业务代码中若出现问题,还没加锁,就执行了释放操作,显然是不合理的,所以我们需要将加锁操作放在try的首行或者直接放在try外面。

公平锁与非公平锁:
输出AABBCCDD

public static void main(String[] args) throws InterruptedException {
        Lock lock=new ReentrantLock(true);
        Thread thread1=new Thread(()->{
            String str="ABCD";
            for(char i:str.toCharArray()){
                lock.lock();
                try {
                    System.out.print(i);
                }finally {
                    lock.unlock();
                }
            }
        });
        Thread thread2=new Thread(()->{
            String str="ABCD";
            for(char i:str.toCharArray()){
                lock.lock();
                try {
                    System.out.print(i);
                }finally {
                    lock.unlock();
                }
            }
        });
        Thread.sleep(10);
        thread2.start();
        thread1.start();
    }

3.3synchronized和Lock的区别

  1. lock的粒度可以更小
  2. lock需要手动操作锁,synchronized是JVM自动操作锁
  3. lock只能修饰代码块,synchronized可以修饰静态方法、普通方法、代码块
  4. lock可以实现公平锁和非公平锁,synchronized只能是非公平锁。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

友农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值