Java并发编程面试题53道-JUC

Java中的JUC是"Java Concurrency Utilities"的缩写,它是指Java平台从Java 5版本开始引入的一系列用于处理多线程并发编程的工具类和框架。这个包(java.util.concurrent)极大地增强了Java在并发编程领域的支持,提供了一系列高级抽象如线程池(ThreadPoolExecutor)、并发集合(ConcurrentHashMap、CopyOnWriteArrayList等)、同步器(Semaphore、CountDownLatch、CyclicBarrier、ReentrantLock等)以及其他并发工具和框架。通过使用JUC,开发者能够更加方便地设计出高效且线程安全的代码,同时减少了编写低级并发控制结构的复杂性,并有助于避免常见的并发错误,比如死锁和竞态条件。JUC让Java程序员可以更专注于业务逻辑的并发实现,而不用过多关注底层并发控制机制的实现细节。

目录

1.Java中线程的创建方式?

2.什么是线程池,有哪几种常用的线程池?

3.线程的生命周期?

4.终止线程的四种方式?

5.sleep()与wait()方法的区别?

6.start()与run()的区别?

7.什么是线程安全?java中如何保证线程安全?Vector是一个线程安全类吗?

8.什么是volatile关键字?它有什么作用?它的实现原理?

9.什么是死锁?如何避免死锁?

10.什么是CAS操作?底层原理?它有什么特点(优缺点)?

11.什么是线程间通信?如何实现线程间通信?

12.线程、进程、协程、程序的区别?

13.什么是原子操作?Java中如何保证原子操作?

14.什么是线程的上下文切换?如何减少上下文切换的开销?

15.什么是线程安全的集合?Java中有哪些线程安全的集合类?

16.说一下JMM(Java内存模型)?再介绍一下JVM的内存模型?

[17.如何创建线程池,线程池中 sumbit() 和 execute() 方法有什么区别?线程池的7大参数?](#17.如何创建线程池,线程池中 sumbit() 和 execute() 方法有什么区别?线程池的7大参数?)

18.为什么使用线程池,线程池的底层原理?

19.线程池的四种拒绝策略?

[20.什么是 AQS 吗?了解 AQS 共享资源的方式吗?](#20.什么是 AQS 吗?了解 AQS 共享资源的方式吗?)

21.sychronized的底层原理?锁升级过程?

[22.说说 AtomicInteger 和 synchronized 的异同点?](#22.说说 AtomicInteger 和 synchronized 的异同点?)

[23.原子类和 volatile 有什么异同?](#23.原子类和 volatile 有什么异同?)

24.什么是CountDownLatch、CyclicBarrier、Semaphore?

25.CountDownLatch、CyclicBarrier、Semaphore的区别?

[26.synchronized 和 lock 有什么区别?synchronized 和 Lock 如何选择?Lock接口的主要方法?](#26.synchronized 和 lock 有什么区别?synchronized 和 Lock 如何选择?Lock接口的主要方法?)

27.tryLock、lock和lockInterruptibly的区别?

28.什么是阻塞队列?列举几个常见的阻塞队列?什么是非阻塞队列?

29.什么是ThreadLocal,它是线程安全的吗?底层原理是什么?会存在内存泄露吗?

30.HashTable、HashMap、ConcurrentHashMap有什么区别?

31、什么是乐观琐,什么是悲观锁,什么是公平锁,什么是非公平琐?

32、java中常见的四种引用类型?

[33、java线程中涉及到的常见方法及其意义?线程基本方法 、线程等待(wait) 、线程睡眠(sleep) 线程让步(yield) 线程中断(interrupt) 、Join等待其他线程终止 、为什么要用join()方法?](#33、java线程中涉及到的常见方法及其意义?线程基本方法 、线程等待(wait) 、线程睡眠(sleep) 线程让步(yield) 线程中断(interrupt) 、Join等待其他线程终止 、为什么要用join()方法?)

34、并发编程的三要素?

35、ReentrantLock与sychronized的区别?实现原理?使用场景?

36、介绍一下ReentrantReadwriteLock?

37、并发编程解决生产者与消费者模型的常用的几种方式?

38、什么是可重入琐与不可重入琐?

39、什么是共享锁与排它锁?

40、什么是自旋锁?

41、Java中Runnable和Callable有什么不同?

42、什么是FutureTask?

43、如何在Java中获取线程堆栈?JVM中哪个参数是用来控制线程的栈堆栈小的?

44、Thread类中的yield方法有什么作用?

45、有三个线程T保它1,T2,T3,怎么确们按顺序执行?

[46、一个线程运行时发生异常会怎样? java中 如何在两个线程间共享数据?](#46、一个线程运行时发生异常会怎样? java中 如何在两个线程间共享数据?)

[47、 Java中notify 和 notifyAll有什么区别? 为什么wait, notify 和 notifyAll这些方法不在thread类里面?](#47、 Java中notify 和 notifyAll有什么区别? 为什么wait, notify 和 notifyAll这些方法不在thread类里面?)

48、Java中堆和栈有什么不同?

[49、Java中活锁和死锁有什么区别? 怎么检测一个线程是否拥有锁?](#49、Java中活锁和死锁有什么区别? 怎么检测一个线程是否拥有锁?)

[50、AQS使用了哪些设计模式?AQS 组件了解吗?](#50、AQS使用了哪些设计模式?AQS 组件了解吗?)

51、线程池有哪几种工作队列?

52.线程池异常怎么处理知道吗?

53.线程池有几种状态吗?


1.Java中线程的创建方式?

1)继承Thread类,重写run()方法

java 复制代码
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread,重写run方法创建线程");
    }
}
 
public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

2)实现Runnable接口,重写run方法

java 复制代码
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable接口,重写run方法");
    }
}
 
public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

3)通过匿名内部类或者lambda表达式的方式实现,本质上还是前面两种

4)实现Callable接口,重写call方法(call方法可以理解为线程需要执行的任务),并且带有返回值,这个返回表示一个计算结果,如果无法计算结果,则引发Exception异常

java 复制代码
class MyCallableTest implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("创建线程:" + Thread.currentThread().getName());
        return 2;
    }
}
 
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask<>(new MyCallableTest());
        Thread thread = new Thread(task);
        thread.start();
        System.out.println("创建线程的返回结果为:" + task.get());
    }
 
}

5)使用线程池创建线程,使用submit方法,把任务提交到线程池中即可,线程池中会有线程来完成这些任务

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Pool {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        pool.submit(new Runnable() {
            @Override
            public void run() {
                //执行业务逻辑
                for(int i = 1; i <= 100; i++) {
                    System.out.println("线程:" + Thread.currentThread().getName() + "执行了任务" + i + "~");
                }
            }
        });
        pool.submit(new Runnable() {
            @Override
            public void run() {
                //执行业务逻辑
                for(int i = 101; i <= 200; i++) {
                    System.out.println("线程:" + Thread.currentThread().getName() + "执行了任务" + i + "~");
                }
            }
        });
        pool.submit(new Runnable() {
            @Override
            public void run() {
                //执行业务逻辑
                for(int i = 201; i <= 300; i++) {
                    System.out.println("线程:" + Thread.currentThread().getName() + "执行了任务" + i + "~");
                }
            }
        });
    }
}
2.什么是线程池,有哪几种常用的线程池?

线程池:事先创建若干个可执行的线程放入一个池(容器) 中, 需要的时候从池中获取线程不用自行创建, 使用完毕不需要销毁线程而是放回池中, 从而减少创建和销毁线程对象的开销。

Java中四种常用的线程池由java.util.concurrent包中的工具类Executors提供,它们是通过不同的参数配置创建出不同特性的ThreadPoolExecutor实例,四种常用的线程池:

1)Executors.newFixedThreadPool(int n):有固定数量线程的线程池。

  • 特点:线程池的大小是固定的,且在核心线程数和最大线程数相等的情况下,所有线程都为非守护线程。
  • 使用场景:适用于处理大量短生命周期的任务,能有效控制并发线程的数量,防止过多线程消耗系统资源。
  • 阻塞队列通常使用无界队列(如LinkedBlockingQueue),当线程池满载时,新提交的任务会被放入队列等待执行。

2)Executors.newCacheThreadPool():可缓存线程池。

  • 特点:线程池的大小是可变的,并且可以无限增长,直到系统资源耗尽。当一个任务完成时,该线程会返回到线程池中进行复用,如果线程池中的线程数量超过核心线程数并且有空闲线程,则会回收一些空闲线程。
  • 使用场景:适合处理大量临时、快速的任务,例如网络请求或IO密集型操作。
  • 阻塞队列通常使用SynchronousQueue,这意味着每个任务都会直接分配给线程,如果没有可用线程,则会尝试创建新的线程来执行任务。

3) Executors.newScheduledThreadPool(int n):支持定时与周期性任务执行的线程池。

  • 特点:这是一个定长线程池(线程池中核心线程的数量是固定的),主要用于执行定时及周期性任务。
  • 使用场景:当你需要按计划调度任务执行时,比如定期执行清理工作、统计报告生成等。
  • 可以指定核心线程数,同时提供了schedule()和scheduleAtFixedRate()等方法来安排任务在固定延迟后执行,或者按照固定速率执行。

4) Executors.newSingleThreadExecutor():

  • 特点:线程池只有一个工作线程,因此所有的任务都是串行执行的。
  • 使用场景:适用于需要保证顺序执行任务的场景,或者系统资源有限不想额外开销更多线程的情况。
  • 同样,由于只有一个工作线程,所以阻塞队列的作用在于存储待执行的任务。
3.线程的生命周期?

线程的生命周期包括:新建、就绪、运行、阻塞、死亡。

新建:当线程对象创建后即进入了新建状态(如:Thread th= new MyThread();)

就绪:当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

运行:当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。(注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;)

阻塞:处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3.其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

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

4.终止线程的四种方式?

1.程序运行结束,线程自动结束。

2.使用退出标志退出线程,定义了一个退出标志 exit,在run()方法中,当 exit 为 true 时,while 循环退出,exit 的默认值为 false。在定义 exit 时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

3.Interrupt 方法结束线程。

4.使用stop方法。

5.sleep()与wait()方法的区别?

1、这两个方法来自不同的类分别是Thread和Object,sleep方法属于Thread类中的静态方法,wait属于Object的成员方法。

2、sleep()是线程类(Thread)的方法,不涉及线程通信,调用时会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,用于线程间的通信,调用时会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才进入对象锁定池准备获得对象锁进入运行状态。

3、wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)。

4、sleep()方法必须捕获异常InterruptedException,而wait()\notify()以及notifyAll()不需要捕获异常。

6.start()与run()的区别?

创建线程:创建态,调用start()方法:就绪态,获取cpu调度:执行run()方法运行态。

1、start方法用来启动相应的线程;

2、run方法只是thread的一个普通方法,在主线程里执行;

3、需要并行处理的代码放在run方法中,start方法启动线程后自动调用run方法;

4、run方法必须是public的访问权限,返回类型为void。

7.什么是线程安全?java中如何保证线程安全?Vector是一个线程安全类吗?

线程安全是多线程编程中的一种概念,它涉及到如何在多个线程同时访问资源时确保资源的正确使用和保护。线程安全意味着一个类或方法在多线程环境中可以安全地被多个线程同时访问,而不会导致数据的不一致或者其他不可预知的结果。

在Java中保障线程安全有多种方式。以下是其中几种常见的方式:

  1. 使用同步方法或同步代码块:通过在方法声明中添加synchronized关键字或在代码块中使用synchronized关键字来确保在同一时间只有一个线程可以访问方法或代码块。这样可以防止多个线程同时访问共享资源。

  2. 使用ReentrantLock类:ReentrantLock类是Java提供的一个可重入锁类,可以通过调用其lock()方法获取锁,并在操作完共享资源后调用unlock()方法释放锁。这样可以确保只有一个线程可以获取到锁,并执行相关操作。

  3. 使用volatile关键字:在多线程环境下,volatile关键字可以确保每次读取变量时都从主内存中读取,并且每次修改变量时都立即写入主内存。这样可以避免线程之间的数据不一致问题。

  4. 使用Atomic类:Atomic类是Java提供的一组原子操作类,可以保证对可变变量的读取和修改操作具有原子性。这样可以确保多个线程同时访问同一个变量时不会发生数据竞争。

  5. 使用线程安全的数据结构:Java提供了一些线程安全的数据结构,如ConcurrentHashMap、ConcurrentLinkedQueue等,它们内部实现了线程安全的操作,可以在多线程环境下安全地使用。

  6. 使用ThreadLocal类:ThreadLocal类可以为每个线程提供独立的变量副本,确保每个线程都可以访问自己的变量副本,避免了线程间的数据竞争。

Vector 是线程安全类。Vector 的实现方式是使用同步锁 synchronized 来保证线程安全。因此,在多线程环境下,多个线程可以同时访问 Vector 中的元素,而不会出现数据错误的问题。不过,由于使用 synchronized 会带来一定的性能损耗,因此在单线程环境下,使用 ArrayList 比使用 Vector 更容易获得更好的性能表现。

8.什么是volatile关键字?它有什么作用?它的实现原理?

volatile关键字修饰的变量可以保证可见性与有序性,无法保证原子性。

当一个变量被定义成volatile之后,它将具备两项特性:第一项是保证此变量对所有线程的可见性,这里的"可见性"是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

使用volatile变量的第二个语义是禁止指令重排序优化,保证有序性,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

原理:

  • 将线程工作内存中的值写回主内存中
  • 通过缓存一致性协议,令其他线程工作内存中的该共享变量值失效
  • 其他线程会重新从主内存中获取最新的值

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障(lock指令)实现其在内存中的语义,即可见性和禁止重排优化。

9.什么是死锁?如何避免死锁?

当两个或多个线程同时持有自己的锁,并且等待对方释放锁,就会形成死锁。简单来说,死锁就是两个或多个线程无限期地阻塞等待对方持有的锁。

避免死锁最简单的方法是破坏死锁的4个必要条件之一。

死锁的产生必须同时满足以下四个必要条件:

  1. 互斥条件(Mutual exclusion):至少有一个资源被持有,且在任意时刻只有一个进程能够使用该资源。

  2. 请求与保持条件(Hold and wait):进程已经持有至少一个资源,并且在等待获取其他进程持有的资源。

  3. 不剥夺条件(Non-preemption):进程已经获得的资源在未使用完之前不能被剥夺,只能自愿释放。

  4. 循环等待条件(Circular wait):进程之间形成一种头尾相接的循环等待资源关系。

这四个条件缺一不可,同时满足这四个条件才会发生死锁。因此,避免死锁必须从这四个条件入手,打破其中任意一个条件都能够避免死锁的发生。

具体地,在java中,为了避免死锁,可以采取以下策略:

避免嵌套锁 :尽量避免在一个线程中同时持有多个锁。如果必须这样做,确保每次只锁定一个资源,然后释放它以获取下一个资源。
锁顺序 :确保所有线程都按照相同的顺序获取锁。这样可以避免循环等待条件,因为每个线程都知道下一个锁的位置,从而避免了死锁。
锁超时 :为锁设置超时时间,以便在等待时间过长时放弃锁定。这样可以避免线程无限期地等待其他线程释放资源。
锁分段 :将一个大的锁分割成多个小的锁,以减少多个线程同时争夺同一个锁的可能性。这样可以降低死锁的风险。
避免在持有锁时进行I/O操作 :I/O操作可能导致线程阻塞,这会增加死锁的风险。尽量在持有锁之前完成I/O操作,或者使用异步I/O来避免阻塞。
使用java.util.concurrent包中的工具:Java提供了java.util.concurrent包中的工具类,如Lock、Semaphore和CountDownLatch等,可以帮助避免死锁。这些工具类提供了更灵活的锁定机制,可以更好地控制并发访问资源的方式。

10.什么是CAS操作?底层原理?它有什么特点(优缺点)?

CAS(Compare and Swap)是一种乐观锁机制,它也被称为无锁机制。CAS算法的作用:解决多线程条件下使用悲观锁造成性能损耗问题的算法,CAS基于乐观锁思想来设计的,其不会引发阻塞,synchronized会导致阻塞。CAS保证原子性,这个原子操作是由CPU来完成的。

CAS原理:CAS算法有三个操作数,通过内存中的值(V)、预期原始值(A)、修改后新值(B)。

(1)如果内存中的值和预期原始值相等, 就将修改后的新值保存到内存中。

(2)如果内存中的值和预期原始值不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作,直到重试成功。

注意:

(1)预期原始值(A)是从偏移位置读取到三级缓存中让CPU处理的值,修改后的新值是预期原始值经CPU处理暂时存储在CPU的三级缓存中的值,而内存指定偏移位置中的原始值。

(2)比较从指定偏移位置读取到缓存的值与指定内存偏移位置的值是否相等,如果相等则修改指定内存偏移位置的值,这个操作是操作系统底层汇编的一个原子指令实现的,保证了原子性。

AtomicInteger等原子类没有使用synchronized锁,而是通过volatile和CAS(Compare And Swap)解决资源的线程安全问题。
(1)volatile保证了可见性和有序性。
(2)CAS保证了原子性,而且是无锁操作,提高了并发效率。

CAS机制实现的锁是自旋锁,如果线程一直无法获取到锁,则一直自旋,不会阻塞

CAS线程不会阻塞,线程一致自旋。

syncronized会阻塞线程,会进行线程的上下文切换,会由用户态切换到内核态,切换前需要保存用户态的上下文,而内核态恢复到用户态,又需要恢复保存的上下文,非常消耗资源。

