参考文章:
一、synchronized锁
1.1 synchronized锁是什么
synchronized是Java的一个关键字,它能够将代码块(方法)锁起来。
它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能。
public synchronized void deal() {
if (time > 0) {
System.out.print(Thread.currentThread().getName() + " time is : " + time + " ");
System.out.println(Thread.currentThread().getName() + "操作:" + --time);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized是一种互斥锁,一次只能允许一个线程进入被锁住的代码块。
synchronized是一种内置锁/监视器锁,Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的。
1.2 synchronized用处是什么
- synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
- synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)
Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。
1.3 synchronized的原理
public class Main {
//修饰方法
public synchronized void test1(){
}
public void test2(){
// 修饰代码块
synchronized (this){
}
}
}
来反编译看一下:
同步代码块: monitorenter和monitorexit指令实现的
同步方法(在这看不出来需要看JVM底层实现):方法修饰符上的ACC_SYNCHRONIZED实现
synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。
具体可参考:
- https://blog.csdn.net/chenssy/article/details/54883355
- https://blog.csdn.net/u012465296/article/details/53022317
1.4 synchronized如何使用
synchronized一般我们用来修饰三种东西:
- 修饰普通方法
- 修饰代码块
- 修饰静态方法
1.4.1 修饰普通方法
用的锁是Tes t对象(内置锁)
public class Test {
public synchronized void test() {
// doSomething
}
}
1.4.2 修饰代码块
用的锁是Test 对象(内置锁)—>this
public class Test {
public void test() {
synchronized (this) {
// doSomething
}
}
}
使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁)
public class Test {
// 使用object作为锁(任何对象都有对应的锁标记,object也不例外)
private Object object;
public void test() {
// 修饰代码块,此时用的锁是自己创建的锁Object
synchronized (object) {
// doSomething
}
}
}
上面那种方式(随便使用一个对象作为锁)在书上称之为–>客户端锁
,这是不建议使用的。
书上想要实现的功能是:给ArrayList
添加一个putIfAbsent()
,这需要是线程安全的。
方法一:
public class Test<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent) {
list.add(x);
}
return absent;
}
}
直接添加synchronized是不可行的,因为list.contains(x)
用的锁不是Test内置锁。
方法二:
public class Test<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x) {
synchronized (list) {
boolean absent = !list.contains(x);
if (absent) {
list.add(x);
}
return absent;
}
}
}
使用客户端锁,会将当前的实现与原本的list耦合了
方法三:使用组合的方式(也就是装饰器模式)
主要代码:
public class Test<E> implements List<E> {
private final List<E> list;
public Test(List<E> list) {
this.list = list;
}
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent) {
list.add(x);
}
return absent;
}
1.4.3 修饰静态方法
获取到的是类锁(类的字节码文件对象):Test.class
public class Test {
public synchronized static void test() {
// doSomething
}
}
等效于:
public class Test {
public static void test() {
synchronized (Test.class) {
// doSomething
}
}
}
1.4.4 类锁与对象锁
synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。
它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的。
public class Test {
//synchronized修饰非静态方法
public synchronized void function() throws InterruptedException {
for (int i = 0; i <3; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " :function running...");
}
}
//synchronized修饰静态方法
public static synchronized void staticFunction()
throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " :Static function running...");
}
}
public static void main(String[] args) {
final Test demo = new Test();
// 创建线程执行静态方法
Thread t1 = new Thread(() -> {
try {
staticFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程1");
// 创建线程执行实例方法
Thread t2 = new Thread(() -> {
try {
demo.function();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程2");
// 启动
t1.start();
t2.start();
}
}
运行结果:
线程2 :function running...
线程1 :Static function running...
线程2 :function running...
线程1 :Static function running...
线程1 :Static function running...
线程2 :function running...
结果证明:类锁和对象锁是不会冲突的
1.5 重入锁
重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。
线程再次获取锁:锁需要识别获取锁的现场是否为当前占据锁的线程,如果是,则再次成功获取。
线程重复n次获取锁,随后在第n次释放该锁后,其他线程能够获取该锁。要求对锁对于获取进行次数的自增,计数器对当前锁被重复获取的次数进行统计,当锁被释放的时候,计数器自减,当计数器值为0时,表示锁成功释放。
重入锁实现重入性:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。
1.5.1 不可重入锁
所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。我们尝试设计一个不可重入锁:
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
使用该锁:
public class Count{
Lock lock = new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执行doAdd()中的逻辑,必须先释放锁。这个例子很好的说明了不可重入锁。
1.5.2 可重入锁
所谓可重入,意味着线程可以进入它已经拥有的锁的同步代码块儿。
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread thread = Thread.currentThread();
while(isLocked && lockedBy != thread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = thread;
}
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
我们设计两个线程调用print()方法,第一个线程调用print()方法获取锁,进入lock()方法,由于初始lockedBy是null,所以不会进入while而挂起当前线程,而是是增量lockedCount并记录lockBy为第一个线程。接着第一个线程进入doAdd()方法,由于同一进程,所以不会进入while而挂起,接着增量lockedCount,当第二个线程尝试lock,由于isLocked=true,所以他不会获取该锁,直到第一个线程调用两次unlock()将lockCount递减为0,才将标记为isLocked设置为false。
同个线程可以进入之前获得锁的同步代码块,这是可重入锁的核心思想
1.5.3 测试
看代码:
public class Widget {
// 锁住了
public synchronized void doSomething() {
System.out.println(Thread.currentThread().getName() + " Wigget doSomething ...");
}
}
public class LoggingWidget extends Widget {
// 锁住了
public synchronized void doSomething() {
System.out.println(Thread.currentThread().getName() + " LoggingWidget: calling doSomething");
super.doSomething();
}
}
- 当线程A进入到LoggingWidget的doSomething()方法时,此时拿到了LoggingWidget实例对象的锁。
- 随后在方法上又调用了父类Widget的doSomething()方法,它又是被synchronized修饰。
- 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的doSomething()方法还需要一把锁吗?
public class Test {
public static void main(String[] args) {
LoggingWidget t = new LoggingWidget();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
t.doSomething();
}
});
thread.start();
}
}
结果:
Thread-0 LoggingWidget: calling doSomething
Thread-0 Wigget doSomething ...
答案是,不需要。
因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续“开锁”进去的!
这就是内置锁的可重入性。
1.6 释放锁的时机
- 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
- 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
不会由于异常导致出现死锁现象
二、 Lock显式锁
2.1 Lock显式锁简单介绍
Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的。
Lock显式锁是一个接口,我们来看看:
看注释:
/**
* {@code Lock} implementations provide more extensive locking
* operations than can be obtained using {@code synchronized} methods
* and statements. They allow more flexible structuring, may have
* quite different properties, and may support multiple associated
* {@link Condition} objects.
*
实现类提供了额外的锁机制,有更好的伸缩性,支持Condition
条件对象
* <p>A lock is a tool for controlling access to a shared resource by
* multiple threads. Commonly, a lock provides exclusive access to a
* shared resource: only one thread at a time can acquire the lock and
* all access to the shared resource requires that the lock be
* acquired first. However, some locks may allow concurrent access to
* a shared resource, such as the read lock of a {@link ReadWriteLock}.
*
通常限定每次一个线程访问共享变量,但ReadWriteLock
允许读锁并发访问共享对象
* <p>The use of {@code synchronized} methods or statements provides
* access to the implicit monitor lock associated with every object, but
* forces all lock acquisition and release to occur in a block-structured way:
* when multiple locks are acquired they must be released in the opposite
* order, and all locks must be released in the same lexical scope in which
* they were acquired.
*
synchronized释放锁的顺序必须是获取锁的相反顺序
* <p>While the scoping mechanism for {@code synchronized} methods
* and statements makes it much easier to program with monitor locks,
* and helps avoid many common programming errors involving locks,
* there are occasions where you need to work with locks in a more
* flexible way. For example, some algorithms for traversing
* concurrently accessed data structures require the use of
* "hand-over-hand" or "chain locking": you
* acquire the lock of node A, then node B, then release A and acquire
* C, then release B and acquire D and so on. Implementations of the
* {@code Lock} interface enable the use of such techniques by
* allowing a lock to be acquired and released in different scopes,
* and allowing multiple locks to be acquired and released in any
* order.
*
一般来说,使用synchronized来加锁会比较方便,减少出错的概率,但Lock锁的灵活性会更高
* <p>With this increased flexibility comes additional
* responsibility. The absence of block-structured locking removes the
* automatic release of locks that occurs with {@code synchronized}
* methods and statements. In most cases, the following idiom
* should be used:
*
* <pre> {@code
* Lock l = ...;
* l.lock();
* try {
* // access the resource protected by this lock
* } finally {
* l.unlock();
* }}</pre>
*
* When locking and unlocking occur in different scopes, care must be
* taken to ensure that all code that is executed while the lock is
* held is protected by try-finally or try-catch to ensure that the
* lock is released when necessary.
*
灵活性高,意味着出错几率也会变高,介绍了一般使用Lock锁的方法,记得释放锁
* <p>{@code Lock} implementations provide additional functionality
* over the use of {@code synchronized} methods and statements by
* providing a non-blocking attempt to acquire a lock ({@link
* #tryLock()}), an attempt to acquire the lock that can be
* interrupted ({@link #lockInterruptibly}, and an attempt to acquire
* the lock that can timeout ({@link #tryLock(long, TimeUnit)}).
*
获取锁是非阻塞、能被中断、可以设置超时的
* <p>A {@code Lock} class can also provide behavior and semantics
* that is quite different from that of the implicit monitor lock,
* such as guaranteed ordering, non-reentrant usage, or deadlock
* detection. If an implementation provides such specialized semantics
* then the implementation must document those semantics.
*
提高语义化,知道哪里加锁了,哪里释放锁
* <p>Note that {@code Lock} instances are just normal objects and can
* themselves be used as the target in a {@code synchronized} statement.
* Acquiring the
* monitor lock of a {@code Lock} instance has no specified relationship
* with invoking any of the {@link #lock} methods of that instance.
* It is recommended that to avoid confusion you never use {@code Lock}
* instances in this way, except within their own implementation.
*
建议在使用的时候不要使用Lock作为内置锁,因为会导致混乱
* <p>Except where noted, passing a {@code null} value for any
* parameter will result in a {@link NullPointerException} being
* thrown.
*
* <h3>Memory Synchronization</h3>
*
* <p>All {@code Lock} implementations <em>must</em> enforce the same
* memory synchronization semantics as provided by the built-in monitor
* lock, as described in
* <a href="https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4">
* The Java Language Specification (17.4 Memory Model)</a>:
* <ul>
* <li>A successful {@code lock} operation has the same memory
* synchronization effects as a successful <em>Lock</em> action.
* <li>A successful {@code unlock} operation has the same
* memory synchronization effects as a successful <em>Unlock</em> action.
* </ul>
*
* Unsuccessful locking and unlocking operations, and reentrant
* locking/unlocking operations, do not require any memory
* synchronization effects.
*
实现内存可见性
* <h3>Implementation Considerations</h3>
*
* <p>The three forms of lock acquisition (interruptible,
* non-interruptible, and timed) may differ in their performance
* characteristics, ordering guarantees, or other implementation
* qualities. Further, the ability to interrupt the <em>ongoing</em>
* acquisition of a lock may not be available in a given {@code Lock}
* class. Consequently, an implementation is not required to define
* exactly the same guarantees or semantics for all three forms of
* lock acquisition, nor is it required to support interruption of an
* ongoing lock acquisition. An implementation is required to clearly
* document the semantics and guarantees provided by each of the
* locking methods. It must also obey the interruption semantics as
* defined in this interface, to the extent that interruption of lock
* acquisition is supported: which is either totally, or only on
* method entry.
*
* <p>As interruption generally implies cancellation, and checks for
* interruption are often infrequent, an implementation can favor responding
* to an interrupt over normal method return. This is true even if it can be
* shown that the interrupt occurred after another action may have unblocked
* the thread. An implementation should document this behavior.
*
具体的实现类相关
* @see ReentrantLock
* @see Condition
* @see ReadWriteLock
*
* @since 1.5
* @author Doug Lea
*/
可以简单概括一下:
- Lock方式来获取锁支持中断、超时不获取、是非阻塞的
- 提高了语义化,哪里加锁,哪里解锁都得写出来
- Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
- 支持Condition条件对象
- 允许多个读线程同时访问共享资源
2.2 synchronized锁和Lock锁使用哪个
前面说了,Lock显式锁给我们的程序带来了很多的灵活性,很多特性都是Synchronized锁没有的。那Synchronized锁有没有存在的必要??
必须是有的。Lock锁在刚出来的时候很多性能方面都比Synchronized锁要好,但是从JDK1.6开始Synchronized锁就做了各种的优化。
- 优化操作:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁。
- 详情可参考:https://blog.csdn.net/chenssy/article/details/54883355
所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大,而Synchronized锁用起来又特别简单。Lock锁还得顾忌到它的特性,要手动释放锁才行(如果忘了释放,这就是一个隐患)
所以说,我们绝大部分时候还是会使用Synchronized锁,用到了Lock锁提及的特性,带来的灵活性才会考虑使用Lock显式锁
2.3 公平锁
公平锁:
- 线程将按照它们发出请求的顺序来获取锁
非公平锁:
- 线程发出请求的时可以“插队”获取锁
Lock和synchronize都是默认使用非公平锁的。如果不是必要的情况下,不要使用公平锁
公平锁会来带一些性能的消耗
三、总结
synchronized好用,简单,性能不差
没有使用到Lock显式锁的特性就不要使用Lock锁了