synchronized
在JavaSE1.6中,锁的状态有:无锁状态、偏向锁状态(锁撤销)、轻量级锁状态(CAS自旋消耗CPU性能)、重量级锁状态。锁可以升级,但是不可以降级,目的是为了提高获得锁和释放锁的效率。
synchronized 实现同步的基础
Java中的每一个对象都可以作为锁。具体表现形式:对于普通同步方法,锁是当前实例对象;对于静态同步方法,锁是当前类的Class对象;对于同步代码块,锁是synchronized括号里配置的对象。当一个线程试图访问同步代码块/同步方法时,必须先得到锁,否则会被阻塞挂起,退出或者抛出异常时必须释放锁。
注意:
synchronized不能被继承,不能使用synchronized关键字修饰接口方法;
构造方法也不能用Synchronized。
synchronized的内存语义
进入synchronized块的语义是 :将synchronized块内使用到的变量从线程的本地内存中清除,这样保证了synchronized块中使用到的变量不会从线程的本地内存中读取,而是直接从主内存中获得。
退出synchronized块的语义是:将synchronized块中对共享变量的修改刷新到主内存。
解决了共享变量内存可见性问题
缺点:会引起线程上下文切换带来的线程调度开销
synchronized的作用
-
原子性:synchronized保证语句块内操作是原子的
-
可见性:synchronized保证共享变量的修改能够及时可见(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)
-
有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)
synchronized的使用
-
修饰实例方法,对当前实例对象加锁
-
修饰静态方法,对当前类的Class对象加锁
-
修饰代码块,对synchronized括号内的对象加锁
synchronized实现原理
1.6以前:底层汇编码是采用**Moniter监视器进行加锁(monitorenter)和解锁(monitorexit),Monitor是依赖于底层操作系统的mutex(互斥量)** 来实现互斥的,存在用户态到内核态的转变,成本高,效率低。
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1,方法执行完后减1,当这个值为0是释放锁。需要一层一层的减。
同步代码块是使用
monitorenter和monitorexit指令实现,而同步方法是其他方式。但是同步方法也可以使用这两个指令实现。同步方法:(静态同步方法有待考证)JVM使用ACC_SYNCHRONIZED标识来实现。即JVM通过在方法访问标识符(flags)中加入ACC_SYNCHRONIZED来实现同步功能。
The Java® Virtual Machine Specification针对同步方法的说明:
Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.
同步方法是隐式的。一个同步方法会在运行时常量池中的method_info结构体中存放ACC_SYNCHRONIZED标识符。当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,则先要获得对应的monitor锁,然后执行方法。当方法执行结束(不管是正常return还是抛出异常)都会释放对应的monitor锁。如果此时有其他线程也想要访问这个方法时,会因得不到monitor锁而阻塞。当同步方法中抛出异常且方法内没有捕获,则在向外抛出时会先释放已获得的monitor锁
同步代码块:JVM使用**
monitorenter和monitorexit**两个指令实现同步。即JVM为代码块的前后真正生成了两个字节码指令来实现同步功能的。Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
大致含义如下:
每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。步骤如下:
每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行
monitorenter)后,该计数器自增变为 1 。
当同一个线程再次获得该monitor的时候,计数器再次自增;
当不同线程想要获得该monitor的时候,就会被阻塞。
当同一个线程释放 monitor(执行
monitorexit指令)的时候,计数器再自减。当计数器为0的时候。monitor将被释放,其他线程便可以获得monitor。The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
当线程执行
monitorexit指令时,会去讲monitor的计数器减一,如果结果是0,则该线程将不再拥有该monitor。其他线程就可以获得该monitor了。同步方法和同步代码块底层都是通过monitor来实现同步的。
两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过
monitorenter和monitorexit来实现我们知道了每个对象都与一个monitor相关联。而monitor可以被线程拥有或释放。
Java SE 1.6以后:为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。这种锁的优化实际上是通过Java对象头中的一些标志位来去实现的;对于锁的访问与改变,实际上都与Java对象头息息相关。
无锁
无锁状态,无锁即没有对资源进行锁定,所有的线程都可以对同一个资源进行访问,但是只有一个线程能够成功修改资源。
如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为**锁记录(Lock Record)**的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图:
偏向锁 从对象头的分配中看到,偏向锁要比无锁多了线程ID 和 epoch,由这两个字段来控制
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况,偏向锁就是在这种情况下出现的,它的主要作用就是优化同一个线程多次获取一个锁的情况。偏向锁的加锁
如果一个synchronized方法被一个线程访问,会通过检查对象头 Mark Word 的锁标志位判断目前锁的状态,如果是 01,说明就是无锁或者偏向锁,然后再根据是否偏向锁 的标示判断是无锁还是偏向锁,如果是无锁情况下,执行下一步
线程使用 CAS 操作来尝试对对象加锁,如果使用 CAS 替换
ThreadID成功,就说明是第一次上锁,那么当前线程就会获得对象的偏向锁,此时会在对象头的 Mark Word 中记录当前线程 ID 和获取锁的时间 epoch 等信息,然后执行同步代码块。当这个线程再次访问同一个synchronized方法时,它会检查这个对象的Mark Word的偏向锁标记以及是否指向了其线程ID,如果是的话,那么该线程就无需再去进入管程(Monitor)了,而是直接进入到该方法体中。
若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,因此它是偏向锁,而第二个线程在争抢时,会发现该对象头中的Mark Word已经是偏向锁,但里面存储的线程ID并不是自己(是第一个线程),那么它会进行CAS(Compare and Swap),从而获取到锁,这里面存在两种情况:
获取锁成功:那么它会直接将Mark Word中的线程ID由第一个线程变成自己(偏向锁标记位保持不变), 这样该对象依然会保持偏向锁的状态。
获取锁失败:(自旋次数达到界限值)则表示这时可能会有多个线程同时在尝试争抢该对象的锁,那么这时 偏向锁就会进行升级,升级为轻量级锁。
偏向锁的撤销
**偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。**偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
全局安全点(Safe Point):全局安全点的理解会涉及到 C 语言底层的一些知识,这里简单理解 SafePoint 是 Java 代码中的一个线程可能暂停执行的位置。关闭偏向锁
偏向锁在Java 6 和Java 7 里是默认启用的。由于偏向锁是为了在只有一个线程执行同步块时提高性能,如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:
-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。开启偏向锁
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0轻量级锁
加锁过程
线程1获取轻量级锁时,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间(官方称为“Displaced Mark Word”),并将锁对象的对象头
MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为Displaced Mark Word),然后使用CAS把锁对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转
自旋
自旋(Spin)其原理是:当发生对Monitor的争用时,若Owner能够在很短的时间内释放掉锁,则那些正在争用的线程就可以稍微等待一下(即所谓的自旋),自旋的时候将一直处于用户态,节省了切换状态的性能消耗。在Owner线程释放锁之后,争用线程可能会立刻获取到锁,从而避免了系统阻塞。不过,当Owner运行的时间超过了临界值后,争用线程自旋一段时间后依然无法获取到锁,这时争用线程则会停止自旋而进入到阻塞状态。所以总体的思想是:先自旋,不成功再进行阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有极大的性能提升。显然,自旋在多处理器(多核心)上才有意义。
JVM对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。
流程图表示(已经包含偏向锁的获取)
重量级锁
JDK1.5之前默认的锁的形态。
若自旋失败(依然无法获取到锁),那么锁就会转化为重量级锁,在这种情况下,无法获取到锁的线程都会进入到Monitor(即内核态)。
重量级锁通过对象内部的监视器(Monitor)实现,其中 Monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
整个加锁流程图
锁的优缺点对比
synchronized可重入实现原理
Monitor在虚拟机中的实现 基于c++由ObjectMonitor实现的 ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } /* 在上面的源码我们可以看到ObjectMonitor中有几个关键属性: _header MarkOop对象头 _waiters 等待线程数 _recursions 重入次数 _owner 指向获得ObjectMonitor的线程 _WaitSet 调用了java中的wait()方法会被放入其中 _cxq | _EntryList 多个线程尝试获取锁时 */synchronized是基于monitor实现的,monitor中存在一个计数器recursions,因此每进入一次会加1,退出一次减1
**注意:**当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法,monitor中的计数器仍会加1。
Lock锁
Lock锁具体实现为ReentrantLock
ReentrantLcok底层
AQS中重要变量
Node {
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
private transient volatile Node head; // 等待队列的
private transient volatile Node tail; // 等待队列的尾部
private volatile int state; // 锁状态,加锁成功则为1,重入+1 解锁则为0
lock.lock()加锁
调用ReentrantLock中的lock方法 调用Sync(继承AQS)内部抽象类的抽象方法lock()public void lock() { sync.lock(); }sync中抽象方法lock()有两个重写方法 即公平锁和非公平锁中的lock()方法
非公平锁的lock()方法
/*先尝试CAS获取锁 获取成功,设置当前线程为锁的持有者 获取失败,调用acquire()方法*/ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
compareAndSetState()/*compareAndSetState()方法 传入 期望值 和要修改的值 CAS操作,尝试获取锁*/ protected final boolean compareAndSetState(int expect, int update) { // 调用java的unsafe类的本地方法进行CAS操作 return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }公平锁的lock()方法
final void lock() { acquire(1); }acquire(1);该线程获得锁或者去队列中排队
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire()尝试获得锁protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
tryAcquire()有四个重写方法
公平锁
FairSync in ReentrantLock非公平锁
NonfairSync in ReentrantLock读写锁中
Sync in ReentrantReadWriteLock线程池中
Worker in ThreadPoolExecutor公平锁中的
tryAcquire()增加
hasQueuedPredecessors()的判断,确保只有AQS队列的第一个线程才可以尝试获取锁资源,保证了公平性protected final boolean tryAcquire(int acquires) { //获取当前线程 final Thread current = Thread.currentThread(); //获取 state 同步状态 0为自由态(即没有线程持有该锁) 1为非自由态 int c = getState(); //判断同步状态是否为自由态 自由态(c=0)尝试加锁 if (c == 0) { //1. 判断自己是否需要排队,不需要 !(hasQueuedPredecessors()返回false) true 则尝试CAS加锁 //1.1 CAS加锁成功,将当前线程为锁的持有者 最终tryAcquire()返回true //1.2 CAS失败后 不进行操作 最终tryAcquire()返回false //2. 需要排队 !(hasQueuedPredecessors()返回true) false 不进行操作 最终tryAcquire()返回false if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //自己不要要排队并且CAS加锁成功,将当前线程为锁的持有者 setExclusiveOwnerThread(current); /**protected final void setExclusiveOwnerThread(Thread thread){ exclusiveOwnerThread = thread;}*/ return true; } } //非自由态 即c=1 判断持有锁的线程是否为当前线程 else if (current == getExclusiveOwnerThread()) { //ReentrantLock的可重入 //是的话,计数器+1 int nextc = c + acquires; //判断 nextc 计数器是否达到最大 当nextc = Integer.MAX_VALUE时,继续加+1 将变为负数 if (nextc < 0) throw new Error("Maximum lock count exceeded"); // 设置重入次数 setState(nextc); return true; } // 锁被人持有 并不是自己持有时,返回false return false; }
hasQueuedPredecessors()体现公平性的核心代码根据队列是否为空,以及头结点的下一个节点是否是自己, 判断自己是否可以去尝竞争锁
public final boolean hasQueuedPredecessors() { Node t = tail; // 尾结点 Node h = head; // 头结点 永远是 thread为null的一个节点 Node s; /** 1. 队列为null(未被初始化)则 h!=t 为false最终则返回false 不需要排队 2. 队列不为空(被初始化) && (头结点的下一个是否为空||头结点的下一个线程是否是当前线程) h!=t && ((s = h.next) ==null || s.thread != Thread.currentThread()) true && (为空 true || true)true 最终返回true h!=t,s=null成立,说明有线程正在入队(分两步:初始化,入队),刚执行完第一步,所以,当前线程也需要排队 无资格CAS获取锁 true && (不为空 false || 如果不为当前线程 则为true)true 最终返回true 队列中有人排队,当前线程需要排队 无资格CAS获取锁 true && (不为空 false || 如果为当前线程 则为false)false 最终返回false 自己是队列中的第一个线程(除head节点),有资格CAS获取锁*/ return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }非公平锁(
NonfairSync extends Sync)的tryAcquire()方法protected final boolean tryAcquire(int acquires) { //调用父类Sync中的nonfairTryAcquire()方法 return nonfairTryAcquire(acquires); }
nonfairTryAcquire()与公平锁的tryAcquire()方法原理相同,但加锁时不需要根据队列情况,判断是否需要排队,可以直接去CAS竞争锁final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //判断当前线程是否是锁的持有者 重入的实现 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); //protected final void setState(int newState) { // state = newState; // } return true; } return false; }
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))不允许竞争锁或竞争锁失败后, 让该节点入队列,入队列后自旋一次去尝试获取锁, 没轮到自己竞争或竞争失败后调用park进行阻塞
addWaiter(Node.EXCLUSIVE)入队private Node addWaiter(Node mode) { //新建一个当前线程的node对象 Node node = new Node(Thread.currentThread(), mode); // 将尾节点赋值给一个临时节点 Node pred = tail; // 判断尾结点是否为null // 不为null,队列已被初始化 直接将node插入队列 if (pred != null) { // 将该node的前驱设置为之前队列的尾结点 node.prev = pred; // CAS判断尾结点是否被改变,为防止多个线程加锁,确保node入队的时候是原子操作 if (compareAndSetTail(pred, node)) { // 设置之前队列的尾结点的后继为当前node节点 pred.next = node; return node; } } // 为null,证明队列未被初始化,调用enq()自旋初始化并将node插入队列 enq(node); //最终返回node,然后调用acquireQueued()对该node进行自旋获取锁一次,失败进行park阻塞 return node; }
初始化并插入队列enq(node);队列未被初始化,调用enq()自旋初始化并将node插入队列private Node enq(final Node node) { // 自旋 for (;;) { // 将尾结点赋值给临时节点t Node t = tail; // 判断尾结点是否为null ,是 代表队列未初始化,进行初始化 if (t == null) { // Must initialize // CAS初始化队列head节点 线程为null的节点 // new Node() 调用无参构造器初始化head节点 if (compareAndSetHead(new Node())) tail = head; } else { // 插入队列,设置前驱后继 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; // 这个返回其实就是为了终止循环,返回出去的t,没有意义 return t; } } } }入队后,传入node节点,自旋一次去尝试获取锁(判断前一个线程是否已经解锁), 没轮到自己竞争或竞争失败后调用park进行阻塞
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // 开始自旋 for (;;) { // 获取到node节点的前一个节点 final Node p = node.predecessor(); // 判断前一个节点是否是head节点 ,即判断自己是否有资格竞争锁 // 是head节点,接着尝试一次获取锁 if (p == head && tryAcquire(arg)) { /** 获取锁成功,将当前node设置为head节点(即设置node的线程为null) 并删除引用,方便GC回收 private void setHead(Node node) { head = node; node.thread = null; // 设置该node(现在的head节点)的前驱为null node.prev = null; } */ setHead(node); // 设置前一个节点(旧的head节点)的后继为null 方便GC回收 p.next = null; // help GC failed = false; // 线程没有被打断,加锁成功,返回false,线程正常往下执行 return interrupted; } // 前一个不是head节点,有人在排队,不会尝试去获得锁 //调用shouldParkAfterFailedAcquire()自旋将前一个人的waitStatus设置为-1(表示已阻塞) if (shouldParkAfterFailedAcquire(p, node) && //前一个人的waitStatus设置成功后,直接park阻塞当前线程, parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }**
shouldParkAfterFailedAcquire()**自旋将前一个人的waitStatus设置为-1(表示已阻塞) ,前一个线程以阻塞,无法自己设置private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * 该节点状态已设置,已阻塞 */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
parkAndCheckInterrupt():阻塞当前线程,知道unpark()唤醒private final boolean parkAndCheckInterrupt() { // 阻塞当前线程 LockSupport.park(this); return Thread.interrupted(); }
解锁过程
public void unlock() {sync.release(1);}public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }尝试释放锁
protected final boolean tryRelease(int releases) { // 该线程的AQS状态-1 int c = getState() - releases; // 如果当前线程不是锁的持有线程,则抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 是持有锁的线程,并且可重入次数为0,则清空锁的持有线程,可重入次数不为0,则仅仅让该线程的可重入次数-1 if (c == 0) { free = true; setExclusiveOwnerThread(null); } // 更新AQS状态 setState(c); // 返回该线程释放完全释放锁 return free; }
ReentrantLock和Synchronized的区别:
相同点:
1. 它们都是加锁方式同步; 2. 都是重入锁; 3. 阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待, 而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善);不同点:
比较方面
SynChronized
ReentrantLock(实现了 Lock接口)
原始构成
它是java语言的关键字,是原生语法层面的互斥,需要jvm实现
它是JDK 1.5之后提供的API层面的互斥锁类
实现
通过JVM加锁解锁
api层面的加锁解锁,需要手动释放锁。
代码编写
采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全,
而ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成,
灵活性
锁的范围是整个方法或synchronized块部分
Lock因为是方法调用,可以跨方法,灵活性更大
等待可中断
不可中断,除非抛出异常(释放锁方式: 1.代码执行完,正常释放锁; 2.抛出异常,由JVM退出等待)
持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,(方法: 1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待; 2.lockInterruptibly()放代码块中,调用interrupt()方法可中断,而synchronized不行)
是否公平锁
非公平锁
两者都可以,默认公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁,
条件Condition
通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能.
提供的高级功能
提供很多方法用来监听当前锁的信息,如:
getHoldCount() getQueueLength() isFair() isHeldByCurrentThread() isLocked()便利性
Synchronized的使用比较方便简洁,由编译器去保证锁的加锁和释放
需要手工声明来加锁和释放锁,
适用情况
资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
关于Lock与synchronized关键字在锁的处理上的重要差别
锁的获取方式:前者是通过程序代码的方式由开发者手工获取,后者是通过JVM来获取(无需开发者干预)
具体实现方式:前者是通过Java代码的方式来实现,后者是通过JVM底层来实现 (无需开发者关注)
锁的释放方式:前者务必通过unlock()方法在finally块中手工释放,后者是通过JVM来释放(无需开发者关注)
锁的具体类型:前者提供了多种,如公平锁、非公平锁,后者与前者均提供了可重入锁