1)ABA问题

如果一个线程t1正修改共享变量的值A,但还没修改,此时另一个线程t2获取到CPU时间片,将共享变量的值A修改为B,然后又修改为A,此时线程t1检查发现共享变量的值没有发生变化,但是实际上却变化了。

解决办法: 使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JUC包里提供了一个类AtomicStampedReference来解决ABA问题。AtomicStampedReference类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

(2)循环时间长开销会比较大:自旋重试时间,会给CPU带来非常大的执行开销

(3)只能保证一个共享变量的原子操作,不能保证同时对多个变量的原子性操作

CAS只能保证变量的原子性,不能保证变量的内存可见性。CAS获取共享变量的值时,需要和volatile配合使用,来保证共享变量的可见性

11.什么是线程间通信?如何实现线程间通信?

线程通信指的是多个线程之间通过共享内存或消息传递等方式来协调和同步它们的执行。在多线程编程中,通常会出现多个线程需要共同完成某个任务的情况,这时就需要线程之间进行通讯,以保证任务能够顺利地执行。

Java中线程通讯的实现方法有以下几种:

  1. 等待和通知机制:使用 Object 类的 wait() 和 notify() 方法来实现线程之间的通讯。当一个线程需要等待另一个线程执行完某个操作时,它可以调用 wait() 方法使自己进入等待状态,同时释放占有的锁,等待其他线程调用 notify() 或 notifyAll() 方法来唤醒它。被唤醒的线程会重新尝试获取锁并继续执行。

  2. 信号量机制:使用 Java 中的 Semaphore 类来实现线程之间的同步和互斥。Semaphore 是一个计数器,用来控制同时访问某个资源的线程数。当某个线程需要访问共享资源时,它必须先从 Semaphore 中获取一个许可证,如果已经没有许可证可用,线程就会被阻塞,直到其他线程释放了许可证。

  3. 栅栏机制:使用 Java 中的 CyclicBarrier 类来实现多个线程之间的同步,它允许多个线程在指定的屏障处等待,并在所有线程都达到屏障时继续执行。

  4. 锁机制:使用 Java 中的 Lock 接口和 Condition 接口来实现线程之间的同步和互斥。Lock 是一种更高级的互斥机制,它允许多个条件变量(Condition)并支持在同一个锁上等待和唤醒。

12.线程、进程、协程、程序的区别?

程序其实就是存在操作系统上的一大堆指令(指令序列),是一个静态的概念。

进程是操作系统分配资源(比如内存)和调度的基本单位,是一个动态的概念。

线程是运行(执行)的基本单位,是轻量级的进程,但是和进程不同的是,线程它没有自主独立的操作空间和资源,因为一个进程往往有很多个线程并发进行,因此它们一般是共享自己本进程的资源和内存的。

一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。协程的本质其实也是一个线程。

13.什么是原子操作?Java中如何保证原子操作?

原子操作是指在执行过程中不会被其他线程中断的操作。它要么全部执行成功,要么全部不执行,不存在中间状态。原子操作可以保证数据的一致性和线程安全性。

在Java中,提供了一些原子操作的类,如AtomicInteger、AtomicLong、AtomicBoolean等。这些类提供了一些常见的原子操作方法,如get、set、incrementAndGet、compareAndSet等。

通过使用原子操作类,我们可以在多线程环境下安全地对共享变量进行操作,而不需要使用锁机制或同步代码块。

14.什么是线程的上下文切换?如何减少上下文切换的开销?

线程上下文切换是指CPU从一个线程中断,转而执行另一个线程的过程。

线程上下文切换一般会发生在如下场景:

1、当一个线程的时间片用完,操作系统会将其挂起,转而执行其他可运行的线程,从而发生上下文切换。

2、等待事件:当一个线程需要等待某个事件发生时,它会进入阻塞状态,此时操作系统会将该线程的上下文保存在内存中,并切换到其他线程。当等待的事件发生后,操作系统会恢复之前被阻塞的线程的上下文,并将其重新调度执行。

3、线程同步:当多个线程需要同时访问共享资源时,由于缓存一致性协议的需要,此时会发生线程上下文的切换。

4、线程被中断:当一个线程被强制终止时,操作系统会回收其上下文信息,并切换到其他正在运行的线程的执行现场。

线程上下文切换是一种比较消耗性能的操作,因为它需要保存和恢复大量的上下文信息。因此,减少线程上下文切换的次数可以提高系统的性能。

无锁并发编程:就是多线程竞争锁时,会引起上下文切换,多线程处理数据时,可以用一 些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

CAS算法:Java的Atomic包使用CAS算法来更新数据,就是它在没有锁的状态下,可以保证多个线程对一个值的更新。

使用最少线程:避免创建不需要的线程。

协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

15.什么是线程安全的集合?Java中有哪些线程安全的集合类?

线程安全集合是指该集合可以在多线程并发读取的状态下,能够保持数据集合有序,不发生同步错误。

VectorArrayList类似,是长度可变的数组,与ArrayList不同的是,Vector是线程安全的,它几乎给所有的public方法都加上了sychronized关键字。由于加锁倒是性能降低,在不需要并发访问时,这种强制性的同步就显得多余,所以现在几乎没有什么人在使用。

java.util.concurrent包中的集合:ConcurrentHashMapHashTable

HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁的是整个对象。而ConcurrentHashMap是有更细粒度的锁。在JDK1.8之前,ConcurrentHashMap加的是分段锁,即Segment,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响。

JDK1.8之后对此进行了进一步的改进,取消了Segment,直接在table元素上加锁,实现对每一行加锁,进一步减小了并发冲突的概率。

16.说一下JMM(Java内存模型)?再介绍一下JVM的内存模型?

Java内存模型(Java Memory Model简称JMM) 是一种抽象的概念,并不真实存在,它描述的是一组规则或规范。通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

本质上JMM是为了在多线程下保证解决 原子性,可见性,有序性问题。

JVM内存模型:一般指运行时数据区,包括5个部分,程序计数器、虚拟机栈、本地方法栈、堆、方法区。

程序计数器是一个记录着当前线程所执行的字节码的行号指示器。

Java虚拟机栈(Java Virtual Machine Stacks)是每个线程运行时所需的内存。每个栈由多个栈帧(Stack Frame)组成,每个方法执行都会创建一个栈帧,对应着该方法调用时所占用的内存,栈帧包含局部变量表、操作数栈、动态连接、方法出口等。

本地方法栈(Native Method Stack)与虚拟机栈作用大致相同。区别是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native 方法服务。

是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。

为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能):

  • 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
  • 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大
  • 元空间(JDK1.8 之前叫永久代):像一些方法中的操作临时对象等,JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存(非堆)

方法区用于存储已被已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码等等。

17.如何创建线程池,线程池中 sumbit() 和 execute() 方法有什么区别?线程池的7大参数?

Java中四种常用的线程池由java.util.concurrent包中的工具类Executors提供,它们是通过不同的参数配置创建出不同特性的ThreadPoolExecutor实例。

区别:

1.返回值

`submit()` 方法可以接受 `Callable` 或 `Runnable` 类型的任务,并返回一个 `Future` 对象,你可以通过 `Future` 对象获取任务的执行结果或者取消任务执行。

`execute()` 方法只接受 `Runnable` 类型的任务,并且没有返回值。

2.异常处理

`submit()` 方法可以捕获任务执行过程中抛出的异常,你可以通过 `Future` 对象来获取任务执行过程中抛出的异常。

`execute()` 方法无法直接捕获任务执行中的异常,需要在任务内部进行异常处理,否则可能导致线程池中的线程被异常终止。

3.方法来源

`execute()` 方法是 `Executor` 接口中定义的方法,较为简单,用于执行 `Runnable` 任务。

`submit()` 方法是 `ExecutorService` 接口中定义的方法,在 `Executor` 的基础上增加了任务提交后可以获取任务执行结果的能力。

线程池的7大参数:

java 复制代码
public ThreadPoolExecutor(int corePoolSize,
                         int maximumPoolSize,
                         long keepAliveTime,
                         TimeUnit unit,
                         BlockingQueue<Runnable> workQueue,
                         ThreadFactory threadFactory,
                         RejectedExecutionHandler handler){}

1.核心线程数:线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。

2.最大线程数:当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。

3.空闲线程存活时间:一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定。

4.空闲线程存活时间单位:空闲线程存活时间的单位。

5.workQueue 工作队列:

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

①ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

6.threadFactory 线程工厂

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。

7.handler 拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:

①CallerRunsPolicy

该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。

②AbortPolicy

该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。

③DiscardPolicy

该策略下,直接丢弃任务,什么都不做。

④DiscardOldestPolicy

该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

18.为什么使用线程池,线程池的底层原理?

线程池是运用场景最多的并发框架,几乎所有需要一步或者并发执行任务的程序都可以使用线程池。使用线程池一般有以下三个好处:

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

