线程安全问题的原因和解决方案

文章讨论了多线程环境下出现的安全问题,如线程中断和抢占式调度导致的顺序不一致。提出了三种解决线程不安全的方案:1)使用synchronized关键字实现同步,确保代码块的原子性;2)利用Lock接口的lock和unlock方法控制锁的获取与释放;3)使用volatile修饰变量,保证内存可见性和禁止指令重排序。文章还强调了避免锁竞争和合理分配线程资源以提高效率的重要性。
摘要由CSDN通过智能技术生成

多线程安全的解决

出现问题

为什么要有多线程,为了进行并发编程,更好利用多核CPU。 在多线程环境调度线程时,会经常出现bug,分析bug出现的原因:线程中断分析进程调度线程是随机的。所以会存在顺序不一致的问题,线程执行存在两种调度方式“抢占式”和“非抢占式”。字面意思理解,两个线程在同一进程中,线程间的调度方式如果是“抢占性”一般都会带有优先级,优先级高的会将正在调度的线程“挤”下,进程选择优先级更高的线程进行调度,原来被调度的线程进入等待(阻塞)。

抢占式:

在这里插入图片描述

“非强占式”就相当于排队打饭,不管你优先级多高,都需要等待当前调度的线程执行完毕,才可以调用(使用资源)。

在这里插入图片描述

原子操作解决方案

我们知道,使用原子操作可以使得结果具有唯一性!那么解决多线程不安全问题是否也可以使用原子操作呢?答案当然是可以的,因为随机调度引发的问题,这里使用原子操作,将每个线程进行加锁🔒,将其行为变成不可拆分的最小单位,保障多线程安全。

1.使用synchronized

1.在public上添加synchronized关键字

class Add{
    public int count;
    //直接在方法上添加
    synchronized public void add(){
            count++;
    }

    public int getCount() {
        return count;
    }
}

2.在代码块上添加synchronized关键字

class Add{
    public int count;
    public void add(){
        //在代码块上使用synchronized
        //这里的this可以替换成任意一个object数据类型,不过该数据在此处无太大意义,只是一个标识作用
        synchronized (this){
            count++;
        }
    }
    public int getCount() {
        return count;
    }
}
//同一main函数,代码如下
public class ThreadDome {
    public static void main(String[] args) throws InterruptedException {
        Add add = new Add();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000 ; i++) {
                add.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000 ; i++) {
                add.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(add.getCount());
    }
}

这两种方法的结果一样,需要注意的是,对代码的执行效率不同。因为一个是对方法加锁,这就表明了当调用类中的这个方法时就开启加锁操作,需要等方法中所有代码执行完毕才会执行解锁操作。而对代码块进行加锁,只是单纯对代码加锁,只要代码块执行完毕就执行解锁操作!

入则锁出则解

在这里插入图片描述

2.使用lock和unlock

lock类需要创建一个锁对象,对不安全的代码或者方法进行加锁操作。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Count{
    public int count;
    synchronized public void add(){
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class ThreadLock {
    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        //定义锁
        Lock l =new ReentrantLock();
        Thread t1 =new Thread(()->{
            for (int i = 0; i < 50000 ; i++) {
                l.lock();//加锁
                count.add();
                l.unlock();//解锁
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                l.lock();
                count.add();
                l.unlock();
            }
        });
        //调用线程
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.getCount());
    }
}

在这里插入图片描述

结果与synchronized一样,这个对象的使用与synchronized不同,Lock类方法需要直接进行解锁,如果你忘了解锁,系统不会帮助你解锁,可能会产生死锁问题,也就是bug。

3.使用volatile

说到volatile就必须提到==内存可见性==:在内存环境中,编译器对代码进行优化,可以保证结果不变,通过语句变换和一些其他操作,然后提高代码的运行效率。

在单线程中,编译器对程序结果不变的判断十分准确。在多线程环境下编译器对代码进行优化后,可能会产生误判,从而引发bug。

和原子性无关

public class ThreadVolier {
    //volatile修饰的变量特性:
    //每次读取该变量就会回到内存中读取,而不是简单读取
    //可以保证变量修改时被察觉
    volatile public static int flag;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (flag == 0){
            }
            System.out.println("循环结束!t1结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            flag=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

volatile的适用场景:一个线程读,一个线程写。

volatile的另一个效果:禁止指令重排序

**指令重排序:**调整代码执行顺序,让程序更加高效,保证逻辑不变即可,而且不改变程序运行结果。

锁竞争

多个线程尝试对同一个锁🔒对象进行加锁,这种情况就会产生锁竞争!如果多个线程对不同对象进行加锁操作,就不会有锁竞争!如果一个线程从锁中出来!其余线程之间如果发生锁竞争,必有一方会发生阻塞,不利于提高进程效率,但是提高了线程安全。

提高进程效率的方法,找出并发线程中的临界区(并发代码),在保证线程安全的情况下,合理分配线程资源,提高线程效率。

总结:线程不安全原因

1.抢占式调度,随机调度

2.多个线程修改同一个变量

3.修改变量操作不是 原子操作

4.内存可见性

5.指令重排序

要创建一个安全的线程应该检查以上五项,解决不安全问题的方法也很多,以上几种就可以解决相应的安全问题。
线程中的临界区(并发代码),在保证线程安全的情况下,合理分配线程资源,提高线程效率。

总结:线程不安全原因

1.抢占式调度,随机调度

2.多个线程修改同一个变量

3.修改变量操作不是 原子操作

4.内存可见性

5.指令重排序

要创建一个安全的线程应该检查以上五项,解决不安全问题的方法也很多,以上几种就可以解决相应的安全问题。

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值