Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是
JDK 实现的 ReentrantLock。
Synchronized
synchronized同步(加锁)的四种方式
对于Synchronized来说,可以同步代码块、方法、类对象、静态方法
- 代码块
public void func() {
synchronized (this) {
// ...
}
}
synchronized同步一个代码块,但是在同步之前,synchronized要先拿到一把锁,也就是this对象。
这就表示着如果两个线程都想运行一个对象的方法func()时,那么他们是互斥的。
对这种情况的线程来说,有则执行,无则等待。
public class SynchronizedPra {
public static void main(String[] args) {
//针对SynchronizedPra类,只有一个对象实例s1
//那么当线程池service产生的多个线程都想运行s1对象的方法func1()时,只有拿到锁的线程可以运行。
SynchronizedPra s1 = new SynchronizedPra();
ExecutorService service = Executors.newCachedThreadPool();
service.execute(() -> s1.func1());
service.execute(() -> s1.func1());
}
//该方法中有一个同步代码块,意味着多线程模式下,谁拿到当前对象的锁,谁执行。
public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
//运行结果是规则的
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
如果两个线程运行两个对象的方法func()时,那么他们是互斥的。
对这种情况的线程来说,无需等待。
public class SynchronizedPra {
public static void main(String[] args) {
//两个对象实例s1,s2
SynchronizedPra s1 = new SynchronizedPra();
SynchronizedPra s2 = new SynchronizedPra();
//service生成两个线程运行的是不同对象的func1()方法,拿到的是不同的锁,不会互相影响。
ExecutorService service = Executors.newCachedThreadPool();
service.execute(() -> s1.func1());
service.execute(() -> s2.func1());
}
public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
//运行结果呈不确定性,这要取决于两个线程什么时候启动
0 1 2 3 4 0 1 2 3 5 6 7 8 9 4 5 6 7 8 9
- 方法
public synchronized void func () {
// ...
}
它和同步代码块一样,作用于同一个对象。
- 类对象
public void func() {
synchronized (SynchronizedExample.class) {
// ... }
}
每个类的类对象在内存都是唯一的,所以synchronized锁住类对象,相当于锁住该类
对所有线程来说,无论是运行哪个对象实例func1()方法,都是有则执行,无则等待。
public class SynchronizedPra {
public static void main(String[] args) {
SynchronizedPra s1 = new SynchronizedPra();
SynchronizedPra s2 = new SynchronizedPra();
ExecutorService service = Executors.newCachedThreadPool();
service.execute(() -> s1.func1());
service.execute(() -> s2.func1());
}
public void func1() {
synchronized (SynchronizedPra.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
- 静态方法
public synchronized static void fun() {
// ...
}
和上面一样,作用于整个类。
ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
public class ReentrantLockPra {
//类初始化时会生成ReentrantLock实例
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
//多个线程运行func1()时,首先运行的线程会先加锁,待运行完毕后释放锁,其他线程才能继续运行。
ReentrantLockPra s1 = new ReentrantLockPra();
ExecutorService service = Executors.newCachedThreadPool();
service.execute(() -> s1.func1());
service.execute(() -> s1.func1());
}
public void func1() {
//加锁
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}finally {
// 确保释放锁,从而避免发生死锁。
lock.unlock();
}
}
}
二者之间的比较
- 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。 - 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。 - 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。 ReentrantLock 可中断,而 synchronized 不行。 - 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。 - 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。 - 使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一 种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
ReentrantLock学习在之后的JUC学习中再继续补充,这里只是列举出Java中两种锁机制