【多线程】什么是线程安全?

“线程安全”问题是“线程”安全么?

其实“线程安全”并不是指线程的安全,而是指内存的安全。为什么呢?这主要和操作系统有关。

现在主流操作系统都是多任务的,也就是多个进程同时执行。所以为了保证安全,每个进程只能访问分配给自己的内存空间,不能访问别人的空间,这是由操作系统来保证的。

每个进程的内存空间里都有一块公共区域:堆(内存)。进程内的所有线程都可以访问到该区域,所以这就存在安全隐患。假设某个线程自己的任务处理到一半累了,休息一会儿,回来时想接着处理,却发现数据已经被修改了,而非当初离开时的样子。这是因为可能被其他线程修改了。

所以,线程安全指的是在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被其他线程修改的风险。

那我们应该如何解决这种线程安全问题呢?下面针对不同的侧重点,分析几种解决方案。

私有物品,请勿观看

前面我们说到,操作系统会为每个进程分配属于他们自己的内存空间,而进程中的每个线程也拥有自己的内存空间,通常称为栈内存,其他线程无权访问。

较为常见的有局部变量。这些数据只有某个线程会使用,其他线程不能操作也无需操作,这些数据就放入了线程的栈内存中。

double avgScore(double[] scores) {
    double sum = 0;
    for (double score : scores) {
        sum += score;
    }
    int count = scores.length;
    double avg = sum / count;
    return avg;
}

这里的变量sum, count, avg都是局部变量,都被分配进线程的栈内存中了。
假设此时有一个线程T1来执行这个方法,这些变量就被分配到T1的栈内存中。同理,线程T2来执行,也会在线程T2的栈内存中分配这些变量。

可见,局部变量会在每个线程的栈内存中分配一份,只允许线程自己访问,其他线程根本不知道它们。也就是每个线程的私有物品,别人并不知道。也可以理解为:这个东西的“位置”只有你自己知道,所以它就是安全的

不争不抢,人人有份

上面分析了程序中处于“私有位置”的东西是安全的,但是并不是所有的东西都会处于私有位置,为了提高利用率,总有东西需要放置在公共区域。那么该如何保证公共区域的东西的安全呢?很简单,人人都有自己的一份,自己玩自己的,这样他们就不会争抢了。

class StudentAssistant {

    ThreadLocal<String> realName = new ThreadLocal<>();
    ThreadLocal<Double> totalScore = new ThreadLocal<>();

    String determineDegree() {
        double score = totalScore.get();
        if (score >= 90) {
            return "A";
        }
        if (score >= 80) {
            return "B";
        }
        if (score >= 70) {
            return "C";
        }
        if (score >= 60) {
            return "D";
        }
        return "E";
    }

    double determineOptionalcourseScore() {
        double score = totalScore.get();
        if (score >= 90) {
            return 10;
        }
        if (score >= 80) {
            return 20;
        }
        if (score >= 70) {
            return 30;
        }
        if (score >= 60) {
            return 40;
        }
        return 60;
    }
}

这个学生助手类有两个成员变量,realName 和 totalScore,都是 ThreadLocal 类型的。每个线程在运行时都会拷贝一份存储到自己的本地。

T1线程运行的是“张三”和“90”,那么这两个数据“张三”和“90”是存储到T1线程对象(Thread类的实例对象)的成员变量里去了。假设此时T2线程也在运行,是“李四”和“85”,那么“李四”和“85”这两个数据是存储到了T2线程对象(Thread类的实例对象)的成员变量里去了。

线程类(Thread)有一个成员变量,类似于Map类型的,专门用于存储ThreadLocal类型的数据。从逻辑从属关系来讲,这些ThreadLocal数据是属于Thread类的成员变量级别的。从所在“位置”的角度来讲,这些ThreadLocal数据是分配在公共区域的堆内存中的。

简单理解,就是把堆内存中的一个数据复制N份,每个线程认领1份,同时规定:每个线程只能用自己的那份,不能影响别人的。

需要说明的是这N份数据都还是存储在公共区域堆内存里的,经常听到的“线程本地”,是从逻辑从属关系上来讲的,这些数据和线程一一对应,仿佛成了线程自己“领地”的东西了。其实从数据所在“位置”的角度来讲,它们都位于公共的堆内存中,只不过被线程认领了而已。

重要的事情讲三遍。
重要的事情讲三遍。
重要的事情讲三遍。

ThreadLocal就是,把一个数据复制N份,每个线程认领一份,各玩各的,互不影响。

可远观而不可亵玩焉

根据我们认知,放在公共区域的东西总是存在着安全风险,不一定会是绝对的安全。不过,也有例外:比如一个非常庞大的东西被放在大街上,大家都搞不动它,它也是安全的。

程序中,我们把这种情况认定为:只能读,不能写。也就是,只能读取,不能修改。其实就是指常量或只读变量,他们对于多线程是安全的,想改也改不了。

class No1 {

    final double passScore = 60;
}

比如一个学生的分数前面用一个 final 修饰,这样所以线程都动不了它。这不就很安全了嘛。

小结

以上分析的情况是比较理想的状态,当意外发生,我们也有其他的解决办法,预知后事如何,且听下回分解~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

杨幂等

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

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

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

打赏作者

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

抵扣说明:

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

余额充值