Synchronized 和 Lock 区别

生产者-消费者问题
生产者-消费者问题指的是:在多线程编程中,生产者线程和消费者线程对共享资源的访问和操作的问题。 核心问题是:确保生产者在缓冲区已满时不会继续生产,消费者不会在缓冲区为空时继续消费,避免多个线程对共享资源的并发访问下导致数据不一致。
判断,等待 -> 执行业务 -> 唤醒
传统方案:使用内置锁(
synchronized)和while判断(来防止虚假唤醒)
package com.drip;
public class A {
public static void main(String[] args) {
// 新建线程池去测试
Date date = new Date();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
date.increment();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "A").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
date.increment();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "B").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
date.decrement();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "C").start();
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
date.decrement();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "D").start();
}
static class Date {
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
while (number != 0) {
this.wait(); //wait会释放锁
}
number++;
System.out.println("number = " + number);
notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while (number == 0) {
this.wait();
}
number--;
System.out.println("number = " + number);
notifyAll();
}
}
}
JUC版的生产者和消费者问题
static class Data2 {
private int number = 0;
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
//+1
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number++;
System.out.println("number = " + number);
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
condition.await();
}
number--;
System.out.println("number = " + number);
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}吹牛之异常--ConcurrentModificationException 并发修改异常
遍历 ArrayList 时通过 list.add() 或 list.remove() 方法修改集合,会触发并发修改异常,目的是防止在集合遍历过程中发生不一致的修改
线程池
池化技术
事先准备好一些资源,要人要用就取,用完之后还回去
线程池的好处
降低资源的开销
方便管理
提高响应速度
可以让线程复用,可以控制最大并发数,管理线程
为什么不建议使用Executor的实现类创建线程池
newFixedThreadPool和newSingleThreadExecutor使用无界队列LinkedBlockingQueue,任务积压时可能导致内存溢出(OOM)。newCachedThreadPool的最大线程数为Integer.MAX_VALUE,在高并发场景下可能创建过多线程,耗尽系统资源。推荐使用
ThreadPoolExecutor构造函数手动创建线程池,这样可以精细控制线程池的参数,例如:使用有界队列(如
ArrayBlockingQueue)避免内存溢出。自定义线程工厂,为线程设置有意义的名称。
配置合适的拒绝策略(如
CallerRunsPolicy)。
谈谈CAS
好的,这个问题在Java并发编程中很常见。我来谈谈我的理解。
CAS,全称是 Compare And Swap(比较并交换),它是 Java 多线程编程中非常核心的一个原子操作。 简单来说,CAS 的思路就是:我拿着一个期望值去对比当前内存中的实际值,如果它们一样,就把内存里的值更新成新的值;如果不一样,就说明有人抢先改过了,我就不动,或者重试。
比如在多线程环境下对计数器进行自增,传统的做法是加锁,而使用CAS可以这样实现:
AtomicInteger count = new AtomicInteger(0);
count.compareAndSet(0, 1); // 如果当前值是0,就设置为1CAS的优点很明显:
相比锁机制性能更好,避免了线程上下文切换
代码实现更简单,不需要复杂的同步控制
当然,CAS 并不是完美的,它有几个小问题要注意:
ABA 问题:假设我看到值是 A,但期间有人把它改成了 B,又改回了 A,我再比较时还以为它没变,其实它变过了。这时候可以引入 版本号(比如 AtomicStampedReference)来解决。
自旋开销大:如果冲突频繁,CAS 会一直重试,会浪费 CPU。
只能保证一个变量的原子操作:多个变量的话,需要用更复杂的手段,比如锁,或者 AtomicReference 这种复合原子类。
在实际开发中,我用到 CAS 最典型的地方是在高并发计数器场景。
比如之前我做过一个接口限流模块,里面有个需求是统计每秒请求量(QPS),而且要求性能特别高,不能因为加锁拖慢请求处理速度。 那种场景下,我一开始用的是简单的 synchronized 包住计数器,结果并发一高,吞吐量立刻掉下来。
后来,我就改用 AtomicInteger + CAS 来做计数。 它每次请求进来就是直接 incrementAndGet(),内部就是通过 CAS 保证的原子性,这样基本上做到无锁且高效累加,TPS 提升了差不多两倍以上。 (这里还能补一句:如果极限并发下,LongAdder 又更适合,因为它是分段累加,进一步优化了热点问题。)
还有一个场景是异步任务调度里抢占任务的时候,为了防止任务被多个线程同时抢走,我自己实现过一个简易的任务状态机。 用的是 AtomicReference 保存任务状态(比如:INIT -> RUNNING -> DONE),每次修改任务状态都通过 CAS,避免了锁的竞争开销。
总结一句话:在需要轻量级线程安全,而又对性能敏感的地方,我基本都会优先考虑 CAS 相关的原子类。 当然,如果冲突太多,反而还得考虑锁或者别的机制,不然自旋也浪费资源。