文章目录
JUC自问自答
Drip
2025-06-20 22:58
累计阅读10
评论0

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() 方法修改集合,会触发并发修改异常,目的是防止在集合遍历过程中发生不一致的修改

线程池

池化技术

事先准备好一些资源,要人要用就取,用完之后还回去

线程池的好处

  1. 降低资源的开销

  2. 方便管理

  3. 提高响应速度

可以让线程复用,可以控制最大并发数,管理线程

为什么不建议使用Executor的实现类创建线程池

  • newFixedThreadPoolnewSingleThreadExecutor 使用无界队列 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,就设置为1

CAS的优点很明显:

  1. 相比锁机制性能更好,避免了线程上下文切换

  2. 代码实现更简单,不需要复杂的同步控制

当然,CAS 并不是完美的,它有几个小问题要注意:

  1. ABA 问题:假设我看到值是 A,但期间有人把它改成了 B,又改回了 A,我再比较时还以为它没变,其实它变过了。这时候可以引入 版本号(比如 AtomicStampedReference)来解决。

  2. 自旋开销大:如果冲突频繁,CAS 会一直重试,会浪费 CPU。

  3. 只能保证一个变量的原子操作:多个变量的话,需要用更复杂的手段,比如锁,或者 AtomicReference 这种复合原子类。

在实际开发中,我用到 CAS 最典型的地方是在高并发计数器场景。

比如之前我做过一个接口限流模块,里面有个需求是统计每秒请求量(QPS),而且要求性能特别高,不能因为加锁拖慢请求处理速度。 那种场景下,我一开始用的是简单的 synchronized 包住计数器,结果并发一高,吞吐量立刻掉下来。

后来,我就改用 AtomicInteger + CAS 来做计数。 它每次请求进来就是直接 incrementAndGet(),内部就是通过 CAS 保证的原子性,这样基本上做到无锁且高效累加,TPS 提升了差不多两倍以上。 (这里还能补一句:如果极限并发下,LongAdder 又更适合,因为它是分段累加,进一步优化了热点问题。)

还有一个场景是异步任务调度里抢占任务的时候,为了防止任务被多个线程同时抢走,我自己实现过一个简易的任务状态机。 用的是 AtomicReference 保存任务状态(比如:INIT -> RUNNING -> DONE),每次修改任务状态都通过 CAS,避免了锁的竞争开销。

总结一句话:在需要轻量级线程安全,而又对性能敏感的地方,我基本都会优先考虑 CAS 相关的原子类。 当然,如果冲突太多,反而还得考虑锁或者别的机制,不然自旋也浪费资源。


评论