当前位置: 首页>后端>正文

Java 锁机制详解

Java中的锁机制是使用最广泛、最基础的多线程同步技术之一,也是保证线程安全的重要手段。本文将从以下几个方面全面详细地讲解Java中的锁机制:

  1. 锁的概念和作用
  2. synchronized关键字及其使用方法
  3. Java中的锁类型
  4. Lock接口及其实现类
  5. 乐观锁与悲观锁
  6. 锁的性能分析
  7. 锁的注意事项

1. 锁的概念和作用

锁是一种同步机制,可以用来协调多个线程的并发访问,以保证对共享资源的安全访问。

在Java中,锁的作用可以概括为以下两个方面:

  • 保证线程安全:多个线程同时访问共享资源时,锁可以保证线程的有序执行,避免出现竞争条件(race condition)等线程安全问题。

  • 提高性能:通过锁的机制,可以提高多线程并发访问共享资源的效率,充分利用系统资源。

2. synchronized关键字及其使用方法

synchronized是Java中最基本、最常用的锁机制。它可以修饰代码块、方法和静态方法,用来保证多个线程对共享资源的安全访问。

2.1 synchronized代码块

synchronized代码块的使用格式为:

synchronized (object) {
    // 代码块
}

其中,object表示要锁定的对象,被锁定的代码块会在获取到object对象的锁后执行,其他线程需要等待当前线程执行完毕并释放锁之后才能继续执行。

需要注意的是,object对象是一个共享对象,所有需要访问共享资源的线程都必须对该对象进行同步操作才能保证线程安全。

下面是一个使用synchronized代码块实现线程同步的例子:

public class SynchronizedDemo {
    private static int count = 0;
    private static Object lock = new Object();

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

在这个例子中,Runnable接口代表一个可运行对象,它包含了一个需要同步的代码块。在main方法中,创建了两个线程,并启动这两个线程来同时执行同步代码块。在执行完毕后,打印count的值。

由于count是一个共享变量,需要保证对它的访问是线程安全的。使用synchronized代码块可以保证多个线程对count的访问是有序的,并且避免了线程安全问题。

2.2 synchronized方法和静态方法

除了代码块外,synchronized还可以修饰方法和静态方法:

public synchronized void method() {
    // 方法体
}

public static synchronized void staticMethod() {
    // 静态方法体
}

这两种方式都可以用来保证线程安全并且实现多个线程对共享资源的安全访问,与synchronized代码块相比,它们具有更加简洁、易于理解和维护的优势。

需要注意的是,不同线程对于同一个类的不同对象的synchronized方法或静态方法是互不干扰的。也就是说,当一个线程访问某个对象的synchronized方法时,其他线程可以访问该对象的非synchronized方法而不会受到影响;当一个线程访问某个类的synchronized静态方法时,其他线程可以同时访问该类的非静态方法而不会受到影响。但是,当一个线程访问某个对象的synchronized方法时,其他线程无法访问该对象的其他synchronized方法,这是因为在Java中,每个对象只有一个锁,对于同一个对象而言,不同的synchronized方法共用同一个锁。

下面是一个使用synchronized方法实现线程同步的例子:

public class SynchronizedDemo {
    private int count = 0;

