多线程---线程安全

本文深入探讨了线程安全的原理,包括线程安全产生的原因、final与volatile关键字的作用、Java DCL单例模式的缺陷及解决方案。文章还详细介绍了如何通过不共享资源、共享不可变资源和共享可变资源等策略编写线程安全的程序,以及ThreadLocal的正确使用方法。
摘要由CSDN通过智能技术生成

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

一、思考:线程安全产生的原因是什么?
二、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中如何使用):

   
   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

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单例是有问题的。

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之后的翻译为机器码指令是下面:

1test(single == null)
2、lock
3test(single == null)
4、$instance = <allocate memory>  //分配内存
5、call constructor
6、singleton = $instance
7、unlock

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

1test(single == null)
2、lock
3test(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行代码,不是原子操作。

int tmp = a;
tmp += 1;
a = tmp; 

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

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

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

public static int add(int a){
     return a + 2;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值