②提高相应速度,当任务到达的时候,任务可以不需要等到线程创建就能立刻执行。

③提高线程的可管理性,线程是稀缺资源,使用线程池可以统一的分配、调优和监控。

线程池的底层原理:

待执行的任务提交后由分配的核心线程进行执行,当待执行的任务数目大于核心线程数,会将任务放到任务队列,当提交的任务装满了任务队列,会通过创建新的线程继续处理,当创建的线程数目达到最大线程数,就采用相应的拒绝策略。

19.线程池的四种拒绝策略?

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4种拒绝策略:

①CallerRunsPolicy

该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。

②AbortPolicy

该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。

③DiscardPolicy

该策略下,直接丢弃任务,什么都不做。

④DiscardOldestPolicy

该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。

20.什么是 AQS 吗?了解 AQS 共享资源的方式吗?

AQS ( Abstract Queued Synchronizer )是一个抽象的队列同步器,通过维护一个共享资源状态( Volatile Int State )和一个先进先出( FIFO )的线程等待队列来实现一个多线程访问共享资源的同步框架。

AQS 定义了两种资源共享方式 :独占式 (Exclusive)和共享式(Share)

  • 独占式:只有一个线程能执行,具体的 Java 实现有 ReentrantLock。
  • 共享式:多个线程可同时执行,具体的 Java 实现有 Semaphore和CountDownLatch。

AQS只是一个框架 ,只定义了一个接口,具体资源的获取、释放都 由自定义同步器去实现。不同的自定义同步器争用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等, AQS 已经在顶层实现好,不需要具体的同步器再做处理。

21.sychronized的底层原理?锁升级过程?

synchronized是JDK自带的一个关键字,用于在多线程的情况下,保证线程安全;在JDK1.5之前是一个重量级锁,1.6之后进行了优化,性能有很大提升。synchronized可以用来同步方法同步代码块同步静态方法。

Synchronized底层通过⼀个monitor的对象来完成,每个对象有⼀个监视器锁(monitor)。当monitor被占⽤时就会处于锁定状态,线程执⾏monitorenter指令时尝 试获取monitor的所有权,过程如下:

(1)如果monitor的进⼊数为0,则该线程进⼊monitor,然后将进⼊数设置为1,该线程即为monitor的所有者。

(2)如果线程已经占有该monitor,只是重新进⼊,则进⼊monitor的进⼊数加1。

(3)如果其他线程已经占⽤了monitor,则该线程进⼊阻塞状态,直到monitor的进⼊数为0,再重新尝试获取monitor的所有权。

执⾏monitorexit的线程必须是object所对应的monitor的所有者。指令执⾏时,monitor的进⼊数减1,如果减1 后进⼊数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。这样也就不难理解Synchronized是可重入锁了。

锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。

无锁:无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

轻量级琐(自旋锁):轻量级锁由偏向锁升级而来,偏向锁运行在一个线程同步块时,第二个线程加入锁竞争的时候,偏向锁就会升级为轻量级锁。

重量级琐:自旋失败,很大概率再一次自旋也是失败,因此直接升级成重量级锁,进行线程阻塞,减少cpu消耗,当锁升级为重量级锁后,未抢到锁的线程都会被阻塞,进入阻塞队列。

22.说说 AtomicInteger 和 synchronized 的异同点?

相同点:都是通过琐的机制解决多线程资源共享存在的安全问题

不同点:AtomicInteger是基于volatile和cas,属于乐观锁,volatile保证有序性与可见性,cas保证原子性,通过不断自旋的方式获琐,不释放cpu;synchronized关键字通过悲观锁的形式保证线程安全,有个琐升级的过程。

23.原子类和 volatile 有什么异同?

volatile用于保证可见性和有序性,可见性:通过内存屏障的方式实现,线程对共享变量的修改会强制写入主存。有序性:禁止指令重排序。volatile不保证原子性。

原子类用于保证多线程共享变量的原子性。

24.什么是CountDownLatch、CyclicBarrier、Semaphore?

在Java并发编程中,CountDownLatch、CyclicBarrier 和 Semaphore 是三种常用的同步工具类,它们都是 `java.util.concurrent` 包的一部分,用于多线程环境中的线程同步和协作。

  1. CountDownLatch
  • CountDownLatch 主要用来实现一个或多个线程等待其他线程完成一组操作后才能继续执行。它通过计数器来控制,构造时设置一个初始的计数值。

  • 当某个线程完成自己的任务后,调用 `countDown()` 方法使计数器减一。

  • 其他线程则可以调用 `await()` 方法进行等待,直到计数器归零(即所有线程都调用了 `countDown()`)。一旦计数器为0,`await()` 将返回,允许这些线程继续执行后续的任务。

  1. CyclicBarrier
  • CyclicBarrier 也用于让一组线程等待至某个点(或者说屏障),但与 CountDownLatch 不同的是,它可以被重用,因此得名"循环栅栏"。

  • 在初始化 CyclicBarrier 时指定一个参与线程的数量,当每个线程到达栅栏位置时调用 `await()` 方法,该方法会阻塞当前线程,直到所有线程都到达栅栏。

  • 所有线程都到达栅栏之后,CyclicBarrier 可以触发一个回调(通过 `CyclicBarrier` 构造函数传入 `Runnable`)或者仅仅释放所有等待的线程继续执行。

  • 当所有线程都到达并越过屏障后,CyclicBarrier 的计数器会自动重置,以便下一轮的使用。

  1. Semaphore
  • Semaphore(信号量)是一种更为通用的同步工具,它维护了一个许可证池,用于控制同时访问特定资源的线程数量。

  • 当一个线程想要访问受保护的资源时,需要首先获取一个许可证(通过调用 `acquire()` 或 `tryAcquire()` 方法),如果此时许可证可用,则线程会获得许可并进入临界区;若无可用许可证,则线程会被阻塞直到其他线程释放许可证。

  • 线程完成对共享资源的访问后,应调用 `release()` 方法释放许可证,使得其他等待的线程有机会获取许可证并执行相关操作。

这三个工具类均提供了灵活的方式来协调多线程间的同步和通信,根据实际场景选择合适的一个来优化程序的性能和正确性。

25.CountDownLatch、CyclicBarrier、Semaphore的区别?

它们三个都是用于实现线程同步的工具类。

CountDownLatch通过减数计数器的方式协调线程同步,当前线程执行完每个任务后,计数器减1,当计数器为0时允许其它线程执行任务。

CyclicBarrier要求所有线程必须到达栅栏位置才允许继续执行。

Semaphore是通过信号量的方式实现线程同步的,通过维护许可池的方式,每个线程想要进入临界区访问资源时,需要尝试获取许可,被允许后则可以访问资源。

26.synchronized 和 lock 有什么区别?synchronized 和 Lock 如何选择?Lock接口的主要方法?

区别:

1.synchronized是关键字,Lock是接口;

2.synchronized是隐式的加锁,lock是显式的加锁;

3.synchronized可以作用于方法上,lock只能作用于代码块;

4.synchronized底层采用的是objectMonitor,lock采用的AQS;

5.synchronized是阻塞式加锁,lock是非阻塞式加锁支持可中断式加锁,支持超时时间的加锁;

6.synchronized在进行加锁解锁时,只有一个同步队列和一个等待队列, lock有一个同步队列,可以有多个等待队列;

7.synchronized只支持非公平锁,lock支持非公平锁和公平锁;

8.synchronized使用了object类的wait和notify进行等待和唤醒, lock使用了condition接口进行等待和唤醒(await和signal);

9.lock支持个性化定制, 使用了模板方法模式,可以自行实现lock方法;

两种琐的选择:

从性能上来说,如果竞争资源不激烈,两者的性能差不多,而竞争资源非常激烈时,此时lock的性能要远远优于synchronized,所以,在具体使用时候,要根据适当情况进行选择。

lock接口的主要方法包括:

lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的。unLock()方法是用来释放锁的。

lock():如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。

tryLock():尝试获取琐成功返回true,否则返回false,可以设置获取琐的时间。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够 响应中断,即中断线程的等待状态。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

27.tryLock、lock和lockInterruptibly的区别?

lock():如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。

tryLock():尝试获取琐成功返回true,否则返回false,可以设置获取琐的时间。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够 响应中断,即中断线程的等待状态。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

28.什么是阻塞队列?列举几个常见的阻塞队列?什么是非阻塞队列?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。

1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。

2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。

7种常见的阻塞队列如下:

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

DelayQueue:一个使用优先级队列实现的无界阻塞队列。

SynchronousQueue:一个不存储元素的阻塞队列。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

非阻塞队列就是插入和移除不进行阻塞处理,在多线程处理生产者和消费者问题时候需要额外加入保证队列为空,其它线程不允许移除,队列为满,其它线程不允许插入操作。

29.什么是ThreadLocal,它是线程安全的吗?底层原理是什么?会存在内存泄露吗?

ThreadLocal主要关注的是如何为每个线程提供独立的数据副本,避免共享,在多线程共享变量的情况下保证线程安全。

ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对

象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的

值;

如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要

把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收,Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿ 动调⽤ThreadLocal的remove⽅法,⼿动清除Entry对象。

30.HashTable、HashMap、ConcurrentHashMap有什么区别?

HashTable、HashMap和ConcurrentHashMap是Java中用于存储键值对数据结构的三个不同实现,它们之间在设计原理、线程安全性和性能等方面存在显著差异。

  1. HashTable
  • 底层原理:HashTable基于散列表(哈希表)实现,它使用链地址法解决哈希冲突,即每个桶(bucket)内部采用链表存储多个哈希值相同的元素。

  • 线程安全性:HashTable是线程安全的,它在执行插入、删除和查找等操作时都会进行同步处理,这意味着每次只有一个线程能够访问Hashtable。它通过在方法上加锁(synchronized关键字修饰)来保证线程安全。

  • 性能:由于它的所有操作都是同步的,因此在高并发环境下,可能会因为竞争导致性能下降,不适合在多线程环境中有大量读写操作的场景。

  1. HashMap
  • 底层原理:HashMap同样基于散列表实现,但在JDK 1.8之前,其内部节点数组+链表/红黑树的方式管理数据;1.8之后,当链表长度超过阈值时会转换为红黑树以优化查询效率。哈希函数用于计算键的哈希码,并确定元素在数组中的位置。

  • 线程安全性:HashMap是非线程安全的,在多线程环境下不保证正确性,如果多个线程同时修改HashMap,可能导致数据不一致、死锁等问题。

  • 性能:HashMap在单线程环境中提供了很高的性能,因为它没有内置的同步机制,因此不需要额外的线程同步开销。

  1. ConcurrentHashMap
  • 底层原理:ConcurrentHashMap也是基于散列表实现,但它从JDK 1.7开始引入了分段锁(Segment + HashEntry)的设计,将整个散列表分为多个segment,每个segment可以独立地进行读写操作,从而实现更高的并发性能;而在JDK 1.8及以后版本,放弃了Segment的概念,改用了一种更细粒度的CAS和Synchronized相结合的方式来保证线程安全,底层的数据结构简化为Node数组,依然在必要时转为红黑树。

  • 线程安全性:ConcurrentHashMap是线程安全的,但与HashTable全表锁不同,它是分区或粒度化的锁策略,这意味着在多线程环境下,不同的部分可以同时进行读写操作,提高了并发性能。

  • 性能:相较于HashTable,ConcurrentHashMap牺牲了一定程度上的简单性,换取了更好的并发性能,适用于多线程环境下的高并发读写操作。

总结:

  • HashTable是最原始且线程安全的实现,但并发性能较差。

  • HashMap提供了最高的非线程安全性能,适合单线程应用。

  • ConcurrentHashMap在线程安全的前提下实现了较好的并发性能,是多线程环境下首选的散列表实现。

31、什么是乐观琐,什么是悲观锁,什么是公平锁,什么是非公平琐?

乐观锁(Optimistic Locking)

  • 原理:乐观锁假设并发环境下数据冲突不频繁,因此在读取数据时并不立即加锁,而是允许所有事务读取和修改数据。当事务准备提交时,才会检查在此期间是否有其他事务对数据进行了修改,通常通过版本号或CAS(Compare and Swap)机制来判断。如果发现有冲突,则事务回滚并重新尝试。

例子:

  • 在数据库中,乐观锁可以通过为表的某个字段添加一个版本号来实现。例如,在更新用户余额时,首先读取当前余额和版本号,然后计算新的余额,最后在更新语句中同时更新余额和版本号,并要求版本号不变。如果有其他事务在这段时间内更新了余额,那么版本号已经改变,该更新将失败,需要重新读取最新的数据再次尝试。

悲观锁(Pessimistic Locking)

  • 原理:悲观锁假设并发环境下数据冲突是常态,所以在访问数据时就先锁定资源,直到事务结束才释放锁,确保其他事务在该时间段内无法修改此数据。

例子:

  • 在数据库中,悲观锁可以使用`SELECT ... FOR UPDATE`语句来获取行级锁。比如在转账操作中,事务A在执行扣款前会锁定账户A的记录,直到事务提交或者回滚后才会释放这个锁,这样在同一时间,事务B就不能再锁定同一账户进行扣款操作,避免了并发问题。

公平锁(Fair Lock)

  • 原理:公平锁是一种线程调度策略,它保证了等待锁最久的线程在锁释放时能够获得锁,即按照线程请求锁的顺序依次分配,不存在"插队"现象。

例子:

  • 在Java中的ReentrantLock类中,可以通过构造函数指定是否启用公平锁。如果是一个公平锁,那么当锁被释放时,它会优先选择已经在等待队列中等待最久的那个线程给它上锁。

非公平锁(Non-Fair Lock)

  • 原理:非公平锁则不保证等待时间最长的线程一定先获得锁,即使有的线程已经等待了很久,新到来的线程也有可能直接获得锁。

例子:

  • Java中的synchronized关键字所实现的锁是非公平锁,默认情况下ReentrantLock也是非公平锁。在这种情况下,一旦锁被释放,任何等待的线程都有可能获得锁,这可能导致某些线程长时间得不到锁而饥饿。
32、java中常见的四种引用类型?

一、强引用

强引用是最常见的,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,只要强引用还在,处于可达状态,垃圾回收器就永远不会回收这个对象。即使系统内存不足,JVM也不会回收该对象来释放内存,而是抛出OOM。

二、软引用

软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。缓存数据,提高数据的获取速度。

三、弱引用

弱引用需要用WeakReference类来实现,它比软引用生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM内存空间是否足够,总会回收该对象占用的内存。短时间缓存某些次要数据。

四、虚引用

虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用,虚引用的主要作用是跟踪对象被垃圾回收的状态。它的唯一目的是为了能在对象被收集器回收时收到一个系统通知,通过与引用队列(ReferenceQueue)联合使用,可以在对象被回收后执行特定的操作。

33、java线程中涉及到的常见方法及其意义?线程基本方法 、线程等待(wait) 、线程睡眠(sleep) 线程让步(yield) 线程中断(interrupt) 、Join等待其他线程终止 、为什么要用join()方法?

Java线程中涉及到的常见方法及其意义:

  1. 线程基本方法
  • `start()`:启动一个新线程并执行其`run()`方法。每个线程只能调用一次`start()`方法。

  • `run()`:线程要执行的任务的具体逻辑,通常用户需要重写Thread类或实现Runnable接口中的`run()`方法。

  • `currentThread()`:返回当前正在执行的线程对象。

  1. 线程等待(wait)
  • `wait()`:在一个已经获取到对象锁的同步方法或同步块中调用,会使当前线程释放该对象锁,并进入等待状态直到被其他线程通过notify()或notifyAll()唤醒,或者超时后自动唤醒。

  • `notify()`:唤醒在此对象监视器上等待的一个单个线程,如果有多个线程在等待,则选择其中一个唤醒。

  • `notifyAll()`:唤醒在此对象监视器上等待的所有线程。

  1. 线程睡眠(sleep)
  • `Thread.sleep(long millis)`:使当前线程暂停指定毫秒数的时间,让出CPU给其他线程。需要注意的是,即使时间到了,也不能保证立即恢复执行,因为这还取决于操作系统的线程调度策略。
  1. 线程让步(yield)
  • `yield()`:提示当前线程放弃处理器使用权,但不保证会让出,只是一个建议行为,具体是否让出由JVM决定。它主要用于减少程序中优先级较高的线程对低优先级线程的阻塞影响,提高并发性能。
  1. 线程中断(interrupt)
  • `interrupt()`:设置线程的中断标志位,不直接终止线程,而是传递一个中断请求信号。线程在运行过程中可以通过检查自身的中断状态(`isInterrupted()`)来响应中断请求,也可以通过捕获`InterruptedException`异常来处理中断。

  • `isInterrupted()`:判断线程是否已被中断,不会清除中断状态。

  • `interrupted()`:判断当前线程是否被中断并清除中断状态。

6.Join等待其他线程终止

  • `join()`:允许一个线程A等待另一个线程B完成执行,当在A线程上调用B线程的join()方法时,A会等待B线程结束后再继续执行。这样可以确保某个线程在其依赖的线程完成后才开始执行,有助于保持程序逻辑的一致性和正确性。

为什么要用join()方法?

  • join()方法在多线程协作场景下非常重要,例如:

  • 当主线程需要等待子线程执行完毕后再进行下一步操作时,可以使用join()方法避免主线程提前结束导致子线程未完成任务的情况。

  • 在多线程间的逻辑依赖关系中,有时必须确保前一个线程先完成工作,后续线程才能基于它的结果执行,此时join()方法能够帮助我们控制线程间的顺序和依赖关系。

  • 用于测试多线程程序时,方便观察线程执行顺序和结果一致性。

