Java-锁

Java 中的锁而言,一把锁也有可能同时占有多个标准,符合多种分类,比如 ReentrantLock 既是可中断锁,又是可重入锁。

根据分类标准我们把锁分为以下 7 大类别,分别是:

  • 偏向锁/轻量级锁/重量级锁;
  • 可重入锁/非可重入锁;
  • 共享锁/独占锁;
  • 公平锁/非公平锁;
  • 悲观锁/乐观锁;
  • 自旋锁/非自旋锁;
  • 可中断锁/不可中断锁

偏向锁/轻量级锁/重量级锁

第一种分类是偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态。

  • 偏向锁

如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。

  • 轻量级锁

JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。

  • 重量级锁

重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态。

image.png

综上所述,偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差

可重入锁/非可重入锁

image.png

共享锁/独占锁

共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有

公平锁/非公平锁

公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,有先来先得的意思。而非公平锁就不那么“完美”了,它会在一定情况下,忽略掉已经在排队的线程,发生插队现象

悲观锁/乐观锁

悲观锁的概念是在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁,所以其他线程不能来影响我。而乐观锁恰恰相反,它并不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。

自旋锁/非自旋锁

自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等

可中断锁/不可中断锁

在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开

悲观锁/乐观锁本质

悲观锁比较悲观,它认为如果不锁住这个资源,别的线程就会来争抢,就会造成数据结果错误,所以悲观锁为了确保结果的正确性,会在每次获取并修改数据时,都把数据锁住,让其他线程无法访问该数据,这样就可以确保数据内容万无一失

image.png

image.png

synchronized 背后的“monitor 锁”

获取和释放 monitor 锁的时机

最简单的同步方式就是利用 synchronized 关键字来修饰代码块或者修饰一个方法,那么这部分被保护的代码,在同一时刻就最多只有一个线程可以运行,而 synchronized 的背后正是利用 monitor 锁实现的。所以首先我们来看下获取和释放 monitor 锁的时机,每个 Java 对象都可以用作一个实现同步的锁,这个锁也被称为内置锁或 monitor 锁,获得 monitor 锁的唯一途径就是进入由这个锁保护的同步代码块或同步方法,线程在进入被 synchronized 保护的代码块之前,会自动获取锁,并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁。

synchronized 和 Lock 如何选择?

相同点

synchronized 和 Lock 的相同点非常多,我们这里重点讲解 3 个比较大的相同点。

1.synchronized 和 Lock 都是用来保护资源线程安全的。

2.都可以保证可见性。

image.png

对于 synchronized 而言,线程 A 在进入 synchronized 块之前或在 synchronized 块内进行操作,对于后续的获得同一个 monitor 锁的线程 B 是可见的,也就是线程 B 是可以看到线程 A 之前的操作的,这也体现了 happens-before 针对 synchronized 的一个原则

而对于 Lock 而言,它和 synchronized 是一样,都可以保证可见性,如图所示,在解锁之前的所有操作对加锁之后的所有操作都是可见的
image.png
3.synchronized 和 ReentrantLock 都拥有可重入的特点

ReentrantLock 是 Lock 接口的一个最主要的实现类,在对比 synchronized 和 Lock 的时候,也会选择 Lock 的主要实现类来进行对比。可重入指的是某个线程如果已经获得了一个锁,现在试图再次请求这个它已经获得的锁,如果它无需提前释放这个锁,而是直接可以继续使用持有的这个锁,那么就是可重入的。如果必须释放锁后才能再次申请这个锁,就是不可重入的。而 synchronized 和 ReentrantLock 都具有可重入的特性

不同点

image.png

  • 加解锁顺序不同

对于 Lock 而言如果有多把 Lock 锁,Lock 可以不完全按照加锁的反序解锁,比如我们可以先获取 Lock1 锁,再获取 Lock2 锁,解锁时则先解锁 Lock1,再解锁 Lock2,加解锁有一定的灵活度,如代码所示。

1
2
3
4
5
6
7
8
9
10
java复制代码
lock1.lock();

lock2.lock();

...

lock1.unlock();

lock2.unlock();

但是 synchronized 无法做到,synchronized 解锁的顺序和加锁的顺序必须完全相反,例如:

1
2
3
4
5
6
7
8
9
10
java复制代码
synchronized(obj1){

    synchronized(obj2){

        ...

    }

}

那么在这里,顺序就是先对 obj1 加锁,然后对 obj2 加锁,然后对 obj2 解锁,最后解锁 obj1。这是因为 synchronized 加解锁是由 JVM 实现的,在执行完 synchronized 块后会自动解锁,所以会按照 synchronized 的嵌套顺序加解锁,不能自行控制。

  • synchronized 锁不够灵活

