并发编程

多线程

  • 请你说说 JUC
JUC 即 java.util.concurrent 包的缩写,是 java 原生的并发包和一些常用的工具类。
  • 说说你对线程池的理解
线程池就像是一个工厂,里面有很多工人,当我们需要做一件事情时,就从工厂中找一个工人来做这件事情,完成后工人回到工厂等待下一次调用。
  • 为什么需要使用线程池?
因为频繁地创建和销毁线程,CPU 需要从就绪状态调度到运行状态,而上下文切换的效率是非常低的。
通过复用已经创建的线程来避免频繁地创建和销毁线程,提高效率,同时还能控制线程池内线程的数量,避免资源过度占用。

image

  • 实际开发中什么情况下会使用线程池?
首先,实际开发中禁止自己显式创建线程,必须通过线程池复用已经创建的线程。
具体的业务场景,比如,邮件批量发送、数据批量导入。
  • 线程池有什么作用?
通过复用已经创建的线程来避免频繁地创建和销毁线程,提高效率,同时还能控制线程池内线程的数量,避免资源过度占用。
还可以对其进行扩展,比如定时线程池 ScheduledThreadPool,在指定时间执行任务。
  • 线程池的创建方式有哪些?
线程池的创建方式总共有 7 种。其中有 6 种通过 Executors 创建,剩下的 1 种通过 ThreadPoolExecutor 创建。
但是,阿里巴巴开发手册规定,线程池不能使用 Executors 创建,只能通过 ThreadPoolExecutor 的方式。
因为通过 Executors 创建的线程池,其底层都是基于 ThreadPoolExecutor 构造函数封装的。
它采用无界队列存放任务,可能会导致内存溢出。
比如当 FixedThreadPool、SingleThreadPool、ScheduledThreadPool 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
比如当 CachedThreadPool 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
通过 Executors 创建的线程池,其底层都是基于 ThreadPoolExecutor 构造函数封装的:
    Executors.newCachedThreadPool 可缓存线程池
    Executors.newFixedThreadPool 固定大小线程池
    Executors.newSingleThreadExecutor 单线程池
    Executors.newScheduledThreadPool 定时任务线程池
    Executors.newSingleThreadScheduledExecutor 单线程定时任务线程池
    Executors.newWorkStealingPool 工作窃取线程池
ThreadPoolExecutor 可以通过设置 7 个参数来创建线程池:
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
  • 线程池实现线程复用的原理
使用线程池和 workQueue 可以实现线程复用,即线程不会被频繁创建和销毁,而是被重复利用。
workQueue 可以存储等待执行的任务,并且提供线程安全的方法,比如入队、出队等。
将任务添加到 workQueue 中,线程池中的线程会自动从 workQueue 中取出任务并执行,当 WorkQueue 中没有任务时,线程会处于空闲状态等待新任务到来。

image

  • 手写线程池
package com.linycat;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * @author linycat
 */
public class MyThreadPool {
    private final BlockingDeque<Runnable> runnableBlockingDeque;

    public MyThreadPool(int dequeSize, int threadNumber) {
        runnableBlockingDeque = new LinkedBlockingDeque<>(dequeSize);
        List<MyWorkThread> myWorkThreadList = new ArrayList<>(threadNumber);
        for (int i = 0; i < threadNumber; i++) {
            myWorkThreadList.add(new MyWorkThread());
            myWorkThreadList.get(i).start();
        }
    }

    public void execute(Runnable r) {
        runnableBlockingDeque.offer(r);
    }

    class MyWorkThread extends Thread {
        @Override
        public void run() {
            while (true) {
                Runnable runnable = runnableBlockingDeque.poll();
                if (runnable != null) {
                    runnable.run();
                }
            }
        }
    }

    public static void main(String[] args) {
        MyThreadPool myThreadPool = new MyThreadPool(10, 2);
        for (int i = 0; i < 15; i++) {
            int val = i;
            myThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":" + val);
                }
            });
        }
    }
}
  • 线程池创建的线程会一直处于运行状态吗?