34、并发编程的三要素?

并发编程的三要素是指在设计和实现多线程程序时需要特别关注的三个方面,以确保线程安全性和正确性。这三要素是:

  1. 原子性(Atomicity)

原子性指的是一个操作或者多个操作要么全部执行完成,要么都不执行。在多线程环境下,如果一个操作不是原子性的,那么可能会导致数据不一致的问题。例如,假设一个操作涉及对两个或更多变量的修改,如果在没有同步措施的情况下被中断,可能导致部分变量已更改而其他变量未更改,从而破坏了数据完整性。在Java中,可以通过锁(如`synchronized`关键字、Lock接口等)来保证某个代码块的原子性。

  1. 可见性(Visibility)

可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到这个新值。由于处理器缓存、编译器优化等因素,如果没有适当的同步机制,可能造成不同线程间对同一变量的不同版本视图。Java提供了volatile关键字来解决这个问题,它能确保对volatile变量的写操作会立即刷新到主内存,并且读操作总是从主内存中获取最新值。

  1. 有序性(Ordering)

有序性是指程序执行的顺序按照代码的逻辑顺序进行。然而,在硬件层面,为了提高性能,处理器可能会重新排序指令执行顺序(即指令重排序),这可能导致多线程环境下的不确定性。在Java中,通过内存模型和happens-before原则提供了一定程度上的有序性保障。synchronized和volatile关键字不仅保证了可见性,同时也一定程度上保证了有序性,禁止特定类型的重排序。

要编写正确的并发程序,开发者必须理解并妥善处理这三个要素,以确保线程间的交互满足预期的行为。

35、ReentrantLock与sychronized的区别?实现原理?使用场景?

ReentrantLock与synchronized的区别:

  1. 获取和释放锁的方式:
  • `synchronized`是Java的内置关键字,通过它修饰的方法或代码块自动获取并释放锁。不需要手动管理锁的生命周期。

  • `ReentrantLock`是Java并发库(java.util.concurrent.locks)中的一个接口`Lock`的实现类,需要显式调用`lock()`和`unlock()`方法来获取和释放锁。

  1. 公平性选择:
  • `synchronized`默认是非公平锁,即无法保证线程等待的顺序,新到达的线程可能抢占已经等待的线程。

  • `ReentrantLock`提供了公平锁和非公平锁的选择,通过构造函数指定`new ReentrantLock(true)`可以获得公平锁,这样等待时间最长的线程将优先获得锁。

  1. 响应中断:
  • 使用`synchronized`时,如果一个线程在等待锁的过程中被中断,它不会立即响应这个中断请求,而是继续等待锁。

  • `ReentrantLock`支持中断操作,当线程在等待锁时可以响应中断,并抛出`InterruptedException`。

  1. 条件队列(Condition):
  • `synchronized`只能进行简单的互斥同步,没有提供类似于条件变量的功能。

  • `ReentrantLock`通过`newCondition()`方法创建多个`Condition`实例,可以灵活地控制多线程间的协作,例如挂起和唤醒满足特定条件的线程。

  1. 锁的尝试与超时控制:
  • `synchronized`不支持尝试获取锁以及设置获取锁的超时时间。

  • `ReentrantLock`可以通过`tryLock()`、`tryLock(long timeout, TimeUnit unit)`等方法尝试获取锁,或者在指定时间内等待锁。

  1. 锁的粒度:
  • `synchronized`的锁机制针对的是对象,其锁的粒度相对较粗。

  • `ReentrantLock`可以更精确地控制锁定范围,适用于复杂的锁策略场景。

实现原理:

  • `synchronized`由JVM直接支持,底层基于监视器(Monitor)机制,包括进入区、退出区和等待集合,通过monitorenter和monitorexit指令实现加锁和解锁操作,且具有内存可见性和原子性保障。

  • `ReentrantLock`基于AbstractQueuedSynchronizer(AQS)框架实现,利用CAS操作和CLH(Craig, Landin, and Hagersten)队列算法,它同样具备可重入特性,同时通过自定义锁实现更多的功能扩展。

使用场景:

  • `synchronized`适合于简单的情况,代码简洁,性能通常也不错,在资源竞争不是很激烈的情况下足够使用,并且异常处理更加安全,因为锁会在异常情况下自动释放。

  • `ReentrantLock`在复杂并发环境下更有优势,比如需要控制多个条件的等待/通知,或者需要支持中断和超时控制,又或者需要更细粒度的锁控制。在这些场景下,ReentrantLock提供的灵活性和可控性更强。

36、介绍一下ReentrantReadwriteLock?

ReentrantReadWriteLock 是Java并发包 `java.util.concurrent.locks` 中的一个类,它实现了读写锁(Read-Write Lock)的概念。与普通的互斥锁不同,读写锁允许多个线程同时对共享资源进行读操作,但在同一时刻只允许一个线程进行写操作。

特点:

  1. 读写分离:ReentrantReadWriteLock 包含两个锁,一个是读锁(也称为共享锁),另一个是写锁(也称为独占锁)。多个读线程可以同时持有读锁并执行读取操作,但当有线程持有了写锁时,其他所有读写线程都必须等待,直到写锁被释放。

2.可重入性:无论是读锁还是写锁都是可重入的,这意味着如果一个已经持有读或写锁的线程尝试再次获取相同的锁,那么这个请求将成功,并且锁计数会增加,确保了递归调用和嵌套同步的支持。

  1. 公平性选择:ReentrantReadWriteLock 提供了公平和非公平两种模式的选择。公平锁遵循FIFO队列策略,即等待时间最长的线程优先获得锁;而非公平锁则不保证这种顺序,可能会让新到来的线程插队获取锁,这通常具有更高的性能,但可能导致线程饥饿现象。

  2. 状态管理:ReentrantReadWriteLock 使用内部状态来跟踪当前有多少读线程和写线程持有锁,以及每个线程的重入次数。

使用场景:

读写锁非常适合那些读多写少的应用场景,例如缓存系统、数据库访问等。在这些场景下,读操作远比写操作频繁,通过允许多个读线程并发执行,能够大大提高系统的并发性能和吞吐量。

示例代码:

```java

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {

private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

public void readOperation() {

readLock.lock();

try {

// 读操作代码块

} finally {

readLock.unlock();

}

}

public void writeOperation() {

writeLock.lock();

try {

// 写操作代码块

} finally {

writeLock.unlock();

}

}

}

```

在这个例子中,`readOperation()` 方法使用读锁保护读取操作,而 `writeOperation()` 方法使用写锁保护写入操作。在实际应用中,确保在finally块中解锁,以防止因异常抛出而导致锁无法释放的情况。

37、并发编程解决生产者与消费者模型的常用的几种方式?

1.synchronized配合wait()与notify()/notify All()方法

2.使用jdk内置的阻塞队列

3.使用lock琐以及相关的condition接口,生产者和消费者可以分别调用不同的条件变量的await()方法进入等待状态,并通过signal()或signalAll()方法唤醒其他线程,以更精细的方式控制线程之间的协作。

4.线程同步工具类:CountDownLatch 、Semaphore、CyclicBarrier

38、什么是可重入琐与不可重入琐?

在Java中,可重入锁(Reentrant Lock)与不可重入锁是两种不同类型的锁机制,它们的主要区别在于线程能否重复获取已经持有的锁。

  1. 可重入锁(ReentrantLock)
  • 可重入锁允许同一个线程多次获取同一把锁。如果一个线程获得了某个对象的锁,并且在没有释放该锁的情况下尝试再次获取这把锁,那么这个线程仍然能够成功获取这把锁。

  • Java中的`java.util.concurrent.locks.ReentrantLock`类就实现了可重入锁的功能。在内部,它会维护一个计数器来记录当前持有锁的线程重入的次数,每次获取锁时计数器加一,释放锁时计数器减一,直到计数器为0时才真正释放锁给其他线程竞争。

  • 可重入性避免了死锁的发生,并且支持公平锁和非公平锁的选择以及更灵活的锁定条件控制。

  1. 不可重入锁
  • 不可重入锁则不允许同一个线程对同一个锁进行两次以上的获取。一旦线程获取了一个不可重入锁,如果再尝试获取同一把锁,将会被阻塞或抛出异常,直到当前线程释放掉已持有的锁。

  • 在Java标准库中并没有直接提供不可重入锁的实现,因为大多数情况下使用可重入锁更为安全和方便。但理论上可以自己设计实现不可重入的锁机制,不过这样做通常不推荐,因为容易导致死锁和其他并发问题。

对于`synchronized`关键字修饰的方法或者代码块,在Java中也是可重入的,即当一个线程持有了对象锁后,仍然可以再次进入由`synchronized`保护的区域。这意味着`synchronized`隐式地提供了可重入锁的功能。

