Java多线程系列(二)—线程安全

多线程下对共享的(多个线程可以访问相同的资源)可变的(没有final关键词修饰)容易出现非线程安全情况,因此对象的线程安全性与其共享性和可变性有关;

1.线程安全的定义

  • 线程安全的定义:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
  • 非线程安全:是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改,值不同步的情况,进而影响程序的执行流程
  • 非线程安全问题只有在一或多个线程向相同资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的未被改变的资源就是安全的;如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
  • 线程安全需要保证可见性和原子性

2.非线程安全出现的原因

多线程并发操作下可能出现内存不可见问题,指令重排序问题,4字节赋值问题等,主要造成线程不安全的还是内存不可见和指令重排序;

(1) 内存不可见

  • Java内存模型(JMM):Java内存模型将内存分为了主内存和工作内存,共享变量都保存在主内存中,每条线程对应的工作内存保存着变量的副本,线程对变量的操作只能在工作内存,而不能直接操作主内存里的变量;
  • 因为线程对变量的操作都是在工作内存上,就容易造成主内存和工作内存数据不一致,出现内存不可见的情况;

(2) 指令重排序

  • 指令重排序是指编译时为了优化执行效率,会把没有数据依赖的操作进行重排序,也就是说重排序后的汇编指令与代码指令不一定相同;
  • volatile关键字定义的变量使用内存屏障解决指令重排序带来的并发问题

3.非线程安全的解决办法

非线程安全出现的情况主要是多线程同时对一个共享可变变量进行读写操作,因此可以通过以下三种方法解决非线程安全问题:

  • 不在线程间共享该可变变量,比如使用ThreadLocal
  • 将可变变量修改为不可变变量,如用final修饰符
  • 在访问时使用线程同步,如volatile,synchronized等

(1)volatile实现线程同步

  • 内存不可见:volatile关键字定义的变量赋值操作后会立即写回主内存,一个线程数据回写到主内存会导致其他线程对应数据无效(通过嗅探总线上传输的数据检查自身数据),线程下次使用时若发现自身数据失效,则会从主内存上取数据更新;
  • 指令重排序:volatile关键字定义的变量通过内存屏障禁止指令重排序来解决指令重排序带来的并发问题

内存屏障

  • 内存屏障的作用
  • 禁止屏障两边重排序
  • 将工作内存写回主内存,并使其他线程工作内存的相应数据失效
  • 内存屏障的种类:StoreStore屏障,StoreLoad屏障,LoadStore屏障,LoadLoad屏障
  • Lock不是一种内存屏障,但是它能完成类似内存屏障的功能,算是一种粗颗粒度的内存屏障,会禁止屏障两边所有重排序;
  • 保守策略下,volatile关键字细颗粒度内存屏障
  • 在volatile写操作之前插入StoreStore屏障,禁止前面的普通写重排序
  • 在volatile写操作之后插入StoreLoad屏障,禁止后面的普通读重排序
  • 在volatile读操作之后插入LoadStore屏障,禁止后面的普通写重排序
  • 在volatile读操作之后插入LoadLoad屏障,禁止后面的普通读重排序

volatile关键字语义的实现

volatile关键字语义的实现是通过Lock前缀,反编译会发现在volatile关键字修饰的变量写操作前有Lock前缀修饰,Lock前缀相当于是粗颗粒度的内存屏障,主要提供三个功能:

  • 禁止屏障两边重排序
  • 将本地内存写回汉族内存
  • 将将其他线程的本地内存相应变量失效

(2) synchronized等加锁实现线程同步

  • synchronized作用
  • synchronized能保证线程互斥访问同步代码段
  • synchronized修饰的代码段在结束时会将线程本地内存写回主内存,并使其他线程本地内存的相关数据失效,实现内存可见;
  • synchronized能够禁止指令排序
  • synchronized的使用
  • synchronized修饰普通方法,对应的锁是当前实例对象
  • synchronized修饰静态方法,对应的锁是类对象
  • synchronized修饰代码块.对象是当前实例对象
  • synchronized语义的实现
  • synchronized修饰的代码块发编译后在代码开始和结束的地方有一对moniterenter和moniterexit,synchronized修饰的普通方法和静态方法,在方法对应的方法区常量池中会存在acc_synchronized标识将该方法标识为同步方法;
  • 当执行moniterenter指令时,或调用方法并检查到该方法方法区常量池中有acc_synchronized标识,会尝试获取对象对应的monitor的持有权,如果获取成功则monitor的计数器为0,标识未被其他线程持有则取锁成功,monitor计数器加一用作可重入锁计数;
  • synchronized在运行过程中不是一下子就到重量级锁这个级别的,它根据线程竞争情况会经过几次升级变化。升级过程主要是无锁状态——偏向锁——轻量级锁——重量级锁;
  • 在出现锁竞争的情况下锁会升级为重量级锁,未竞争到锁的线程会被阻塞放入排队队列中;为了避免频繁的线程阻塞唤醒,会采用自旋锁的方式;自旋锁是指线程未竞争到锁并不会立即休眠进入排队队列而是执行一个无意义的循环,循环结束后会查看锁时候释放,如果没释放继续自旋,长时间的自旋会消耗CPU资源,因此自旋次数超出一定范围后会线程阻塞进入队列;
  • 锁重入:
  • 一个线程获取某对象的锁,可以调用本对象的其他的sync方法
  • 子类可以通过“可重入锁”调用父类的同步方法
  • 锁重入:是指持有锁的线程可以再调用进入该锁定义的其他方法

总结

  • 实现线程安全的关键在于可见性和原子性,volatile保证了可见性,没有保证原子性,因此单独只用volatile不能保证线程安全,synchronized保证了可见性和原子性,可以保证线性安全;
  • 实现线程同步一般通过阻塞线程同步和非阻塞线程同步的方法,非阻塞线程同步包括CAS和volatile,其中volatile是最轻量级的线程同步的方法;阻塞线程同步就是指synchronized等加锁的方法实现的线程同步;
  • volatile语义的实现是通过lock前缀,lock前缀相当于一个内存屏障,会禁止指令前后的重排序同时会将工作内存写回主内存,让其他线程的工作内存的相应数据失效;
  • synchronized语义的实现是通过一对指令monitorenter和monitorexit实现,当执行monitorenter指令时会尝试获取monitor的使用权,如果monitor计数器为0则获取成功,否则线程阻塞进入排队队列;