当多个线程同时访问共享资源时,就需要使用同步机制来保证线程的安全性。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中用于实现同步的机制,它们各有优缺点,在实际开发中应该根据具体情况选择合适的机制。同时,对于多线程编程,还需要注意线程安全、死锁、饥饿等问题。