39、什么是共享锁与排它锁?

共享锁:加琐后不同线程可以同时进行读操作,不可以进行写操作

排它琐:加琐后线程可以进行读写操作,其它线程将被阻塞

40、什么是自旋锁?

自旋锁(Spinlock)是一种简单的锁机制,主要用于多线程编程环境中的同步控制。当一个线程试图获取已被其他线程持有的自旋锁时,该线程不会立即进入阻塞状态并交出CPU执行权,而是会不断地循环检查锁的状态(即"自旋"),直到锁变为可用为止。

在自旋期间,请求锁的线程会持续占用处理器资源,不断地重复测试和检查锁变量,期望在不久的将来能够获取到锁。这种机制适用于等待时间较短且线程切换开销较大的场景,因为它避免了上下文切换的开销,提高了在某些特定条件下的并发性能。

然而,如果持有锁的线程需要很长时间才能释放锁,那么自旋锁会导致请求锁的线程消耗大量的CPU资源而没有做任何实际的工作,从而影响系统的整体性能。因此,在设计并发程序时,使用自旋锁必须谨慎,并结合实际情况考虑是否适合使用,以及如何合理设置自旋次数以防止过度消耗CPU。在许多现代操作系统中,自旋锁通常和其他更复杂的同步原语(如信号量、互斥锁等)结合使用,或者仅用于非常小的临界区代码片段。

41、Java中Runnable和Callable有什么不同?

Java中的`Runnable`和`Callable`接口都用于定义任务,这些任务可以提交给线程执行,但它们之间存在一些关键的区别:

  1. Runnable接口
  • `java.lang.Runnable`是一个非常基础且历史悠久的接口,只有一个`run()`方法。

  • `run()`方法没有返回值,也不抛出受检异常(checked exception)。

  • 通常通过创建实现`Runnable`接口的类实例并传递给`Thread`构造器来创建新线程,或者在Executor框架中作为可执行任务提交给线程池。

  1. Callable接口
  • `java.util.concurrent.Callable`是在Java 5引入并发包时新增的一个接口,提供了更强大的功能。

  • `Callable`接口有一个`call()`方法,此方法可以有返回值,并且可以抛出受检异常。

  • 当任务完成时,调用`call()`方法会返回一个泛型结果,这使得它非常适合需要获取计算结果的任务。

  • Callable任务不能直接启动新的线程,通常通过`FutureTask`包装后与`Thread`结合使用,或直接提交到`ExecutorService`以异步方式执行,并能通过`Future`对象获取计算结果以及检查任务是否已完成。

总结来说,如果你只需要定义一个不返回任何结果、不需要处理异常的任务,那么可以使用`Runnable`。而如果你希望任务能够返回一个具体的计算结果,并且可能需要抛出异常,那么应该选择`Callable`接口。此外,`Callable`更适合在高级并发框架如`ExecutorService`中使用,因为它允许你等待任务完成并获取其结果。

42、什么是FutureTask?

`FutureTask` 是Java并发包 `java.util.concurrent` 中的一个类,它结合了 `Future` 接口和 `Runnable` 接口的功能。`FutureTask` 代表一个可以取消的异步计算任务,它提供了对计算结果的获取、判断是否完成以及取消任务的方法。

具体来说:

  1. 异步计算:当你提交一个实现了 `Callable` 或 `Runnable` 的任务给 `FutureTask` 后,`FutureTask` 可以在一个独立线程中执行这个任务。这意味着主线程可以在等待任务完成的同时进行其他操作,而不是阻塞等待任务结束。

  2. 结果查询:通过 `get()` 方法,你可以阻塞地获取任务的结果(如果任务实现的是 `Callable` 接口)或得知任务是否已经完成(如果任务实现的是 `Runnable` 接口,则 `get()` 返回默认值 `null`)。

  3. 取消任务:调用 `cancel(boolean mayInterruptIfRunning)` 方法可以尝试取消该任务,参数表示是否应该在任务正在运行时中断它。

  4. 状态检查:`isDone()` 方法用于检查任务是否已完成(不论是正常完成还是被取消),而 `isCancelled()` 方法则用来确定任务是否已取消。

  5. 可重入性与线程安全性:`FutureTask` 实现了可重入锁机制,确保任务只能被执行一次,并且其内部状态管理是线程安全的。

由于 `FutureTask` 兼容 `Runnable` 和 `Future` 接口,它可以方便地与 `ExecutorService` 结合使用,将任务提交到线程池执行,并在之后获取计算结果。同时,它也支持手动启动任务,例如直接调用 `run()` 方法来执行任务。

43、如何在Java中获取线程堆栈?JVM中哪个参数是用来控制线程的栈堆栈小的?

在Java中,可以通过Thread类的getStackTrace()方法来获取当前线程的堆栈信息。

JVM中用来控制线程栈大小的参数是 -Xss(或 -XX:ThreadStackSize),它用于设置每个线程的Java虚拟机栈空间大小。

44、Thread类中的yield方法有什么作用?

在Java中,`Thread`类中的`yield()`方法是一个静态 native 方法,它的作用是提示当前正在执行的线程主动让出CPU执行时间片。调用该方法时,当前线程会暂停自己的执行,并将执行机会交给优先级相同或更高优先级的线程。

然而,需要注意的是:

  • `yield()`方法并不保证其他线程能够立即获得CPU控制权,因为这完全取决于操作系统的线程调度策略。

  • 它只是一个建议性的操作,Java虚拟机(JVM)可以选择忽略这个提示。

  • 使用`yield()`方法并不能确保线程之间的交替执行顺序,也无法用于精确控制多线程间的同步。

总的来说,`yield()`方法在多线程编程中主要用于优化程序性能,减少线程间的资源争抢,特别是在循环体中适当使用可以使得多个线程能更均衡地分享CPU时间,但其效果依赖于具体的系统环境和线程调度器实现,因此不是一个可靠的线程同步机制。在实际开发中,若需要实现更为严格的线程同步与通信,通常会采用`synchronized`、`Lock`等更加确定性的同步工具。

45、有三个线程T保它1,T2,T3,怎么确们按顺序执行?

1.使用Thread类的join()方法,调用线程等待join()线程执行完成后才继续执行。

2.synchronized关键字琐与wait()/notify()方法配合使用。

3.ReentranLock配合Condition条件变量的await()与signal()方法。

4.使用线程同步工具类:CountDownLatch、CyclicBarrier、Semaphore

46、一个线程运行时发生异常会怎样? java中 如何在两个线程间共享数据?

在Java中,当一个线程在其run()方法或在调用的方法中抛出未捕获的异常时:

  • 如果没有为该线程设置UncaughtExceptionHandler,JVM会默认将异常信息输出到标准错误流(System.err),并终止该线程。其他非守护线程不受影响,程序继续执行。
  • 如果为线程设置了UncaughtExceptionHandler,则当线程抛出未捕获的异常时,JVM会调用这个异常处理器的uncaughtException(Thread t, Throwable e)方法来处理异常。

多线程共享数据有多种方法,比如:原子类,琐,线程安全的容器

47、 Java中notify 和 notifyAll有什么区别? 为什么wait, notify 和 notifyAll这些方法不在thread类里面?

在Java中,`notify()` 和 `notifyAll()` 都是 `java.lang.Object` 类的方法,用于线程间协作和同步。它们的区别在于:

  1. notify()
  • 当调用一个对象的 `notify()` 方法时,它会唤醒在此对象监视器(锁)上等待的单个线程。具体唤醒哪一个线程是不确定的,由JVM自行决定。
  1. notifyAll()
  • 而当调用 `notifyAll()` 方法时,它会唤醒所有在这个对象监视器上等待的线程。这些被唤醒的线程都将变为可运行状态,并开始竞争获取该对象的监视器锁以便继续执行。

至于为什么 `wait()`, `notify()` 和 `notifyAll()` 这些方法不在 `Thread` 类里面,这是因为这些方法实际上是针对"对象监视器"或"锁"的操作,而非线程本身。在Java中,每个对象都有一个与之关联的内置锁或监视器,当线程进入某个对象的 `synchronized` 方法或代码块时,就会获得这个锁。因此,这些方法定义在 `Object` 类中,使得任何对象都能调用它们进行线程间的同步和通信。

通过将这些方法设计为 `Object` 类的一部分,意味着任何Java对象都可以作为同步的基础,并支持线程之间的等待/通知机制。这样设计更符合面向对象编程的原则,同时也简化了Java并发模型的设计和实现。

48、Java中堆和栈有什么不同?

