Java并发---synchronized、Lock、Condition的区别

本文探讨Java并发中synchronized关键字与Lock接口的区别,包括ReentrantLock和ReadWriteLock的特性。Lock提供更灵活的锁机制,如显式申请和释放锁,而synchronized可能导致线性等待。此外,Condition作为线程间通信的工具,可以更精确地控制线程唤醒。

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

        synchronized是Java关键字, Lock是Java接口。

      在Android开发中大多数情况下都使用synchronized,例如常用的StringBuffer、HashTable等线程安全的数据结构;但后台开发更多的用Lock。 


1、 synchronized保证任何时刻最多只有一个线程执行被修饰的函数/代码块,不用显示申请锁、释放锁; 缺点是所有等待线程是线性逐个运行的。具体用法参考Java生产者消费者模式

2、 Lock更加灵活, 支持申请锁、释放锁等。

ReentrantLock类实现了Lock接口, 提供了申请锁、释放锁等基本功能;

ReentrantReadWriteLock类实现了ReadWriteLock接口, 提供了读写分离功能,即无写时支持多线程读,只能支持一个线程写。

StampedLock是升级版的读写锁, 写的时候也支持读, 但读时可判断当前是否在写并重读。

     

     下面看Lock接口的定义, 包含申请锁、释放锁等功能。 在申请锁后必须要释放锁, 否则会出现死锁问题。

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
       API文档里介绍了Lock的用法, 先申请锁l.lock(), 得到锁后执行代码逻辑, 最后在finally里释放锁。

lock函数是无返回值的阻塞函数, 如果拿不到锁则阻塞等待, 直到获取到锁后继续向下执行。

 Lock l = ...;
 l.lock();
 try {
   // access the resource protected by this lock
  }finally {
   l.unlock();
 }


tryLock是有返回值的非阻塞函数(能立即返回), 成功拿到锁后返回true, 失败时返回false。

 Lock lock = ...;
 if (lock.tryLock()) {
   try {
     // manipulate protected state
    }finally {
     lock.unlock();
   }
 } else {
   // perform alternative actions
 }}

tryLock(long time, TimeUnit unit)方法和tryLock()方法类似, 区别是如果拿不到锁会等待一段时间, 超时返回false。

void testLock() throws InterruptedException {
        Lock lock = new ReentrantLock();
        if (lock.tryLock(3, TimeUnit.SECONDS)) {  //最多等待3秒
            try {
                // manipulate protected state
            } finally {
                lock.unlock();
            }
        } else {
            // perform alternative actions
        }
    }

lockInteruptibly函数跟lock函数的区别是在其它线程调用当前线程的interupt方法后, 当前线程如果处于阻塞状态能够继续向下执行(效果类似于拿到了锁)。

        Lock lock = new ReentrantLock();
        //如果当前线程阻塞在这里,在其它线程调用当前线程的interupt方法后当前线程继续执行
        lock.lockInterruptibly();   
        try {
            //.....
        }
        finally {
            lock.unlock();
        }
     ReentrantLock类支持了基本的线程互斥操作, 即任何时刻只有一个线程能拿到锁, 缺点是最多一个线程在运行。


 Java为了支持高并发的读写操作, 又提供了读写所ReadWriteLock, 支持多读单写, 即无写时可以多读, 写时不能读,最多一个线程写。

    public interface ReadWriteLock {
        Lock readLock();
        Lock writeLock();
    }
      一个是读锁,一个是写锁。  
public static class Reader implements Runnable {
        private ReentrantReadWriteLock rwl;

