MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java里synchronized与Lock的特性对比

2022-02-064.7k 阅读

一、Java 同步机制概述

在多线程编程中,确保线程安全是至关重要的。Java 提供了多种同步机制来解决线程并发访问共享资源时可能出现的数据不一致问题。其中,synchronized关键字和Lock接口是两种常用的同步手段。synchronized是 Java 语言内置的同步机制,而Lock是 Java 5.0 引入的更灵活、功能更强大的同步工具。深入理解它们的特性对比,有助于开发者在不同场景下选择最合适的同步方案,从而提高程序的性能和可靠性。

二、synchronized 特性剖析

(一)内置锁与监视器

  1. 原理synchronized关键字基于 Java 的内置锁机制,每个对象都有一个与之关联的监视器(monitor)。当一个线程进入被synchronized修饰的代码块或方法时,它会获取该对象的监视器锁。只有获得锁的线程才能执行同步代码,其他线程则被阻塞,直到锁被释放。
  2. 示例
public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

在上述代码中,increment方法和getCount方法都被synchronized修饰,这意味着它们在执行时会获取SynchronizedExample实例的监视器锁。如果一个线程正在执行increment方法,其他线程调用incrementgetCount方法时,就必须等待锁的释放。

(二)锁的获取与释放

  1. 获取:线程获取锁的过程是自动的。当线程进入同步代码块或方法时,Java 虚拟机(JVM)会自动尝试获取锁。如果锁可用,线程立即获得锁并开始执行同步代码;如果锁不可用,线程进入阻塞状态,等待锁的释放。
  2. 释放:锁的释放也是自动的。当线程执行完同步代码块或方法,或者在同步代码块中抛出未捕获的异常时,JVM 会自动释放锁,允许其他等待的线程竞争获取锁。
  3. 示例
public class SynchronizedBlockExample {
    private Object lock = new Object();
    private int value = 0;

    public void updateValue() {
        synchronized (lock) {
            value++;
        }
    }
}

在这个例子中,updateValue方法通过synchronized (lock)语句块来同步访问value变量。当线程进入该语句块时,会获取lock对象的锁,执行完语句块后,锁会自动释放。

(三)可重入性

  1. 概念synchronized关键字是可重入的。这意味着同一个线程可以多次获取同一个对象的锁。每次获取锁时,锁的持有计数会增加,每次释放锁时,持有计数会减少。当持有计数为 0 时,锁被完全释放。
  2. 示例
public class ReentrantSynchronizedExample {
    public synchronized void outerMethod() {
        System.out.println("进入 outerMethod");
        innerMethod();
    }

    public synchronized void innerMethod() {
        System.out.println("进入 innerMethod");
    }
}

在上述代码中,outerMethodinnerMethod都是同步方法,并且outerMethod调用了innerMethod。由于synchronized的可重入性,同一个线程在调用innerMethod时,不需要再次获取锁,避免了死锁的发生。

(四)公平性

  1. 特性synchronized关键字是非公平的。这意味着在锁可用时,等待队列中的线程并不会按照先来后到的顺序获取锁,而是由 JVM 随机选择一个等待线程获取锁。这种非公平性在一定程度上提高了系统的吞吐量,因为减少了线程切换的开销。
  2. 影响:非公平锁可能导致某些线程长时间等待锁,出现“饥饿”现象。但在大多数情况下,非公平锁的性能优于公平锁,因为公平锁需要额外的开销来维护等待队列的顺序。

(五)锁的类型

  1. 对象锁:当synchronized修饰实例方法或代码块时,使用的是对象锁。每个对象实例都有自己独立的锁,不同对象实例之间的同步操作不会相互影响。
  2. 类锁:当synchronized修饰静态方法时,使用的是类锁。类锁是针对类的,所有该类的实例共享同一个类锁。这意味着当一个线程获取了类锁,其他线程无论是访问该类的静态同步方法还是实例同步方法,都需要等待锁的释放。
  3. 示例
public class SynchronizedLockTypeExample {
    public synchronized void instanceMethod() {
        System.out.println("实例方法同步");
    }

    public static synchronized void staticMethod() {
        System.out.println("静态方法同步");
    }
}

