13.2 线程安全
13.2.1 java语言中的线程安全
java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
一、不可变
不可变的对象一定是线程安全了(废话,线程的不安全主要体现在增删改,只读没有任何不安全的地方)
如:java中的字符串实例是一个典型的不可变对象,用户调用内部的substring()、replace()这些方法都是返回一个新构造的字符串对象。
二、绝对线程安全
一 个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”可能需要付出非常高昂的, 甚至不切实际的代价。
三、相对线程安全
相对线程安全就是我们通常意义上所讲的线程安全。它需要保证对这个对象单次的操作是线程安全的。
四、线程兼容
对象本身不是线程安全的,可以通过调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用。
五、线程对立
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。
如Thread类的suspend()和resume()方法,多线程不能同时去操作,会产生死锁。
13.2.2 线程安全的实现方法
一、互斥同步(悲观的并发策略)
同步:在多个线程访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。
面临的主要问题:线程阻塞和唤醒带来的性能开销,因此也称阻塞同步(Blocking Synchronization)
互斥是实现同步的一种手段。
有以下几种方式:
-
临界区(Critical Section)
-
互斥量(Mutex)
-
信号量(Semaphore)
synchronized
java里最基本的互斥同步手段是synchronized
synchronized经过javac编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个都需要一个reference类型的参数来指明要锁定和解锁的对象。
执行步骤:
-
执行monitorenter指令时,没有被锁或者当前线程已持有锁(可重入),把锁的计数器+1,
-
执行monitorexit 指令时会将锁的计数器-1;
-
计数器的值减到0,锁随即释放;
在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入内核态中。
重入锁-ReentrantLock
是java.util.concurrent.lockes.Lock接口的一种实现
相比synchronized增加了三项功能:
-
等待可中断:持有线程不释放锁的时候,等待的线程可以选择放弃等待,改为处理其他事情;
-
公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来一次获取锁,非公平不保证这一点,公平锁会导致性能下降;
-
锁绑定多个条件:可以绑定多个Condition对象
ps: 注意下 ReentrantLock必须在finally中进行手动释放,必须由程序员来保证;
二、非阻塞同步(基于冲突检测的乐观并发策略)
先操作,没有争抢直接成功,有争抢产生冲突,进行补偿,不断的去重试,直到成功,因为不需要把线程阻塞挂起,因此称为非阻塞同步,也称无锁编程;
常用指令:
-
测试并设置(Test and Set)
-
获取并增加(Fetch and Increment)
-
交换(Swap)
-
比较并交换(Compare and swap CAS)是一个原子操作
-
加载链接/条件存储(Load-Linked /Store-Conditional)
CAS的ABA问题:初始值是A,准备赋值的时候仍然是A,可能被别的改成了B又改了回来,就是ABA的问题;
三、无同步方案
同步只是保障在共享数据争用时正确性的手段
两类天然线程安全的:
-
可重入代码 :特征:不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的变量都是由参数中传入,forkjoin,递归;
-
本地线程存储(ThreadLocal)
每一个Thread对象中都有一个ThreadLocalMap对象,存储了一组已ThreadLocal.threadLockHashCode为键,以本地线程变量为值的K-V值对。
每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值
13.3 锁优化
13.3.1 自旋锁和自适应锁
自旋锁:为了让现场等待,我们只需让现场执行一个忙循环(自旋);
自适应自旋锁:意味着自旋的时间不再是固定的,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定;
13.3.2 锁消除
在即时编译器在运行时对一些代码要求同步,但是检测到不可能存在共享数据竞争的锁进行消除;
为了优化提高代码执行的效率;
13.3.4 锁粗化
锁的范围尽可能的小;
如果同一操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,虚拟机会把加锁的范围扩展(粗化)到整个操作序列外部;
13.3.4 轻量级锁
利用对象的对象头
对象头信息是与对象自身定义的数据无关的额外存储成本。
Mark Work被设计成一个非固定的动态数据结构
工作过程:
-
代码即将进入同步块的时候,对象没被锁定(处于01状态),
-
虚拟机先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间
-
存储对象目前的Mark Work的拷贝;
-
虚拟机使用CAS尝试把对象的Mark Work更新为指向Lock Record的指针;
-
更新成功后,锁标志位将变成“00”(表示处于轻量级锁定状态);
-
更新失败,说明有竞争,虚拟机检查对象的Mark Word是否指向当前线程的栈帧,如果是说明当前线程已经拥有了这个对象的锁,如果否,说明两条以上线程争抢锁,必须要膨胀为重量级锁,锁标记为“10”;
-
解锁也通过CAS操作,把当前的Mark Word和线程中复制的Displace Mark Word 替换回来;
13.3.5 偏向锁
也是jdk6中引入的一项锁优化措施
文章评论