一旦 synchronized 锁已经被某个线程获得了,此时其他线程如果还想获得,那它只能被阻塞,直到持有锁的线程运行完毕或者发生异常从而释放这个锁。如果持有锁的线程持有很长时间才释放,那么整个程序的运行效率就会降低,而且如果持有锁的线程永远不释放锁,那么尝试获取锁的线程只能永远等下去。

相比之下,Lock 类在等锁的过程中,如果使用的是 lockInterruptibly 方法,那么如果觉得等待的时间太长了不想再继续等待,可以中断退出,也可以用 tryLock() 等方法尝试获取锁,如果获取不到锁也可以做别的事,更加灵活。

  • synchronized 锁只能同时被一个线程拥有,但是 Lock 锁没有这个限制

例如在读写锁中的读锁,是可以同时被多个线程持有的,可是 synchronized 做不到。

如何选择

讲完了 synchronized 和 Lock 的相同点和区别,最后我们再来看下如何选择它们,在 Java 并发编程实战和 Java 核心技术里都认为:

如果能不用最好既不使用 Lock 也不使用 synchronized。因为在许多情况下你可以使用 java.util.concurrent 包中的机制,它会为你处理所有的加锁和解锁操作,也就是推荐优先使用工具类来加解锁。

如果 synchronized 关键字适合你的程序, 那么请尽量使用它,这样可以减少编写代码的数量,减少出错的概率。因为一旦忘记在 finally 里 unlock,代码可能会出很大的问题,而使用 synchronized 更安全。

如果特别需要 Lock 的特殊功能,比如尝试获取锁、可中断、超时功能等,才使用 Lock

Lock常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码
public interface Lock {

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();

}

接下来重点分析这 5 种方法的作用和用法,这 5 种方法分别是 lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()、unlock()。

lock() 方法

在 Lock 接口中声明了 4 种方法来获取锁(lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()),那么这 4 种方法具体有什么区别呢?

首先,lock() 是最基础的获取锁的方法。在线程获取锁时如果锁已被其他线程获取,则进行等待,是最初级的获取锁的方法。

对于 Lock 接口而言,获取锁和释放锁都是显式的,不像 synchronized 那样是隐式的,所以 Lock 不会像 synchronized 一样在异常时自动释放锁(synchronized 即使不写对应的代码也可以释放),lock 的加锁和释放锁都必须以代码的形式写出来,所以使用 lock() 时必须由我们自己主动去释放锁,因此最佳实践是执行 lock() 后,首先在 try{} 中操作同步资源,如果有必要就用 catch{} 块捕获异常,然后在 finally{} 中释放锁,以保证发生异常时锁一定被释放,示例代码如下所示。

公平锁与非公平锁 为什么需要非公平锁?

对比公平和非公平的优缺点

image.png

公平锁的优点在于各个线程公平平等,每个线程等待一段时间后,都有执行的机会,而它的缺点就在于整体执行速度更慢,吞吐量更小,相反非公平锁的优势就在于整体执行速度更快,吞吐量更大,但同时也可能产生线程饥饿问题,也就是说如果一直有线程插队,那么在等待队列中的线程可能长时间得不到运行。

  • 源码分析