在上述代码中,instanceMethod使用的是对象锁,staticMethod使用的是类锁。

三、Lock 特性剖析

(一)接口与实现类

  1. 接口定义Lock是一个接口,定义了一系列控制锁的方法,如lock()unlock()tryLock()等。它提供了比synchronized更灵活的同步控制。
  2. 实现类:Java 提供了多个Lock接口的实现类,如ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock等。其中,ReentrantLock是最常用的实现类,它具有可重入性,与synchronized类似,但提供了更多的功能。
  3. 示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private Lock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

在上述代码中,使用ReentrantLock来实现同步。lock()方法用于获取锁,unlock()方法用于释放锁,并且为了确保锁的正确释放,通常将unlock()方法放在finally块中。

(二)锁的获取与释放

  1. 获取:与synchronized自动获取锁不同,Lock需要手动调用lock()方法来获取锁。如果锁不可用,调用lock()方法的线程会进入阻塞状态,直到锁可用并被该线程获取。
  2. 释放Lock也需要手动调用unlock()方法来释放锁。这就要求开发者必须确保在合适的时机调用unlock()方法,否则可能导致死锁。为了避免这种情况,通常将unlock()方法放在finally块中,以保证无论同步代码块中是否抛出异常,锁都会被正确释放。
  3. 示例
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ManualLockUnlockExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void awaitMethod() {
        lock.lock();
        try {
            System.out.println("等待条件");
            condition.await();
            System.out.println("条件满足,继续执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signalMethod() {
        lock.lock();
        try {
            System.out.println("发出信号");
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

在这个例子中,awaitMethodsignalMethod方法展示了如何使用LockCondition进行线程间的协作,同时也体现了手动获取和释放锁的过程。

(三)可重入性

  1. 概念ReentrantLock同样具有可重入性。与synchronized类似,同一个线程可以多次获取同一个ReentrantLock实例的锁,每次获取锁时,锁的持有计数会增加,每次释放锁时,持有计数会减少。
  2. 示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private Lock lock = new ReentrantLock();

    public void outerMethod() {
        lock.lock();
        try {
            System.out.println("进入 outerMethod");
            innerMethod();
        } finally {
            lock.unlock();
        }
    }

    public void innerMethod() {
        lock.lock();
        try {
            System.out.println("进入 innerMethod");
        } finally {
            lock.unlock();
        }
    }
}

在上述代码中,outerMethod调用innerMethod,由于ReentrantLock的可重入性,同一个线程可以多次获取锁,不会导致死锁。

(四)公平性

  1. 特性ReentrantLock可以通过构造函数来选择是否为公平锁。默认情况下,ReentrantLock是非公平的,但如果在构造ReentrantLock实例时传入true,则可以创建一个公平锁。公平锁会按照线程等待的先后顺序来分配锁,确保先等待的线程先获得锁。
  2. 示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class FairReentrantLockExample {
    private Lock lock = new ReentrantLock(true);

    public void fairMethod() {
        lock.lock();
        try {
            System.out.println("执行公平方法");
        } finally {
            lock.unlock();
        }
    }
}

在上述代码中,通过new ReentrantLock(true)创建了一个公平锁。公平锁虽然能避免线程“饥饿”现象,但由于需要维护等待队列,性能相对非公平锁会有所降低。

(五)锁中断

  1. 特性Lock接口提供了lockInterruptibly()方法,允许在获取锁的过程中响应中断。当一个线程调用lockInterruptibly()方法获取锁时,如果在等待锁的过程中被其他线程中断,该线程会抛出InterruptedException异常,并停止等待锁。
  2. 示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockInterruptExample {
    private Lock lock = new ReentrantLock();

    public void interruptedMethod() {
        try {
            lock.lockInterruptibly();
            try {
                System.out.println("获取到锁,执行任务");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println("线程被中断");
            }
        } catch (InterruptedException e) {
            System.out.println("获取锁时被中断");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

在上述代码中,interruptedMethod方法通过lockInterruptibly()方法获取锁。如果在获取锁的过程中线程被中断,会捕获InterruptedException异常并进行相应处理。

(六)尝试获取锁

  1. 特性Lock接口提供了tryLock()tryLock(long timeout, TimeUnit unit)方法,允许线程尝试获取锁。tryLock()方法会立即返回,如果锁可用则获取锁并返回true,否则返回falsetryLock(long timeout, TimeUnit unit)方法则会在指定的时间内尝试获取锁,如果在指定时间内获取到锁则返回true,否则返回false
  2. 示例
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockExample {
    private Lock lock = new ReentrantLock();

    public void tryLockMethod() {
        if (lock.tryLock()) {
            try {
                System.out.println("成功获取锁");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("获取锁失败");
        }
    }

    public void tryLockWithTimeoutMethod() {
        try {
            if (lock.tryLock(2, TimeUnit.SECONDS)) {
                try {
                    System.out.println("在 2 秒内成功获取锁");
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println("2 秒内获取锁失败");
            }
        } catch (InterruptedException e) {
            System.out.println("线程被中断");
        }
    }
}

在上述代码中,tryLockMethod方法使用tryLock()方法尝试获取锁,tryLockWithTimeoutMethod方法使用tryLock(long timeout, TimeUnit unit)方法在 2 秒内尝试获取锁。

(七)条件变量(Condition)

  1. 概念Lock接口通过newCondition()方法可以创建一个或多个Condition对象,Condition对象提供了更灵活的线程间通信机制,类似于Object类的wait()notify()notifyAll()方法,但功能更强大。
  2. 示例
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean flag = false;

    public void producer() {
        lock.lock();
        try {
            while (flag) {
                condition.await();
            }
            flag = true;
            System.out.println("生产者生产数据");
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void consumer() {
        lock.lock();
        try {
            while (!flag) {
                condition.await();
            }
            flag = false;
            System.out.println("消费者消费数据");
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

在上述代码中,producer方法和consumer方法通过Condition对象实现了线程间的协作。await()方法用于使线程等待条件满足,signalAll()方法用于唤醒所有等待在该Condition上的线程。

四、synchronizedLock 特性对比总结

(一)功能特性对比

  1. 锁的获取与释放synchronized自动获取和释放锁,代码简洁,但缺乏灵活性;Lock需要手动获取和释放锁,开发者需要更加小心,但可以实现更复杂的同步逻辑。
  2. 可重入性:两者都具有可重入性,避免了同一线程多次获取锁导致的死锁问题。
  3. 公平性synchronized是非公平的,能提高系统吞吐量;ReentrantLock可以选择公平或非公平,公平锁能避免线程“饥饿”,但性能相对较低。
  4. 锁中断synchronized在获取锁时无法响应中断,而Lock通过lockInterruptibly()方法可以在获取锁时响应中断,提供了更灵活的线程控制。
  5. 尝试获取锁synchronized没有提供尝试获取锁的机制,而Lock通过tryLock()tryLock(long timeout, TimeUnit unit)方法可以尝试获取锁,增加了程序的灵活性。
  6. 条件变量synchronized通过Object类的wait()notify()notifyAll()方法实现线程间通信,功能相对有限;Lock通过Condition对象提供了更灵活、更强大的线程间通信机制。

(二)性能特性对比

  1. 在竞争不激烈时synchronized的性能与Lock相近,因为JVMsynchronized进行了很多优化,如偏向锁、轻量级锁等。
  2. 在竞争激烈时ReentrantLock的非公平锁性能通常优于synchronized,因为synchronized的非公平性在竞争激烈时可能导致某些线程长时间等待,而ReentrantLock的非公平锁能更快地将锁分配给等待线程。但如果使用公平锁,ReentrantLock的性能会因为维护等待队列而下降。

(三)使用场景对比

  1. 简单同步场景:如果同步逻辑比较简单,使用synchronized关键字能使代码更简洁,因为它自动管理锁的获取和释放,不需要手动编写复杂的代码。
  2. 复杂同步场景:当需要更灵活的同步控制,如可中断的锁获取、尝试获取锁、公平锁或更复杂的线程间通信时,Lock接口及其实现类能提供更强大的功能,满足复杂的需求。

综上所述,synchronizedLock各有优缺点,开发者在选择同步机制时,应根据具体的应用场景和需求来决定。在简单场景下,优先考虑synchronized;在复杂场景下,充分利用Lock的特性来实现高效、可靠的多线程程序。