Java并发编程 之 Synchronized 和 ReentrantLock 区别

本文深入探讨Java中的并发编程,详细解析synchronized关键字与ReentrantLock的底层原理及使用技巧,对比两者优劣,讲解如何利用Condition实现精确的线程间通信,以及JVM对锁的优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Synchronized

Synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
在 Java 中,每个对象都会有一个 monitor 对象。
通过monitor entermonitor exit两个指令实现原子性。

// 普通同步方法,锁是当前实例对象。
public synchronized void add(){
  
}
// 静态同步方法,锁是当前类的Class对象。
public static synchronized void add(){
 
}
// 同步方法块
public int add(){
  // 锁是obj对象。
  synchronized(obj){
  }
  // 锁是当前实例对象。
  synchronized(this){
  }
}

使用Synchronized有哪些要注意点

  • 锁对象不能为空,因为锁的信息都保存在对象头里
  • 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
  • 避免死锁
  • 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错

Synchronized关键字的底层原理是什么

synchronized是用来做线程同步的,可以对类和对象进行加锁,是跟jvm指令和monitor有关系,如果用synchronized关键字修饰一段代码块后,编译后,会有monitorenter和monitorexit两个指令,执行被修饰的代码块的时候是会先执行monitorenter指令,退出方法后会执行monitorexit指令。
执行过程就是每个对象都会关联一个monitor,它里面有一个计数器,初始是0,如果有线程要获取monitor的锁,会先看计数器是不是0,如果是0说明没有线程在获取锁,那这个线程就可以获取lock锁,然后对计数器加1。如果不为0,判断线程是不是自己,是的话就重入锁,计数器+1,如果不是的话线程会陷入block阻塞状态。线程执行完代码退出后,底层会执行monitorexit指令,这个时候对应的monitor计数器会减1,如果是多次重入锁,那么就会多次减1,直到为0,然后block状态的线程可以再次获取锁。
在这里插入图片描述

ReentrantLock

Java中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,支持公平锁实现,支持中断响应、限时等待等等,可以配合一个或多个Condition条件方便的实现等待通知机制,使用起来更为灵活,也更适合复杂的并发场景。

ReentrantLock结合Condition实现等待通知机制

使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。
ReentrantLock结合Condition接口同样可以实现这个功能,而且相比前者使用起来更清晰也更简单。

Condition使用简介

Condition由ReentrantLock对象创建,并且可以同时创建多个。

static Condition notEmpty = lock.newCondition();
static Condition notFull = lock.newCondition();

Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()方法将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程。使用方式和wait/notify类似。

一个使用condition的简单例子

public class ReentrantLocSimpleConditionDemo {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "\t await");
                // 释放锁并等待其他线程的通知
                condition.await();
                System.out.println(Thread.currentThread().getName() + "\t execute");
            } catch (Exception e) {
            } finally {
                lock.unlock();
            }
        }, "AA").start();

        new Thread(() -> {
            try {
                // 保证线程 AA 先执行
                TimeUnit.SECONDS.sleep((long) new Random().nextInt(5) + 1);
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "\t signal");
                condition.signal();
            } catch (Exception e) {
            } finally {
                lock.unlock();
            }
        }, "BB").start();

    }
}

打印:

AA	 await
BB	 signal
AA	 execute

使用Condition实现简单的阻塞队列

// ReentrantLock 使用 Condition 案例,简单模拟了 Queue
class MyQueue {

    // 容器初始值
    private Integer data = 1;
    // 容器最大值
    private Integer max = 3;
    private Lock lock = new ReentrantLock();
    // 容器满时 等待
    private Condition fullCondition = lock.newCondition();
    // 容器空时 等待
    private Condition emptyCondition = lock.newCondition();

    public void put() {
        lock.lock();
        try {
            while (data == max) {
                // 容器已满,等待
                System.out.println(Thread.currentThread().getName() + "\t put await");
                fullCondition.await();
            }
            data++;
            System.out.println(Thread.currentThread().getName() + "\t put:" + data);
            // 容器不是空的,唤醒一个取数据线程
            emptyCondition.signal();
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }

    public void take() {
        lock.lock();
        try {
            while (data == 0) {
                // 容器空的,等待
                System.out.println(Thread.currentThread().getName() + "\t take await");
                emptyCondition.await();
            }
            data--;
            System.out.println(Thread.currentThread().getName() + "\t take:" + data);
            // 容器不是满的,唤醒一个入数据线程
            fullCondition.signal();
        } catch (Exception e) {

        } finally {
            lock.unlock();
        }
    }

}

public class ReentrantLockConditionDemo {

    public static void main(String[] args) {
        MyQueue queue = new MyQueue();
        for (int i = 1; i <= 15; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep((long) new Random().nextInt(5) + 1);
                } catch (InterruptedException e) {

                }
                queue.put();
            }, "AA").start();
        }

        for (int i = 1; i <= 15; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep((long) new Random().nextInt(5) + 1);
                } catch (InterruptedException e) {

                }
                queue.take();
            }, "BB").start();
        }
    }
}

使用 Condition 精确唤醒案例

/**
 * 多线程之间按顺序调用,实现 A - B - C 三个线程启动。
 * AA 打印5次,BB打印10次,CC打印15次
 * 接着。。。
 * AA 打印5次,BB打印10次,CC打印15次
 * 来10轮
 */
class ShareResource {
    private int number = 1;
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void print5() {
        lock.lock();
        try {
            // 判断
            while (number != 1) {
                c1.await();
            }
            // 干活
            number = 2;
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 唤醒
            c2.signal();
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }
    }

    public void print10() {
        lock.lock();
        try {
            // 判断
            while (number != 2) {
                c2.await();
            }
            // 干活
            number = 3;
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 唤醒
            c3.signal();
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }
    }

    public void print15() {
        lock.lock();
        try {
            // 判断
            while (number != 3) {
                c3.await();
            }
            // 干活
            number = 1;
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 唤醒
            c1.signal();
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }
    }
}

public class ReentrantLockConditionDemo {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                shareResource.print5();
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                shareResource.print10();
            }
        }, "BB").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                shareResource.print15();
            }
        }, "CC").start();
    }
}

深入理解ArrayBlockingQueue:
ArrayBlockingQueue 是一个先进先出的数据有界队列。
java.util.concurrent.ArrayBlockingQueue<E>:定义在Java并发包中的,所以是支持多线程安全的,内部使用ReentrantLock实现线程安全,使用Condition线程间通信。
源码解析:

final ReentrantLock lock = new ReentrantLock(true); // 所有的入队出队都是用同一锁,使用公平锁
private final Condition notEmpty = lock.newCondition();//由lock创键的Condition notEmpty非空即当取出元素时使用
private final Condition notFull = lock.newCondition(); // 同样由lock创建,添加时使用

public void put(E e) throws InterruptedException {
     checkNotNull(e);
     final ReentrantLock lock = this.lock;
     lock.lockInterruptibly();  // 此时采用优先响应中断的加锁方式
     try {
         while (count == items.length)
             notFull.await();  // 如果队列已满,添加线程等待
         insert(e);  // 否则调用insert方法
     } finally {
         lock.unlock();
     }
 }

private void insert(E x) {
    items[putIndex] = x; 
    putIndex = inc(putIndex); // 添加后 坐标加1
    ++count;
    notEmpty.signal(); // 取出线程被唤醒
}

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await(); // 当队列为空时,阻塞等待
        return extract();
    } finally {
        lock.unlock();
    }
}

private E extract() {
    final Object[] items = this.items;
    E x = this.<E>cast(items[takeIndex]);
    items[takeIndex] = null; //取出时,将该位置置为null
    takeIndex = inc(takeIndex); //取出坐标加一
    --count;
    notFull.signal(); //唤醒添加线程
    return x;
}

ReentrantLock和synchronized区别

  • 锁的实现: synchronized是关键字依赖JVM实现的,底层是通过monitor对象来完成。(monitorenter、monitorexitReenTrantLock是具体类 java.util.concurrent.locks.LockJDK实现的,类似于操作系统控制实现和用户自己写代码实现。

  • 可重入性: ReenTrantLocksynchronized使用的锁都是可重入的,两者都是同一个线程每进入一次,锁的计数器都自增1,所以等到锁的计数器下降为0时才能释放锁。

  • 性能的区别: synchronizedJDK5优化后,两者性能差不多了。如果两种方法都可以使用的情况下,官方建议使用synchronized,因为使用便利。

  • 使用方法: synchronized由编译器加锁和释放,默认是非公平锁,ReenTrantLock手动加锁和释放锁。

  • ReenTrantLock独有的能力:
    ReenTrantLock可以指定是公平锁还是非公平锁,synchronized只能是非公平锁,所谓的公平锁就是先等待的线程先获取锁。
    ReenTrantLock提供了中断锁和等待锁的功能,通过lock.lockInterruptibly()实现中断锁,通过lock.tryLock()实现等待锁。
    ReenTrantLock提供了一个Condition类,实现了线程之间的精确唤醒;而不是像 synchronized 只能通过 wait/notify 唤醒,要么随机唤醒一个线程,要么唤醒全部线程。

  • 什么情况下使用ReenTrantLock
    如果应用中需要有排队功能,比如客服分配,必须先到先得服务,不能出现饿死现象,可以使用ReenTrantLock的公平锁,new ReenTrantLock(true)表示公平锁。
    如果需要精确唤醒某个线程执行任务,比如:ArrayBlockingQueue 阻塞队列,底层是通过 ReenTrantLock+Condition进行通信的。

JVM中锁的优化

在JVM中monitorentermonitorexit字节码依赖于底层的操作系统的Mutex Lock(Mutex 采取独占方式控制对资源的并发访问)来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;

在JDK1.6中对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁等技术来减少锁操作的开销。已经和ReenTrantLock性能相差无几了。

线程并发量不大的情况下,Synchronized因为自旋锁、偏向锁、轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋。

只要线程可以在30到50次自旋里拿到锁,那么Synchronized就不会升级为重量级锁,而等待的线程也就不用被挂起,我们也就少了挂起和唤醒这个上下文切换的过程开销。

在 JDK1.6 之前synchronized被称为重量级锁,这个重指的是它会阻塞和唤醒线程。而1.6之后,对synchonized进行了优化。加入了偏向锁以及轻量级锁。在使用synchronized的过程中,随着竞争的加剧,锁会经历由:无锁->偏向锁->轻量级锁->重量级锁的升级过程。

为什么synchronized要设计一个升级过程呢?
答:阻塞和唤醒线程带来的上下文切换的开销,往往比线程执行的开销要大得多。为了避免这种开销,尽量减少线程阻塞和唤醒的次数。

无锁、偏向锁、轻量级锁、重量级锁

无锁(自旋锁)

自旋锁其实就是无锁。
无锁是指线程通过循环来执行更新操作,如果执行成功就退出循环,如果执行失败(有其他线程更新了值),则继续执行,直到成功为止。
CAS操作就属于无锁。如果从性能的角度来看,无锁状态的性能是非常高的。
自旋锁的前提假设是锁被其它线程占用的时间很短。如果其它线程占用锁的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而带来性能上的浪费。
自旋次数的默认值是10次,用户可以通过使用参数-XX:PreBlockSpin来更改。

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

重量级锁

线程阻塞,响应时间缓慢。

参考:
ReentrantLock(重入锁)功能详解和应用演示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值