和朱晔一起复习Java并发(三):锁(含锁性能测试)

这个专题我发现怎么慢慢演化为性能测试了,遇到任何东西我就忍不住去测一把。本文我们会大概看一下各种锁数据结构的简单用法,顺便也会来比拼一下性能。

各种并发锁

首先,我们定一个抽象基类,用于各种锁测试的一些公共代码:

  • 我们需要使用锁来保护counter和hashMap这2个资源
  • write字段表示这个线程是执行写操作还是读操作
  • 每一个线程都会执行loopCount次读或写操作
  • start的CountDownLatch用于等待所有线程一起执行
  • finish的CountDownLatch用于让主线程等待所有线程都完成
@Slf4j
abstract class LockTask implements Runnable {
    protected volatile static long counter;
    protected boolean write;
    protected static HashMap<Long, String> hashMap = new HashMap<>();
    int loopCount;
    CountDownLatch start;
    CountDownLatch finish;

    public LockTask(Boolean write) {
        this.write = write;
    }

    @Override
    public void run() {
        try {
            start.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < loopCount; i++) {
            doTask();
        }
        finish.countDown();
    }

    abstract protected void doTask();
}

下面我们实现最简单的使用synchronized来实现的锁,拿到锁后我们针对hashMap和counter做一下最简单的操作:

@Slf4j
class SyncTask extends LockTask {
    private static Object locker = new Object();

    public SyncTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        synchronized (locker) {
            if (write) {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } else {
                hashMap.get(counter);
                //log.debug("{}, {}", this.getClass().getSimpleName(), value);
            }
        }
    }
}

然后是ReentrantLock,使用也是很简单,需要在finally中释放锁:

@Slf4j
class ReentrantLockTask extends LockTask {
    private static ReentrantLock locker = new ReentrantLock();

    public ReentrantLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        locker.lock();
        try {
            if (write) {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } else {
                hashMap.get(counter);
            }
        } finally {
            locker.unlock();
        }
    }
}

然后是ReentrantReadWriteLock,可重入的读写锁,这屋里我们需要区分读操作还是写操作来获得不同类型的锁:

@Slf4j
class ReentrantReadWriteLockTask extends LockTask {
    private static ReentrantReadWriteLock locker = new ReentrantReadWriteLock();

    public ReentrantReadWriteLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        if (write) {
            locker.writeLock().lock();
            try {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } finally {
                locker.writeLock().unlock();
            }
        } else {
            locker.readLock().lock();
            try {
                hashMap.get(counter);
            } finally {
                locker.readLock().unlock();
            }
        }
    }
}

然后是可重入锁和可重入读写锁的公平版本:

@Slf4j
class FairReentrantLockTask extends LockTask {
    private static ReentrantLock locker = new ReentrantLock(true);

    public FairReentrantLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        locker.lock();
        try {
            if (write) {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } else {
                hashMap.get(counter);
            }
        } finally {
            locker.unlock();
        }
    }
}

@Slf4j
class FairReentrantReadWriteLockTask extends LockTask {
    private static ReentrantReadWriteLock locker = new ReentrantReadWriteLock(true);

    public FairReentrantReadWriteLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        if (write) {
            locker.writeLock().lock();
            try {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } finally {
                locker.writeLock().unlock();
            }
        } else {
            locker.readLock().lock();
            try {
                hashMap.get(counter);
            } finally {
                locker.readLock().unlock();
            }
        }
    }
}

最后是1.8推出的StampedLock:

@Slf4j
class StampedLockTask extends LockTask {
    private static StampedLock locker = new StampedLock();

    public StampedLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        if (write) {
            long stamp = locker.writeLock();
            try {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } finally {
                locker.unlockWrite(stamp);
            }
        } else {
            long stamp = locker.tryOptimisticRead();
            long value = counter;

            if (!locker.validate(stamp)) {
                stamp = locker.readLock();
                try {
                    value = counter;
                } finally {
                    locker.unlockRead(stamp);
                }
            }
            hashMap.get(value);
        }
    }
}

这里同样区分读写锁,只是读锁我们先尝试进行乐观读,拿到一个戳后读取我们需要保护的数据,随后校验一下这个戳如果没问题的话说明数据没有改变,乐观锁生效,如果有问题升级为悲观锁再读取一次。因为StampedLock很复杂很容易用错,真的打算用的话务必研读官网的各种锁升级的例子(乐观读到读,乐观读到写,读到写)。

