线程安全

当多个线程访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替运行, 也不需要进行额外的同步, 或者在调用方进行任何其他的协调操作, 调用这个对象的行为都可以获得正确的结果, 那这个对象就是线程安全的

Java语言中的线程安全

将线程安全的程度由强至弱考虑, 将java语言中的数据分为以下5类
1.不可变
不可变的对象一定线程安全, 无论是对象的方法实现还是方法调用
例如:
final+基本数据类型
String类型的数据(substring等方法不改变原来的值, 只是返回一个新的对象)

2.绝对线程安全

3.相对线程安全
通常意义上的线程安全, 保证对这个对象的单独的操作是线程安全的,
但对于一些特定顺序的连续调用, 可能需要在调用端使用额外的同步手段来保证调用的正确性
例如: Vector, HashTable

4.线程兼容
对象本身不是线程安全的, 但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中安全的使用

5.线程对立
不管调用端是否采用同步措施, 都无法在并发环境中使用

线程安全的实现方法

1.互斥同步
synchronized关键字

  • 同步块在执行完之前会阻塞后面其他线程进入, 而阻塞和唤醒线程需要操作系统频繁从用户态和核心态转换, 这部分耗时较长

ReentrantLock, 相比synchronized主要多了三项功能

  • 等待可中断, 当持有锁的线程长期不释放锁的时候, 正在等待的线程可以选择放弃等待, 改为处理其他事情
  • 公平锁, synchronizd的锁是非公平的, ReentrantLock可以通过带布尔值的构造函数要求使用公平锁

    公平锁指多个线程在等待同一个锁时, 必须按照申请锁的时间顺序来依次获得锁 非公平锁则不保证这一点, 在锁被释放时, 任何一个等待锁的线程都有机会获得锁

  • 锁可以绑定多个条件

2.非阻塞同步
阻塞同步是悲观的并发策略
基于冲突检测的乐观并发策略, 是先进行操作, 如果没有其他线程争用共享数据, 那操作就成功了; 如果共享数据有争用, 产生了冲突, 那就再进行其他的补偿措施, 比如不断重试, 知道试成功, 这种乐观的并发策略许多实现都不需要将线程挂起, 因此这种同步操作被称为非阻塞同步

3.无同步方案

  • 可重入代码

可以在代码执行的任何时刻中断它, 转而去执行另外一段代码, 而在控制权返回后, 原来的程序不会出现任何错误

  • 线程本地存储

将共享数据的可见范围限制在一个线程内执行
比如Web交互模型中一个请求对应一个服务器线程, ThreadLocal类的实现

锁优化

自旋锁与自适应自旋

互斥同步对性能影响最大的就是阻塞的实现, 挂起线程和恢复线程的操作都需要转入内核态中完成
因此引入自旋锁, 让等待的线程执行一个忙循环(自旋), 但不放弃处理器的执行时间
自适应自旋, 是指自旋的时间不再固定, 而由虚拟机对程序锁的状况预测来不断调整

锁消除

虚拟机在即时编译运行时, 对一些代码上要求同步, 但是被监测到不可能存在共享数据竞争的锁进行消除.
锁消除主要是判定一段代码中, 堆上的所有数据都不会逃逸出去被其他线程访问到, 即可以当做栈上数据对待

锁粗化

虚拟机检测到有一串零碎的操作都是对同一个对象加锁, 就会把锁同步的范围粗化到整个操作序列的外部

轻量级锁

传统的是重量级
轻量级锁使用CAS操作避免互斥量的开销, 若有两条以上的线程争用同一个锁, 则会膨胀为重量级锁

偏向锁

锁会偏向于第一个获得它的线程, 如果在接下来的执行过程中, 该锁没有被其他的线程获取, 则持有偏向锁的线程将永远不需要再进行同步;
当有另外一个线程去尝试获取这个锁时, 偏向模式宣告结束
image.png