前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >多线程---线程安全

多线程---线程安全

作者头像
用户9854323
发布2022-06-25 10:55:14
发布2022-06-25 10:55:14
60600
代码可运行
举报
文章被收录于专栏:小陈飞砖小陈飞砖
运行总次数:0
代码可运行

线程安全是开发者在开发多线程任务时最关心的问题,那么线程安全需要注意哪些呢?

一、思考:线程安全产生的原因是什么? 二、final,volatile关键字的作用? 三、1.5之前的javaDCL有什么缺陷? 四、如何编写线程安全的程序? 五、ThreadLocal使用的注意事项有哪些?

一、思考:线程安全产生的原因是什么?

原因:可变资源(内存)线程间共享

由Java的内存模型:各线程都有自己的工作内存 和 虚拟机的主内存。 工作内存中保存了该线程使用的变量的副本,各线程对变量的所有操作必须在工作内存中进行,即操作的是变量的副本。

看下面这个过程: 例如:对于 int a = 3; 线程A、B、C 都要操作变量a++;

  • 当线程A操作a++后:主内存 a=3,A的工作内存 a = 4; B工作内存中 a=3; C 中 a=3;
  • 然后B也操作a++: 主内存 a=3,A的工作内存 a = 4; B工作内存中 a=4; C 中 a=3;
  • 然后A工作内存中的a同步到主内存:主内存 a=4,A的工作内存 a = 4; B工作内存中 a=4; C 中 a=3;
  • 然后主内存同步到其他线程:主内存 a=4,A的工作内存 a = 4; B工作内存中 a=4; C 中 a=4;

这时就出问题了:a = 5才对,因为A,B线程都执行了a++,两次a++。

二、如何实现线程安全呢?

根据线程安全原因:可变资源(内存)线程间共享可得出:

  • 不共享资源
  • 共享不可变资源
  • 共享可变资源(可见性、操作原子性、禁止重排序)
1、不共享资源

ThreadLocal:

如何使用ThreadLocal(看Loop.java中如何使用):

代码语言:javascript
代码运行次数:0
运行
复制
   static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

   private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
    
   public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

ThreadLocal原理: ThreadLocal.java

代码语言:javascript
代码运行次数:0
运行
复制
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

//这个map是绑定在线程上的    
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocal使用建议:

  • 声明为全局静态final成员,ThreadLocal在一个线程里有一个就够了,没必要每次初始化都创建一个。另外ThreadLocalMap是以ThreadLocal的引用为key,如果引用经常变的话,map中就匹配不到了。
  • 避免存储大量对象,由底层数据结构决定。
  • 用完后及时移除对象,如果线程一直存在,这个引用就一直不会被移除。

final的禁止重排序:

y不是final的,会被重排序,如果被重排序到构造函数之外,就会出现上面情况。

volatile的禁止重排序:

在JDK1.5的时候volatile的语义被增强了,增加了【禁止指令重排序】,JDK1.4之前下面的DCL单例是有问题的。

代码语言:javascript
代码运行次数:0
运行
复制
public class Singleton {
    private static volatile Singleton singleton;  //volatile很关键
    private Singleton() {}
    public static Singleton getInstance() {
        if (null == singleton) {           //提高程序的效率
            synchronized (Singleton.class) {
                if (null == singleton) {   //解决多线程下的安全性问题,也就是保证对象的唯一
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

jdk1.5之后的翻译为机器码指令是下面:

代码语言:javascript
代码运行次数:0
运行
复制
1、test(single == null)
2、lock
3、test(single == null)
4、$instance = <allocate memory>  //分配内存
5、call constructor
6、singleton = $instance
7、unlock

而jdk1.4之前加volatile或者jdk1.5后不加volatile的指令可能是下面:

代码语言:javascript
代码运行次数:0
运行
复制
1、test(single == null)
2、lock
3、test(single == null)
4、$instance = <allocate memory>  //分配内存
5、singleton = $instance
6、unlock
7、call constructor

结论:这样的指令造成构造函数的调用在锁外面了,当多线程时,其他线程得到锁并且判断单例不null,直接来用,但是可能这个单例并没有被构造完成,会造成不可预知的问题。

保证可见性的方法

  1. 使用final关键字
  2. 使用volatile关键字
  3. 加锁,锁释放时会强制将工作内存刷新至主内存。 对于3的解释:由【Happens-before规则第一条:内置锁的释放锁操作发生在该锁随后的加锁操作之前】
原子性:

例如a++,其实际代码如下,被拆分成了3行代码,不是原子操作。

代码语言:javascript
代码运行次数:0
运行
复制
int tmp = a;
tmp += 1;
a = tmp; 

保证原子性的方法: 1、加锁,保证操作的互斥性 2、使用CAS指令(如,Unsafe.compareAndSwapInt) 3、使用原子数值类型(如,AtomicInteger) 4、使用原子属性更新器(AtoicReferenceFieldUpdater)

结论:如何编写线程安全的程序
  1. 不变性,能不共享就不共享,写可重入函数(见补充)
  2. 可见性,如果必须要共享,保证变量的可见性
  3. 原子性,保证操作的原子性
  4. 禁止指令重排序,保证代码执行的顺序

补充: 可重入函数,不涉及其他内存:

代码语言:javascript
代码运行次数:0
运行
复制
public static int add(int a){
     return a + 2;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-11-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、思考:线程安全产生的原因是什么?
  • 二、如何实现线程安全呢?
    • 1、不共享资源
      • 结论:如何编写线程安全的程序
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档