什么是 Java 中的线程同步?
在 Java 里,线程同步是一种用于处理多线程环境下资源共享问题的机制。当多个线程同时访问和修改共享资源时,若没有合适的同步机制,就可能出现数据不一致、脏读等问题,这些问题会破坏程序的正确性和稳定性。线程同步能确保在同一时刻只有一个线程可以访问共享资源,从而避免这类问题的发生。
下面为你介绍 Java 中实现线程同步的几种常用方法:
1. synchronized 关键字
这是 Java 中最常用的同步机制,它可以修饰方法或者代码块。
-
同步方法:在方法声明时添加
synchronized
关键字,这样同一时刻仅能有一个线程调用该方法。
class Counter {
private int count = 0;
// 同步方法
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
- 同步代码块:当仅需对方法中的部分代码进行同步时,可使用同步代码块。
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
// 同步代码块
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
2. ReentrantLock 类
ReentrantLock
是 Java 5 引入的一个可重入锁,它提供了比 synchronized
更灵活的同步机制。
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
3. volatile 关键字
volatile
关键字主要用于保证变量的可见性,即一个线程修改了该变量的值,其他线程能立即看到修改后的值。不过,volatile
无法保证原子性,所以不能替代 synchronized
或 ReentrantLock
来实现复杂的同步操作。
class SharedData {
// 使用 volatile 关键字保证可见性
private volatile int value;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
通过这些同步机制,你可以在 Java 中安全地处理多线程访问共享资源的问题,确保程序的正确性和稳定性。
synchronized 关键字底层原理,真实项目实战案例使用
synchronized 关键字底层原理
在 Java 中,synchronized 关键字是实现线程同步的重要方式,它的底层实现主要依赖于 Java 对象头和 Monitor(监视器)机制。以下为你详细阐述:
- Java 对象头
在 JVM 里,每个 Java 对象都有一个对象头(Object Header),其包含了一些与对象自身相关的运行时数据,像哈希码、分代年龄等。此外,对象头还会保存与锁相关的信息,这是实现 synchronized 锁的基础。对象头中的锁状态有以下几种:
无锁状态:对象头未被任何线程锁定。
偏向锁状态:在大多数情况下,锁总是由同一个线程多次获得,这时会使用偏向锁来提高性能。当一个线程首次访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时无需进行 CAS 操作来加锁和解锁,只需简单地测试对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。
轻量级锁状态:当有另一个线程尝试竞争偏向锁时,持有偏向锁的线程会释放偏向锁,将对象头设置为轻量级锁状态。轻量级锁适用于线程交替执行同步块的场景,若多个线程同时访问同步块,轻量级锁会膨胀为重量级锁。
重量级锁状态:当多个线程同时竞争同一个锁时,轻量级锁会膨胀为重量级锁。重量级锁依赖于操作系统的互斥量(Mutex)来实现,会导致线程的阻塞和唤醒,性能开销较大。 - Monitor(监视器)机制
Monitor 是 Java 中实现同步的基础,它是一个同步工具,也可以理解为一种锁的实现。每个 Java 对象都可以关联一个 Monitor,当一个线程尝试访问被 synchronized 修饰的代码块或方法时,它需要先获得该对象关联的 Monitor。具体过程如下:
进入同步块:线程尝试获取对象的 Monitor,如果 Monitor 处于空闲状态,线程会将其占用并进入同步块执行代码。
竞争 Monitor:如果 Monitor 已经被其他线程占用,当前线程会被阻塞,进入等待队列,直到持有 Monitor 的线程释放它。
释放 Monitor:当线程执行完同步块中的代码后,会释放 Monitor,并唤醒等待队列中的其他线程,让它们有机会竞争 Monitor。
真实项目实战案例
以下是一个使用 synchronized 关键字的实际项目案例,模拟一个银行账户的转账操作。在多线程环境下,多个用户可能同时对同一个账户进行转账操作,为了保证账户余额的一致性,需要使用 synchronized 关键字来实现线程同步。
class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
// 同步方法,保证线程安全的转账操作
public synchronized boolean transfer(BankAccount target, double amount) {
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
System.out.println("转账成功:转出 " + amount + " 元,当前余额:" + this.balance + " 元");
return true;
} else {
System.out.println("转账失败:余额不足");
return false;
}
}
// 同步方法,保证线程安全的查询余额操作
public synchronized double getBalance() {
return balance;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account1 = new BankAccount(1000);
BankAccount account2 = new BankAccount(500);
// 创建两个线程进行转账操作
Thread thread1 = new Thread(() -> {
account1.transfer(account2, 200);
});
Thread thread2 = new Thread(() -> {
account1.transfer(account2, 300);
});
// 启动线程
thread1.start();
thread2.start();
// 等待线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终余额
System.out.println("账户1余额:" + account1.getBalance() + " 元");
System.out.println("账户2余额:" + account2.getBalance() + " 元");
}
}
代码解释
BankAccount 类:表示银行账户,包含一个 balance 字段用于存储账户余额。transfer 方法用于将当前账户的指定金额转账到目标账户,getBalance 方法用于查询当前账户的余额。这两个方法都使用了 synchronized 关键字进行修饰,确保在多线程环境下对账户余额的操作是线程安全的。
Main 类:创建了两个 BankAccount 对象,并启动两个线程分别对账户进行转账操作。通过 join 方法等待两个线程执行完毕,最后输出两个账户的最终余额。
通过使用 synchronized 关键字,确保了在多线程环境下对账户余额的操作是原子性的,避免了数据不一致的问题。
在真实项目中,synchronized关键字可能会带来哪些性能问题?如何优化?
synchronized
关键字可能带来的性能问题
1. 线程阻塞和唤醒开销
当一个线程获得 synchronized
修饰的锁时,其他试图获取该锁的线程会被阻塞。线程的阻塞和唤醒操作涉及到用户态和内核态的切换,这种切换的开销是比较大的。在高并发场景下,大量线程频繁地阻塞和唤醒会导致性能严重下降。
2. 锁的粒度问题
如果 synchronized
修饰的范围过大(锁的粒度太粗),会导致同一时间内只有一个线程能执行该范围内的代码,即使这些代码中的不同部分实际上可以并行执行。这会限制程序的并发度,降低系统的整体性能。
3. 偏向锁、轻量级锁的膨胀开销
在 Java 中,synchronized
锁存在偏向锁、轻量级锁和重量级锁的状态转换。当竞争激烈时,偏向锁和轻量级锁会膨胀为重量级锁,这个过程涉及到一系列的状态转换和操作,会带来额外的性能开销。
4. 死锁风险
使用 synchronized
关键字时,如果多个线程相互持有对方需要的锁,并且都在等待对方释放锁,就会导致死锁。死锁会使程序陷入无限等待的状态,严重影响系统的可用性和性能。
优化方案
1. 减小锁的粒度
尽量缩小 synchronized
修饰的代码范围,只对需要同步的关键代码进行加锁。这样可以减少线程之间的竞争,提高并发度。
例如,在一个电商系统中,对商品库存的更新操作需要加锁,但其他一些与库存无关的操作可以并行执行:
class Product {
private int stock;
public void updateStock(int quantity) {
// 只对更新库存的关键代码加锁
synchronized (this) {
if (stock >= quantity) {
stock -= quantity;
}
}
// 其他与库存无关的操作,无需加锁
doOtherThings();
}
private void doOtherThings() {
// 一些与库存无关的操作
}
}
2. 使用细粒度的锁
如果一个对象包含多个独立的状态变量,可以使用多个锁来分别保护这些变量,而不是使用一个大锁来保护整个对象。
例如,一个数据缓存类中包含两个独立的缓存:
class DataCache {
private final Object cache1Lock = new Object();
private final Object cache2Lock = new Object();
private int[] cache1;
private int[] cache2;
public void updateCache1(int[] newData) {
synchronized (cache1Lock) {
this.cache1 = newData;
}
}
public void updateCache2(int[] newData) {
synchronized (cache2Lock) {
this.cache2 = newData;
}
}
}
3. 锁分离
对于读写操作,可以使用读写锁(ReentrantReadWriteLock
)来替代 synchronized
关键字。读写锁允许多个线程同时进行读操作,但在写操作时会独占锁,这样可以提高并发性能。
例如:
import java.util.concurrent.locks.ReentrantReadWriteLock;
class DataContainer {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private int data;
public int readData() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
public void writeData(int newData) {
writeLock.lock();
try {
data = newData;
} finally {
writeLock.unlock();
}
}
}
4. 无锁算法和数据结构
在某些场景下,可以使用无锁算法和数据结构来替代 synchronized
关键字,避免锁带来的性能开销。例如,使用 AtomicInteger
、AtomicLong
等原子类来实现线程安全的计数操作。
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
5. 避免死锁
在设计多线程程序时,要合理安排锁的获取顺序,避免出现死锁的情况。可以使用资源分级的方法,确保所有线程按照相同的顺序获取锁。
例如,有两个资源 resource1
和 resource2
,所有线程都先获取 resource1
的锁,再获取 resource2
的锁:
class ResourceManager {
private final Object resource1 = new Object();
private final Object resource2 = new Object();
public void operateResources() {
synchronized (resource1) {
synchronized (resource2) {
// 执行操作
}
}
}
}
通过以上优化方法,可以有效减少 synchronized
关键字带来的性能问题,提高程序的并发性能和响应速度。
偏向锁、轻量级锁、重量级锁分别在什么场景下使用?
偏向锁、轻量级锁和重量级锁是 Java 中 synchronized
关键字在不同场景下使用的锁状态,它们各自适用于不同的并发场景,以下为你详细介绍:
偏向锁
适用场景
偏向锁适用于只有一个线程频繁访问同步块的场景。在大多数实际应用中,很多同步块在整个生命周期内往往只被一个线程访问,此时偏向锁可以提供非常高效的同步机制。
原理与优势
当一个线程首次访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID。以后该线程在进入和退出同步块时,无需进行 CAS(Compare-And-Swap)操作来加锁和解锁,只需简单地测试对象头的 Mark Word 里是否存储着指向当前线程的偏向锁。如果测试成功,表示该线程已经获得了锁,直接进入同步块执行代码,避免了加锁和解锁的开销,从而提高了性能。
示例场景
比如在一个单线程的 GUI 程序中,某个组件的状态更新方法被 synchronized
修饰,但在整个程序运行过程中,只有一个事件处理线程会调用这个方法,此时偏向锁就能发挥很好的作用。
轻量级锁
适用场景
轻量级锁适用于多个线程交替访问同步块的场景,即线程之间不存在锁竞争,或者锁竞争非常轻微。在这种场景下,使用轻量级锁可以避免重量级锁带来的线程阻塞和唤醒开销,提高程序的性能。
原理与优势
当线程进入同步块时,如果对象头的锁状态为无锁状态,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record),并使用 CAS 操作将对象头的 Mark Word 复制到锁记录中,同时将对象头的 Mark Word 指向锁记录的地址。如果 CAS 操作成功,线程就获得了轻量级锁;如果失败,表示有其他线程已经获得了锁,当前线程会尝试自旋等待锁的释放。轻量级锁的自旋操作避免了线程的阻塞和唤醒,减少了用户态和内核态的切换开销。
示例场景
在一个简单的生产者 - 消费者模型中,生产者线程和消费者线程交替访问共享的缓冲区,它们之间的锁竞争非常轻微,此时轻量级锁可以提供较好的性能。
重量级锁
适用场景
重量级锁适用于多个线程同时竞争同一个锁的场景,即锁竞争非常激烈的情况。当多个线程同时尝试获取锁时,轻量级锁会膨胀为重量级锁。
原理与优势
重量级锁依赖于操作系统的互斥量(Mutex)来实现。当一个线程尝试获取重量级锁时,如果锁已经被其他线程占用,该线程会被阻塞,进入等待队列,直到持有锁的线程释放锁并唤醒等待队列中的其他线程。虽然重量级锁的性能开销较大,但在高并发、锁竞争激烈的场景下,它可以保证线程的安全和正确执行。
示例场景
在一个高并发的 Web 服务器中,多个请求线程同时访问数据库连接池中的连接资源,由于连接资源有限,线程之间的锁竞争非常激烈,此时使用重量级锁可以保证连接资源的安全使用。
综上所述,偏向锁适用于单线程频繁访问同步块的场景,轻量级锁适用于线程交替访问同步块的场景,重量级锁适用于锁竞争激烈的场景。Java 的锁状态会根据实际的并发情况自动进行转换,以提供最佳的性能。
Java中的synchronized关键字和ReentrantLock有什么区别?
在 Java 里,synchronized
关键字和 ReentrantLock
都可用于实现线程同步,不过它们存在诸多区别,下面从多个方面为你详细介绍:
语法与使用方式
-
synchronized
关键字:它属于 Java 内置的关键字,使用起来较为简便。可以直接修饰方法或者代码块,无需手动释放锁,当同步方法或者同步代码块执行结束,锁会自动释放。
class SynchronizedExample {
private int count = 0;
// 同步方法
public synchronized void increment() {
count++;
}
public void anotherMethod() {
// 同步代码块
synchronized (this) {
count++;
}
}
}
-
ReentrantLock
:这是java.util.concurrent.locks
包下的一个类,使用时需手动加锁和解锁。一般要在try
块前调用lock()
方法加锁,在finally
块中调用unlock()
方法释放锁,以此确保锁一定会被释放。
import java.util.concurrent.locks.ReentrantLock;
class ReentrantLockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
锁的获取与释放
-
synchronized
关键字:锁的获取和释放是隐式的,由 JVM 自动处理。当线程进入同步方法或者同步代码块时,会自动获取锁;当线程执行完毕或者抛出异常时,会自动释放锁。 -
ReentrantLock
:锁的获取和释放是显式的,需要手动调用lock()
和unlock()
方法。这种显式的控制方式让代码的可读性和可维护性有所降低,但同时也提供了更高的灵活性。
锁的特性
- 可重入性:两者都具备可重入性,也就是同一个线程可以多次获取同一把锁,而不会出现死锁的情况。当线程第一次获取锁时,锁的计数器会加 1;每重入一次,计数器就再加 1;每释放一次锁,计数器就减 1;当计数器为 0 时,锁才会被真正释放。
-
公平性:
-
synchronized
关键字:属于非公平锁,即线程获取锁的顺序和请求锁的顺序无关,可能后请求的线程反而先获得锁,这样虽然能提高吞吐量,但可能会导致某些线程长时间得不到锁。 -
ReentrantLock
:既可以是公平锁,也可以是非公平锁。在创建ReentrantLock
对象时,可以通过构造函数指定是否为公平锁。公平锁会按照线程请求锁的顺序来分配锁,保证每个线程都有机会获得锁,但会降低一定的吞吐量。
-
import java.util.concurrent.locks.ReentrantLock;
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);
// 创建非公平锁
ReentrantLock nonFairLock = new ReentrantLock(false);
锁的中断与超时
-
synchronized
关键字:当线程获取锁时,如果锁已经被其他线程持有,该线程会一直阻塞,直到获取到锁,并且无法被中断。 -
ReentrantLock
:提供了更多的灵活性,支持锁的中断和超时。可以使用lockInterruptibly()
方法在获取锁的过程中响应中断,使用tryLock(long timeout, TimeUnit unit)
方法在指定的时间内尝试获取锁,如果超时仍未获取到锁,则返回false
。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
class LockInterruptiblyExample {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() throws InterruptedException {
// 可中断地获取锁
lock.lockInterruptibly();
try {
// 执行操作
} finally {
lock.unlock();
}
}
public void tryLockWithTimeout() throws InterruptedException {
// 在指定时间内尝试获取锁
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 执行操作
} finally {
lock.unlock();
}
} else {
// 超时未获取到锁
}
}
}
条件变量
-
synchronized
关键字:配合Object
类的wait()
、notify()
和notifyAll()
方法来实现线程之间的等待 - 通知机制,但这些方法的使用比较复杂,并且只能关联一个条件。 -
ReentrantLock
:通过newCondition()
方法可以创建多个Condition
对象,每个Condition
对象都可以实现独立的等待 - 通知机制,使用起来更加灵活。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class ConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void await() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
}
综上所述,synchronized
关键字使用简单,适合大多数简单的同步场景;而 ReentrantLock
提供了更多的高级特性,如公平锁、锁的中断和超时、条件变量等,适合对同步控制要求较高的复杂场景。
可重入锁和不可重入锁的区别是什么?
可重入锁和不可重入锁是两种不同类型的锁机制,它们在实现和使用场景上有明显的区别,下面从多个方面为你详细介绍:
定义与基本概念
-
可重入锁:也叫递归锁,指的是同一个线程在已经持有该锁的情况下,可以再次获取该锁而不会被阻塞。每次获取锁时,锁的计数器会加 1;每次释放锁时,计数器会减 1。只有当计数器为 0 时,锁才会真正被释放。Java 中的
synchronized
关键字和ReentrantLock
都属于可重入锁。 - 不可重入锁:意味着同一个线程在已经持有该锁的情况下,如果再次尝试获取该锁,会被阻塞,直到锁被释放。这种锁不支持线程对同一把锁的重复获取。
代码示例对比
可重入锁示例
import java.util.concurrent.locks.ReentrantLock;
class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void outerMethod() {
lock.lock();
try {
System.out.println("获取外层锁"