乐观锁与悲观锁独占锁与共享锁互斥锁与读写锁公平锁与非公平锁可重入锁轻量级锁|重量级锁)锁优化技术(锁粗化、锁淘汰)乐观锁和悲观锁悲观锁悲观锁对应生活中悲观的人,悲观的人总是认为事情会往不好的方向发展。举个生活中的例子,假设厕所只有一个坑,悲观地锁上厕所就会马上锁上门,让其他人只能等在门外上厕所。此状态为“阻塞”。回到代码世界,给一个共享数据加上悲观锁,每次一个线程要操作这个数据的时候,都会假设其他线程也可能操作这个数据,所以每次操作前都会加锁,这样其他线程想要操作这个数据,如果拿不到锁,就只能阻塞。在Java语言中,synchronized和ReentrantLock就是典型的悲观锁,一些使用了synchronized关键字的容器类,比如HashTable,也是悲观锁的应用。乐观锁Optimisticlock对应生活中乐观的人,乐观的人总是认为事情会往好的方向发展。举个生活中的例子,假设厕所只有一个坑,乐观锁认为:在荒野中,没有人,没有人会抢我的坑,关上锁上是浪费时间每次都开门,或者没锁上。你看,乐观锁天生就是乐观的!回到代码世界,乐观锁在操作数据的时候不会加锁。更新时会判断这段时间是否有其他线程更新这条数据。乐观锁可以使用版本号机制和CAS算法来实现。Java语言中java.util.concurrent.atomic包下的原子类是使用CAS乐观锁实现的。两种锁的使用场景。悲观锁和乐观锁没有优劣之分,各有适合的场景。乐观锁适用于写入较少(冲突较少)的场景,因为不需要加锁和释放锁,节省了加锁的开销,从而提高吞吐量。如果是写多读少的场景,即冲突严重,线程间竞争被激发,使用乐观锁会导致线程不断重试,也可能会降低性能。在这种场景下,使用悲观锁是比较合适的。独占锁和共享锁独占锁独占锁是指一个锁在同一时间只能由一个线程持有。如果一个线程对数据加了独占锁,那么其他线程就不能对数据加任何类型的锁。获取独占锁的线程既可以读取也可以修改数据。JDK中synchronized和java.util.concurrent(JUC)包中Lock的实现类是排它锁。共享锁共享锁意味着一个锁可以被多个线程持有。如果一个线程给数据加了共享锁,那么其他线程只能给数据加共享锁,不能加排他锁。获取共享锁的线程只能读取数据,不能修改数据。JDK中的ReentrantReadWriteLock是共享锁。互斥锁和读写锁互斥锁互斥锁是排他锁的一种常规实现方式,也就是说同一时间只允许一个访问者访问一个资源,具有唯一性和排他性。一个互斥量一次只能由一个线程拥有,其他线程只能等待。读写锁读写锁是共享锁的一种具体实现。读写锁管理一组锁,一个是只读锁,一个是写锁。在没有写锁的情况下,读锁可以被多个线程同时持有,写锁是独占的。写锁的优先级高于读锁。获取读锁的线程必须能够看到先前释放的写锁的更新内容。与互斥锁相比,读写锁的并发度更高。一次只有一个写线程,但是可以有多个线程同时并发读。JDK中定义了读写锁接口:ReadWriteLockpublicinterfaceReadWriteLock{/***获取读锁*/LockreadLock();/***获取写锁*/LockwriteLock();}ReentrantReadWriteLock实现了ReadWriteLock接口,并且具体实现在这里,不展开,后面会深入源码分析。公平锁和非公平锁公平锁公平锁是指多个线程按照申请锁的顺序获取锁。这类似于排队买票。先来的先买,后来的排在队尾。这是公平的。.在java中,可以通过构造函数初始化一个公平锁/***来创建一个可重入锁,true表示公平锁,false表示非公平锁。默认非公平锁*/Locklock=newReentrantLock(true);非公平锁非公平锁是指多个线程获取锁的顺序不按照申请锁的顺序。有可能后申请的线程比先申请的线程先获取锁,在高并发环境下,可能会造成优先级倒置,或者饥饿状态(一个线程一直没有被加锁)。java中synchronized关键字是非公平锁,ReentrantLock默认也是非公平锁。/***创建可重入锁,true表示公平锁,false表示非公平锁。默认非公平锁*/Locklock=newReentrantLock(false);可重入锁可重入锁也叫递归锁,意思是同一个线程在外层方法获取锁,进入内层方法时自动获取锁。对于JavaReentrantLock,他的名字可以看成是可重入锁。对于Synchronized来说,也是一个可重入锁。敲黑板:可重入锁的一个好处就是可以在一定程度上避免死锁。以synchronized为例,看下面的代码:已经获取了锁然后调用methodB,不需要再次获取锁,这是可重入锁的特性。如果不是可重入锁,mehtodB可能不会被当前线程执行,可能会造成死锁。自旋锁自旋锁是指在没有获取到锁时,线程不是直接挂起,而是执行一个忙循环。这个繁忙的循环就是所谓的自旋。自旋锁的目的是减少线程被挂起的机会,因为线程的挂起和唤醒也是很耗资源的操作。如果锁被另一个线程长时间占用,即使自旋后当前线程仍会挂起,忙循环就会成为浪费系统资源的操作,从而降低整体性能。因此,自旋锁不适合锁需要很长时间的并发情况。在Java中,AtomicInteger类有自旋操作。我们看一下代码:CAS操作失败,会一直循环获取当前值再试。另外自适应自旋锁也需要了解一下。JDK1.6引入了自适应自旋,更加智能。自旋时间不再是固定的,而是由同一把锁上的前一次自旋时间和锁拥有者的状态决定的。如果虚拟机认为本次自旋很可能再次成功,则需要更多时间。如果自旋很少成功,那么以后可以直接省略自旋过程,避免浪费处理器资源。分段锁分段锁是一种锁设计,并不是一种特定的锁。分段锁设计的目的是进一步细化锁的粒度。当操作不需要更新整个数组时,只锁定数组中的一项。在Java语言中,CurrentHashMap底层使用的是段锁。使用Segment,可以并发使用。锁升级(无锁|偏向锁|轻量级锁|重量级锁)JDK1.6为了提高性能,降低获取和释放锁的开销,引入了4种锁状态:无锁,偏向锁,轻量级锁和重量级锁,分别会随着多线程竞争逐渐升级,但不能降级。lock-free和lock-free状态其实就是上面说的乐观锁,这里不再赘述。偏向锁Java偏向锁(BiasedLocking)的意思是会偏向第一个访问锁的线程。如果在运行过??程中只有一个线程访问锁定的资源,没有多线程竞争,那么该线程是不需要重复获取锁的,这种情况下,就会给该线程加偏向锁。偏向锁的实现是通过控制对象MarkWord的标志位来实现的。如果当前状态是可偏置的,则需要进一步判断对象头中保存的线程ID是否与当前线程ID一致,如果一致则直接进入。轻量级锁当线程竞争越来越激烈时,偏向锁会升级为轻量级锁。轻量级锁认为虽然存在竞争,但理想情况下竞争程度很低,通过自旋等待前一个线程释放锁。重量级锁如果线程并发进一步加剧,线程自旋超过一定次数,或者一个线程持有锁,一个线程正在自旋,第三个线程访问(无论如何,竞争不断增加),轻量级锁会扩展成重量级锁,重量级锁此时会阻塞除了拥有锁的线程之外的所有线程。升级到重量级锁其实就是互斥锁。如果一个线程获得了锁,则其余线程将处于阻塞等待状态。在Java中,synchronized关键字的内部实现原理是锁升级的过程:无锁-->偏向锁-->轻量级锁-->重量级锁。这个过程在后面讲解synchronized关键字的原理时会详细介绍。锁优化技术(锁粗化、锁消除)锁粗化锁粗化是减少多个同步块的个数,扩大单个同步块的作用范围,本质上是将多个加锁和解锁请求组合成一个同步请求。比如一个循环体中有一个代码同步块,每次循环都会进行加锁和解锁操作。privatestaticfinalObjectLOCK=newObject();for(inti=0;i<100;i++){synchronized(LOCK){//dosomemagicthings}}锁粗化后变成如下:synchronized(LOCK){for(inti=0;i<100;i++){//dosomemagicthings}}锁消除锁消除是指虚拟机编译器在运行时检测到没有竞争共享数据的锁,然后消除这些锁。举个例子让大家更好的理解。publicStringtest(Strings1,Strings2){StringBufferstringBuffer=newStringBuffer();stringBuffer.append(s1);stringBuffer.append(s2);returnstringBuffer.toString();}上面代码中有一个test方法,主要作用是结合字符串s1和字符串s2连接在一起。测试方法中的s1、s2、stringBuffer这三个变量都是局部变量。局部变量在栈上,栈是线程私有的,所以即使有多个线程访问测试方法,也是线程安全的。我们都知道StringBuffer是线程安全的类,append方法是同步方法,但是test方法本身就是线程安全的。为了提高效率,虚拟机帮我们消除了这些同步锁。这个过程称为锁消除。StringBuffer.class//append是一个同步方法publicsynchronizedStringBufferappend(Stringstr){toStringCache=null;super.append(str);returnthis;}一张图总结:前面讲了Java语言的各种锁,最后通过六个来总结提出问题:
