一、synchronized实现方法和原理
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础。从语法上讲,Synchronized总共有三种用法:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
我们先写如下方法对应上面三种用法
public class MainTest {
public void method1() {
synchronized(this) {
}
}
public synchronized void method2() {
}
public synchronized static void method3() {
}
}
然后用 javap -verbose MainTest.class 查看代码
Classfile /D:/eclipse-jee-luna-SR2-win32-x86_64/ws/JavaTest/src/test/MainTest.class
Last modified 2021-2-21; size 454 bytes
MD5 checksum 56b092821f8394976e88544cf994100e
Compiled from "MainTest.java"
public class test.MainTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#17 // java/lang/Object."<init>":()V
#2 = Class #18 // test/MainTest
#3 = Class #19 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 method1
#9 = Utf8 StackMapTable
#10 = Class #18 // test/MainTest
#11 = Class #19 // java/lang/Object
#12 = Class #20 // java/lang/Throwable
#13 = Utf8 method2
#14 = Utf8 method3
#15 = Utf8 SourceFile
#16 = Utf8 MainTest.java
#17 = NameAndType #4:#5 // "<init>":()V
#18 = Utf8 test/MainTest
#19 = Utf8 java/lang/Object
#20 = Utf8 java/lang/Throwable
{
public test.MainTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
LineNumberTable:
line 6: 0
line 8: 4
line 9: 14
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ class test/MainTest, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void method2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 13: 0
public static synchronized void method3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 17: 0
}
SourceFile: "MainTest.java"
method1 的反编译代码
注意看 monitorenter 和 monitorexit。具体解释如下
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
method2 ,method3 的反编译代码
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
现在我们应该知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。(这一部分简称锁优化,文末有链接)
二、synchronized缺陷和Lock
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
- 1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
- 2)线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:
- 1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
- 2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
接下来看一下Lock的定义
/**
* @see ReentrantLock
* @see Condition
* @see ReadWriteLock
*/
public interface Lock {
/**
* 用来获取锁。如果锁已被其他线程获取,则进行等待。
*/
void lock();
/**
* lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
* 也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
*/
void lockInterruptibly() throws InterruptedException;
/**
* 用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,
* 这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
*/
boolean tryLock();
/**
* 通 tryLock。加入了获取锁的超时时间
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 用来释放锁的
*/
void unlock();
/**
* https://www.cnblogs.com/gemine/p/9039012.html 《Java并发之Condition》 这里面有介绍
*/
Condition newCondition();
}
基本使用方法如下:(一般来说,使用Lock必须在try{}catch{}块中进行)
Lock lock = new ReentrantLock();
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
另外注意一下 Lock 的文档,在定义的最上面还提到了ReentrantLock和ReadWriteLock。接下来就讲这些
三、ReentrantLock
先看一下构造函数
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认非公平锁。但也可以指定公平锁。
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
其实查阅整个源代码,会发现所有的方法都会去调用Sync里面的方法。而Sync继承自AbstractQueuedSynchronizer(AQS)
(相关AQS的代码在文末的《Java并发之AQS详解》有介绍。这里不贴出来了。主要是还是晦涩难懂,本人没有看明白,只看懂了两个点)
- AQS内部有一个FIFO线程等待队列,多线程争用资源被阻塞时会进入此队列
- 线程在等待过程中是通过死循环+CAS来判断状态的
四、ReadWriteLock 和 ReentrantReadWriteLock
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
相关解析在文末的《Java并发编程--ReentrantReadWriteLock》。说实在的,依然没怎么看懂。。。。。。
五、Lock和synchronized的选择
总结来说,Lock和synchronized有以下几点不同:
- 1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
- 2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- 3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- 5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
六、自定义锁
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,也就是如下四个函数
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
(前面两个是排他锁(写锁),后面两个是共享锁(读锁))
至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
七、参考资料
JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)
网友评论