不会,线程池会根据不同的参数来控制线程的数量和状态。
corePoolSize 是线程池的核心线程数,maximumPoolSize 是线程池的最大线程数,keepAliveTime 是非核心线程的存活时间。
非核心线程的空闲时间一旦达到 keepAliveTime 就会被销毁,而核心线程还是会继续运行。
  • 线程池的工作原理
1、当线程数大于核心线程数,而且任务队列没满时,将任务放进任务队列。
2、当线程数大于核心线程数,但是任务队列已满时。如果线程数小于最大线程数,则创建新线程执行任务,否则按照拒绝策略处理任务。
也就是说,最大并发任务数 = 任务队列容量 + 最大线程数
  • 线程池要设置多大?
《Java 并发编程实战》中提到了一个计算线程池最优大小的公式。
  • 当任务队列已满,并且线程数达到最大值,当有新任务提交时,任务会丢失吗?
不会,按照拒绝策略处理任务。
  • 线程池的拒绝策略有哪些?
AbortPolicy:默认策略,抛出异常。
CallerRunsPolicy:使用调用者线程执行。
DiscardOldestPolicy:丢弃最老的任务并再次提交当前任务。
DiscardPolicy:直接丢弃任务。
如果以上四种策略不满足需求,我们还可以自定义拒绝策略,只需要实现 RejectedExecutionHandler 接口即可。
  • 为什么阿里巴巴开发手册规定,线程池不能使用 Executors 创建?
因为通过 Executors 创建的线程池,其底层都是基于 ThreadPoolExecutor 构造函数封装的。
它采用无界队列存放任务,可能会导致内存溢出。
比如当 FixedThreadPool、SingleThreadPool、ScheduledThreadPool 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
比如当 CachedThreadPool 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
  • 内存泄露与内存溢出的区别
内存泄露就是内存不释放。比如一个教室的学生上完课了不走,导致下一节课的学生进不来。
内存溢出就是内存不够用。比如一个教室只能容纳一个班的学生,但是来了两个班的学生。
  • 请你说说多线程?
  • 说说你对 ThreadLocal 的理解
ThreadLocal 是 Java 中的一个类,它允许我们创建一个线程本地变量,即每个线程都有自己独立的变量副本。
可以将 ThreadLocal 理解为一个容器,它可以存储一些对象,并且这些对象对于不同的线程来说是独立的。
常用的方法有 set()、get()、remove()。
public class ThreadLocalDemo {
    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        // 创建三个线程,并分别设置 threadLocal 的值
        Thread thread1 = new Thread(() -> {
            threadLocal.set(1);
            // Thread 1: 1
            System.out.println("Thread 1: " + threadLocal.get());
        });

        Thread thread2 = new Thread(() -> {
            threadLocal.set(2);
            // Thread 2: 2
            System.out.println("Thread 2: " + threadLocal.get());
        });

        Thread thread3 = new Thread(() -> {
            threadLocal.set(3);
            // Thread 3: 3
            System.out.println("Thread 3: " + threadLocal.get());
        });