性能测试和分析

同样我们定义性能测试的类型:

@ToString
@RequiredArgsConstructor
class TestCase {
    final Class lockTaskClass;
    final int writerThreadCount;
    final int readerThreadCount;
    long duration;
}

每一种测试可以灵活选择:

  • 测试的锁类型
  • 写线程数量
  • 读线程数量
  • 最后测试结果回写到duration

下面是性能测试的场景定义:

 @Test
public void test() throws Exception {
    List<TestCase> testCases = new ArrayList<>();

    Arrays.asList(SyncTask.class,
            ReentrantLockTask.class,
            FairReentrantLockTask.class,
            ReentrantReadWriteLockTask.class,
            FairReentrantReadWriteLockTask.class,
            StampedLockTask.class
    ).forEach(syncTaskClass -> {
        testCases.add(new TestCase(syncTaskClass, 1, 0));
        testCases.add(new TestCase(syncTaskClass, 10, 0));
        testCases.add(new TestCase(syncTaskClass, 0, 1));
        testCases.add(new TestCase(syncTaskClass, 0, 10));

        testCases.add(new TestCase(syncTaskClass, 1, 1));
        testCases.add(new TestCase(syncTaskClass, 10, 10));
        testCases.add(new TestCase(syncTaskClass, 50, 50));
        testCases.add(new TestCase(syncTaskClass, 100, 100));
        testCases.add(new TestCase(syncTaskClass, 500, 500));
        testCases.add(new TestCase(syncTaskClass, 1000, 1000));

        testCases.add(new TestCase(syncTaskClass, 1, 10));
        testCases.add(new TestCase(syncTaskClass, 10, 100));
        testCases.add(new TestCase(syncTaskClass, 10, 200));
        testCases.add(new TestCase(syncTaskClass, 10, 500));
        testCases.add(new TestCase(syncTaskClass, 10, 1000));

        testCases.add(new TestCase(syncTaskClass, 10, 1));
        testCases.add(new TestCase(syncTaskClass, 100, 10));
        testCases.add(new TestCase(syncTaskClass, 200, 10));
        testCases.add(new TestCase(syncTaskClass, 500, 10));
        testCases.add(new TestCase(syncTaskClass, 1000, 10));

    });

    testCases.forEach(testCase -> {
        System.gc();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            benchmark(testCase);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });

    StringBuilder stringBuilder = new StringBuilder();
    int index = 0;
    for (TestCase testCase : testCases) {
        if (index % 20 == 0)
            stringBuilder.append("\r\n");
        stringBuilder.append(testCase.duration);
        stringBuilder.append(",");
        index++;
    }
    System.out.println(stringBuilder.toString());
}

在这里可以看到,我们为这6个锁定义了20种测试场景,覆盖几大类:

  • 只有读的情况
  • 只有写的情况
  • 读写并发的情况,并发数渐渐增多
  • 读比写多的情况(这个最常见吧)
  • 写比读多的情况

每一次测试之间强制触发gc后休眠1秒,每20次结果换行一次输出。
测试类如下:

private void benchmark(TestCase testCase) throws Exception {
    LockTask.counter = 0;
    log.info("Start benchmark:{}", testCase);
    CountDownLatch start = new CountDownLatch(1);
    CountDownLatch finish = new CountDownLatch(testCase.readerThreadCount + testCase.writerThreadCount);
    if (testCase.readerThreadCount > 0) {
        LockTask readerTask = (LockTask) testCase.lockTaskClass.getDeclaredConstructor(Boolean.class).newInstance(false);
        readerTask.start = start;
        readerTask.finish = finish;
        readerTask.loopCount = LOOP_COUNT / testCase.readerThreadCount;
        if (testCase.lockTaskClass.getSimpleName().startsWith("Fair")) readerTask.loopCount /= 100;
        IntStream.rangeClosed(1, testCase.readerThreadCount)
                .mapToObj(__ -> new Thread(readerTask))
                .forEach(Thread::start);
    }
    if (testCase.writerThreadCount > 0) {
        LockTask writerTask = (LockTask) testCase.lockTaskClass.getDeclaredConstructor(Boolean.class).newInstance(true);
        writerTask.start = start;
        writerTask.finish = finish;
        writerTask.loopCount = LOOP_COUNT / testCase.writerThreadCount;
        if (testCase.lockTaskClass.getSimpleName().startsWith("Fair")) writerTask.loopCount /= 100;
        IntStream.rangeClosed(1, testCase.writerThreadCount)
                .mapToObj(__ -> new Thread(writerTask))
                .forEach(Thread::start);
    }

    start.countDown();
    long begin = System.currentTimeMillis();
    finish.await();
    if (testCase.writerThreadCount > 0) {
        if (testCase.lockTaskClass.getSimpleName().startsWith("Fair")) {
            Assert.assertEquals(LOOP_COUNT / 100, LockTask.counter);
        } else {
            Assert.assertEquals(LOOP_COUNT, LockTask.counter);
        }
    }
    testCase.duration = System.currentTimeMillis() - begin;
    log.info("Finish benchmark:{}", testCase);
}

