synchronized、Lock、volatile、Condition介绍

本文介绍Java并发编程中的加锁机制,用于防止多线程竞争资源。详细讲解了synchronized、Lock、ReadWriteLock、volatile、Condition等锁机制的特点、使用方法及相关概念,还对它们进行了比较,如volatile与synchronized在阻塞、修饰范围、原子性等方面的差异。

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

https://www.cnblogs.com/yiwangzhibujian/p/6219047.html
多线程怎么防止竞争资源,即防止对同一资源进行并发操作,那就是使用加锁机制。这是Java并发编程中必须要理解的一个知识点。其实使用起来还是比较简单,但是一定要理解。
有几个概念一定要牢记:

  • 加锁必须要有锁
  • 执行完后必须要释放锁
  • 同一时间、同一个锁,只能有一个线程执行

synchronized

synchronized的特点是自动释放锁,作用在方法时自动获取锁,任意对象都可做为锁,它是最常用的加锁机制,可以锁定对象,可以锁定方法,也可以锁定几行代码,如下:

//--------同步方法1
public synchronized void test(){
    //一段代码
}
//--------同步方法2
private Object lock=new Object();
public void test2(){
    synchronized(lock){
        
    }
}

synchronized可以手动指定锁,当作用在方法时会自动获取锁:

  • 作用于普通方法获得当前对象锁,等价于synchronized(this)
  • 作用于静态方法获得类锁,等价于synchronized(类.class)

Lock

Lock的特点是,必须自己创建锁(锁类型已经指定为Lock的实现类,不能使用其它对象),必须自己释放锁。代码结构如下:

Lock l = ...; 
l.lock();
try {
    // 执行代码
} finally {
    l.unlock();
}

注意一定要在finally中释放锁,保证即便抛出异常也可以释放。

ReentrantLock是Lock的一个实现实例
在这里插入图片描述
ReentrantLock(可重入锁),只有一个属性即是否公平。公平的含义是当有多个线程竞争锁时,按先来后到获得锁,但使用公平策略时,对效率有一定的影响。

ReentrantLock() :最常用,获取一个不公平的锁,
ReentrantLock(boolean fair):获取指定公平策略的锁。

加锁与解锁:

void lock() :获取锁,这是最常用的方法。
void unlock() :释放锁,必须要的方法,使用完一定要释放锁。
boolean tryLock() :仅在调用时锁未被另一个线程保持的情况下,才获取该锁。会破坏公平性原则。
boolean tryLock(long timeout, TimeUnit unit) :如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
void lockInterruptibly() :如果当前线程未被中断,则获取锁。

查询当前锁的相关状态:
boolean isLocked() :查询此锁是否由任意线程保持。
boolean isHeldByCurrentThread() :查询当前线程是否保持此锁。
boolean isFair() :如果此锁的公平设置为 true,则返回 true。
int getHoldCount() :查询当前线程保持此锁的次数。
int getQueueLength() :返回正等待获取此锁的线程估计数。
boolean hasQueuedThread(Thread thread) :查询给定线程是否正在等待获取此锁。
boolean hasQueuedThreads() :查询是否有些线程正在等待获取此锁。

Condition相关:
Condition newCondition() :返回用来与此 Lock 实例一起使用的 Condition 实例。
int getWaitQueueLength(Condition condition):返回等待与此锁相关的给定条件的线程估计数。

ReadWriteLock

当有一种情况,一个类中有多个方法需要同步,其中有读有写,如果所有的方法都使用同步,虽然可以保证数据的准确性,但当读取次数远大于写入次数的时候,同步就会对性能产生较大的影响。这时候,就有一种同步策略,读操作和读操作不互斥,读操作和写操作互斥,写操作和写操作互斥,这样可以提供性能。