        // 启动三个线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
  • ThreadLocal 的应用场景
保存线程上下文信息。
假设一个程序有多个用户,每个用户可以使用不同的语言访问。
我们想要存储每个用户的语言偏好,以便在不同的请求之间共享。
那么我们可以在用户登录时将其语言偏好存储到 ThreadLocal 中,在后续的请求中使用该偏好。
  • Synchronized 与 ThreadLocal 的区别
  • ThreadLocal 的底层实现原理
ThreadLocal 的实现原理是通过每个线程都拥有自己的 ThreadLocalMap 来实现的。
ThreadLocalMap 是一个自定义的哈希表,以 ThreadLocal 对象为键,以对应的值为值。
每个线程都有一个 ThreadLocalMap,可以通过调用 Thread 类的 getMap 方法获取。
当线程调用 ThreadLocal 的 set 方法时,会将当前线程的 ThreadLocalMap 作为参数传入,然后将自己作为键值,存储对应的值。
当线程调用 ThreadLocal 的 get 方法时,会获取当前线程的 ThreadLocalMap,然后将自己作为键值,获取对应的值。
  • 为什么线程缓存的是 ThreadLocal 对象?
如果每个线程都需要创建自己独立的本地变量,那么就需要每个线程都创建自己独立的 ThreadLocal 对象。
这样就会导致创建大量的 ThreadLocal 对象,会导致内存开销过大,而且也不方便管理。
线程缓存 ThreadLocal 对象,是为了方便在多个线程中使用同一个 ThreadLocal 对象来创建本地变量。
由于每个线程都拥有自己独立的 ThreadLocalMap 对象,因此不同线程中的同一个 ThreadLocal 对象所创建的本地变量是互相独立的,不会相互干扰。
  • ThreadLocal 为什么会引发内存泄漏?
因为 ThreadLocalMap 的生命周期和 Thread 一样,而且 ThreadLocalMap 中的 key 是弱引用,value 是强引用。
如果没有正确删除 key,就会导致这些强引用的 value 对象无法被 GC 回收,从而引发内存泄漏。
  • 如何预防 ThreadLocal 内存泄漏问题?
每次使用完 ThreadLocal 都调用 remove() 方法。
  • 谈谈强、软、弱、虚引用的区别
强引用表示必须的对象,即使内存不足 GC 也不会回收对象。
软引用表示有用但不是必须的对象,内存不足时 GC 会回收对象。
弱引用表示不是必须的对象,虚引用表示即将被回收的对象。
  • 为什么 ThreadLocal 采用弱引用而不是强引用?
假设 ThreadLocalMap 的 key 为强引用:
  即使 ThreadLocal 实例没有被线程持有,但 ThreadLocalMap 仍然持有 ThreadLocal 实例的强引用,导致 ThreadLocal 实例无法被 GC 回收,从而导致内存泄漏。
而 ThreadLocalMap 的 key 为弱引用时,当 ThreadLocal 实例没有被线程持有时,ThreadLocal 实例就可以被 GC 回收,避免了内存泄漏。
当 ThreadLocalMap 的 key 为 null 时,ThreadLocalMap 会在下次调用 set()、get() 或 remove() 方法时清除所有 key 为 null 的 Entry。
这是为了确保当 ThreadLocal 实例被 GC 回收时,与之关联的 Entry 也会被清理掉,从而避免内存泄漏。
  • Lock 的底层实现原理
Lock 是一个接口,常见的锁实现类包括 ReentrantLock 和 ReentrantReadWriteLock,它们的底层实现都基于 AQS + Cas + LockSupport。
  • 谈谈 LockSupport 的用法
LockSupport 是一个提供线程阻塞和唤醒的工具类。
import java.util.concurrent.locks.LockSupport;

public class LockSupportExample {
    public static void main(String[] args) {
        Thread thread = new Thread(new Task());
        thread.start();
        // 等待 2 秒钟
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 唤醒线程
        LockSupport.unpark(thread);
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            System.out.println("Thread started.");
            // 阻塞线程
            LockSupport.park();
            System.out.println("Thread resumed.");
        }
    }
}
  • Lock 的公平锁与非公平锁
new ReentrantLock(true) 公平锁,new ReentrantLock(false) 非公平锁。
它们的父类都是 AbstractQueuedSynchronizer。
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private static final int THREAD_COUNT = 5;
    // 公平锁
    private static final ReentrantLock FAIR_LOCK = new ReentrantLock(true);
    // 非公平锁
    private static final ReentrantLock NON_FAIR_LOCK = new ReentrantLock(false);

    public static void main(String[] args) {
        // 使用公平锁
        for (int i = 0; i < THREAD_COUNT; i++) {
            Thread thread = new Thread(new Worker(FAIR_LOCK));
            thread.start();
        }
        // 使用非公平锁
        for (int i = 0; i < THREAD_COUNT; i++) {
            Thread thread = new Thread(new Worker(NON_FAIR_LOCK));
            thread.start();
        }
    }

    static class Worker implements Runnable {
        private final ReentrantLock lock;

        public Worker(ReentrantLock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            for (int i = 0; i < 2; i++) {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " get the lock");
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                    System.out.println(Thread.currentThread().getName() + " release the lock");
                }
            }
        }
    }
}
  • AQS 核心参数有哪些?