代码主要干了几件事情:

  • 根据测试用例的读写线程数,开启一定量的线程,根据类名和读写类型动态创建类型
  • 每一个线程执行的循环次数是按比例均匀分配的,公平类型的两次测试数/100,因为实在是太慢了,等不了几小时
  • 使用两个CountDownLatch来控制所有线程开启,等待所有线程完成,最后校验一下counter的总数

在这里,我们把循环次数设置为1000万次,在阿里云12核12G机器JDK8环境下运行得到的结果如下:

这里,我们进行两次测试,其实一开始我的测试代码里没有HashMap的读写操作,只有counter的读写操作(这个时候循环次数是1亿次),所有第一次测试是仅仅只有counter的读写操作的,后一次测试是这里贴的代码的版本。

所以这个表格中的数据不能直接来对比因为混杂了三种循环次数,上面那个表是1亿从循环的时间,下面那个是1000万次,黄色的两条分别是100万次和10万次循环。

这个测试信息量很大,这里说一下我看到的几个结论,或者你还可以从这个测试中品味出其它结论:

  • synchronized关键字经过各种优化进行简单锁的操作性能已经相当好了,如果用不到ReentrantLock高级功能的话,使用synchronized不会有什么太多性能问题
  • 在任务非常轻的时候可重入锁比synchronized还是快那么一点,一般场景下不可能只是++操作,这个时候两者差不多
  • 并发上来之后各种锁的执行耗时稍微增多点,没有增多太厉害,并发不足的时候反而性能还不好
  • 在任务很轻的时候StampedLock性能碾压群雄,在只有读操作的时候因为只是乐观锁,所以性能好的夸张
  • 在任务没有那么轻的时候读写锁的性能几乎都比普通锁好,看下面那个表格,在任务实在是太轻的时候读写锁因为复杂的锁实现开销的问题不如普通的可重入锁
  • 公平版本的锁非常非常慢,可以说比非公平版本的慢100倍还不止,而且执行的时候CPU打满,其它版本的锁执行的时候CPU利用在12核的20%左右,其实想想也对,不管是多少线程,大部分时候都阻塞了

所以说对于这些锁的选择也很明确:

  • 如果用不到ReentrantLock的什么高级特性,synchronized就可以
  • 一般而言ReentrantLock完全可以替代synchronized,如果你不嫌麻烦的话
  • ReentrantReadWriteLock用于相对比较复杂的任务的读写并发的情况
  • StampedLock用于相对比较轻量级任务的高并发的情况,用起来也比较复杂,能够实现极致的性能
  • 只有有特殊需求的话才去开启ReentrantLock或ReentrantReadWriteLock的公平特性

再来看看ReentrantLock

之前也提到了可重入锁相对synchronized有一些高级特性,我们写一些测试代码:

  • 我们先在主线程锁10次
  • 输出一下锁的一些信息
  • 循环10次开启10个线程尝试获取锁,等待时间是1秒到10秒,显然主线程释放锁之前是获取不到锁的
  • 1秒一次定时输出锁的一些信息
  • 5秒后主线程释放锁
  • 休眠一下观察子线程是否拿到锁了
