面试知识点之JUC并发编程

本文深入探讨多线程的基础概念和技术细节,包括线程的优势、线程安全、上下文切换、同步机制、线程池的使用及优化策略等内容。

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

多线程有什么用?

  1. 发挥多核CPU的优势

  2. 防止阻塞

  3. 便于建模


进程与线程

进程是程序的一次执行,是系统进行资源分配和调度的独立单位,他的作用是是程序能够并发执行提高资源利用率和吞吐率。

由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产生大量的时间和空间的开销,进程的数量不能太多,而线程是比进程更小的能独立运行的基本单位,他是进程的一个实体,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。

线程基本不拥有系统资源,只有一些运行时必不可少的资源,比如程序计数器、寄存器和栈,进程则占有堆、栈。


线程安全

多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。


线程的创建方式

  1. 继承 Thread 类

  2. 实现 Runnable 接口

  3. 匿名内部类创建线程对象

  4. 实现 Callable 接口

  5. 定时器 Timer

  6. 线程池创建线程

  7. 利用java8新特性 stream 实现并发


如何停止一个正在运行的线程

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

  2. 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的
    方法。

  3. 使用interrupt方法中断线程。


start()方法和run()方法的区别

只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。

如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。


Runnable 接口和 Callable 接口的区别

  1. Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而
    已。

  2. Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取
    异步执行的结果。


FutureTask是什么

FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。


线程有哪些基本状态

就绪态(Runnable):新创建了一个线程对象。

运行态(Running):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位
于可运行线程池中,变得可运行,等待获取CPU的使用权

等待态(Wait):等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

阻塞态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

阻塞的情况分三种:
1. 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
2. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
3. 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

终止态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。


在多线程中,什么是上下文切换

单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。


如何确保线程安全?

  • 对非安全的代码进行加锁控制

  • 使用线程安全的类

  • 多线程并发情况下,线程共享的变量改为方法级的局部变量


用户线程和守护线程有什么区别

  • 守护线程都是为JVM中所有非守护线程的运行提供便利服务: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

  • 用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了


线程安全的级别

  • 不可变(Integer、String 和 BigInteger 等。)

  • 无条件的线程安全(Random 、ConcurrentHashMap、Concurrent集合、atomic)

  • 有条件的线程安全( Hashtable 或者 Vector 或者返回的迭代器)

  • 非线程安全(ArrayList、HashMap)

  • 线程对立(System.setOut()、System.runFinalizersOnExit())


线程优先级

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OSdependent)。

可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级。


volatile关键字的作用

  • 保证可见性

  • 不保证原子性

如果不加lock和synchronized ,怎么样保证原子性?
答:使用原子类 AtomicInteger,解决原子性问题

  • 禁止指令重排

什么是指令重排?

我们写的程序,计算机并不是按照我们自己写的那样去执行的
源代码 –> 编译器优化重排 –> 指令并行也可能会重排 –> 内存系统也会重排 –> 执行

volatile可以避免指令重排:

volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。

内存屏障:CPU指令。作用:

  1. 保证特定的操作的执行顺序;
  2. 可以保证某些变量的内存可见性(利用这些特性,就可以保证 volatile 实现的可见性)

sleep方法和wait方法有什么区别

  • sleep()是 Thread 的一个方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复。

  • wait()是 Object 的方法,调用会放弃对象锁,进入等待队列,待调用 notify() / notifyAll() 唤醒指定的线程或者所有线程,才会进入锁池,竞争同步锁,进而得到执行。


sleep()方法和yield()方法有什么区别

  1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

  2. 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;

  3. sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;

  4. sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。


Thread.sleep(0)的作用是什么

由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。


notify()和notifyAll()有什么区别

notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。

  • void notify(): 唤醒一个正在等待该对象的线程。
  • void notifyAll(): 唤醒所有正在等待该对象的线程。

notify可能会导致死锁,而notifyAll则不会。

任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码使用notifyall,可以唤醒所有处于wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。