AQS 全称 AbstractQueuedSynchronizer,是 Java 并发包中的一个基类。
AQS 中的核心参数包括 state、head 和 tail、exclusiveOwnerThread、waitStatus。
state 是一个 int 类型的变量,表示同步状态,可以根据业务需求来自定义其含义,如表示锁是否被占用、被占用的线程数等等
head 和 tail 是 Node 类型的变量,表示双向链表的头结点和尾结点,每个节点代表一个等待线程
exclusiveOwnerThread 表示当前锁被哪个线程占用
waitStatus 表示当前节点的等待状态,可以用来表示等待线程需要唤醒、已取消等待、在条件队列中等待等等
  • AQS 的实现原理
https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
  • AQS + Cas + LockSupport 手写 Lock 锁
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class MyLock {
    /**
     * 锁状态
     */
    private final AtomicInteger lockState = new AtomicInteger(0);
    /**
     * 获取到锁的线程
     */
    private Thread getLockThread = null;
    /**
     * 没有获取到锁的线程
     */
    private final ConcurrentLinkedDeque<Thread> concurrentLinkedDeque = new ConcurrentLinkedDeque<>();

    /**
     * 加锁操作
     *
     * @return 是否成功获取锁
     */
    public boolean lock() {
        // 循环获取锁
        while (true) {
            // 尝试获取锁
            if (compareAndSet(0, 1)) {
                // 获取到锁、记录当前获取到锁的线程
                getLockThread = Thread.currentThread();
                return true;
            }
            // 没有获取到锁、将线程加入队列
            concurrentLinkedDeque.add(Thread.currentThread());
            // 阻塞线程
            LockSupport.park();
        }
    }

    /**
     * 修改锁状态
     *
     * @param expect 期望值
     * @param update 更新值
     * @return 是否更新成功
     */
    private boolean compareAndSet(int expect, int update) {
        return lockState.compareAndSet(expect, update);
    }

    /**
     * 解锁操作
     *
     * @return 是否成功释放锁
     */
    public boolean unLock() {
        // 当前线程没有获取到锁
        if (getLockThread == null || getLockThread != Thread.currentThread()) {
            return false;
        }
        // 修改锁状态
        boolean result = compareAndSet(1, 0);
        // 修改成功
        if (result && !concurrentLinkedDeque.isEmpty()) {
            // 唤醒线程 (公平锁)
            Thread first = concurrentLinkedDeque.getFirst();
            LockSupport.unpark(first);
            return true;
        }
        return false;
    }
}
  • 谈谈 Lock 与 synchronized 的区别
锁的获取和释放:Lock 锁需要手动获取和释放,而 synchronized 锁是由 JVM 自动获取和释放的。
可重入性:Lock 锁是可重入的,同一个线程可以多次获取同一个锁,而 synchronized 锁也是可重入的,但是重入时会自动获取锁,而不是尝试获取锁。
粒度:Lock 锁的粒度比 synchronized 锁更细,可以实现更精细的线程同步。
等待可中断:Lock 锁提供了一个 lockInterruptibly () 方法,可以响应中断,而 synchronized 锁不支持响应中断。
条件变量:Lock 锁提供了 Condition 类来实现线程之间的协调,而 synchronized 锁不支持条件变量。
  • Semaphore 信号量底层原理
Semaphore(信号量)是基于 AQS(AbstractQueuedSynchronizer)的并发控制机制,可以用来控制同时访问某个资源的线程数。
Semaphore 维护了一个许可证的计数器,表示当前可用的许可证数量。
线程可以通过 acquire() 方法来获取许可证,如果当前可用许可证数量大于 0,则线程可以直接获取许可证并继续执行;否则线程会进入等待队列,等待其他线程释放许可证。
当一个线程调用 release() 方法释放许可证时,Semaphore 内部会将可用许可证数量加 1,并唤醒等待队列中的一个线程。
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final Semaphore SEMAPHORE = new Semaphore(3);
    private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 获取信号量
                        SEMAPHORE.acquire();
                        // 对共享变量进行操作
                        count++;
                        System.out.println(Thread.currentThread().getName() + " add count to " + count);
                        // 释放信号量
                        SEMAPHORE.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 获取信号量
                        SEMAPHORE.acquire();
                        // 对共享变量进行操作
                        count--;
                        System.out.println(Thread.currentThread().getName() + " sub count to " + count);
                        // 释放信号量
                        SEMAPHORE.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}
  • CountDownLatch 底层原理
