当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。
Java语言中的线程安全各种操作共享的数据可以按照安全程度由强到弱分为以下五类
不可变:
只要一个不可变的对象被正确的构建出来,那其外部的可见状态都永远不会改变,如String绝对线程安全
Java API中标注自己是线程安全的类,大多数也都不是绝对的线程安全相对线程安全
相对线程安全就是指我们通常意义上的线程安全,他需要保证对这个对象的单次操作是线程安全的,在调用时不需要进行额外的保障措施。但一些连续的调用需要采用额外的同步手段,如ConcurrentHashMap等线程兼容
指对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全地使用,如ArrayList线程对立
指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码,如Thread类的suspend()和resume()方法
线程安全的实现方法
互斥同步
保证共享数据在同一时刻只能被一条线程使用,互斥是因,同步是果。
互斥同步的主要问题是进行线程阻塞和唤醒所带来的的性能开销,因此这种同步也被称为阻塞同步。从解决问题的方式上看,互斥同步属于一种悲观的并发策略,其总是认为只要不去做正确的同步措施(例如加锁),就一定会出现问题,无论共享数据是否真的会出现竞争,他都会进行加锁,这将导致用户态到核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等开销
synchronized
synchronized关键字经过javac编译之后会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象
在执行monitorenter指令的时候,会去尝试获取锁,如果没有被锁定,或者已经持有了那个对象的锁,则获得锁并计数器+1
在执行monitorexit指令的时候,会将锁计数器-1,如果为0则释放锁
从执行成本上看,synchronized是一个重量级锁,因为java线程是映射到操作系统的原生内核线程之上的,需要由用户态内核态切换的系统调用
lock
lock是从JDK5之后的,可以使用非块的结构来实现互斥同步,从而摆脱了语言的特性,改为在类库层面实现同步
随着硬件的发展,现在我们可以使用基于冲突检测的乐观并发策略,通俗的说就是不管风险,先进行操作,如果没有其他线程来争用共享数据,那操作就直接成功了;如果共享的数据被争用,产生了冲突,就采用额外的补偿措施(CPU),最常用的补偿措施就是不断地进行重试,直到没有竞争出现。
硬件的发展可以允许我们修改和冲突检测在一个硬件指令中完成,来保证原子性
测试并设置获取并增加交换比较并交换(CAS)加载链接、条件存储 CAS
CAS指令需要三个操作数,分别是内存位置、旧值、新值。只有当旧值和当前值相同的情况下才会进行修改操作,否则不会修改。然后不管是否更新都会返回旧值
无同步方案同步只是保障存在共享数据争用时正确的手段,如果能让一个方法本来就不涉及共享数据,那么自然也就不用任何同步措施。如线程本地存储
ThreadLocal每一个线程的Thread对象中都会有一个ThreadLocalMap独享,这个对象存储了《ThreadLocal.threadLocalHashCode为键,本地线程变量》这样的《k-v》键值对。其中ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个HashCode值,利用其找到本地变量
锁优化 自旋锁与自适应自旋互斥同步对性能影响最大的就是阻塞的实现,挂起线程和恢复线程都需要转入内核态中完成,为减少Java虚拟机的压力。我们可以让需要争抢锁的线程“稍等一会”,不放弃cpu时间片,看看锁持有者在这期间是否会释放锁,这时线程执行的就是自旋忙等待。
自旋等待并不能代替阻塞,首先他需要多核CPU,同时虽然避免了线程切换的开销,但是它需要占用处理器的时间,是用CPU去补偿线程切换的开销。如果锁长时间被占用,无限制的自旋会给CPU带来压力,所以自旋时间需要一个设定值:默认10次,JDK6之后调整为动态的由上次这个锁竞争成功所需的自旋次数来决定(我上个人旋转50圈就获得了,我也转50圈)
锁消除锁消除是指虚拟机即时编译器在运行时检测到某段需要同步的代码根本不可能存在共享数据竞争而实施的一种对锁进行消除的优化策略。如果一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那么就可以把他们当作栈上数据对待,认为他们是线程私有的,也就无需加锁
锁粗化在编写代码的时候我们总是把同步块的作用范围限制的尽可能小,但如果一系列的连续操作都对同一个对象进行加锁,比如 “加锁-解锁-加锁-解锁-加锁-解锁”,那么就将这个锁粗化为 “加锁-解锁”这一个过程,由原来的多次重复加锁解锁转变为一次的加锁解锁操作。
轻量级锁 对象头的结构在JDK6之后对synchronized的性能进行了一些优化
轻量级锁是JDK6之后加入的新型锁机制,轻量级是相对于使用操作系统互斥量来实现的传统锁而言的。
在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标记位为01状态)虚拟机首先将在当前线程的栈桢中建立一个名为锁记录(Lock Record)的空间,用于存储所对象目前Mark Word的拷贝。然后虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果成功则代表该线程拥有了这个对象的锁,并且对象的锁标志位变为00,表示该对象处于轻量级锁定状态如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。这时虚拟机会检查对象的Mark Word是否指向当前线程的栈桢,如果是这说明当前对象已经拥有了这个对象的锁,那么直接进入执行,否则说明这个锁对象已经被别人抢占了,如果出现两条以上线程争用同一个锁的情况下,那么轻量级锁升级为重量级锁,锁标记位编程10
在解锁过程中,如果对象的Mark Word仍然指向线程的锁记录,那么就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果成功替换则释放锁成功,如果失败(锁升级了),则说明有其他线程尝试过获取该锁,就要在释放锁的同时唤起被挂起的线程
如果没有竞争,轻量级锁便通过CAS操作成功避免了CAS操作的开销
如果有竞争,轻量级锁反而会比传统的重量级锁更慢
偏向锁目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。轻量级锁在无竞争的条件下采用CAS操作来消除同步使用的互斥量,偏向锁则是在无竞争的条件下将整个同步都消除掉锁会偏向于第一个获得他的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步
当锁对象第一次被线程获取到的时候,虚拟机会将对象头中的标志位设置为“01”、把偏向模式设置为“1”表示进入偏向模式,并使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。
一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式马上宣告结束。根据锁目前是否处于被锁定状态决定是否撤销偏向,撤销后标志位恢复到未锁定(01)或轻量级锁定(00),后续的同步就按照上面介绍的轻量级锁去执行
对象头的哈希码
在Java当中一个对象如果已经计算过哈希码,那么就应该一直保持不变,否则很多依赖对象哈希码的API都可能出现出错风险。而作为绝大多数对象哈希码来源的Object::hashCode()方法,返回的是对象的一致性哈希码(Identity Hash Code),这个值是能强制保证不变的,当一个对象计算一致性哈希码之后会记录在对象头中,再次调用这个对象直接返回对象头当中的哈希码。
因此一旦一个对象已经计算过一致性哈希码之后,它就再也无法进入偏向锁的状态了;同样,当一个对象正处于偏向锁状态,又需要计算其一致性哈希码,那么就会撤销偏向状态,并且锁会膨胀为重量级锁。