@Test
public void test() throws InterruptedException {

    ReentrantLock reentrantLock = new ReentrantLock(true);
    IntStream.rangeClosed(1, 10).forEach(i -> reentrantLock.lock());
    log.info("getHoldCount:{},isHeldByCurrentThread:{},isLocked:{}",
            reentrantLock.getHoldCount(),
            reentrantLock.isHeldByCurrentThread(),
            reentrantLock.isLocked());

    List<Thread> threads = IntStream.rangeClosed(1, 10).mapToObj(i -> new Thread(() -> {
        try {
            if (reentrantLock.tryLock(i, TimeUnit.SECONDS)) {
                try {
                    log.debug("Got lock");
                } finally {
                    reentrantLock.unlock();
                }
            } else {
                log.debug("Cannot get lock");
            }
        } catch (InterruptedException e) {
            log.debug("InterruptedException Cannot get lock");
            e.printStackTrace();
        }
    })).collect(Collectors.toList());

    Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> log.info("getHoldCount:{}, getQueueLength:{}, hasQueuedThreads:{}, waitThreads:{}",
            reentrantLock.getHoldCount(),
            reentrantLock.getQueueLength(),
            reentrantLock.hasQueuedThreads(),
            threads.stream().filter(reentrantLock::hasQueuedThread).count()), 0, 1, TimeUnit.SECONDS);

    threads.forEach(Thread::start);

    TimeUnit.SECONDS.sleep(5);
    IntStream.rangeClosed(1, 10).forEach(i -> reentrantLock.unlock());
    TimeUnit.SECONDS.sleep(1);
}

输出如下:

08:14:50.834 [main] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:10,isHeldByCurrentThread:true,isLocked:true
08:14:50.849 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:10, hasQueuedThreads:true, waitThreads:10
08:14:51.849 [Thread-0] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:51.848 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:9, hasQueuedThreads:true, waitThreads:9
08:14:52.849 [Thread-1] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:52.849 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:8, hasQueuedThreads:true, waitThreads:8
08:14:53.846 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:8, hasQueuedThreads:true, waitThreads:8
08:14:53.847 [Thread-2] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:54.847 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:7, hasQueuedThreads:true, waitThreads:7
08:14:54.849 [Thread-3] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:55.847 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:6, hasQueuedThreads:true, waitThreads:6
08:14:55.850 [Thread-4] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:55.850 [Thread-5] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.851 [Thread-6] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.852 [Thread-7] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.852 [Thread-8] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.852 [Thread-9] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:56.849 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:0, hasQueuedThreads:false, waitThreads:0

从这个输出可以看到:

  • 一开始显示锁被主线程锁了10次
  • 随着时间的推移等待锁的线程数量在增加
  • 5个线程因为超时无法获取到锁
  • 5秒后还有5个线程拿到了锁

这也可以看到可重入锁相比synchronized功能更强大点:

  • 可以超时等待获取锁
  • 可以查看到锁的一些信息
  • 可以中断锁(这里没有演示)
  • 之前提到的公平性
  • 可重入特性并不是它特有的功能,synchronized也能重入

提到了可重入,我们进行一个无聊的实验看看可以重入多少次:

@Test
public void test2() {
    ReentrantLock reentrantLock = new ReentrantLock(true);
    int i = 0;
    try {
        while (true) {
            reentrantLock.lock();
            i++;
        }
    } catch (Error error) {
        log.error("count:{}", i, error);
    }
}

结果如下:

锁误用的例子

最后再提下最简单的锁误用的例子,虽然没有那么高大上,但是这种因为锁范围和锁保护对象的范围不一致导致误用的问题在业务代码中到处都是,比如:

@Slf4j
public class LockMisuse {

    @Test
    public void test1() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        IntStream.rangeClosed(1, 100000).forEach(i -> executorService.submit(new Container()::test));
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.HOURS);
        log.info("{}", Container.counter);
    }
}

class Container {
    static int counter = 0;
    Object locker = new Object();

    void test() {
        synchronized (locker) {
            counter++;
        }
    }
}

在代码里我们要保护的资源是静态的,但是锁却是对象级别的,不同的实例持有不同的锁,完全起不到保护作用:

小结

本文我们简单测试了一下各种锁的性能,我感觉这个测试可能还无法100%模拟真实的场景,真实情况下不仅仅是读写线程数量的不一致,更多是操作频次的不一致,不过这个测试基本看到了我们猜测的结果。在日常代码开发过程中,大家可以根据实际功能和场景需要来选择合适的锁类型。