CountDownLatch 是基于 AQS (AbstractQueuedSynchronizer) 实现的同步工具类,可以用来协调多个线程之间的执行顺序。
在 CountDownLatch 中,主要使用了两个变量 count 和 Sync,其中 count 表示需要等待的线程数量,Sync 是一个内部类,继承自 AQS,用于实现同步操作。
在 Sync 类中,有两个核心方法,tryAcquireShared () 和 tryReleaseShared (),分别用于获取和释放共享资源。
当调用 await () 方法时,会调用 tryAcquireShared () 方法,该方法会阻塞当前线程,直到 count 的值减为 0。
当调用 countDown () 方法时,会调用 tryReleaseShared () 方法,该方法会将 count 的值减 1,并检查 count 的值是否为 0,如果为 0 则唤醒所有等待的线程。
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 5;
        CountDownLatch latch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(new Task(latch)).start();
        }
        latch.await();
        System.out.println("All threads finished, do other thing.");
    }

    static class Task implements Runnable {
        private final CountDownLatch latch;

        public Task(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " started.");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " finished.");
            latch.countDown();
        }
    }
}
  • 锁的升级过程
  • 说说你了解的线程通信方式
  • 说说你了解的线程同步方式
  • 说说怎么保证线程安全
  • 说说 wait() 和 sleep() 的区别
wait() 会释放锁,而 sleep() 不会释放锁。
调用 wait() 的线程会等待其他线程来唤醒它,而调用 sleep() 的线程会在指定时间后自动唤醒。
  • 说说线程的状态
    image
java.lang.Thread.State 枚举类中定义了六种线程的状态,分别是 NEW 新建、RUNNABLE 可运行、BLOCKED 阻塞、WAITING 等待、TIMED_WAITING 超时等待、TERMINATED 已终止。
可以使用 Thread.currentThread().getState() 获取当前线程的状态。
  • 说说线程的创建方式
  • 请你说说多线程

CAS

  • 什么是悲观锁?什么是乐观锁?
悲观锁和乐观锁都是用于解决多线程访问共享资源的问题。
悲观锁认为在访问共享资源时会出现冲突,因此先把共享资源锁住,以确保每个线程独占资源,避免出现冲突。
如果一个线程已经得到了悲观锁,那么其他线程需要等待锁被释放才能访问共享资源。
悲观锁适用于写操作较多的场景,可以保证数据的一致性,但会影响并发性能。
通常使用 synchronized 或者 ReentrantLock 实现。
乐观锁认为在访问共享资源时不会出现冲突,因此不会显式地加锁,而是通过比较版本号的方式来检查共享资源是否被其他线程修改过。
如果共享资源没有被修改,当前线程可以直接更新共享资源,否则需要重试或者放弃操作。
乐观锁适用于读操作较多的场景,可以提高并发性能,但要保证数据的一致性。
通常使用 CAS 实现。
  • 锁的分类有哪些?
1、悲观锁、乐观锁
悲观锁有 synchronized、ReentrantLock,乐观锁有 AtomicInteger、AtomicReference、AtomicStampedReference。
2、公平锁、非公平锁
公平锁和非公平锁都是针对 ReentrantLock 而言的,ReentrantLock(true) 是公平锁,ReentrantLock(false) 是非公平锁。
3、自旋锁、可重入锁
自旋锁有 synchronized、AtomicInteger,可重入锁有 synchronized、ReentrantLock。
synchronized 在 JDK1.6 之后引入了适应性自旋锁。
4、重量级锁、轻量级锁
重量级锁有 synchronized,轻量级锁有 ReentrantLock、AtomicInteger。
5、独占锁、共享锁
独占锁有 ReentrantLock,共享锁有 ReadWriteLock。
  • 谈谈你对公平锁与非公平锁的理解
公平锁按照请求的先后顺序分配锁,能够保证每个线程都能获取到锁。
非公平锁按照抢占的方式分配锁,不能保证每个线程都获取到锁。
公平锁能够避免出现线程饥饿,而非公平锁则能够减少锁的竞争,提高锁的性能。
synchronized 是非公平锁。
  • 公平锁的底层是如何实现的?