        public Reader(ReentrantReadWriteLock lock) {
            this.rwl = lock;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (Exception ex) {

                }
                Lock lock = rwl.readLock();
                lock.lock();
                try {
                    System.out.println(System.currentTimeMillis() + Thread.currentThread().getName() + "正在进行读操作");
                    Thread.sleep(500);
                } catch (Exception ex) {

                } finally {
                    lock.unlock();
                }
            }
        }
    }

    public static class Writer implements Runnable {
        private ReentrantReadWriteLock rwl;

        public Writer(ReentrantReadWriteLock lock) {
            this.rwl = lock;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (Exception ex) {

                }
                Lock lock = rwl.writeLock();
                lock.lock();
                try {
                    System.out.println(System.currentTimeMillis() + Thread.currentThread().getName() + "正在进行写操作");
                    Thread.sleep(500);
                } catch (Exception ex) {

                } finally {
                    lock.unlock();
                }
            }
        }

    }

    public static void main(String[] args) {
        ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        new Thread(new Writer(rwl), "写线程A").start();
        new Thread(new Writer(rwl), "写线程B").start();
        new Thread(new Writer(rwl), "写线程C").start();
        new Thread(new Writer(rwl), "写线程D").start();

        new Thread(new Reader(rwl), "读线程1").start();
        new Thread(new Reader(rwl), "读线程2").start();
        new Thread(new Reader(rwl), "读线程3").start();
        new Thread(new Reader(rwl), "读线程4").start();
    }
结果:

1513073055136写线程A正在进行写操作
1513073055639写线程B正在进行写操作
1513073056139写线程C正在进行写操作
1513073056641写线程D正在进行写操作
1513073057141读线程1正在进行读操作
1513073057142读线程2正在进行读操作
1513073057142读线程3正在进行读操作
1513073057142读线程4正在进行读操作
1513073057647写线程A正在进行写操作
1513073058149写线程B正在进行写操作
1513073058651写线程C正在进行写操作
      可以看出任意时刻只有1个线程可以写, 但是可以有多个线程同时读。

Java8对读写锁进行了改进,新增StampedLock类支持乐观锁, 实现读不阻塞写, 在读的时候如果发生了写操作则重新读。下面代码摘自源码注释,区别是在读函数里调用validate方法判断释放有写操作发生并重新获取读锁。

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();

   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
      finally {
       sl.unlockWrite(stamp);
     }
   }

   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead();
     double currentX = x, currentY = y;
     if (!sl.validate(stamp)) {
        stamp = sl.readLock();
        try {
          currentX = x;
          currentY = y;
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }

   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) {
         long ws = sl.tryConvertToWriteLock(stamp);
         if (ws != 0L) {
           stamp = ws;
           x = newX;
           y = newY;
           break;
         }
         else {
           sl.unlockRead(stamp);
           stamp = sl.writeLock();
         }
       }
     } finally {
       sl.unlock(stamp);
     }
   }
 }}


Condition可以替换Object的wait/notify/notifyAll实现线程间通讯,await替换wait,signal替换notify,signalAll替换notifyAll。

PS: Conditon也是继承于Object类的, 但Object类的wait、notify和notifyAll都是用final修饰的, 所以Condition要新增函数。


JDK的示例代码是单个生产者单个消费者模式, 比起Object的wait/notify/notifyAll(随机唤醒读线程或写线程)的优势在于创建了读Condition和写Condition, 从而可以只唤醒读线程或写线程

     改造一下JDK示例代码使用signalAll(唤醒所有await线程但必须拿到唯一的锁后才能继续运行),将仓库容量给为3个, 使用2个写线程和2个读线程实现生产者消费者模式。

public class Main {
    static class BoundedBuffer {
        final Lock lock = new ReentrantLock();          //锁对象
        final Condition notFull = lock.newCondition();  //写线程锁,仓库满时等待
        final Condition notEmpty = lock.newCondition(); //读线程锁,仓库空时等待

        final Object[] items = new Object[3];//数据仓库,最多容纳3个商品
        int putptr;  //写索引
        int takeptr; //读索引
        int count;   //items记录数

