1 Synchronized
1.1 定义
java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。
1.2 使用场景
1.2.1 方法锁
方法声明时使用,放在范围操作符(public等)后,其返回类型声明(void等)之前。即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。
例如:
public synchronized void MethodA() { //方法体 } 复制代码
1.2.2 类加锁
如果线程进入类锁,则线程在该类中所有操作不能进行,包括静态变量和静态方法,实际上,对于含有静态方法和静态变量的代码块的同步
例如:
public static void MethodA() { //加入对象锁 synchronized (Student.class) { //第二次检查 } } 复制代码
1.2.3 代码块加锁
对某一代码块使用,synchronized后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块.此时,线程获得的是成员锁.
例如:
public Object MethodA(Object obj){ synchronized(obj){ //方法体(一次只能有一个线程进入) } } 复制代码
1.2.4 对象锁
如果线程进入对象锁,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行.在对象级使用锁通常是一种比较粗糙的方法
例如:
public static void MethodA() { //加入对象锁 synchronized (this) { //代码块 } } 复制代码
1.3 作用
- 原子性:确保线程互斥的访问同步代码;
- 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
- 有序性:有效解决重排序问题;
1.4 底层原理
/** * @author 17122 */ public class Test01 { public static synchronized void sayHello(){ System.out.println("Hello"); } public void method() { synchronized (this) { System.out.println("Method 1 start"); } } public static void main(String[] args) { sayHello(); } } 复制代码
进行编译和反编译:
## javac Test01.java ## javap -c Test01.class ## 结果 Compiled from "Test01.java" public class test0706.Test01 { public test0706.Test01(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static synchronized void sayHello(); Code: 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public void method(); Code: 0: aload_0 1: dup 2: astore_1 3: monitorenter # 监视器锁 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #5 // String Method 1 start 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_1 13: monitorexit #监视器锁释放锁 14: goto 22 17: astore_2 18: aload_1 19: monitorexit 20: aload_2 21: athrow 22: return Exception table: from to target type 4 14 17 any 17 20 17 any public static void main(java.lang.String[]); Code: 0: invokestatic #6 // Method sayHello:()V 3: return } 复制代码
synchronized关键字的语义底层是通过一个monitor的对象来完成
,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出llegalMonitorStateException的异常的原因。
2 ReentrantLock
2.1 定义
可重入互斥锁与使用synchronized
方法和语句访问的隐式监视器锁具有相同的基本行为和语义 ,但具有扩展功能。一个ReentrantLock
是拥有该线程最后成功锁定,但尚未解锁的。lock
当锁不为另一个线程所有时,调用线程 将返回,成功获取锁。如果当前线程已经拥有锁,该方法将立即返回。
2.2 使用场景
2.2.1 一般使用(悲观锁)
Lock lock = new ReentrantLock(); public void MethodA()){ lock.lock(); // 细节问题:lock.lock(); lock.unlock(); // lock 锁必须配对,否则就会死在里面 try { //代码块 } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } 复制代码
2.2.2 公平锁
非常公平, 不能够插队,必须先来后到
/** * 声明公平锁 */ ReentrantLock reentrantLock = new ReentrantLock(true); 复制代码
2.2.3 非公平锁
非常不公平,可以插队 (看源码可知,默认是不公平的锁)
2.2.4 响应中断
public class Test01 { static Lock lock1 = new ReentrantLock(); static Lock lock2 = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { //该线程先获取锁1,再获取锁2 Thread thread1 = new Thread(new ThreadDemo(lock1, lock2)); //该线程先获取锁2,再获取锁1 Thread thread2 = new Thread(new ThreadDemo(lock2, lock1)); thread1.start(); thread1.start(); //第二个线程中断 thread2.interrupt(); } static class ThreadDemo implements Runnable { Lock firstLock; Lock secondLock; public ThreadDemo(Lock firstLock, Lock secondLock) { this.firstLock = firstLock; this.secondLock = secondLock; } @Override public void run() { try { firstLock.lockInterruptibly(); sleep(10);//更好的触发死锁 secondLock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); } finally { firstLock.unlock(); secondLock.unlock(); System.out.println(Thread.currentThread().getName() + "正常结束!"); } } } } 复制代码
除非当前线程被中断,否则获取锁 。
如果其他线程没有持有锁,则获取该锁并立即返回,将锁持有计数设置为 1。
如果当前线程已经持有这个锁,那么持有计数就会增加一并且该方法立即返回。
如果锁被另一个线程持有,那么当前线程将被禁用以进行线程调度并处于休眠状态,直到发生以下两种情况之一:
- 锁被当前线程获取;或者
- 一些其他线程中断当前线程。
如果当前线程获取了锁,则锁保持计数设置为 1。
如果当前线程:
- 在进入此方法时设置其中断状态;或者
- 在获取锁时被中断,
然后InterruptedException
被抛出并清除当前线程的中断状态。
2.2.5 限时等待
如果在给定的等待时间内没有被另一个线程持有并且当前线程没有被中断,则获取锁 。
如果锁未被另一个线程持有,则获取该锁并立即返回值true
,将锁持有计数设置为 1。如果此锁已设置为使用公平排序策略,则如果任何其他线程正在等待该锁,则不会获取可用锁。这与tryLock()
方法相反。如果你想要一个tryLock
允许插入公平锁的定时,那么将定时和非定时形式组合在一起:
if (lock.tryLock() ||lock.tryLock(timeout, unit)) { ... } 复制代码
如果当前线程已持有此锁,则持有计数将增加 1 并且该方法返回true
。
如果锁被另一个线程持有,那么当前线程将被禁用以进行线程调度并处于休眠状态,直到发生以下三种情况之一:
- 锁被当前线程获取;或者
- 其他一些线程中断当前线程;或者
- 经过指定的等待时间
如果获取了锁,true
则返回该值并将锁保持计数设置为 1。
如果当前线程:
- 在进入此方法时设置其中断状态;或者
- 在获取锁时被中断,然后
InterruptedException
被抛出并清除当前线程的中断状态。
如果指定的等待时间过去,则false
返回该值。如果时间小于或等于零,则该方法根本不会等待。
在此实现中,由于此方法是显式中断点,因此优先响应中断而不是正常或可重入获取锁,并优先报告等待时间的过去。
2.3 作用
- ReentrantLock是可重入的独占锁
- 比起synchronized功能更加丰富
- 支持公平锁实现
- 支持中断响应以及限时等待等等
- 可以配合一个或多个Condition条件方便的实现等待通知机制。
2.4 原理
- ReentrantLock是基于AbstractQueuedSynchronizer(AQS)的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。
- AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。其内部定义了三个重要的静态内部类,Sync,NonFairSync,FairSync。
- Sync作为ReentrantLock中公用的同步组件,继承了AQS(要利用AQS复杂的顶层逻辑嘛,线程排队,阻塞,唤醒等等);NonFairSync和FairSync则都继承Sync,调用Sync的公用逻辑,然后再在各自内部完成自己特定的逻辑(公平或非公平)。
3 相同点
- 语义基本相同
- 都可以实现线程安全
4 不同点
- ReentrantLock是Lock的实现类,而synchronized是Java中的一个关键字
- Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
- Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块等