wait() 应配合 while 循环使用,不应使用 if,务必在 wait() 调用前后都检查条件,如果不满足,必须调用 notify() 唤醒另外的线程来处理,自己继续 wait() 直至条件满足再往下执行。

notify() 是对 notifyAll() 的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死锁。正确的场景应该是 WaitSet 中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果唤醒的线程无法正确处理,务必确保继续 notify() 下一个线程,并且自身需要重新回到 WaitSet 中。


线程类的构造方法、静态块是被哪个线程调用的

线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

例子:Thread2 中 new 了 Thread1,main 函数中 new 了 Thread2

  1. Thread2 的构造方法、静态块是main线程调用的,Thread2 的 run() 方法是 Thread2 自己调用的
  2. Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run() 方法是 Thread1 自己调用的

同步方法和同步块,哪个是更好的选择

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。

在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说 StringBuffer,它是一个线程安全的类,自然最常用的 append() 方法是一个同步方法,我们写代码的时候会反复 append 字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次 append 方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到 append 方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。


什么是CAS

CAS,全称为Compare and Swap,即比较并替换。

假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。

CAS一定要 volatile 变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次 CAS 操作失败,永远都不可能成功。


CAS 的缺陷

  • ABA 问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
解决方式:乐观锁,AtomicStampedReference

  • 循环时间长开销

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销

  • 只能保证一个变量的原子操作。

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
解决方式:

  • 使用互斥锁来保证原子性;
  • 将多个变量封装成对象,通过AtomicReference来保证原子性。

什么是 AQS

AQS 以双向队列的形式连接所有的 Entry,比方说 ReentrantLock,所有等待的线程都被放在一个 Entry 中并连成双向队列,前面一个线程使用ReentrantLock 好了,则双向队列实际上的第一个 Entry 开始运行。
AQS 定义了对双向队列所有的操作,而只开放了 tryLock 和 tryRelease 方法给开发者使用,开发者可以根据自己的实现重写 tryLock 和 tryRelease 方法,以实现自己的并发功能。


CountDownLatch

AQS实现,volatile 变量 state 维持倒数状态,多线程共享变量可见

  1. 主线程调用CountDownLatch.await()时 会尝试获得锁 获取成功的标志是 state=0(该state 初始化CountDownLatch时传入的)

  2. 如果获取失败就会进入同步队列阻塞。

  3. 其它线程 调用CountDownLatch.countDown时,会尝试释放锁,释放成功的条件是 state由1变成0,这时会唤醒同步队列中的主线程。

  4. 主线程醒来后再循环中继续尝试获取锁,这时候state已经等于0,获取锁成功,返回await方法返回,主线程继续执行。

原理:
countDownLatch.countDown(); // 每个线程都数量 -1
countDownLatch.await(); // 等待计数器归零 然后向下执行
每次线程调用 countDown() 数量 -1,假设计数器变为0,await() 就会被唤醒,继续执行


CyclicBarrier

可重用栅栏

用于当前线程需要等待其他线程操作完成后继续操作,内部维护一个count标志需要同步的线程数,每个线程调用CycliecBarrier.await方法时都会–count,如果count!= 0 执行condition.awiat 进入等待队列,所以最后一个线程执行时 count=0,会唤醒所有阻塞线程。


CyclicBarrier 和 CountDownLatch 的区别

两个看上去有点像的类,都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上,二者的区别在于:

  1. CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行; CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。

  2. CyclicBarrier 只能唤起一个任务, CountDownLatch 可以唤起多个任务。

  3. CyclicBarrier 可重用, CountDownLatch 不可重用,计数值为0该 CountDownLatch 就不可再用了。


Semaphore

Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可,内部类 Sync extends AbstractQueuedSynchronizer设置 state为 信号量数量,在acquire 和 release时 维护数量,acquire时 remaining = available - acquires remaining < 0 则进入同步队列 阻塞,release时唤醒同步队列中的阻塞线程

