Java中哪些是线程安全的,哪些是线程不安全的?使用final修饰的就是安全的,Vector是一个线程安全的容器,ArrayList线程不安全的,为了解决线程安全问题,引入了锁机制。锁其实就是自己使用的时候加上锁,不让别人用,用完了再把锁打开,让别人用,保证了一人一坑,解决了线程安全问题,但也带来了一些效率问题。锁的分类有不少,但都是为了在线程之间更高效共享数据,达到高效并发的效果。
假如持有锁的线程很开就能完事并释放资源,那么正在等待竞争锁的线程就无需在内核态和用户态之间进行切换进入阻塞状态,只需要原地转一转等一等(自旋),等持有锁的线程释放锁后就可以立即获取锁。
自旋锁能够在锁竞争不激烈的情况下,减少线程的阻塞,避免用户线程和内核切换的消耗,因为自旋消耗小于阻塞消耗,所以对部分代码来讲可以带来性能提升。
自旋本身是占用cpu资源,但是不做功,如果竞争激烈的话,或持有锁的线程需要较长时间,此时自旋的消耗就远大于线程阻塞挂起的消耗,造成cpu浪费。
自旋锁自旋时间需要控制,时间长了占用cpu资源,时间短了自旋没有意义。以前JDK1.5的时候默认是10次;JDK1.6的时候由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,也就是自适应自旋锁;JDK1.7之后就有JVM控制。
偏向锁总会偏向于第一个获得它的线程,如果在运行过程中,该锁没有被其他线程获取,也就是不存在多线程竞争的情况,则该线程不需要再进行同步,减少不必要的CAS操作。如果运行过程中遇到了其他线程抢锁的情况,则持有偏向锁的线程就会被挂起,JVM就会消除它身上的偏向锁,将锁恢复到未锁定或轻量锁状态。
使用偏向锁是为了消除资源无竞争的情况下的同步原语,进一步提高程序运行性能。获取过程如下:
CAS
更有效,更灵活的一种原子操作。基本思路:如果该地址上的值和期望的值相等,则将新值赋值给该地址,否则啥也不做。就是如果想要修改某一地址上的值,得先告诉我这个地址上的值是多少,回答正确了才可以修改,否则回去。
Mark Word
存储对象的hashcode或锁信息等。
锁状态 | 25bit | 4bit | 是否是偏向锁1bit | 锁标志位2bit |
---|---|---|---|---|
锁当前状态 | 对象的hashcode | 对象的分代年龄age | 0 | 01 |
轻量级锁是由偏向锁升级而来的,偏向锁运行在一个线程进入同步块的情况下,当出现其他线程争用锁的情况,偏向锁就会升级为轻量级锁。
当代码进入同步块时,如果同步对象锁是无状态的且不允许进行偏向,虚拟机会先在当前线程的栈帧中建立一个锁记录空间Lock Record,用于存储锁对象当前Mark Work的拷贝,官方的叫法是Displaced Mark Work。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。若更新成功,当前的线程就拥有了该对象的锁,并将对象Mark Word的锁标志置为00,表示此对象处于轻量级锁定状态。若更新失败了,虚拟机会首先检查对象的Mark Word是否指向当前线程的栈帧,如果是则说明当前对象已经拥有了该对象的锁,就可以直接进入同步块执行,否则说明这个锁对象已经被其他的线程抢占了。
在轻量级锁的状态下,如果有两条以上的线程争用同一个锁,那么轻量级锁就不再有效,就会膨胀为重量级锁,锁标志也会被置为10,Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。
锁的状态一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。锁状态会随着竞争情况逐渐升级,只能升级但是不能降级,这样才能提高获得锁和释放锁的效率。
锁 | 优点 | 缺点 | 场景 |
---|---|---|---|
偏向锁 | 加解锁无需额外的消耗,而且可执行同步方法相比基本一样快 | 若线程之间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序响应速度 | 若始终得不到锁,线程会使用自旋,会消耗CPU | 同步块执行速度非常块,追求时间 |
重量级锁 | 线程竞争不会使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
两个或两个以上的线程在执行的过程中,由于竞争资源或由于彼此通信而造成的一种阻塞现象,如果没有外力,该阻塞情况会一直持续下去,此时就进入死锁状态。
死锁产生也不是随随便便就能产生的,需要几个条件。
知道了死锁的产生条件,只要打破其中一个条件就可以预防或解除死锁。
两个或多个线程在竞争锁的过程中,出现过度谦让的情况,总是出现一个线程总拿到同一把锁,想拿其他的锁总拿不到,而将自己原本持有的锁又释放了。
线程之间错开拿锁时间,比如线程休眠随机时间。