java面试常问,了解Java中的锁机制:从概念到实现

814 阅读7分钟

suo.jpg

一、什么是锁?

在Java中,锁是用来控制多线程访问共享资源的机制。通过使用锁,可以确保在同一时刻只有一个线程能够访问共享资源,从而避免并发访问导致的数据不一致性和竞争条件问题。

二、synchronized锁

Java里面的锁主要有synchronized,reentrantLock,Lock三种。

synchronized关键字是Java语言提供的原生锁机制,可以用来修饰方法或代码块,实现对关键代码段的同步访问。当一个线程获取了某个对象的synchronized锁时,其他线程将会被阻塞,直到当前线程释放了该锁。

synchronized是Java中最基本的锁机制,可以用于实现对代码块或方法的同步。它属于独占锁、悲观锁、可重入锁、非公平锁。

  1. 独占锁:独占锁是一种只允许一个线程获取锁的锁机制,也称为排他锁。当一个线程获取了独占锁后,其他线程必须等待该线程释放锁才能获取锁。独占锁可以保证临界区代码的原子性和线程安全性。

  2. 悲观锁:悲观锁是一种假设会发生并发冲突的锁机制,它认为在临界区代码执行期间会有其他线程来竞争锁。因此,在使用悲观锁时,线程会先获取锁,然后再执行临界区代码,以确保数据的一致性和完整性。

  3. 可重入锁:可重入锁是指一个线程可以多次获取同一把锁,而不会出现死锁的情况。在可重入锁中,线程每次获取锁时,会记录获取的次数,只有当释放锁的次数和获取锁的次数相等时,才会真正释放锁。

  4. 非公平锁:非公平锁是一种不保证线程获取锁的顺序的锁机制。在非公平锁中,当有多个线程竞争同一把锁时,锁会随机分配给其中一个线程,而不考虑等待时间或优先级。相对于公平锁,非公平锁可能会导致某些线程长时间无法获取锁,但可以提高系统的吞吐量。

public class SynchronizedExample {
    private int count = 0;

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

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + example.count);
    }
}

在上面的示例中,increment方法使用synchronized关键字修饰,确保了count变量的安全访问。两个线程分别调用increment方法来递增count变量,并最终输出结果为2000。

由于increment()方法使用了synchronized关键字修饰,当一个线程进入increment()方法时,会获取example对象的监视器锁,其他线程必须等待该线程释放锁后才能继续执行。这样可以保证对count变量的操作是线程安全的,避免了多线程并发访问导致的数据不一致问题。

三、ReentrantLock锁

ReentrantLock锁是Java中提供的显示锁实现,具有更灵活的锁定机制。与synchronized相比,ReentrantLock提供了更多的方法和功能,如可中断锁、尝试获取锁、超时获取锁、公平锁等。它继承了Lock接口,属于可重入锁、悲观锁、独占锁、互斥锁、同步锁。

ReentrantLock可以通过lock()方法获取锁,通过unlock()方法释放锁,也支持公平锁和非公平锁的机制。

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

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

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

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + example.count);
    }
}
  1. ReentrantLockExample类定义了一个私有变量count用来存储计数值,并创建了一个ReentrantLock实例lock用于同步访问count。
  2. increment()方法是一个线程安全的方法,通过调用lock.lock()获取锁,然后对count进行自增操作,最后在finally块中释放锁。
  3. 在main方法中,创建了一个ReentrantLockExample实例example。
  4. 创建两个线程t1和t2,分别对example的increment()方法进行1000次递增操作。
  5. 启动线程t1和t2,然后使用t1.join()和t2.join()等待两个线程执行完成。
  6. 最后输出example的count值,即两个线程共同递增后的结果。

通过使用ReentrantLock,可以确保对共享资源的访问是线程安全的,避免了多个线程同时访问导致的数据竞争问题。通过lock和unlock方法来实现对count变量的安全访问。这种方式相比synchronized更为灵活,可以根据具体需求进行更多的控制。

四、Lock接口

Lock是Java中锁的通用接口,定义了锁的基本操作方法,如获取锁、释放锁、获取条件等。ReentrantLock实现了Lock接口,可以替代synchronized关键字进行同步操作。

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

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

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

    public static void main(String[] args) {
        LockExample example = new LockExample();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            }).start();
        }

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + example.count);
    }
}

在这段代码中,我们定义了一个LockExample类,其中包含一个count变量和一个ReentrantLock对象lock。在increment()方法中,我们使用lock.lock()获取锁,对count进行自增操作,然后使用lock.unlock()释放锁。

在main方法中,我们创建了5个线程,每个线程对LockExample对象的increment()方法进行1000次调用。最后输出最终的count值,确保多个线程对count的操作是同步的。

五、Lock和syncronized的区别

1.实现方式

  • synchronized是Java语言的关键字,是在语言层面提供的原生支持,可以直接在方法或代码块上使用。
  • Lock是一个接口,位于java.util.concurrent.locks包下,提供了更加灵活的锁定机制,需要通过其实现类(如ReentrantLock)来使用。

2.获取锁的方式

  • synchronized是隐式获取锁的方式,当一个线程进入synchronized代码块或方法时,会自动获取对象的监视器锁。
  • Lock接口提供了显式获取锁和释放锁的方法,例如lock()和unlock(),需要手动控制获取和释放锁的过程。

3.可中断性

  • synchronized在获取锁时无法被中断,即线程在等待获取锁时无法被中断。
  • Lock接口提供了可以响应中断的锁获取方式,可以在等待锁的过程中响应中断。

4.性能

通常情况下,synchronized的性能会比Lock接口的实现类(如ReentrantLock)要好,因为synchronized是在JVM层面进行优化的。 但是在某些情况下,特别是在高并发环境下,使用Lock接口可能会比synchronized更加高效。

5.可重入性

  • synchronized是可重入的,同一个线程可以多次获取同一个对象的监视器锁。
  • Lock接口的实现类(如ReentrantLock)也是可重入的,同一个线程可以多次获取同一个锁。

6.可重入性灵活性

  • Lock接口提供了更多的灵活性和功能,例如设置超时时间、支持公平锁或非公平锁等,适用于复杂的线程同步需求。
  • synchronized是一种简单且方便的线程同步机制,在一般情况下使用起来更加方便。

六、最后的话

通过合理使用锁机制,可以保证多线程程序的正确性和效率。通过锁机制确保了共享资源的安全访问。在多线程编程中和在实际开发中,应根据具体情况选择合适的锁技术,以确保多线程程序的稳定性和性能。

能力一般,水平有限,本文可能存在纰漏或错误,如有问题欢迎指正,感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!祝生活愉快!