有的时候高大上的一些锁因为使用复杂容易导致误用、错用、死锁、活锁等问题,我反而建议在没有明显问题的情况下先从简单的『悲观』锁开始使用。还有就是像最后的例子,使用锁的话务必需要认证检查代码,思考锁和保护对象的关系,避免锁不产产生效果导致隐藏的Bug。

同样,代码见我的Github,欢迎clone后自己把玩,欢迎点赞。

欢迎关注我的微信公众号:随缘主人的园子

原文地址:https://www.cnblogs.com/lovecindywang/p/11216570.html

时间: 2024-10-07 08:46:27

和朱晔一起复习Java并发(三):锁(含锁性能测试)的相关文章

和朱晔一起复习Java并发(一):线程池

和我之前的Spring系列文章一样,我们会以做一些Demo做实验的方式来复习一些知识点. 本文我们先从Java并发中最最常用的线程池开始. 从一个线程池实验开始 首先我们写一个方法来每秒一次定时输出线程池的基本信息: private void printStats(ThreadPoolExecutor threadPool){ Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> { log.info("

Java并发编程:Concurrent锁机制解析

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

Java 并发:内置锁 Synchronized

摘要: 在多线程编程中,线程安全问题是一个最为关键的问题,其核心概念就在于正确性,即当多个线程访问某一共享.可变数据时,始终都不会导致数据破坏以及其他不该出现的结果.而所有的并发模式在解决这个问题时,采用的方案都是序列化访问临界资源 .在 Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock.本文针对 synchronized 内置锁 详细讨论了其在 Java 并发 中的应用,包括它的具体使用场景(同步方法.同步代码块.实例对象锁 和 Class 对象锁).可重

Java并发编程之StampedLock锁源码探究

StampedLock是JUC并发包里面JDK1.8版本新增的一个锁,该锁提供了三种模式的读写控制,当调用获取锁的系列函数的时候,会返回一个long 型的变量,该变量被称为戳记(stamp),这个戳记代表了锁的状态. try系列获取锁的函数,当获取锁失败后会返回为0的stamp值.当调用释放锁和转换锁的方法时候需要传入获取锁时候返回的stamp值. StampedLockd的内部实现是基于CLH锁的,CLH锁原理:锁维护着一个等待线程队列,所有申请锁且失败的线程都记录在队列.一个节点代表一个线程

Java并发集合操作中对锁的应用。

下面以List结合为例子, 先来看以下代码: public static ArrayList<String>datas=new ArrayList<String>(); //初始化数据 public static void initData(){ for(int i=0;i<20;i++){ datas.add(""+i); } } //线程1,读取集合的数据 public static Thread thread1=new Thread(){ publi

JAVA并发编程学习笔记------锁顺序死锁

一.需求描述: 将资金从一个账户转移到另一个账户. 二.程序实现: (1)账户类: public class Account { private long account; public Account(String user, long account) { this.account = account; } public Account() { super(); } public long getAccount() { return account; } public void setAcc

Java并发(三) ThreadLocal关键字

TheadLocal称为线程本地存储,就是说一个变量,每个线程都有它的一个副本,并且相互之间是独立的. ThreadLocal类的实现 下面是该类的提供的关键的几个方法: public T get() { } public void set(T value) { } public void remove() { } protected T initialValue() { } 通过查看jdk中该类的源码,可以大致看到上述方法的实现,其中: /** * Returns the value in t

Java并发编程与技术内幕:聊聊锁的技术内幕(上)

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 一.基础知识 在Java并发编程里头,锁是一个非常重要的概念.就如同现实生活一样,如果房子上了锁.别人就进不去.Java里头如果一段代码取得了一个锁,其它地方再想去这个锁(或者再执行这个相同的代码)就都得等待锁释放.锁其实分成非常多.比如有互斥锁.读写锁.乐观锁.悲观锁.自旋锁.公平锁.非公平锁等.包括信号量其实都可以认为是一个锁. 1.什么时需要锁呢? 其实非常多的场景,如共享实例变量.共

Java 并发:Lock 框架详解

摘要: 我们已经知道,synchronized 是java的关键字,是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问,但 synchronized 粒度有些大,在处理实际问题时存在诸多局限性,比如响应中断等.Lock 提供了比 synchronized更广泛的锁操作,它能以更优雅的方式处理线程同步问题.本文以synchronized与Lock的对比为切入点,对Java中的Lock框架的枝干部分进行了详细介绍,最后给出了锁的一些相关概念. 一. synchronized 的局限性