目录
同步
1. 互斥同步(阻塞同步)
1.1 Synchronized关键字
1.2 ReentrantLock
读写锁
重入锁
2. 非阻塞同步
3.互斥同步中的锁优化
3.1 自旋锁
3.2 锁消除
3.3 锁粗化
3.4 轻量级锁
3.5 偏向锁
多个线程并发访问共享数据时,保证数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。
实现同步的手段之一,存在线程阻塞和唤醒带来的性能问题,是一种悲观的并发策略:无论共享数据是否出现竞争都要加锁、维护锁计数器、用户态到核心态的转换(Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,即重量级,有优化空间,见第6点)。临界区、信号量(semaphore)、互斥量(mutex)是主要的互斥实现方式(系统层面)。Volatile是最轻量的同步机制
java实现互斥同步的方法:
锁的是对象的引用,对象实例或者静态类。经编译后会在同步块前后形成monitorenter和monitorexit两个字节码,前者尝试获取对象锁,成功则锁计数器加1,失败则线程进入同步队列,状态变为BLOCKED。后者会将锁计数器减1,计数器为0,释放锁。
同步方法 public synchronized void method()
自定义一个对象 Object object = new Object(); public void method(){ synchronized(object){}}
同步代码块 synchronized(M.class){}
同步方法 public static synchronized void Method(),静态确保了对于该类的任何实例对象,该方法都只有一份
自定义一个静态对象:类似于类锁,因为obj在虚拟机只有一份 :
private static Object obj = new Object();
private void method(){
Synchronized(obj){}
}
lock()和unlock()配合try/finally语句块来完成。它有几个特点:锁绑定多个条件:指一个ReentrantLock对象可以同时绑定多个Condition对象,如果要和多于一个的条件关联的时候,只需要多次调用newCondition()方法即可 ;等待可中断:指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助;不公平锁:不保证按申请锁的时间顺序来依次获得锁,但reentranlock支持带布尔值的构造函数来实现公平锁。synchronized也是不公平锁。公平锁避免了饥饿现象,但需要维持一个有序队列,需要空间去操作,而且每次把挂起的线程公平执行需要上下文切换耗费时间,因此公平锁在时间空间上都比非公平锁效率低。
ReentrantLock lock = new ReentrantLock(); //参数默认false,。
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
try {
//操作
} finally {
lock.unlock(); //释放锁,如果不在finally里,若操作抛了异常,则锁无法被释放
}
ReentrantReadWriteLock implements ReadWriteLock。从名可知这是可重入锁。Synchronized和reentrantLock都是排它锁,即同时只能有一个线程持有锁;而读写锁允许多线程同时读但不能写,写的时候排它,要考虑数据一致性
“读写分离”的思想:内部分别实现了ReadLock和WriteLock,大大提高了性能,因为读总比写多,如果都是排他锁则耗时很长,时间累加
private ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock getLock = lock.readLock();//获取读锁
private final Lock setLock = lock.writeLock();//获取写锁
//读取商品的方法getnum():
getLock.lock();//上读锁
try{
读取 return this.goodsInfo
}finally{
getLock.unlock();//释放锁
}
//写商品的方法里setnum(number):
setLock.lock();
try{
//修改
goodsInfo.changenum(number);
}
finally{
setLock.unlock();
}
一个线程可以反复多次拿同一把锁,不至于自己被自己锁死。具体概念就是:自己可以再次获取自己的内部锁。Synchronized和ReentrantLock均是可重入锁。以下是示例
补充:Synchronized内置锁和lock显式锁的区别
lock 可中断锁(lockInterruptibly() 的用法体现了Lock的可中断性)、可绑定多个contidtion、可创建公平锁(构造方法参数设置为true)
乐观并发策略,先尝试操作,数据有争用,再采取补偿措施,一般为不断重试直到成功为止,不需要把线程挂起。
典型,CAS操作:本质是处理器的CAS指令。包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。Java中 java.util.concurrent.atomic 包相关类就是 CAS的实现。CAS的缺陷:
(1)ABA问题:如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。JUC提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性
(2) 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
(3) 只能保证一个共享变量的原子操作。解决:把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
请求不到锁的时候,让线程执行一个忙循环,稍微等待下看是否锁被释放。这适合于锁被占用时间很短的情况。自旋次数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。 在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
原则上加锁的范围越小越好,将同步块的作用范围限制到尽可能小。但如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁状态:虚拟机用CAS操作,将对象头mark word部分更新为指向线程栈帧中的lock record的指针,mark word的锁标志位置为00,表示该同步对象处于轻量级锁定状态。膨胀状态:如果更新操作失败,虚拟机会检查mark word是否指向当前线程的栈帧,如果是则直接进入同步代码块,否则说明对象已被其他线程抢占,此时两线程抢占同一个对象,轻量级锁不再有效,膨胀为重量级锁,即mark word被更新为指向重量级锁(互斥量)的指针。
相对于轻量级锁来说,线程一旦持有偏向锁,并且该锁没有被其他线程获取,线程以后将不再进行任何同步操作,连CAS操作都不需要。当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如Locking、Unlocking及对Mark Word的Update等)。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。(启用参数-XX:+UseBiasedLocking,这是JDK 1.6的默认值
补充:
对象头:HotSpot虚拟机的对象头分两部分,mark word部分(32bit或64bit)存储哈希码、GC分代年龄、锁标志位,另一部分存储指向方法区对象类型数据的指针。对于一个同步对象和一个要访问它的线程来说,初始状态是对象没有被锁定,此时虚拟机为当前线程创建lock record的空间,存储着mark word的拷贝