天天看点

Java synchronized关键字实现原理及Lock对比

作者:我心永恒224077694

当多个线程同时访问共享资源时,就需要使用同步机制来保证线程的安全性。Java中的synchronized关键字就是一种同步机制,它可以让某个代码块或方法在同一时刻只能被一个线程访问。

synchronized实现原理: 在Java中,每个对象都有一个内部锁(或称为监视器锁或互斥锁),当一个线程要访问被synchronized修饰的代码块或方法时,它必须先获得这个对象的内部锁。如果这个锁已经被其他线程持有,则该线程将被阻塞,直到其他线程释放了这个锁。

synchronized的实现原理主要涉及到以下三个部分:

1. monitorenter指令:当线程尝试获取对象的锁时,JVM会执行monitorenter指令,如果获取成功,则进入临界区,否则进入阻塞状态等待锁的释放。

1. monitorexit指令:当线程退出临界区时,JVM会执行monitorexit指令,释放对象的锁,以便其他线程能够获取锁并进入临界区。

1. 对象头中的Mark Word:在Java中,每个对象都有一个对象头,其中包含了对象的元数据信息,比如对象的哈希码和锁状态等。在synchronized实现中,对象头中的Mark Word用于记录锁状态,当一个线程获得了对象的锁时,JVM会将Mark Word中的锁状态设置为“已锁定”,当线程释放锁时,JVM会将锁状态设置为“未锁定”。

下面是synchronized的源码实现:

```

public synchronized void method() { // 代码块 }

```

编译后的字节码:

```

public void method();

Code:

0: aload_0

1: monitorenter

2: aload_0

3: invokevirtual #1 // Method java/lang/Object.hashCode:()I

6: aload_0

7: monitorexit

8: return

......

```

在方法前面加了synchronized关键字后,编译器会在方法的字节码中插入monitorenter和monitorexit指令,确保在执行方法时只能有一个线程获得锁。

2. 修饰代码块

```

public void method() {

synchronized (this) {

// 代码块

}

}

```

编译后的字节码:

```

public void method();

Code:

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

```

在代码块前面加了synchronized关键字后,编译器会在代码块的字节码中插入monitorenter和monitorexit指令,保证在执行代码块时只能有一个线程获得锁。

需要注意的是,在使用synchronized时,应该尽量避免对整个方法或类进行加锁,而是应该尽可能地缩小同步范围,以提高程序的并发性能。

另外,从JDK1.5开始,Java提供了一种新的同步机制——Lock,它相对于synchronized来说更加灵活和高效,但也更加复杂。在实际开发中,应该根据具体情况选择合适的同步机制。

Lock机制相对于synchronized来说,具有以下优点:

1. 可重入性:与synchronized类似,Lock也具有可重入性,同一个线程可以多次获取同一把锁而不会发生死锁。

1. 精确控制锁的释放:Lock允许程序员手动释放锁,而synchronized只能在代码块执行完毕或抛出异常时自动释放锁。这个特性在某些场景下非常有用,比如死锁恢复。

1. 公平性:在synchronized中,无法保证多个线程获取锁的公平性,有可能会出现饥饿现象。而在Lock中,可以通过构造函数指定是否采用公平锁,来保证线程获取锁的公平性。

1. 支持多个条件变量:与synchronized只能支持一个条件变量相比,Lock可以支持多个条件变量,这在某些复杂的同步场景下非常有用。

下面是一个使用Lock实现同步的例子:

```

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class SynchronizedExample {

private int count = 0;

private Lock lock = new ReentrantLock();

public void increment() {

lock.lock();

try {

count++;

} finally {

lock.unlock();

}

}

public int getCount() {

lock.lock();

try {

return count;

} finally {

lock.unlock();

}

}

}

```

在这个例子中,我们使用ReentrantLock实现了一个线程安全的计数器,使用lock()方法获取锁,在try块中执行操作,使用unlock()方法释放锁。在getCount()方法中也使用了lock()和unlock()方法来保证线程安全。在实际开发中,可以根据具体的需求选择合适的Lock实现类。

除了ReentrantLock之外,Java中还提供了其他几种Lock实现类,下面介绍一下常用的几种:

1. ReentrantLock:重入锁,与synchronized类似,具有可重入性,支持公平和非公平锁,默认是非公平锁。

1. ReadWriteLock:读写锁,允许多个线程同时读共享数据,但只允许一个线程写共享数据,写锁是排他的。Java中提供了ReadWriteLock接口和ReentrantReadWriteLock实现类,后者支持重入和公平非公平锁。

1. StampedLock:乐观读锁,相对于ReadWriteLock,StampedLock具有更高的并发性能。StampedLock有三种模式:写模式、读模式和乐观读模式,其中写模式和读模式与ReadWriteLock类似,乐观读模式则不需要加锁,只需要使用乐观读方法tryOptimisticRead()获取版本号,然后读取数据,最后再根据版本号判断数据是否被修改过。

1. Condition:条件变量,与synchronized中的wait()、notify()和notifyAll()方法类似,Condition也提供了类似的await()、signal()和signalAll()方法,用于等待某个条件的满足和通知等待的线程。Condition需要与Lock一起使用。

在使用Lock机制时,需要注意以下几点:

1. 必须手动释放锁,否则可能会导致死锁。

1. 应该使用try-finally块来保证锁一定会被释放。

1. 应该根据具体情况选择合适的Lock实现类和锁的公平性。

1. 不要在锁的作用域内执行耗时操作,以避免降低程序的并发性能。

综上所述,synchronized和Lock都是Java中用于实现同步的机制,它们各有优缺点,在实际开发中应该根据具体情况选择合适的机制。同时,对于多线程编程,还需要注意线程安全、死锁、饥饿等问题。

继续阅读