公平锁是基于一个队列和一个标记实现的。
ReentrantLock(true) 就是一种公平锁,它利用了 Condition 队列和 AtomicInteger 原子变量。
当一个线程请求获取锁时,如果锁已经被其他线程持有,那么该线程会被加入到队列的末尾,等待前面的线程释放锁之后才会被唤醒。
当锁被释放时,队列中的第一个线程会被唤醒获取锁。
  • 什么是锁的可重入性?
锁的可重入性指的是同一线程可以多次获取同一个锁而不会发生死锁。
在实现可重入锁时,需要在锁的内部维护一个计数器,记录当前锁被持有的次数。
可重入锁也称为递归锁,最常见应用场景是递归函数。
当一个递归函数需要在多个层次上使用同一个锁时,如果该锁是可重入的,则递归调用可以正常进行,不会发生死锁,比如 synchronized。
  • 谈谈你对 CAS 锁的理解
CAS 锁是一种基于比较交换(Compare And Swap)的同步机制。
CAS 操作需要提供三个参数:内存地址 V、期望值 E 和新值 N。
首先读取内存地址 V 中的值,然后将它与期望值 E 进行比较。如果相等则更新为新值 N,如果不相等说明已经被其他线程修改过,当前线程获取锁失败,需要进行重试。
使用 CAS 锁可以避免传统锁机制中的死锁和饥饿,没有获取到锁的线程是不会阻塞的,而是通过循环控制不停地获取锁。
但是,使用 CAS 锁可能会出现 ABA 问题,也就是说,如果共享变量的值被多次修改,但最终回到了原来的值,CAS 会误以为没有被修改过。
public class SynchronizedTest extends Thread {
    private static int val = 0;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (val > 50000) {
                    break;
                }
                val++;
                System.out.println(Thread.currentThread().getName() + ":" + val);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        Thread thread01 = new Thread(synchronizedTest);
        Thread thread02 = new Thread(synchronizedTest);
        thread01.start();
        thread02.start();
        thread01.join();
        thread02.join();
        long endTime = System.currentTimeMillis();
        // 50001:200ms
        System.out.println(val + ":" + (endTime - startTime) + "ms");
    }
}
import java.util.concurrent.atomic.AtomicInteger;

public class CasTest extends Thread {
    private static final AtomicInteger val = new AtomicInteger(0);

    @Override
    public void run() {
        while (true) {
            if (val.get() > 50000) {
                break;
            }
            int i = val.incrementAndGet();
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        CasTest casTest = new CasTest();
        Thread thread01 = new Thread(casTest);
        Thread thread02 = new Thread(casTest);
        thread01.start();
        thread02.start();
        thread01.join();
        thread02.join();
        long endTime = System.currentTimeMillis();
        // 50001:200ms
        System.out.println(val.get() + ":" + (endTime - startTime) + "ms");
    }
}
  • CAS 底层实现原理
CAS 操作需要提供三个参数:内存地址 V、期望值 E 和新值 N。
首先读取内存地址 V 中的值,然后将它与期望值 E 进行比较。如果相等则更新为新值 N,如果不相等说明已经被其他线程修改过,当前线程获取锁失败,需要进行重试。
public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(0);
    boolean compareAndSet01 = atomicInteger.compareAndSet(0, 1);
    // true:1
    System.out.println(compareAndSet01 + ":" + atomicInteger.get());
    boolean compareAndSet02 = atomicInteger.compareAndSet(0, 2);
    // false:1
    System.out.println(compareAndSet02 + ":" + atomicInteger.get());
}
  • 利用 CAS 实现锁
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

public class CasLock {
    private final AtomicInteger atomicInteger = new AtomicInteger(0);
    private volatile Thread currentThread = null;

    public boolean tryLock() {
        boolean success = atomicInteger.compareAndSet(0, 1);
        if (success) {
            currentThread = Thread.currentThread();
        } else {
            Thread.yield();
        }
        return success;
    }

    public void unlock() {
        if (Thread.currentThread() == currentThread) {
            currentThread = null;
            atomicInteger.set(0);
        }
    }