在 ReentrantLock 类包含一个 Sync 类,这个类继承自AQS(AbstractQueuedSynchronizer),代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
public class ReentrantLock implements Lock, java.io.Serializable {

 

private static final long serialVersionUID = 7373984872572414699L;

 

/** Synchronizer providing all implementation mechanics */

 

private final Sync sync;

Sync 类的代码:

1
2
java复制代码
abstract static class Sync extends AbstractQueuedSynchronizer {...}

根据代码可知,Sync 有公平锁 FairSync 和非公平锁 NonfairSync两个子类:

1
2
3
4
java复制代码
static final class NonfairSync extends Sync {...}

static final class FairSync extends Sync {...}

公平锁与非公平锁的加锁方法的源码

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
java复制代码
protected final boolean tryAcquire(int acquires) {

    final Thread current = Thread.currentThread();

    int c = getState();

    if (c == 0) {

        if (!hasQueuedPredecessors() && //这里判断了 hasQueuedPredecessors()

                compareAndSetState(0, acquires)) {

            setExclusiveOwnerThread(current);

            return true;

        }

    } else if (current == getExclusiveOwnerThread()) {

        int nextc = c + acquires;

        if (nextc < 0) {

            throw new Error("Maximum lock count exceeded");

        }

        setState(nextc);

        return true;

    }

    return false;

}

非公平锁的锁获取源码如下:

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
java复制代码
final boolean nonfairTryAcquire(int acquires) {

    final Thread current = Thread.currentThread();

    int c = getState();

    if (c == 0) {

        if (compareAndSetState(0, acquires)) { //这里没有判断      hasQueuedPredecessors()

            setExclusiveOwnerThread(current);

            return true;

        }

    }

    else if (current == getExclusiveOwnerThread()) {

        int nextc = c + acquires;

        if (nextc < 0) // overflow

        throw new Error("Maximum lock count exceeded");

        setState(nextc);

        return true;

    }

    return false;

}

我们可以明显的看出公平锁与非公平锁的 lock() 方法唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors() 为 false,这个方法就是判断在等待队列中是否已经有线程在排队了。这也就是公平锁和非公平锁的核心区别,如果是公平锁,那么一旦已经有线程在排队了,当前线程就不再尝试获取锁;对于非公平锁而言,无论是否已经有线程在排队,都会尝试获取一下锁,获取不到的话,再去排队。

这里有一个特例需要我们注意,针对 tryLock() 方法,它不遵守设定的公平原则。

例如,当有线程执行 tryLock() 方法的时候,一旦有线程释放了锁,那么这个正在 tryLock 的线程就能获取到锁,即使设置的是公平锁模式,即使在它之前已经有其他正在等待队列中等待的线程,简单地说就是 tryLock 可以插队。

看它的源码就会发现:

1
2
3
4
5
6
java复制代码
public boolean tryLock() {

    return sync.nonfairTryAcquire(1);

}

这里调用的就是 nonfairTryAcquire(),表明了是不公平的,和锁本身是否是公平锁无关。

公平锁就是会按照多个线程申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待情况,直接尝试获取锁,所以存在后申请却先获得锁的情况,但由此也提高了整体的效率。

读写锁

在没有读写锁之前,我们假设使用普通的 ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源,因为如果多个读操作同时进行,其实并没有线程安全问题,我们可以允许让多个读操作并行,以便提高程序效率。

但是写操作不是线程安全的,如果多个线程同时写,或者在写的同时进行读操作,便会造成线程安全问题。

整体思路是它有两把锁,第 1 把锁是写锁,获得写锁之后,既可以读数据又可以修改数据,而第 2 把锁是读锁,获得读锁之后,只能查看数据,不能修改数据。读锁可以被多个线程同时持有,所以多个线程可以同时查看数据。

读写锁的获取规则

我们在使用读写锁时遵守下面的获取规则:

1.如果有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功。

2.如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作。

3.如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写。

一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。也可以总结为:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)

案例

ReentrantReadWriteLock 是 ReadWriteLock 的实现类,最主要的有两个方法:readLock() 和 writeLock() 用来获取读锁和写锁。

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
java复制代码
/**

* 描述: 演示读写锁用法

*/

public class ReadWriteLockDemo {

private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(

false);

private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock

.readLock();

private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock

.writeLock();

private static void read() {

readLock.lock();

try {

System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");

Thread.sleep(500);

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

System.out.println(Thread.currentThread().getName() + "释放读锁");

readLock.unlock();

}

}

private static void write() {

writeLock.lock();

try {

System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");

Thread.sleep(500);

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

System.out.println(Thread.currentThread().getName() + "释放写锁");

writeLock.unlock();

}

}

public static void main(String[] args) throws InterruptedException {

new Thread(() -> read()).start();

new Thread(() -> read()).start();

new Thread(() -> write()).start();

new Thread(() -> write()).start();

}

}

总结ReentrantReadWriteLock

  • 插队策略
  1. 公平策略下,只要队列里有线程已经在排队,就不允许插队。

2.非公平策略下:

1).如果允许读锁插队,那么由于读锁可以同时被多个线程持有,所以可能造成源源不断的后面的线程一直插队成功,导致读锁一直不能完全释放,从而导致写锁一直等待,为了防止“饥饿”,在等待队列的头结点是尝试获取写锁的线程的时候,不允许读锁插队

2).写锁可以随时插队,因为写锁并不容易插队成功,写锁只有在当前没有任何其他线程持有读锁和写锁的时候,才能插队成功,同时写锁一旦插队失败就会进入等待队列,所以很难造成“饥饿”的情况,允许写锁插队是为了提高效率。

  • 升降级策略:

只能从写锁降级为读锁,不能从读锁升级为写锁。

自旋锁

自旋锁:

“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己在这里不停地循环,直到目标达成。而不像普通的锁那样,如果获取不到锁就进入阻塞

|-> 对比自旋和非自旋的获取锁的流程

image.png

|-> 自旋锁的好处

首先,阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大。

在很多场景下,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,有时我只需要稍等一下,就可以避免上下文切换等开销,提高了效率。

那就是自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销

案例: 可重入的自旋锁

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
java复制代码
package lesson27;

 

import java.util.concurrent.atomic.AtomicReference;

import java.util.concurrent.locks.Lock;

 

/**

 * 描述:     实现一个可重入的自旋锁

 */