        //写操作,每次生产一个商品
        public void put(Object x, String name) throws InterruptedException {
            lock.lock(); //锁定
            try {
                System.out.println(System.currentTimeMillis() + "--- 写线程" + name+"进入. 剩余商品" + count + "个");
                // 如果仓库满则等待读锁
                while (count == items.length) {
                    System.out.println(System.currentTimeMillis() + "--- 写线程"+name+"等待");
                    notFull.await();  //释放锁并等待signal
                    System.out.println(System.currentTimeMillis() + "--- 写线程"+name+"继续执行");
                }
                //创建商品
                items[putptr] = x;
                if (++putptr == items.length) putptr = 0;
                ++count;  //创建了一个商品

                System.out.println(System.currentTimeMillis() + "--- 写线程" + name+"退出,并signal读线程. 剩余商品" + count + "个");
                //如果读线程等待则唤醒读线程
                notEmpty.signalAll();
            } finally {
                lock.unlock();//释放锁
            }
        }

        //读操作,每次消费一个商品
        public Object take(String name) throws InterruptedException {
            lock.lock(); //获取锁
            try {
                System.out.println(System.currentTimeMillis() + "--- 读线程" + name+"进入. 剩余商品" + count + "个");
                //如果仓库空则等待写线程
                while (count == 0) {
                    System.out.println(System.currentTimeMillis() + "--- 读线程"+name+"等待");
                    notEmpty.await(); //释放锁并等待signal
                    System.out.println(System.currentTimeMillis() + "--- 读线程"+name+"继续执行");
                }

                //读取队列,并更新读索引
                Object x = items[takeptr];
                if (++takeptr == items.length) takeptr = 0;
                --count; //消费了一个商品

                System.out.println(System.currentTimeMillis() + "--- 读线程"+name+"退出,并signal写线程. 剩余商品"+ count + "个");
                //如果写线程等待则唤醒写线程
                notFull.signalAll();
                return x;
            } finally {
                lock.unlock();//解除锁定
            }
        }
    }


    public static void main(String[] args) {
        final BoundedBuffer obj = new BoundedBuffer();

        //写线程A
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        obj.put(new Object(), "A");

                        Thread.sleep(100);
                    } catch (Exception ex) {

                    }
                }
            }
        }).start();

        //写线程B
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        obj.put(new Object(), "B");

                        Thread.sleep(50);
                    } catch (Exception ex) {

                    }
                }
            }
        }).start();

        //读线程1
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        obj.take("1");
                        Thread.sleep(100);
                    } catch (Exception ex) {

                    }
                }
            }
        }).start();

        //读线程2
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        obj.take("2");
                        Thread.sleep(80);
                    } catch (Exception ex) {

                    }
                }
            }
        }).start();
    }
}
运行程序, 任意时刻只能有1个线程拿到锁, 但通过awati/signal可以唤醒读线程或写线程。 即写线程等待时肯定是读线程拿到锁, 读线程等待时肯定是写线程拿到锁。

1513132664275--- 读线程1进入. 剩余商品3个
1513132664275--- 读线程1退出,并signal写线程. 剩余商品2个
1513132664275--- 写线程A继续执行
1513132664275--- 写线程A退出,并signal读线程. 剩余商品3个
1513132664280--- 读线程2进入. 剩余商品3个
1513132664280--- 读线程2退出,并signal写线程. 剩余商品2个
1513132664284--- 写线程B进入. 剩余商品2个
1513132664284--- 写线程B退出,并signal读线程. 剩余商品3个
1513132664337--- 写线程B进入. 剩余商品3个
1513132664337--- 写线程B等待
1513132664364--- 读线程2进入. 剩余商品3个
1513132664364--- 读线程2退出,并signal写线程. 剩余商品2个
1513132664365--- 写线程B继续执行
1513132664365--- 写线程B退出,并signal读线程. 剩余商品3个
1513132664376--- 读线程1进入. 剩余商品3个
1513132664376--- 读线程1退出,并signal写线程. 剩余商品2个
1513132664376--- 写线程A进入. 剩余商品2个
1513132664376--- 写线程A退出,并signal读线程. 剩余商品3个
1513132664419--- 写线程B进入. 剩余商品3个
1513132664419--- 写线程B等待
1513132664448--- 读线程2进入. 剩余商品3个
1513132664448--- 读线程2退出,并signal写线程. 剩余商品2个
1513132664448--- 写线程B继续执行
1513132664448--- 写线程B退出,并signal读线程. 剩余商品3个




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值