在说到Java锁之前,先回顾一下Java异步编程中的多线程。
维基百科-->多线程(英语: multithreading ),是指从 软件 或者 硬件 上实现多个 线程 并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个 线程 ,进而提升整体处理性能。具有这种能力的系统包括 对称多处理机 、 多核心 处理器以及 芯片级多处理 (Chip-level multithreading)或 同时多线程 (Simultaneous multithreading)处理器。
在我们代码程序中,将一个程序分割成相互独立的区域,或者将一个程序转换成多个独立运行的子任 务。像这样的每个子任务把它叫作一个“线程”( Thread)。 有了多线程做开头,接下来归纳下Java中的锁。
Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8)、使用场景进行整理,作为一个简单的总结吧。
Java中往往是按照是否含有某一特性来定义锁,然后通过特性将锁进行分组归类,下面给出本文内容的总体分类目录:
根据如上图所示,大概Java中锁就有这几种:
这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计。
乐观锁与悲观锁是一种宏观上的概念,体现了看待线程同步的不同角度。在Java和数据库中(MySQL中的锁)都有此概念对应的实际应用。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
Java中,synchronized关键字和Lock的实现类都是悲观锁。而乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,先加锁可以保证写操作时数据正确,而乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。 结合代码来看下乐观锁和悲观锁的调用方式示例:
1.悲观锁的俩种调用方式
synchronized实现
lock实现
private static Lock lock = new ReentrantLock(); 复制代码
2.乐观锁调用方式
public class AtomicIntegerTest { private static AtomicInteger n2 = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread() { public void run() { for(int i = 0; i < 1000; i++) { n2.incrementAndGet(); } }; }; Thread t2 = new Thread() { public void run() { for(int i = 0; i< 1000; i++) { n2.incrementAndGet(); } } }; t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("最终n2的值为:" + n2.toString()); } } 复制代码
多次运行,发现结果永远是2000,由此可以证明AtomicInteger的操作是原子性的。
看完上述试例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?乐观锁貌似是直接利用原子类的自增属性执行自增1,类似于MySQL中的乐观锁实现方案版本号加1,这种实现方法就是 “CAS”技术。先介绍一下CAS:
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:
需要读写的内存值 V。
进行比较的值 A。
要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。这个和之前记录的那篇文章(MySQL中的锁)中的乐观锁原理基本是一样的,理解起来也容易。 然后在上面提到java.util.concurrent包中的原子类AtomicInteger,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义:
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; 复制代码
以上为AtomicInteger中的部分源码,在这里说下其中的value,这里value使用了volatile关键字,volatile的作用是使得多个线程可以共享变量,但是问题在于使用volatile将使得JVM优化失去作用,导致效率较低。 valueOffset是存储value在AtomicInteger中的偏移量。 那么unsafe类是什么东西呢?
Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。 复制代码
接下来,我们看下AtomicInteger的自增函数incrementAndGet()的源码,发现自增函数底层调用的是unsafe.getAndAddInt(),然后找到getAndAddInt的源码:
/** * Atomically increments by one the current value. * * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 复制代码
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } 复制代码
所以incrementAndGet是将自增后的值返回,相当于++i,而getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
这里就顺便把AtomicInteger原子类也整理了下。
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是CAS,下面代码中的while循环就是一个自旋操作:
public class SpinLock { private AtomicReference<Thread> sign =new AtomicReference<>(); public void lock(){ Thread current = Thread.currentThread(); while(!sign .compareAndSet(null, current)){ } } public void unlock (){ Thread current = Thread.currentThread(); sign .compareAndSet(current, null); } } 复制代码
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
这四种锁是根据状态去分别的,并且是针对Synchronized,在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段(Mark Word)来表明的。目前锁一共有这4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。也就是说要保证安全线程,并不是一定就要进行加锁,两者没有因果关系。加锁只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
偏向锁、轻量级锁以及重量级锁是通过Java对象头实现的。
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
那如何能保证每个线程都能拿到锁呢,队列FIFO是一个完美的解决方案,也就是先进先出,ReenTrantLock通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。所以ReenTrantLock是根据队列实现的公平锁和非公平锁。
公平锁:
final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } #hasQueuedPredecessors的实现 public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } 复制代码
非公平锁:
final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } 复制代码
通过源码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。 再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
synchronized void setA() throws Exception{ Thread.sleep(1000); setB(); } synchronized void setB() throws Exception{ Thread.sleep(1000); } 复制代码
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
独享锁也叫排他锁或者互斥锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
比如,ReentrantLock 是互斥锁,ReentrantReadWriteLock 中的写锁是也是互斥锁 JDK中的synchronized也是互斥锁。 但是ReentrantReadWriteLock 中的读锁就是共享锁。 最后再根据ReentrantReadWriteLock的源码看看独享锁和共享锁:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { private static final long serialVersionUID = -6992448646407690164L; /** Inner class providing readlock */ private final ReentrantReadWriteLock.ReadLock readerLock; /** Inner class providing writelock */ private final ReentrantReadWriteLock.WriteLock writerLock; /** Performs all synchronization mechanics */ final Sync sync; /** * Creates a new {@code ReentrantReadWriteLock} with * default (nonfair) ordering properties. */ public ReentrantReadWriteLock() { this(false); } /** * Creates a new {@code ReentrantReadWriteLock} with * the given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } /** 复制代码
我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,这俩把锁就是上文提到的一个读锁一个写锁,合称“读写锁”。
然后点进去发现ReadLock和WriteLock是靠内部类Sync实现的锁,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。
俩者具体的区别就是写锁加锁的tryAcquire()和读锁加锁的tryAcquireShared()的区别,后面再研究下。