谈论到线程安全肯定就是多线程中的共享的数据的安全,如果不是共享的数据的安全那就是线程安全的,因为在单线程还是多线程中,只要不共享数据,那其他的线程对应此线程没任何影响
Java的线程安全
对于各种操作共享数据分为5种
- 不可变
- 绝对线程安全
- 相对线程安全
- 线程兼容
- 线程对立
(1)不可变
这个很好理解,对象或者基本数据类型一经初始化就不再变了,就是不能修改了,关键字final,这个你肯定知道
(2)绝对线程安全
定义:不管运行环境如何,调用者都不需要任何额外的同步措施,
但是代价非常高,很多java API中标注自己是线程安全的大部分都不是绝对线程安全,而是相对线程安全
(3)相对线程安全
就是通常意义上的线程安全,它需要保证对象单独的操作是线程安全的,
比如Vector,里面用的Synchronized 来保证对里面的数组操作是安全的,但是多线程中如果一个遍历删除值,一个遍历获取值,就有可能发生数组越界的问题,所以不一定是线程安全的,需要我们在外部添加额外的同步手段来保证调用的正确性。
(4)线程兼容
对象本身不是线程安全的,但可以通过调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用
(5)线程对立
无论调用端是否采用了同步措施,都无法在多线程环境下并发使用的代码
对于java语言天生就具备多线程的特性,这个线程排斥是很少出现,并且通常是有害的,应该尽量避免
举个例子:Thread类的suspend()和resume(),一个中断线程,一个尝试恢复线程,如果两个线程分别调用者两个方法,就有可能产生死锁
2、线程安全的实现方法
(1)互斥同步
包哦正共享数据在同一时刻只能被一个(或多个,使用信号量)线程使用:
临界区、互斥量、信号量都是主要的互斥方式
在java中基本的互斥同步手段就是Synchronized关键字,它是在同步代码块的前后分别形成monitorenter
和monitorexit
这两个字节码指令来锁定一个对象,但是如果是方法则用ACC_AYNCHRONIZED
来锁定
不光这个还有很多,只要是锁都可以,比如还有事先lock接口的ReentrantLock
来实现,和上面的功能相似,不过一个是java API类型的锁一个是字节码形态的,而ReentrantLock
又增加了一些新特性:等待可中断,实现公平锁,以及锁可以绑定多个条件
在JDK1.6发布以后synchronized
和ReentrantLock
性能基本持平,性能因素不再是选择ReentrantLock
的理由了,现在提倡在synchronized
能满足实现需求的情况下,优先考虑使用synchronized
来进行同步
(2)非阻塞同步
基于冲突检测的乐观并发策略,通俗点讲就是先进行操作,如果没有其他线程竞争那操作就成功了,但是如果有争用,就采取补救措施(不断尝试,直到成功),这种乐观的并发策略不需要把线程挂起,
优点:可以优化掉加锁而需要的资源,比如用户态内核态转换的开销,维护锁计数器等。
通常有5中指令:
- 测试并设置
- 获取并增加
- 交换
- 比较并交换(CAS)
- 加载链接,比较存储
前三条是以前处理器就有的,后面是新加的,建议看看CAS
这里会出现一个问题:ABA问题,
因为如果用CAS(V(内存地址),A(旧值),B(新值)),只有旧值一样才修改成新值,但是你怎么知道这个A没被改过?不能出现A值改成了C值,又来一个线程把C值改成了A值?这就是著名的ABA问题
解决这个问题有两种一个是原子引用类,一种是互斥同步,建议互斥同步
(3)无同步方案
不涉及到共享数据
- 可重入代码:也叫纯代码,比如递归参数用基本数据类型,可以在任何时刻中断它去执行另一段代码,控制权返回后对原程序没有任何干扰
- 线程本地存储(一个线程一个副本,更改值互不干扰)
这里主要讲第二种
java.lang.ThreadLocal
类有实现线程本地存储的功能,每一个线程的Thread对象都有一个ThreadLocalMap对象,看到Map就知道是K-V存储,这里的Key就是ThreadLocal,都有一个独一无二的threadLocalHashCode的值,V里存储的就是共享变量的一个副本,修改这个副本对原变量没任何影响,也不会对其他线程的副本造成影响。
这里也会有一个问题:造成OOM问题也就是内存溢出了
这个就不在这里细说了,看这里ThreadLocal出现OOM内存溢出的场景和原理分析
3、锁优化
- 适应性自旋
- 锁消除
- 锁粗化
- 轻量级锁
- 偏向锁
(1)自旋锁和适应性自旋
自旋锁肯定知道是怎么回事:就是让线程一直在那忙循环(自旋),减少线程切换,但是这段时间是占着CPU的,如果长时间得不到锁那还不如线程切换呢,所以在JDK1.6加入了自适应的自旋锁,
自旋的时间由前一次在同一个锁上的自旋时间及锁的拥有者的态度来决定,如果在统一个锁对象上,上一个自旋等待刚刚成功获得过锁,那虚拟机就会认为这次肯定也没问题,就把自旋时间加长一点,如果自旋很少成功,那这个锁可能就会省略这个自旋的时间,直接切换线程状态
(2)锁消除
对一些代码要求同步,但是被检测到不可能存在共享数据竞争问题,就会把锁消除,判断的依据来源于逃逸分析的数据支持。
(3)锁粗化
原则上总是推荐同步块的作用范围越小越好,但是也不尽然
如果一系列的连续操作需要对同一个对象反复加锁,解锁,甚至加锁出现在循环体内,那即便没有现成竞争,也会导致性能下降,所以可以把加锁同步的范围扩展到整个操作序列的外部,用完再释放锁就可以了,只进行可一次
(4)轻量级锁
是一种状态,而不是指锁的类型
没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,在对象头的MarkWord中锁标志位为00,这里用的CAS
如果有两条以上(不包括两条)的线程争用同一个资源就会膨胀到重量级锁
(5)偏向锁
消除在无竞争的情况下的同步原语,进一步提高程序的性能,就是在无竞争情况下把整个同步都消除掉,连CAS都不要了
偏向锁就是偏向第一个获取它的线程,在接下来的过程中,如果该锁没有被其他线程获取,那持有偏向锁的线程永远不需要再进行同步,直到有另一个线程获取该锁,就会取消偏向锁恢复到未锁定(对象未被锁定)或者轻量级锁(对象已经被锁定)的状态,
下面是偏向锁和轻量级锁的准状态转换