    public static void main(String[] args) {
        CasLock casLock = new CasLock();
        IntStream.range(0, 10).forEach(i -> {
            new Thread(
                    () -> {
                        if (casLock.tryLock()) {
                            System.out.println(Thread.currentThread().getName() + "获取锁成功");
                            casLock.unlock();
                        } else {
                            System.out.println(Thread.currentThread().getName() + "获取锁失败");
                        }
                    }
            ).start();
        });
        /**
         * Thread-0获取锁成功
         * Thread-6获取锁失败
         * Thread-4获取锁成功
         * Thread-5获取锁失败
         * Thread-2获取锁失败
         * Thread-8获取锁失败
         * Thread-1获取锁失败
         * Thread-9获取锁失败
         * Thread-7获取锁成功
         * Thread-3获取锁失败
         */
    }
}
  • CAS 锁的优缺点
优点:
  高效性:因为它基于硬件的原子性操作,不需要加锁和解锁。
  无死锁:因为它不会阻塞线程,只会让线程重试。
  无饥饿:因为它保证了公平性,每个线程都有机会执行。
  线程安全:因为只有一个线程可以修改共享变量。
缺点:
  ABA 问题:使用 AtomicStampedReference
  自旋时间过长:控制循环次数,避免 CPU 飙高。
  只能保证一个共享变量的原子性:使用 AtomicReference
  • CAS 如何避免 ABA 问题?
1、加版本号:
在共享变量上加上版本号,每次修改共享变量时,版本号加一。
这样,在 CAS 操作时,除了判断共享变量的值是否相等,还需要判断版本号是否相等,只有值和版本号都相等时,才会进行修改操作。
这种方法可以避免 ABA 问题。
2、使用 AtomicStampedReference 类:
AtomicStampedReference 类是 Java 中提供的一种带时间戳的原子引用类。
它可以在 CAS 操作时,同时比较引用和时间戳是否相等,只有引用和时间戳都相等时,才会进行修改操作。
这种方法也可以避免 ABA 问题。
import java.util.concurrent.atomic.AtomicStampedReference;

public class CasDemo {
    public static void main(String[] args) {
        String initialRef = "initial value";
        int initialStamp = 0;
        AtomicStampedReference<String> atomicStampedRef = new AtomicStampedReference<>(initialRef, initialStamp);

        String newRef = "new value";
        int newStamp = initialStamp + 1;

        // 首先进行一次 CAS 操作,将 initialRef 修改为 newRef
        boolean casResult = atomicStampedRef.compareAndSet(initialRef, newRef, initialStamp, newStamp);
        // CAS result 1: true
        System.out.println("CAS result 1: " + casResult);

        // 再进行一次 CAS 操作,将 newRef 修改回 initialRef
        boolean casResult2 = atomicStampedRef.compareAndSet(newRef, initialRef, newStamp, initialStamp + 1);
        // CAS result 2: true
        System.out.println("CAS result 2: " + casResult2);

        // 再进行一次 CAS 操作,尝试将 initialRef 修改为 newRef
        boolean casResult3 = atomicStampedRef.compareAndSet(initialRef, newRef, initialStamp, newStamp + 1);
        // CAS result 3: false
        System.out.println("CAS result 3: " + casResult3);
    }
}

===== =====

线程池

synchronized

public class SynchronizedCounter {
    private int count;

    public synchronized void increment() {
        count++;
    }

    public synchronized void decrement() {
        count--;
    }

    public synchronized int getCount() {
        return count;
    }

    public static void main(String[] args) {
        SynchronizedCounter synchronizedCounter = new SynchronizedCounter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronizedCounter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronizedCounter.decrement();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(synchronizedCounter.getCount());
    }
}

ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockCounter {
    private int count;
    private final ReentrantLock lock = new ReentrantLock(false);

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public void decrement() {
        lock.lock();
        try {
            count--;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        ReentrantLockCounter reentrantLockCounter = new ReentrantLockCounter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                reentrantLockCounter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                reentrantLockCounter.decrement();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(reentrantLockCounter.getCount());
    }
}
posted @ 2023-03-11 15:48  linycat  阅读(23)  评论(0)    收藏  举报