原理:

  • semaphore.acquire() 获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!
  • semaphore.release() 释放,会将当前的信号量释放+1,然后唤醒等待的线程!

作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!


线程池作用

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控


如何创建线程池

三大方法:

ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线程
ExecutorService threadPool2 = Executors.newFixedThreadPool(5); // 创建一个固定的线程池的大小
ExecutorService threadPool3 = Executors.newCachedThreadPool(); // 可伸缩的

线程池的七大参数

本质:三种方法都是开启的 ThreadPoolExecutor

ThreadPoolExecutor 源码:

public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
                          int maximumPoolSize, // 最大的线程池大小
                          long keepAliveTime, // 超时了没有人调用就会释放
                          TimeUnit unit, // 超时单位
                          BlockingQueue<Runnable> workQueue, // 阻塞队列
                          ThreadFactory threadFactory, // 线程工厂,创建线程的一般不用动
                          RejectedExecutionHandler handler // 拒绝策略
                         ) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

在阿里巴巴开发手册中,建议用 ThreadPoolExecutor 创建。

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM

自定义线程池:

public class PollDemo {
    public static void main(String[] args) {
        // 获取cpu 的核数
        int max = Runtime.getRuntime().availableProcessors();

        ExecutorService service =new ThreadPoolExecutor(
                2, // 默认开启2个窗口
                5, // 最多5个窗口
                3, // 超时了没有人调用就会释放
                TimeUnit.SECONDS, // 超时单位
                new LinkedBlockingDeque<>(3), // 候客区人数
                Executors.defaultThreadFactory(), // 线程工厂,创建线程的一般不用动
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );
        try {
            for (int i = 1; i <= 10; i++) {
                service.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "ok");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            service.shutdown();
        }
    }
}

线程池四种拒绝策略

  • new ThreadPoolExecutor.AbortPolicy():丢弃任务并抛出RejectedExecutionException异常。

  • ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。

  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。


Executor 和 ExecutorService 区别

  • ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口。

  • Executor 和 ExecutorService 第二个区别是:Executor 接口定义了 execute()方法用来接收一个
    Runnable接口的对象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable
    接口的对象。

  • Executor 和 ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,
    而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。

  • Executor 和 ExecutorService 接口第四个区别是除了允许客户端提交一个任务,ExecutorService
    还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。


Synchronized

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。


Synchronized 的三种应用方式

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。

  2. 修饰静态方法,作用于当前类加锁,进入同步代码前要获得当前类的锁。

  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象。


Synchronized 底层原理

对于一个synchronized修饰的方法(代码块)来说:

  1. 当多个线程同时访问该方法,那么这些线程会先被放进 _EntryList 队列,此时线程处于blocked状态。

  2. 当一个线程获取到了对象的monitor后,那么就可以进入 running 状态,执行方法,此时,ObjectMonitor 对象的 /_owner 指向当前线程,_count 加 1 表示当前对象锁被一个线程获取。

  3. 当 running 状态的线程调用 wait() 方法,那么当前线程释放monitor对象,进入 waiting 状态,ObjectMonitor 对象的 /_owner 变为 null,_count 减 1,同时线程进入 _WaitSet 队列,直到有线程调用 notify() 方法唤醒该线程,则该线程进入 _EntryList 队列,竞争到锁再进入 _Owner 区。

  4. 如果当前线程执行完毕,那么也释放 monitor 对象,ObjectMonitor 对象的 /_owner 变为 null,_count 减 1。

原文在这!写的很详细


Synchronized 锁优化(升级)

synchronized 锁有四种状态,无锁,偏向锁,轻量级锁,重量级锁

锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态

jdk1.6之前都是重量级锁,大多数时候是不存在锁竞争的,如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入锁升级。

偏向锁