    public synchronized void increment() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) {
        final SynchronizedDemo demo = new SynchronizedDemo();

        Runnable runnable = new Runnable() {
            public void run() {
                demo.increment();
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("count: " + demo.getCount());
    }
}

在这个例子中,SynchronizedDemo类包含了两个synchronized方法increment和getCount。在main方法中创建了两个线程,并启动这两个线程来同时执行increment方法,最终打印count的值。

2.3 synchronized的缺点和局限性

synchronized是Java中最基本、最常用的锁机制,但它也存在许多缺点和局限性。

  • 效率问题:使用synchronized会降低系统的效率,特别是在并发比较高的情况下,会导致系统的响应时间变长。

  • 携带信息过多:使用synchronized时需要对共享资源进行加锁和解锁,这些操作会带来很多额外的信息,例如锁申请、锁释放等。

  • 扩展性问题:使用synchronized时需要对每个访问共享资源的地方都进行加锁和解锁处理,这样就会使代码扩展起来很困难。

  • 只能保证互斥访问:synchronized只能保证对共享资源的互斥访问,无法保证对共享资源访问的顺序,例如读写锁是可以保证读取操作的顺序的。

2.4 synchronized的优化

针对synchronized产生的效率问题,Java SE 5引入了两个关键字volatile和JVM提供的重量级锁,可以用来优化synchronized。

  • volatile关键字: volatile关键字是Java中的一种轻量级同步机制,它可以保证对于某个变量的写操作对其他线程可见,在多线程并发访问同一个共享变量时,可以避免读取到过期值的情况。volatile关键字的使用格式为:
public volatile int num = 0;

需要注意的是,volatile并不能替代synchronized来保护共享资源,它只能用来保证可见性。因此在多个线程需要进行写操作时,仍然需要采用synchronized等机制来保证线程安全。

  • JVM提供的重量级锁:JVM提供了一种更加高效的锁实现方式——偏向锁、轻量级锁和重量级锁,其中重量级锁是最常用的一种锁实现。在多线程并发访问共享资源时,JVM会自动地根据具体的情况选择适当的锁实现方式。

3. Java中的锁类型

除了使用synchronized关键字来实现锁机制外,Java中还提供了Lock接口及其实现类,用来实现锁机制。

锁机制在Java中有很多不同的类型,主要包括以下几种:

  • 可重入锁(ReentrantLock):可重入锁是一种递归的互斥锁,即允许单个线程对共享资源进行重复加锁,以保证当前线程对共享资源的访问是安全的。ReentrantLock是Java中实现可重入锁的一种方式,它具有较高的灵活性和可扩展性

  • 读写锁(ReadWriteLock):读写锁是一种共享锁,允许多个线程同时读取共享资源,但只允许一个线程进行写操作。在读多写少的情况下,使用读写锁可以提高系统的并发性能。

  • 信号量(Semaphore):信号量是一种计数器,用来控制同时访问某个共享资源的线程数。在并发访问红绿灯、车位等场景中,使用信号量可以很好地控制访问的并发数,从而避免系统资源的浪费。

  • 倒计时门闩(CountDownLatch):倒计时门闩是一种同步工具,它可以让一个或多个线程等待其他线程的任务执行完毕后再继续执行。适用于分部并发控制的情况,例如多个线程分别处理不同子任务,需要等待所有子任务完成后才能进行下一步操作。

  • 循环屏障(CyclicBarrier):循环屏障是一种同步工具,它可以让多个线程相互等待,直到所有线程都到达某个同步点才继续执行。与倒计时门闩不同的是,循环屏障可以被重复使用,适用于多个线程需要等待其他线程完成某个共同任务的情况。

4. Lock接口及其实现类

除了使用synchronized关键字外,Java中还提供了Lock接口及其实现类,用来实现锁机制。

Lock接口定义了一组与锁相关的操作方法,常用的实现类有ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock等。与synchronized相比,Lock接口及其实现类具有更高的灵活性和可扩展性,可以实现一些synchronized无法实现的功能,例如超时等待、公平锁和非阻塞锁等。

下面是使用ReentrantLock实现锁机制的例子:

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

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            public void run() {
                lock.lock();
                try {
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                } finally {
                    lock.unlock();
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

在这个例子中,LockDemo类包含了一个静态变量count和一个静态锁对象lock。在main方法中,创建了两个线程,并启动这两个线程来同时执行同步代码块。在执行完毕后,打印count的值。

与前面使用synchronized实现的例子相比,使用ReentrantLock实现锁机制更加灵活,可以实现一些复杂的锁操作。

5. 乐观锁与悲观锁

乐观锁和悲观锁是两种不同的锁机制:

  • 悲观锁:在进行并发控制时,假设会有其他线程竞争共享资源并且会导致冲突,因此会将共享资源加锁,以避免其他线程对其进行访问。悲观锁适用于并发读写比较频繁的情况,例如数据库事务并发控制。

  • 乐观锁:在进行并发控制时,假设不会有其他线程竞争共享资源并且不会导致冲突,因此不对共享资源进行加锁,而是在进行更新操作时检查数据版本号等信息,以保证操作的合法性。乐观锁适用于并发读比较频繁而写比较少的情况,例如缓存并发控制。

6. 锁的性能分析

在进行锁机制的设计和使用时,需要考虑锁的性能问题。Java中的锁机制性能表现受到多个因素的影响,例如锁的类型、并发读写比例、锁的粒度等。

一般来说,悲观锁机制由于需要对共享资源进行加锁和解锁,因此会带来额外的性能损失;而乐观锁机制由于不需要进行加锁操作,因此在并发读取时可以获得更高的性能表现。同时,在多个线程同时访问同一个共享资源的情况下,锁的粒度也会对性能产生影响,过细的锁粒度会导致锁竞争变得激烈,从而影响系统的效率。

7. 锁的注意事项

在进行锁机制的设计和使用时,需要注意以下几点:

  • 避免死锁:多个线程互相持有对方所需的锁时,可能会产生死锁问题。为了避免死锁,需要对所有可能的锁顺序进行合理排列,尽量避免出现环路。

  • 避免饥饿:在并发访问共享资源时,可能会出现某个线程一直无法获取到所需的锁的情况,从而导致饥饿问题。为了避免饥饿,需要合理处理各种竞争条件,例如使用公平锁来保证资源的均衡分配。

  • 避免公共锁:在多个线程同时对同一个共享资源进行操作时,如果对该资源使用公共锁,可能会产生性能瓶颈。为了避免公共锁的问题,可以考虑使用细粒度锁或者并发数据结构等方式来实现线程安全。

  • 避免“伪共享”:在多个线程并发访问共享资源时,由于CPU缓存机制的影响,可能会产生“伪共享”问题,即某个线程修改共享资源后,其他线程也需要重新从内存中读取该资源。为了避免“伪共享”问题,可以使用一些技术手段来提高锁的效率,例如使用线程本地缓存(Thread-Local Cache)来减少对共享资源的访问次数、使用避免冲突的数据结构(例如哈希表)等。

另外,在进行锁机制的设计和使用时,还需要注意以下几点:

  • 充分利用硬件特性:现代CPU常常内置了多处理器核心和多级缓存等硬件特性,在多线程并发访问共享资源时,可以利用这些硬件特性来提高锁的效率。

  • 减少锁的竞争:在多线程并发访问共享资源时,尽量减少锁的竞争,避免出现锁的争用现象。可以通过锁分离、分段锁、读写分离等方式来减少锁的竞争。

  • 减小锁的粒度:在多线程并发访问共享资源时,尽量将锁的粒度调整到最小,避免出现锁的过度竞争现象,从而提高锁的效率。

  • 减小锁的持有时间:在进行锁机制的设计和使用时,尽量减小锁的持有时间,避免出现阻塞和死锁等问题,从而提高系统的并发性能。

总之,在进行锁机制的设计和使用时,需要根据具体的业务需求、系统架构和硬件特性等因素来进行合理的选择和优化,以便充分发挥锁的作用,提高系统的稳定性和并发性能。


https://www.xamrdz.com/backend/3wy1994162.html

相关文章: