Java多线程基础面试总结(四)

Lock锁初步介绍

这里只是初步介绍,在后续文章中我会详细介绍,后续文章完成后我会将连接更新到这里。

JDK1.5后新增功能,与采用synchronized相比,lock可提供多种锁方案,更灵活

Lock和syncronized的区别

  1. synchronized是Java语言的关键字,Lock是一个接口。
  2. synchronized不需要用户去手动释放锁,发生异常或者线程结束时自动释放锁;Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
  3. Lock可以配置公平策略,实现线程按照先后顺序获取锁。提供了trylock()方法 可以试图获取锁,获取到或获取不到时,返回不同的返回值让程序可以灵活处理。
  4. lock()和unlock()可以在不同的方法中执行,可以实现同一个线程在上一个方法中lock()在后续的其他方法中unlock(),比syncronized灵活的多。
  5. Lock只有代码块锁,synchronized有代码块锁和方法锁。
  6. Lock锁可以对读不加锁,对写加锁,synchronized不可以。
  7. Lock锁可以有多种获取锁的方式,可以从sleep的线程中抢到锁,synchronized不可以。
  8. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好;并且具有更好的扩展性(提供更多的子类)。

详细介绍 Lock和syncronized的区别

Java中的Lock和synchronized都是用于实现线程同步的机制,但是它们之间有以下几个区别:

  1. 锁的获取和释放方式不同
    synchronized关键字是隐式锁,在进入同步代码块或方法时自动获取锁,在退出同步代码块或方法时自动释放锁。而Lock是显式锁,需要手动获取锁和释放锁,分别通过lock()和unlock()方法实现。

  2. 锁的粒度不同
    synchronized关键字只能锁住整个方法或代码块,而不能对其中的一部分代码进行加锁。而Lock可以灵活地控制锁的粒度,可以只锁住代码中的一部分。例如,一个List中的某个元素需要更新,使用synchronized需要锁住整个List,而使用ReentrantLock可以只锁住该元素。

  3. 锁的可中断性不同
    使用synchronized时,如果一个线程正在执行同步代码块或方法,其他线程只能等待或阻塞,不能中断。而Lock提供了可中断的获取锁方式,即tryLock()和lockInterruptibly()方法,如果一个线程正在执行Lock的lockInterruptibly()方法,其他线程可以通过调用该线程的interrupt()方法来中断它。

  4. 锁的公平性不同
    synchronized关键字不保证线程的执行顺序,如果有多个线程在等待锁,当锁被释放后,哪个线程能够获取到锁是不确定的。而Lock提供了可重入锁(ReentrantLock)和公平锁(FairLock)两种方式,公平锁会按照线程的等待时间来获取锁,保证了线程的公平性,但是性能比可重入锁差。

  5. 锁的性能不同
    在竞争不激烈的情况下,synchronized的性能优于Lock,因为synchronized是由JVM内部实现的,而Lock需要通过Java代码实现。但是在竞争激烈的情况下,Lock的性能优于synchronized,因为它提供了更多的锁获取和释放方法,可以减少线程上下文切换的次数。
    总之,synchronized和Lock都可以用于实现线程同步,但是Lock提供了更多的控制和扩展性,适用于更复杂的多线程场景。但是,在并发度不高的情况下,synchronized可以提供更简单、更安全、更高效的同步机制。

代码案例(第2点)

下面是使用synchronized关键字实现的线程安全的List的例子,它使用synchronized关键字锁住整个方法:

import java.util.ArrayList;
import java.util.List;
public class SynchronizedList<E> {
    private List<E> list = new ArrayList<>();
    public synchronized boolean add(E element) {
        return list.add(element);
    }
    public synchronized E get(int index) {
        return list.get(index);
    }
    public synchronized int size() {
        return list.size();
    }
}

在上面的例子中,我们使用synchronized关键字锁住了add()、get()和size()方法,保证了它们的线程安全性。
下面是使用ReentrantLock实现的线程安全的List的例子,它使用ReentrantLock锁住List中的某个元素:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockedList<E> {
    private List<E> list = new ArrayList<>();
    private Lock lock = new ReentrantLock();
    public boolean add(E element) {
        lock.lock();
        try {
            return list.add(element);
        } finally {
            lock.unlock();
        }
    }
    public E get(int index) {
        lock.lock();
        try {
            return list.get(index);
        } finally {
            lock.unlock();
        }
    }
    public int size() {
        lock.lock();
        try {
            return list.size();
        } finally {
            lock.unlock();
        }
    }
    public void update(int index, E element) {
        lock.lock();
        try {
            list.set(index, element);
        } finally {
            lock.unlock();
        }
    }
}

在上面的例子中,我们使用ReentrantLock锁住了add()、get()、size()和update()方法,其中update()方法只锁住了List中的某个元素,而不是整个List。使用ReentrantLock可以灵活地控制锁的粒度,提高代码的并发性能。

代码案例(第3点)

下面是一个使用ReentrantLock实现的可中断的线程安全队列的例子,使用lockInterruptibly()方法来获取锁。如果一个线程正在执行put()方
下面是一个简单的Java代码例子,演示了Lock的lockInterruptibly()方法如何在其他线程调用interrupt()方法时被中断:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

    public void doSomething() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            // 执行一些需要锁保护的操作
            // ...
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        InterruptibleLockExample example = new InterruptibleLockExample();

        Thread thread1 = new Thread(() -> {
            try {
                example.doSomething();
            } catch (InterruptedException e) {
                System.out.println("Thread 1 is interrupted");
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                example.doSomething();
            } catch (InterruptedException e) {
                System.out.println("Thread 2 is interrupted");
            }
        });

        thread1.start();
        Thread.sleep(1000); // 确保thread1先获得锁
        thread2.start();
        Thread.sleep(1000); // 确保线程都进入等待状态
        thread2.interrupt(); // 中断thread2
    }
}

在上面的例子中,InterruptibleLockExample类有一个lock成员变量,该变量使用ReentrantLock实现。doSomething()方法获取锁,执行一些需要锁保护的操作,最后释放锁。这个方法可以响应中断,即其他线程可以通过调用Thread.interrupt()方法来中断它。

在main()方法中,创建了两个线程thread1和thread2,并分别启动它们。首先让thread1获取锁,然后等待1秒钟,再让thread2去获取锁。由于lock是不可重入锁,因此thread2会阻塞,一直等到thread1释放锁才能获取锁。

最后,等待1秒钟,确保线程都进入等待状态,然后调用thread2.interrupt()方法来中断thread2。由于thread2正在执行lockInterruptibly()方法,因此它可以被中断,并且会抛出InterruptedException异常。在catch块中,输出"Thread 2 is interrupted"的提示信息。

这里的 确保线程都进入等待状态 是什么意思?

在上面的Java代码例子中,通过调用Thread.sleep()方法来让程序等待一段时间。这个等待时间的目的是确保两个线程都进入了等待状态,即:

  1. 线程1已经获得了锁,并正在执行doSomething()方法。
  2. 线程2尝试获取锁,但由于锁被线程1持有,因此处于等待状态。

在不等待一段时间,直接调用thread2.interrupt()方法可能会导致线程2还没有进入等待状态就被中断,从而无法测试lockInterruptibly()方法响应中断的情况。

综上可以得出,使用 lockInterruptibly()方法 获取锁最大的好处就是可以调用对应线程的 interrupt() 方法,如 thread2.interrupt(),避免该线程一直等待。

你可能感兴趣的:(#,Java面试总结,java,面试,jvm)