1.Synchronized同步关键字
1.1.Synchronized同步关键字简介
synchronized是属于JVM层面的一个关键字,底层是通过一个monitor对象(/管程对象)来完成,
由于wait()/notify()等方法也依赖于monitor对象,所以只有在同步的块或者方法中才能调用wait/notify等方法
1.2.同步代码块中synchronized底层实现
说明:
- Synchronized同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功.如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1.倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0,其他线程将有机会持有 monitor ;
- 为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令.从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令;
1.1.1.monitorenter(进入管程对象)
每个对象有一个管程对象(monitor),当monitor被占用时就会处于锁定状态;
线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入计数器为0,则该线程进入monitor,然后将进入计数器设置为1,该线程即为monitor的所有者;
- 如果线程已经之前占用了该monitor,本次只是重新进入,则将monitor的进入计数器加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入计数器为0,再重新尝试获取monitor的所有权;
1.1.2.monitorexit(退出管程对象,有两个
)
- 执行monitorexit的线程必须是objectref(即对象锁)所对应的monitor的所有者;
- 指令执行时,monitor的进入计数器减1,如果减1后进入计数器为0,那线程退出monitor,不再是这个monitor的所有者.其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权.
1.3.同步方法中synchronized底层实现
说明:
方法级的同步是隐式,即无需通过字节码指令(monitorenter和monitorexit)来控制的,它实现在方法调用和返回操作之中.JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法.当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor.在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor.如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放.
2.Lock锁
2.1.Lock锁简介
Lock是Java并发包(java.util.concurrent)下的一个具体类,他是API层面的锁;
Lock主要是通过CAS和ASQ(AbstractQueuedSynchronizer)来实现,通过加锁和解锁的过程来分析锁的实现;总体来讲线程获取(非公平)锁要经历以下过程:
①.调用lock方法,会先进行cas操作看下设置同步状态1可否成功,如果成功执行临界区代码
②.如果不成功获取同步状态,如果状态是0那么cas设置为1;
③.如果同步状态既不是0也不是自身线程持有,那就说明当前锁资源已经被其他线程占用,当前线程需要等待,那么会把当前线程构造成一个节点.
④.把当前线程节点以CAS的方式放入队列中,行为上线程阻塞,内部自旋获取同步状态.
⑤.线程释放锁,唤醒队列第一个节点,参与竞争.重复上述.
2.2.Lock非公平锁实现细节
AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,经过调查线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞.
2.3.Lock方法实现
当有线程竞争锁时,当前线程会首先尝试获得锁而不是在队列中进行排队等候,这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来.源码如下 : |
---|
对于刚来竞争的线程首先会通过CAS设置状态,如果设置成功那么直接获取锁,执行临界区的代码,反之调用acquire(1)进入同步队列中.如果已经存在Running线程,那么CAS肯定会失败,则新的竞争线程会通过CAS的方式被追加到队尾. |
2.3.1.解析acquire(1)方法
当CAS设置同步状态为1失败时才会执行上面的代码,上面的代码的作用是完成同步状态的获取,构造用于放入队列中的节点(可以理解为线程任务), 加入到队列中,单个节点自己自旋用于检查目前队列中的状况以及当前节点或者是线程阻塞.该方法主要由以下几个方法构成 tryAcquire() ,addWaiter()和AcquireQueued(); |
2.3.2.acquireQueued(addWaiter(Node mode))方法
acquireQueued()方法的主要作用是把已经追加到队列的线程节点(addWaiter()方法返回值)进行阻塞 ,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回;仔细看看这个方法是个无限循环,感觉如果 p == head && tryAcquire(arg) 条件不满足循环将永远无法结束,当然不会出现死循环,parkAndCheckInterrupt()方法会把当前线程挂起,从而阻塞住线程的调用栈. |
2.3.3.addWaiter(Node mode)方法
addWaiter()方法负责把当前无法获得锁的线程包装为一个Node添加到队尾. |
其中参数mode是独占锁还是共享锁,默认为null,独占锁.追加到队尾的动作分两步: 1、如果当前队尾已经存在(tail!=null),则使用CAS把当前线程更新为Tail; 2、如果当前Tail为null或则线程调用CAS设置队尾失败,则通过enq()方法继续设置Tail; |
2.3.4.enq(Node node)
该方法就是循环调用CAS,即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头). 总而言之,上面的addWaiter()方法的目的就是通过CAS把当前线程追加到队尾,并返回包装后的Node实例. |
2.3.5.parkAndCheckInterrupt()
LockSupport.park()方法最终把线程交给系统(Linux)内核进行阻塞. 当然也不是马上把请求不到锁的线程进行阻塞,还要检查该线程的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中;shouldParkAfterFailedAcquire()方法就是靠前继节点判断当前线程是否应该被阻塞, 如果前继节点处于CANCELLED状态,则顺便删除这些节点重新构造队列. |
3.Synchronized同步关键字和Lock锁的区别
3.1.使用方法
- Synchronized不需要用户去手动释放锁,当synchronized代码执行完成之后系统会自动让线程释放对锁的占用;
- ReentrantLock则需要用户手动去释放锁,若没有主动释放锁,就有可能导致出现死锁现象;还需要lock()与unlock方法配合try/finally语句块来完成;
3.2.等待是否可中断
- synchronized不可中断,除非抛出异常或者正常运行完成;
- ReentrantLock可以中断;
tryLock(long, TimeUnit); //设置超时方法
lockinterruptibly(); //放在代码块中,调用interrupt()方法可中断
3.3.加锁是否公平
- Synchronized非公平锁;
- ReentrantLock默认非公平,可以在构造方法中加入boolean类型的参数指定公平锁(true)与非公平锁(false);
3.4.锁绑定多个条件condition
①.Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set).其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了Object 监视器方法的使用;
②.Condition(也称为条件队列 或条件变量)为线程提供了一种手段,在某个状态条件下直到接到另一个线程的通知,否则一直处于挂起状态(即"等待").因为访问此共享状态信息发生在不同的线程中,所以它必须受到保护,因此要将某种形式的锁与 Condition相关联;
③.Condition 实例实质上被绑定到一个锁上.要为特定 Lock 实例获得 Condition 实例,可以使用其 newCondition() 方法;
④.调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态.相当于同步队列的首节点(获取了锁的节点)移动到了Condition的等待队列中;
⑤.调用Condition的signal()方法,将会唤醒在等待队列中等待最长的节点(首节点),在唤醒之前,会将节点移到同步队列中,然后再使用LockSupport唤醒该节点的进程,被唤醒后的线程,将从await()方法中退出,进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中;
- synchronized同步关键字中没有该对象;
- ReentrantLock对象用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized关键字那样要么随机唤醒一个线程要么唤醒全部线程;
4.案例
4.1.需求:
多个线程之间按照顺序调用,实现A->B->C三个线程启动,要求如下:
①.AA打印5次,BB打印10次,CC打印15次,然后AA打印5次,BB打印10次,CC打印15次…,以此类推,共打印10轮
4.2.代码实现:
public class SyncReentrantLockDemo {
public static void main(String[] args) {
ShareResource shareResource=new ShareResource();
//线程AA
new Thread(()->{
for (int i = 0; i <3 ; i++) {
shareResource.printAA();
}
},"AA").start();
//线程BB
new Thread(()->{
for (int i = 0; i <3 ; i++) {
shareResource.printBB();
}
},"BB").start();
//线程CC
new Thread(()->{
for (int i = 0; i <3 ; i++) {
shareResource.printCC();
}
},"CC").start();
}
}
//线程操作资源类
class ShareResource {
private int number = 1; //表示当前线程,1:AA,2:BB,3:CC
//锁
private Lock lock = new ReentrantLock();
private Condition conditionAA = lock.newCondition();
private Condition conditionBB = lock.newCondition();
private Condition conditionCC = lock.newCondition();
//打印方法,AA线程调用
public void printAA() {
try {
//加锁
lock.lock();
//如果当前线程不是AA,线程AA还在等待被唤醒
while (number != 1) {
conditionAA.await();
}
//如果当前线程是AA,则打印
for (int i = 0; i < 1; i++) {
System.out.println(Thread.currentThread().getName() + "\t AA" + i);
}
//唤醒线程BB
number = 2;
conditionBB.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//打印方法,BB线程调用
public void printBB() {
try {
//加锁
lock.lock();
//如果当前线程不是BB,线程BB还在等待被唤醒
while (number != 2) {
conditionBB.await();
}
//如果当前线程是BB,则打印
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "\t BB" + i);
}
//唤醒线程CC
number = 3;
conditionCC.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//打印方法,CC线程调用
public void printCC() {
try {
//加锁
lock.lock();
//如果当前线程不是CC,线程CC还在等待被唤醒
while (number != 3) {
conditionCC.await();
}
//如果当前线程是CC,则打印
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "\t CC" + i);
}
//唤醒线程AA
number = 1;
conditionAA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
//打印结果