Java中堆和栈是两种不同的内存区域,它们在内存分配、生命周期以及用途上存在显著区别:

  1. 栈(Stack)
  • 栈是线程私有的,每个线程都有自己的栈空间。

  • 栈用于存储方法的局部变量、方法参数以及返回地址等信息。

  • 栈上的内存分配由编译器完成,且内存大小在编译期就已经确定,因此栈内存的空间较小但快速高效。

  • 当方法调用结束时,其对应的栈帧会自动弹出栈顶,栈中的局部变量也会随之销毁,无需手动释放。

  • 如果栈内存不足(例如递归调用过深导致栈溢出),会抛出`StackOverflowError`异常。

  1. 堆(Heap)
  • 堆是所有线程共享的一块内存区域,主要用于存放对象实例和数组。

  • 在堆中创建的对象具有动态的生命周期,它们的内存分配和回收主要通过JVM的垃圾回收机制进行管理。

  • 对象的创建通常使用关键字 `new` 进行,内存大小不固定,在运行期间决定,因此可能会面临内存不足的情况(即OOM:Out of Memory)。

  • 垃圾回收器会在特定条件下识别并回收不再使用的对象所占用的内存。不过,由于GC需要耗费时间和资源,堆内存的分配与回收相比栈来说较慢且复杂。

  • 若堆内存不足,JVM无法继续为新对象分配内存时,将抛出`OutOfMemoryError`异常。

总结:

  • 栈是程序执行期间临时存储数据的地方,特别关注于方法调用过程中的局部变量和控制流程。

  • 堆则是Java对象的主要存储区域,重点关注对象的生命周期管理和内存分配。

49、Java中活锁和死锁有什么区别? 怎么检测一个线程是否拥有锁?

在Java中,活锁和死锁是两种不同的并发问题:

  1. 活锁(Livelock)

    • 活锁是指两个或多个线程都处于不断重试某种操作的状态,由于每个线程都在等待对方释放资源以便自己可以继续执行,但每个线程都不肯让步,导致所有线程都无法进行任何实际的进展。活锁中的线程会一直循环执行而无法向前推进。
    • 例如,两个线程在一个狭窄的走廊相遇,它们都试图给对方让路,但是让的方式相同(比如都向左移动),结果就是双方一直在原地左右移动,没有一方能够通过走廊。
  2. 死锁(Deadlock)

    • 死锁是指两个或多个线程互相持有对方需要的资源,而又都在等待对方释放资源,从而形成一个僵局,没有任何线程能够继续执行的情况。这种情况下,线程不会像活锁那样不断地尝试,而是完全停止了进一步的执行,除非外部干预或者系统资源耗尽引发异常。
    • 例如,在银行转账场景中,线程A持有了账户B的资金锁定权并等待账户C的资金锁定权,同时线程B持有了账户C的资金锁定权并等待账户A的资金锁定权,这样就形成了一个典型的死锁。

java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。

50、AQS使用了哪些设计模式?AQS 组件了解吗?

AQS(AbstractQueuedSynchronizer)是Java并发包中用于构建锁和其他同步组件的基础框架,它使用了多种设计模式:

  1. 模板方法模式:

AQS定义了一系列的抽象方法,如`tryAcquire(int arg)`和`tryRelease(int arg)`等,子类需要重写这些方法来提供具体的同步策略。而诸如获取、释放锁以及线程排队等待的公共逻辑则在AQS基类中以模板方法的形式实现。

  1. 状态模式:

AQS内部维护了一个volatile int类型的变量表示同步状态,通过这个共享的状态信息可以实现各种不同的同步器行为,比如独占锁、共享锁等。这种对同一接口的不同实现方式体现了状态模式的思想。

  1. 责任链模式(或称为节点链表模式):

AQS使用一个FIFO(先进先出)队列(基于双向链表Node)管理等待获取资源的线程。当资源不可用时,请求资源的线程会被构造成一个Node并加入到等待队列尾部,形成一个等待线程的链表结构。

  1. 迭代器模式(间接体现):

虽然AQS并没有直接实现迭代器模式,但在其内部处理同步队列时,实质上是对队列中的节点进行了类似遍历的操作,例如在进行取消等待操作时会从队列头开始查找待删除的节点。

  1. CAS操作(非设计模式,但重要机制):

AQS大量使用了乐观锁机制,即Compare and Swap(CAS),这是一种无锁编程技术,而非严格意义上的设计模式。AQS利用CAS原子性地更新同步状态,从而避免了传统的互斥锁定带来的性能开销。

AQS组件:

基于AQS构建的常见组件包括但不限于以下几种:

  • `ReentrantLock`:可重入锁,支持公平与非公平锁选择。

  • `Semaphore`:信号量,控制同时访问特定资源的线程数。

  • `CountDownLatch`:计数门栓,允许一个或多个线程等待其他线程完成一组操作后才能继续执行。

  • `CyclicBarrier`:循环栅栏,让一组线程在一个点上互相等待,直到所有线程都到达栅栏位置后一起继续执行。

  • `ReentrantReadWriteLock`:读写锁,允许多个线程同时读取共享资源,但在任何时刻只允许一个线程写入资源。

51、线程池有哪几种工作队列?

线程池在Java中通常使用`java.util.concurrent.ExecutorService`及其相关类来实现,其中工作队列(Work Queue)是线程池中的一个关键组件,用于存储待执行的任务。以下是Java中几种常见的工作队列类型:

  1. 无界队列(如:`LinkedBlockingQueue`)
  • 无界队列允许任务无限量地加入到队列中,只要内存足够,不会拒绝提交的任务。这意味着如果生产者速度过快,而消费者处理速度跟不上时,队列可能会持续增长导致内存溢出。因此,在没有适当控制的情况下,无界队列可能导致系统资源耗尽。
  1. 有界队列(如:`ArrayBlockingQueue`、`LinkedBlockingDeque`)
  • 有界队列具有预定义的最大容量,当队列满时,再尝试添加任务将会阻塞或抛出异常(取决于线程池配置)。有界队列有助于防止资源耗尽,因为它可以限制等待执行的任务数量。
  1. 优先级队列(如:`PriorityBlockingQueue`)
  • 优先级队列按照任务的优先级顺序进行处理,优先级高的任务会先被执行。它是一个无界的并发队列,但队列内的元素需要实现`Comparable`接口或者在构造时提供`Comparator`以确定任务之间的优先级关系。
  1. SynchronousQueue
  • `SynchronousQueue` 不实际存储元素,而是将每个插入操作匹配对等的移除操作。这种队列每次插入必须等待另一个线程的移除操作,反之亦然,非常适合传递消息而不是存储任务的场景。由于其特殊的无缓冲特性,对于线程池而言,一般意味着当任务提交时如果没有空闲的工作线程立即可用,则会拒绝任务。

通过选择不同类型的队列,可以根据应用需求调整线程池的行为和性能。例如,使用无界队列可以简化编程模型,但可能增加内存消耗;而使用有界队列则可以更好地控制系统的负载压力,并且可以通过调整队列大小来优化线程池性能。

52.线程池异常怎么处理知道吗?

1.可以使用try-catch进行异常捕获与处理

2.也可以定义全局的线程处理器

53.线程池有几种状态吗?

是的,Java中的线程池(具体指`java.util.concurrent.ThreadPoolExecutor`)有多种状态。线程池的状态主要通过内部维护的变量来表示,并且这些状态在执行过程中会发生转换。以下是ThreadPoolExecutor中定义的主要状态:

  1. RUNNING
  • 线程池创建后默认处于运行状态,能够接受新任务并执行队列中的任务。
  1. SHUTDOWN
  • 当调用`shutdown()`方法后,线程池进入关闭状态。此时不再接受新的任务提交,但会继续处理已提交的任务队列直到清空为止。
  1. STOP
  • 调用`shutdownNow()`方法后,线程池进入停止状态。此状态下不仅不接受新的任务,还会尝试中断正在执行的任务,并且不再处理等待队列中的剩余任务。
  1. TIDYING
  • 当所有任务都已经终止并且工作队列为空时,无论是在SHUTDOWN还是STOP状态下,线程池都会转换到整理状态(TIDYING)。在此状态下,线程池会执行`terminated()`钩子方法。
  1. TERMINATED
  • 在TIDYING状态之后,当`terminated()`方法执行完毕后,线程池就会变成终止状态(TERMINATED)。此时线程池已经完全停止,不再处理任何任务,也无法再提交新的任务。

这些状态之间有一定的转换顺序和条件,通常按照正常生命周期从RUNNING开始,最终达到TERMINATED状态。在不同状态下,线程池的行为有所不同,例如是否接收新任务、如何处理待执行任务等。

相关推荐
哎呦没11 分钟前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
编程、小哥哥38 分钟前
netty之Netty与SpringBoot整合
java·spring boot·spring
IT学长编程2 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
杨哥带你写代码2 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
郭二哈2 小时前
C++——模板进阶、继承
java·服务器·c++
A尘埃3 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23073 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
沉登c3 小时前
幂等性接口实现
java·rpc
代码之光_19803 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端