public class ReentrantSpinLock  {

 

    private AtomicReference<Thread> owner = new AtomicReference<>();

 

    //重入次数

    private int count = 0;

 

    public void lock() {

        Thread t = Thread.currentThread();

        if (t == owner.get()) {

            ++count;

            return;

        }

        //自旋获取锁

        while (!owner.compareAndSet(null, t)) {

            System.out.println("自旋了");

        }

    }

 

    public void unlock() {

        Thread t = Thread.currentThread();

        //只有持有锁的线程才能解锁

        if (t == owner.get()) {

            if (count > 0) {

                --count;

            } else {

                //此处无需CAS操作,因为没有竞争,因为只有线程持有者才能解锁

                owner.set(null);

            }

        }

    }

 

    public static void main(String[] args) {

        ReentrantSpinLock spinLock = new ReentrantSpinLock();

        Runnable runnable = new Runnable() {

            @Override

            public void run() {

                System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");

                spinLock.lock();

                try {

                    System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");

                    Thread.sleep(4000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                } finally {

                    spinLock.unlock();

                    System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");

                }

            }

        };

        Thread thread1 = new Thread(runnable);

        Thread thread2 = new Thread(runnable);

        thread1.start();

        thread2.start();

    }

}
  • 缺点

最大的缺点就在于虽然避免了线程切换的开销,但是它在避免线程切换开销的同时也带来了新的开销,因为它需要不停得去尝试获取锁。如果这把锁一直不能被释放,那么这种尝试只是无用的尝试,会白白浪费处理器资源。也就是说,虽然一开始自旋锁的开销低于线程切换,但是随着时间的增加,这种开销也是水涨船高,后期甚至会超过线程切换的开销,得不偿失。

  • 适用场景

自旋锁适用于并发度不是特别高的场景,以及临界区比较短小的情况,这样我们可以利用避免线程切换来提高效率

可是如果临界区很大,线程一旦拿到锁,很久才会释放的话,那就不合适用自旋锁,因为自旋会一直占用 CPU 却无法拿到锁,白白消耗资源

JVM 锁优化

相比于 JDK 1.5,在 JDK 1.6 中 HotSopt 虚拟机对 synchronized 内置锁的性能进行了很多优化,包括自适应的自旋、锁消除、锁粗化、偏向锁、轻量级锁等。有了这些优化措施后,synchronized 锁的性能得到了大幅提高,下面我们分别介绍这些具体的优化

自适应自旋锁

在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率,以及当前锁的拥有者的状态等多种因素来共同决定。自旋的持续时间是变化的,自旋锁变“聪明”了。比如,如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间;但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率

锁消除

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
@Override

public synchronized StringBuffer append(Object obj) {

    toStringCache = null;

    super.append(String.valueOf(obj));

    return this;

}

从代码中可以看出,这个方法是被 synchronized 修饰的同步方法,因为它可能会被多个线程同时使用。

但是在大多数情况下,它只会在一个线程内被使用,如果编译器能确定这个 StringBuffer 对象只会在一个线程内被使用,就代表肯定是线程安全的,那么我们的编译器便会做出优化,把对应的 synchronized 给消除,省去加锁和解锁的操作,以便增加整体的效率。

粗化锁

如果我们释放了锁,紧接着什么都没做,又重新获取锁,例如下面这段代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码
public void lockCoarsening() {

    synchronized (this) {

        //do something

    }

    synchronized (this) {

        //do something

    }

    synchronized (this) {

        //do something

    }

}

其实这种释放和重新获取锁是完全没有必要的,如果我们把同步区域扩大,也就是只在最开始加一次锁,并且在最后直接解锁,那么就可以把中间这些无意义的解锁和加锁的过程消除,相当于是把几个 synchronized 块合并为一个较大的同步块。这样做的好处在于在线程执行这些代码时,就无须频繁申请与释放锁了,这样就减少了性能开销。

不过,我们这样做也有一个副作用,那就是我们会让同步区域变大。如果在循环中我们也这样做,如代码所示:

1
2
3
4
5
6
7
8
9
10
JAVA复制代码
for (int i = 0; i < 1000; i++) {

    synchronized (this) {

        //do something

    }

}

也就是我们在第一次循环的开始,就开始扩大同步区域并持有锁,直到最后一次循环结束,才结束同步代码块释放锁的话,这就会导致其他线程长时间无法获得锁。所以,这里的锁粗化不适用于循环的场景,仅适用于非循环的场景。

锁粗化功能是默认打开的,用 -XX:-EliminateLocks 可以关闭该功能。

偏向锁/轻量级锁/重量级锁

这三种锁是特指 synchronized 锁的状态的,通过在对象头中的 mark word 来表明锁的状态

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%