线程1获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID
线程1获取该锁时,比较threadID是否一致 ------- 一致 -> 直接进入而无需使用CAS来加锁、解锁
线程2获取该锁时,比较threadID是否一致 ------- 不一致 -> 检查对象的threadID线程是否还存活
存活:代表该对象被多个线程竞争,于是升级成轻量级锁
不存活:将锁重置为无锁状态,锁头重新标记线程2为新的threadID
(如果线程1和线程2的执行时间刚好错开,那么锁只会在偏向锁之间切换而不会升级为轻量级锁,在使用synchronized的情况下避开了获取锁的成本,所以效率和无锁状态非常接近)

轻量级锁

对象被多个线程竞争时,锁由偏向锁升级为轻量级锁,轻量级锁采用自旋+CAS方式不断获取锁。

重量级锁

当线程的自旋次数过长依旧没获取到锁,为避免CPU无端耗费,锁由轻量级锁升级为重量级锁。
获取锁的同时会阻塞其他正在竞争该锁的线程,依赖对象内部的监视器(monitor)实现,monitor又依赖操作系统底层,需要从用户态切换到内核态,成本非常高。


Lock

在jdk1.5以后,增加了juc并发包且提供了Lock接口用来实现锁的功能,它除了提供了与synchroinzed关键字类似的同步功能,还提供了比synchronized更灵活api实现。
在这里插入图片描述


ReentrantLock

可重入锁。如果当前线程t1通过调用lock方法获取了锁之后,再次调用lock,是不会再阻塞去获取锁的,直接增加重入次数就行了。与每次lock对应的是unlock,unlock会减少重入次数,重入次数减为0才会释放锁。


ReentrantReadWriteLock

可重入读写锁。读写锁维护了一个读锁,一个写锁。

读锁同一时刻允许多个读线程访问。

写锁同一时刻只允许一个写线程,其他读/写线程都需要阻塞。

在这里插入图片描述


Synchronized 与 Lock 的区别

  1. Synchronized 内置的Java关键字,Lock 是一个Java类

  2. Synchronized 无法判断获取锁的状态,Lock 可以判断

  3. Synchronized 会自动释放锁,Lock 必须要手动加锁和手动释放锁!可能会遇到死锁

  4. Synchronized 线程1(获得锁->阻塞)、线程2(等待);Lock 就不一定会一直等待下去,Lock 会有一个trylock 去尝试获取锁,不会造成长久的等待。

  5. Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;

  6. Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码;


Synchronized 和 ReentrantLock 的区别

  1. 底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
    synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。

  2. 是否可手动释放:
    synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。

  3. 是否可中断
    synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

  4. 是否公平锁
    synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。

  5. 锁是否可绑定条件Condition
    synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。

  6. 锁的对象
    synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。


死锁

在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再程序往下执行。我们只能通过中止并重启的方式来让程序重新执行。

死锁的四个必要条件

  1. 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
  2. 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  3. 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。

ThreadLocal 是什么

ThreadLocal,即线程本地变量。如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题


ThreadLocal 原理

  • Thread对象持有一个成员变量 ThreadLocal.ThreadLocalMap threadLocals,即每个线程都有一个属于自己的 ThreadLocalMap。

  • ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 本身,value 是 ThreadLocal 的泛型值。

  • 每个线程在往 ThreadLocal 里设置值的时候,都是往自己的 ThreadLocalMap 里存,读也是以某个ThreadLocal 作为引用,在自己的 map 里找对应的 key,从而实现了线程隔离。


ThreadLocal 内存泄露问题

弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。

弱引用比较容易被回收。因此,如果 ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是因为 ThreadLocalMap 生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap 的key没了,value 还在,这就会造成了内存泄漏问题。

解决方式:使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间。


参考文章:
https://zhuanlan.zhihu.com/p/126085068
https://juejin.cn/post/6855840932869996558#heading-7
https://juejin.cn/post/6855840932869996558#heading-14
https://blog.youkuaiyun.com/zzti_erlie/article/details/103997713

推荐文章:《我想进大厂》之Java基础夺命连环16问

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值