虽然解释的很通俗但是使用它们还是要考虑以下情况(全部来自jdk api):

  1. 在 writer 释放写入锁时,reader 和 writer 都处于等待状态,在这时要确定是授予读取锁还是授予写入锁。Writer 优先比较普遍,因为预期写入所需的时间较短并且不那么频繁。Reader 优先不太普遍,因为如果 reader 正如预期的那样频繁和持久,那么它将导致对于写入操作来说较长的时延。公平或者“按次序”实现也是有可能的。
  2. 在 reader 处于活动状态而 writer 处于等待状态时,确定是否向请求读取锁的 reader 授予读取锁。Reader 优先会无限期地延迟 writer,而 writer 优先会减少可能的并发。
  3. 确定是否重新进入锁:可以使用带有写入锁的线程重新获取它吗?可以在保持写入锁的同时获取读取锁吗?可以重新进入写入锁本身吗?
  4. 可以将写入锁在不允许其他 writer 干涉的情况下降级为读取锁吗?可以优先于其他等待的 reader 或 writer 将读取锁升级为写入锁吗?

创建ReentrantReadWriteLock:
ReentrantReadWriteLock():创建默认非公平的锁。
ReentrantReadWriteLock(boolean fair):创建指定公平策略的锁。

获得读或者写锁:
readLock() :返回用于读取操作的锁。
writeLock() :返回用于写入操作的锁。

  1. 锁顺序是否可以按读锁或者写锁来优先指定
    不可以,要么是随机的,要么是按照公平策略,优先安排等待时间最长的线程获取它想要的锁。
  2. 什么是锁重入
    允许 reader 和 writer 按照 ReentrantLock 的样式重新获取读取锁或写入锁。在写入线程保持的所有写入锁都已经释放后,才允许重入 reader 使用它们。
    此外,writer 可以获取读取锁,但反过来则不成立。在其他应用程序中,当在调用或回调那些在读取锁状态下执行读取操作的方法期间保持写入锁时,重入很有用。如果 reader 试图获取写入锁,那么将永远不会获得成功。
  3. 什么是锁降级
    重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。

volatile

volatile是一个轻量级的同步机制。用来修饰共享可变变量,对volatile变量的读写操作都是从高速缓存或者主内存中读取。
volatile关键字被称为轻量级锁,能保证可见性(强制刷新在主存,其他线程缓存无效)和有序性(禁止指令重排序)。不同的是原子性方面仅能保证写volatile变量操作的原子性,但没有锁的排他性。而且不会引起上下文的切换。
https://www.cnblogs.com/chenssy/p/6379280.html

Condition

这是由新增Lock类而同时增加的类,毕竟对象的wait和notify方法要在synchronized语句块中,既然现在用Lock了当然要新增一种新的等待唤醒机制了,JDK API已经说得很清楚了:

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length) 
         notFull.await();
       items[putptr] = x; 
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0) 
         notEmpty.await();
       Object x = items[takeptr]; 
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   } 
 }

没有具体的构造方法,通过Lock实现对象来获取Condition对象,Lock有下面的方法:newCondition() :返回用来与此 Lock 实例一起使用的 Condition 实例。

Condition的方法和对象的等待唤醒类似:

void await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
boolean await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
long awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
void awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。
boolean awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
void signal() :唤醒一个等待线程。
void signalAll() :唤醒所有等待线程。
等待变成了await方法,唤醒变成了signal方法。

比较

volatile 不会发生线程阻塞,而 synchronized 会发生线程阻塞。
volatile 只能修饰变量,而 synchronized 可以修饰方法、代码块等。
volatile 不能保证原子性(不能保证线程安全),而 synchronized 可以保证原子性。
volatile 解决的是变量在多线程之间的可见性,而 synchronized 解决的是多线程之间访问资源的同步性。

使用lock时在finally中必须释放锁,不然容易造成线程死锁;而使用synchronized时,获取锁的线程会在执行完同步代码后释放锁(或者JVM会在线程执行发生异常时释放锁)。
使用lock时线程不会一直等待;而使用synchronized时,假设A线程获得锁后阻塞,其他线程会一直等待。
lock可重入、可中断、可公平也可不公平;而synchronized可重入但不可中断、非公平。
https://blog.youkuaiyun.com/u012102104/article/details/79231159

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值