Java之synchronize锁和lock锁

Java中多线程主要两种加锁机制

  • Synchronize
  • 显示Lock

Synchronized锁

1.1synchronized锁是什么?

  • synchronized是Java的一个关键字,它能够将代码块(方法)锁起来。使用起来非常简单,只要在代码块(方法)添加关键字synchronized即可实现同步功能。

    1
    2
    3
    public synchronized void test() {
    ......
    }
  • synchronized是一种互斥锁一次只能允许一个线程进入被锁的代码块

  • synchronized是一种内置锁/监视器锁,Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的!(锁的是对象,但我们同步的是方法/代码块)

1.2synchronize用处是什么?

  • synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
  • synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)

Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。

1.3synchronized的原理

synchronized修饰方法和代码块的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
// 修饰方法
public synchronized void test1() {

}

public void test2() {
// 修改代码块
synchronized(this) {

}
}
}

代码编译后,进行反编译一下:

image-20201201142548132

同步代码块

  • monitorenter和monitorexit指令实现的

同步方法(在这看不出来,需要看JVM底层实现)

  • 方法修饰符上的ACC_SYNCHRONIZED实现

synchronized底层是通过monitor对象对象有自己的对象头,存储了很多信息,其中一个信息标识是被哪个线程持有

1.4synchronized如何使用

synchronized一般用来修饰三种东西:

  • 修饰普通方法
  • 修饰代码块
  • 修饰静态方法

1.4.1修饰普通方法

Test对象(内置锁)

1
2
3
4
5
6
public class Test {
//修饰普通方法,此时用的锁是Test对象(内置锁)
public synchronized void test() {
//……
}
}

1.4.2修饰代码块

Test对象(内置锁)–> this

1
2
3
4
5
6
7
8
public class Test {
public void test() {
//修饰代码块,此时用的锁是Test对象(内置锁)--> this
synchronized(this) {
//……
}
}
}

使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁),代码如下:

1
2
3
4
5
6
7
8
9
10
11
public class Test {
//使用object作为锁(任何对象都有对应的锁标记,object也不例外)
private Object object = new Object();

public void test() {
//修饰代码块,此时用的锁是自己创建的锁Object
synchronized(object) {
//……
}
}
}

这种方式(随便使用一个对象作为锁)被称为–>客户端锁,这是不建议使用的

1.4.3修饰静态方法

获取到的是类锁(类的字节码文件对象):Test.class

1
2
3
4
5
6
public class Test {
//修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)-->Test.class
public static synchronized void test() {
// ……
}
}

1.4.4类锁与对象锁

synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的对象锁

类锁和对象锁是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.thread;

/**
* @author jingLv
* @date 2020/12/01
*/
public class SynchronizedDemo {

// synchronized修饰非静态方法
public synchronized void function() throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000L);
System.out.println("function running......");
}
}

// synchronized修饰静态方法
public static synchronized void staticFunction() throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000L);
System.out.println("Static function running......");
}
}

public static void main(String[] args) {
final SynchronizedDemo demo = new SynchronizedDemo();
//创建线程执行静态方法
Thread thread1 = new Thread(() -> {
try {
staticFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 创建线程执行实例方法
Thread thread2 = new Thread(() -> {
try {
demo.function();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

// 启动
thread1.start();
thread2.start();
}
}

结果证明:类锁和对象锁是不会冲突的!

image-20201201181204916

1.5重入锁

先看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Widget {
// 锁住了
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
// 锁住了
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
  1. 当线程A进入到LoggingWidget的doSomething()方法时,此时拿到了LoggingWidget实例对象的锁
  2. 随后在方法上又调用了父类Widget的doSomething()方法,它又是被synchronized修饰
  3. 那现在我们LoggingWidget实例对象的锁还没有释放,进⼊⽗类Widget的 doSomething() ⽅法还需要⼀把锁吗?– 不需要

因为锁的持有者是“线程”⽽不是“调⽤”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续“开锁”进去的!

这就是内置锁的可重入性。记住,持有锁的是线程

1.6释放锁的时机

  1. 当方法(代码块)执行完毕后会自动释放锁,不需要任何操作。
  2. 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。不会由于异常导致出现死锁现象。

Lock显示锁

2.1Lock显示锁简单介绍

Lock显示锁是JDK1.5之后有的,之前都是使用synchronized锁来使线程安全。

Lock显示锁是一个接口:

image-20201202180612371

看下Lock的注释的介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* {@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 更好地伸缩性,支持Condition条件对象
* {@link Condition} objects.
*
* <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}.通常限定每次一个线程访问共享变量,但ReadWriterLock允许读锁并发访问共享资源
*
* <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 Synchronized释放锁的顺序必须是获取锁的相反顺序
* order, and all locks must be released in the same lexical scope in which
* they were acquired.
*
* <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
* &quot;hand-over-hand&quot; or &quot;chain locking&quot;: 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 灵活性大,那么出错的几率高,我们一般都是这样使用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.
*
* <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实例作为内置锁,因为会导致混乱(与真正的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.2synchronized锁和Lock锁使用哪个

从上面的介绍,Lock显示锁给我们的程序带来了很多的灵活性,很多特性都是synchronized锁没有的。那我们只使用Lock显示锁吗?

自然不是,Lock锁在刚出来的时候很多性能方面都比synchronized锁要好,但是从jdk1.6开始synchronized锁就做了各种优化(synchronized是Jdk原生的)

优化操作:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁

所以,到现在Lock锁和Synchronized锁的性能其实差别不是很⼤!⽽Synchronized锁⽤起来⼜特别简单。 Lock锁还得顾忌到它的特性,要⼿动释放锁才⾏(如果忘了释放,这就是⼀个隐患)

所以说,我们绝⼤部分时候还是会使⽤Synchronized锁,⽤到了Lock锁提及的特性,带来的灵活性才会考虑使⽤Lock显式锁

2.3公平锁

  • 公平锁理解起来⾮常简单:线程将按照它们发出请求的顺序来获取锁
  • ⾮公平锁就是:线程发出请求的时可以“插队”获取锁
  • Lock和synchronize都是默认使⽤⾮公平锁的。如果不是必要的情况下,不要使⽤公平锁
    • 公平锁会来带⼀些性能的消耗的