Java并发编程之锁的艺术:面试与实战指南(三)
文章目录
- Java并发编程之锁的艺术:面试与实战指南(三)
🌈你好呀!我是 山顶风景独好
💝欢迎来到我的博客,很高兴能够在这里和您见面!
💝希望您在这里可以感受到一份轻松愉快的氛围!
💝不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
🚀 欢迎一起踏上探险之旅,挖掘无限可能,共同成长!
前言
本系列地址:
Java并发编程之锁的艺术:面试与实战指南(一)
Java并发编程之锁的艺术:面试与实战指南(二)
Java并发编程之锁的艺术:面试与实战指南(三)
Java并发编程之锁的艺术:面试与实战指南(四)
十七、Java中线程和进程的区别是什么?
- 定义与关系:进程是程序在处理机上的一次动态执行过程,而线程是进程的一个实体,是CPU调度和分派的基本单位。一个操作系统中可以拥有多个进程,一个进程里可以拥有多个线程,线程在进程内执行。
- 资源占用:进程拥有自己独立的内存空间和系统资源,而线程使用进程的内存空间,并和该进程的其他线程共享这个空间。
- 通信方式:线程可以使用wait(), notify(), notifyAll()等方法直接与其他线程(同一进程)通信;而进程需要使用“进程间通信”(IPC)来与操作系统中的其他进程通信。
- 创建与切换开销:由于进程拥有独立的资源,因此创建和销毁进程的开销通常比线程大。而线程是处理器任务调度和执行的基本单位,线程的创建、切换和销毁的开销相对较小。
- 独立性:进程是系统中独立存在的实体,是一个能独立运行的单位;而线程不拥有系统资源,只拥有一点在运行中必不可少的资源,但可以与同属一个进程的其他线程共享进程所拥有的全部资源。
十八、什么是Java内存模型(JMM)?它在并发编程中有什么作用?
Java内存模型(JMM)是Java虚拟机(JVM)规范中定义的一种内存模型,它描述了Java程序中各种变量(包括实例字段、静态字段和数组元素)的访问规则,以及在多线程环境中这些变量的可见性、原子性和有序性的保证。JMM并不真实存在,它仅仅是一组规则或规范,通过这组规范定义了程序中各个变量的读写访问方式。
在并发编程中,JMM的作用主要体现在以下几个方面:
- 屏蔽系统和硬件的差异:由于不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,JMM能够屏蔽这些差异,使得Java程序能够在不同的系统环境下达到相同的访问结果,实现“一次编写,到处运行”的目标。
- 保证多线程之间对共享变量操作的原子性、可见性和有序性:JMM通过定义如何通过synchronized和其他同步方式来保证这些特性。原子性指的是一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行;可见性指的是当一个线程修改了共享变量的值,其他线程能够立即得知这个修改;有序性指的是程序执行的顺序按照代码的先后顺序执行。
- 定义线程和主内存之间的抽象关系:JMM定义了JVM在计算机内存(RAM)中的工作方式,每个线程都有自己的独立工作内存,里面保存了该线程使用的变量的副本。线程对共享变量的所有操作都必须从自己的工作内存中读写,不能直接从主内存中读写。不同线程之间不能直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
十九、volatile关键字的作用是什么?能保证线程安全吗?
-
volatile关键字的主要作用是确保多线程环境下的变量可见性。当一个变量被声明为volatile时,它会保证修改的值会立即被更新到主内存,当有其他线程需要读取这个变量时,它会去主内存中读取新值。这样可以避免由于线程的工作内存和主内存中的数据不一致而导致的“脏读”问题。
-
此外,volatile关键字还可以禁止JVM的指令重排优化,这有助于保持程序执行的顺序性。
-
然而,需要注意的是,虽然volatile关键字可以保证可见性和禁止指令重排优化,但它并不能保证复合操作的原子性。也就是说,如果多个线程同时对同一个volatile变量进行复杂的读写操作(如自增、自减等),仍然可能出现线程安全问题。
二十、什么是线程局部变量(ThreadLocal)?它在什么场景下使用?
线程局部变量(ThreadLocal)是一种特殊的变量类型,它可以让多个线程并发访问时每个线程都有自己的变量副本,互不干扰。这种机制使得每个线程都可以独立地拥有和操作自己的变量副本,而不会影响到其他线程。
在Java中,可以使用ThreadLocal类来实现线程局部变量。ThreadLocal对象通常是一个静态成员变量,可以在多个线程间共享。每个线程通过ThreadLocal对象获取和设置自己独立的变量副本,不会与其他线程的变量产生冲突。每个线程都可以独立地修改自己的副本,而不需要加锁。
线程局部变量的应用场景:
- 线程安全的共享变量:在多线程环境中,如果我们想要在不同的线程间共享一些变量,但又不想让这些变量被多个线程修改,这个时候就可以使用ThreadLocal。例如,在一个网络应用中,我们可能需要在每个线程中维护一个与客户端的连接信息。使用ThreadLocal,我们可以在每个线程中创建一个连接信息的副本,这样每个线程都可以独立地操作自己的连接信息,而不会影响到其他线程。
- 线程专属的数据:有些时候,我们可能需要在每个线程中维护一些专属的数据,这些数据在其他线程中是不可见的。使用ThreadLocal,我们可以在每个线程中创建一个数据库连接的副本,这样每个线程都可以独立地操作自己的数据库连接,而不会影响到其他线程。
二十一、什么是阻塞队列?它在Java并发包中是如何实现的?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加操作是:在队列为空时,获取元素的线程会等待队列变为非空;当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。
阻塞队列的实现依赖于Java的内置锁或显式Lock来实现线程间的同步,并使用了Condition来实现线程间的等待/通知机制。具体来说,当队列为空时,消费者线程会调用Condition的await()方法进入等待状态,直到生产者线程向队列中插入了元素并调用Condition的signal()或signalAll()方法唤醒等待的线程;当队列满时,生产者线程也会调用Condition的await()方法进入等待状态,直到消费者线程从队列中取出了元素并唤醒了等待的生产者线程。
JDK 8中阻塞队列的实现:
ArrayBlockingQueue:
- 这是一个基于数组实现的有界阻塞队列。队列按照先进先出(FIFO)的原则对元素进行排序。
LinkedBlockingQueue:
- 这是一个基于链表实现的有界阻塞队列。其默认和最大长度为Integer.MAX_VALUE,也就是说,它是一个几乎无界的队列。队列同样按照先进先出的原则进行排序。
PriorityBlockingQueue:
- 这是一个支持优先级排序的无界阻塞队列。默认情况下,元素按照自然排序(升序)进行排列。如果元素实现了Comparable接口,那么元素就会按照其compareTo()方法的返回值进行排序;如果元素没有实现Comparable接口,但在创建PriorityBlockingQueue时传入了Comparator对象,那么元素就会按照Comparator的compare()方法的返回值进行排序。
DelayQueue:
- 这是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现,队列中的元素必须实现Delayed接口。在创建元素时,可以指定多久才能从队列中获取当前元素,只有在延时期满时才能从队列中提取元素。队列头的元素是最先“到期”的元素。
LinkedTransferQueue:
- 这是一个由链表结构构成的无界阻塞TransferQueue队列。相对于其他阻塞队列,它多了tryTransfer和transfer方法。这个队列是LinkedBlockingQueue、SynchronousQueue(公平模式)和ConcurrentLinkedQueue三者的集合体,它综合了这三者的方法,并提供了更加高效的实现方式。
二十二、什么是Future和Callable?它们在并发编程中有什么应用?
- Callable:Callable是一个接口,它允许我们执行一个任务并返回结果。与Runnable接口不同,Runnable任务不返回任何结果,而Callable可以返回一个V类型的值。Callable的任务通常在线程池中执行,可以利用Future来获取任务的结果。Callable接口只包含一个call()方法,该方法可以抛出异常,这使得错误处理更加灵活。
- Future:Future是一个接口,用于获取异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。在并发编程中,我们通常将Callable任务提交给线程池执行,并通过Future对象来跟踪任务的状态和结果。
在并发编程中的应用
- 并行计算:使用Callable和Future可以方便地实现并行计算,将一个大任务拆分为多个小任务并在多个线程中并行执行。通过将计算任务分配给不同的线程,可以提高计算速度和系统的吞吐量。
- 异步IO:在网络编程和文件处理等场景中,使用Callable和Future可以实现异步IO操作。可以将IO操作封装为Callable任务,并通过Future对象获取IO操作的结果。这样可以充分利用CPU资源,同时不会阻塞主线程。
二十三、什么是ForkJoinPool?它适用于哪些场景?
ForkJoinPool是JDK7提供的一种基于“分治算法”的多线程并行计算框架。其核心思想是将大的任务拆分成多个小任务(即fork),然后再将多个小任务处理汇总到一个结果上(即join),非常像MapReduce处理原理。它特别适用于任务分解与合并的场景。
ForkJoinPool适用于那些可以自然分解为多个独立子任务,并且这些子任务之间不需要太多通信或同步的问题。
使用场景:
- 并行数组处理:如排序、过滤、映射等。
- 并行集合处理:如归约操作等。
- 科学计算中的并行算法:如矩阵乘法、快速傅里叶变换等。
相比于ThreadPoolExecutor,ForkJoinPool可以更好地实现计算的负载均衡,提高资源利用率。例如,当存在一个大任务和多个小任务时,ThreadPoolExecutor可能会导致一个线程忙于大任务,而其他线程则处于空闲状态。而ForkJoinPool则可以将大任务拆分成多个小任务,然后这些小任务被所有的线程执行,从而实现任务计算的负载均衡。
此外,ForkJoinPool还引入了“工作窃取”机制,在多CPU计算机上处理性能更佳。当一个线程完成了自己的任务后,它可以从其他线程的工作队列中“窃取”一个任务来执行,从而充分利用系统资源。
二十四、如何在Java中实现线程间的通信?
-
共享变量:
线程之间可以通过共享变量进行通信。但是,必须确保对共享变量的访问是同步的,以防止并发修改导致的数据不一致。可以使用synchronized关键字、Lock接口或Atomic类来确保同步。 -
wait/notify/notifyAll:
Object类提供了wait(), notify(), 和 notifyAll() 方法,这些方法可以用于在线程之间通信。一个线程可以调用共享对象的wait()方法进入等待状态,直到其他线程调用该对象的notify()或notifyAll()方法将其唤醒。这种方法通常与synchronized一起使用,以确保线程安全。 -
BlockingQueue:
Java并发包(java.util.concurrent)中的BlockingQueue接口为线程间的通信提供了一种安全高效的方式。BlockingQueue实现类(如ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue等)提供了put(), take(), offer(), poll()等方法用于在队列中添加和移除元素。这些方法在队列为空或满时会阻塞线程,从而实现线程间的通信。 -
Semaphore:
Semaphore是一个基于计数的信号量,可以用来控制对多个共享资源的访问。它也可以用于实现线程间的通信。通过减少信号量的值(acquire()方法)来阻塞线程,通过增加信号量的值(release()方法)来唤醒线程。 -
CyclicBarrier:
CyclicBarrier是一个可以让一组线程互相等待,直到所有线程都到达某个公共屏障点的同步工具。在屏障点处,线程可以选择继续执行或执行一些特殊操作,从而实现线程间的通信。 -
CountDownLatch:
CountDownLatch是一个同步辅助类,允许一个或多个线程等待一组其他线程完成操作。当调用countDown()方法时,计数器会减一;当计数器到达零时,等待的线程将被唤醒,从而实现线程间的通信。 -
Exchanger:
Exchanger是一个用于两个线程之间交换数据的同步点。两个线程通过exchange()方法交换数据,如果其中一个线程先到达交换点,它会一直等待另一个线程到达才进行交换,从而实现线程间的通信。 -
Future和Callable:
虽然Future和Callable本身不直接用于线程间通信,但它们可以用于获取异步计算的结果。通过Future的get()方法,一个线程可以等待另一个线程完成计算并获取结果,从而实现间接的线程间通信。
二十五、什么是CAS操作?它在Java并发中有什么应用?
CAS(Compare-And-Swap)操作是一种无锁操作,它通过比较内存中的值与预期值是否相等来实现原子操作,解决并发环境下的数据竞争问题。
- CAS操作包含三个值:V(内存地址存放的实际值)、O(预期的值,即旧值)和N(更新的新值)。
- 它的工作原理是,当线程需要使用某个共享变量时,会先将其值(V)与预期值(O)进行比较,如果两者相等,则说明该值没有被其他线程修改过,线程可以将该值更新为新值(N)。这个过程是原子的,即不会被其他线程打断。
在Java中,CAS操作通常通过sun.misc.Unsafe类实现,该类提供了硬件级别的原子操作。由于CAS操作不需要加锁,因此它可以避免加锁操作所带来的性能开销,提高程序的并发性能。
CAS应用场景:
- 原子性操作:CAS操作可以用于实现原子性操作,如计数器的自增、自减等。由于CAS是一种无锁操作,因此它可以避免使用锁机制所带来的开销,使得原子性操作更加高效。
- 并发控制:CAS操作可以用于实现乐观锁机制,通过不断尝试更新共享变量的值来实现并发控制。如果更新失败(即预期值与内存中的值不相等),则说明有其他线程正在修改该值,当前线程可以选择重试或放弃操作。
- 无锁数据结构:CAS操作可以用于实现无锁数据结构,如无锁队列、无锁链表等。这些数据结构通过CAS操作来确保线程安全,避免了使用锁机制所带来的开销和死锁问题。
需要注意的是,虽然CAS操作具有高效性和无阻塞性等优点,但它也存在一些问题和限制。例如,CAS操作只能保证单个共享变量的原子性操作,对于多个共享变量的复合操作则无法保证原子性。此外,CAS操作还存在ABA问题(即一个值被其他线程修改后又改回原来的值,但当前线程并不知道这个变化过程),这可能会导致程序出现错误。
二十六、Java中的原子类有哪些?它们是如何保证原子性的?
Java中的原子类(Atomic Classes)主要用于在高并发的情况下,实现线程安全的操作。这些原子类位于java.util.concurrent.atomic包中,包括AtomicInteger、AtomicLong、AtomicBoolean等基本类型的原子类,以及AtomicReference引用类型的原子类。此外,还有AtomicIntegerFieldUpdater、AtomicLongFieldUpdater等原子更新器类,用于对某个类的字段进行原子更新。
-
原子类保证原子性的主要方式是通过底层使用CAS(Compare-And-Swap)机制。CAS是一种基于硬件支持的原子操作,它包含三个参数:内存地址V、预期的原值A和新值B。当且仅当内存地址V的值等于预期的原值A时,才会将V的值更新为新值B。如果V的值与A不相等,说明已经有其他线程修改了V的值,此时当前线程可以选择重新读取V的值并再次尝试更新,或者选择放弃更新。这种机制可以确保在并发环境下对共享变量的更新是原子性的。
-
与synchronized关键字和Lock锁相比,原子类在粒度上更细,可以把竞争范围缩小到变量级别,从而获得更细粒度的并发控制。此外,原子类通常比使用锁的效率更高,除了在高度竞争的情况下。这是因为CAS操作是一种无锁操作,它避免了加锁和解锁的开销,减少了线程间的竞争和阻塞。
-
需要注意的是,虽然原子类提供了线程安全的操作,但在使用时仍需要注意避免ABA问题(即一个值被其他线程修改后又改回原来的值,但当前线程并不知道这个变化过程)和循环时间长开销大等问题。此外,对于多个共享变量的复合操作,仍需要使用锁或其他同步机制来确保原子性。