JAVA并发编程学习笔记(一)

1 并发和并行概念

并行与并发是计算机科学中两个既相似又有所区别的概念,它们都与多任务处理有关,但实现方式和侧重点有所不同。

  1. 并行(Parallelism):
    并行指的是在同一时刻,有多个任务或事件在同时进行。这通常发生在有多个处理器或者多核处理器的情况下,每个处理器或核心独立地执行一部分任务,从而实现任务的并行处理。并行计算可以显著提高程序的执行效率,因为它能够充分利用多核处理器或多处理器的计算能力。
  2. 并发(Concurrency):
    并发则是指在同一个时间段内,宏观上有多个程序在同时运行。这意味着虽然单个CPU核心在同一时刻只能执行一个任务,但通过在多个任务之间快速切换(通常是微秒级别的切换),可以给人一种多个任务同时在进行的错觉。并发编程的目标是最大化地利用CPU资源,尽管在单个时间点上只有一个任务在执行。

对比:

  • 并行处理是真的在同一时刻有多个任务在执行,而并发处理则是通过任务切换来模拟多个任务的同时执行。
  • 并行处理通常需要多个处理器或多核处理器,而并发处理可以在单个处理器上实现,通过操作系统的任务调度机制来管理多个任务。
  • 并行处理主要用于提高计算密集型任务的执行速度,而并发处理则更多地用于提高I/O密集型任务或需要与用户交互的任务的响应性。

在实际应用中,并行和并发往往结合使用,以充分利用多核处理器和I/O资源,提高程序的执行效率和响应性。例如,在Web服务器中,可以使用并发处理来同时处理多个用户请求,而使用并行处理来加速单个请求中的计算密集型任务。

2 进程和线程的关系及区别

关系:

  1. 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。进程要想执行任务,必须得有线程,进程至少要有一条线程。程序启动会默认开启一条线程,这条线程被称为主线程或UI线程。
  2. 进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存中。进程是资源分配的最小单位,线程是程序执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源(如内存、I/O、CPU等)。

区别:

  1. 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。这意味着同一进程内的线程可以直接访问该进程的数据段,但进程之间的数据是相互独立的。
  2. 资源拥有:同一进程内的线程共享本进程的资源(如内存、I/O、CPU等),但进程之间的资源是独立的。这使得线程之间的切换比进程之间的切换更快,因为线程之间共享了部分资源。但这也意味着如果一个线程崩溃,可能会影响整个进程,而进程之间的崩溃不会互相影响。
  3. 通信机制:由于线程共享数据段,所以线程之间的通信机制相对简单,可以直接读写全局变量来进行通信。但这也需要注意同步和互斥的问题,以避免数据竞争和不一致性。而进程之间的通信机制相对复杂,需要使用管道、信号、消息队列、共享内存、套接字等方式进行通信。
  4. 健壮性:多进程的程序要比多线程的程序健壮,因为进程之间互相独立,一个进程的崩溃不会影响其他进程。而线程之间共享进程的资源,一个线程的崩溃可能会导致整个进程的崩溃。
  5. CPU系统:线程使得CPU系统更加有效,因为操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。这可以充分利用多核处理器的计算能力。而进程在切换时需要耗费较大的资源,效率相对较差。

需要注意的是,对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。因为进程之间无法直接共享数据,而线程可以共享进程的数据段。但这也需要注意同步和互斥的问题,以避免数据竞争和不一致性。

3 JAVA四种使用线程的方式

  1. 继承Thread类:这是最基本的方式。你可以通过创建一个继承自Thread类的类,并重写其run()方法来实现线程。然后,你可以创建该类的对象,并调用其start()方法来启动线程。但请注意,Java不支持多重继承,因此如果你的类已经继承了其他类,那么你就不能再使用这种方式。
  2. 实现Runnable接口:如果你的类已经继承了其他类,或者你不希望使用继承,那么你可以实现Runnable接口。Runnable接口只有一个run()方法需要实现。然后,你可以创建一个Thread对象,将你的Runnable对象作为参数传递给Thread的构造函数,然后调用Thread对象的start()方法来启动线程。
  3. 实现Callable和Future接口:Callable接口类似于Runnable,但是它允许线程返回一个值。Future接口则用于获取Callable线程返回的值。通常,我们使用ExecutorService的submit()方法来启动Callable线程,该方法会返回一个Future对象。然后,我们可以调用Future对象的get()方法来获取线程返回的值。
  4. 使用线程池:Java提供了ExecutorService框架来管理和控制线程。ExecutorService框架允许你创建和管理一个线程池,这样你就可以复用线程,而不是为每个任务都创建一个新的线程。这可以提高性能,并降低系统开销。你可以使用Executors类的静态方法来创建不同类型的线程池。

4 线程池核心参数:

  1. corePoolSize(核心线程数):
    线程池创建后初始化和保持的线程数量。即使这些线程空闲,它们也会保持在线程池中,除非设置了allowCoreThreadTimeout,则超过keepAliveTime的空闲核心线程也会被终止。

  2. maximumPoolSize(最大线程数):
    线程池所能容纳的最大线程数量。当队列已满,并且已创建的线程数小于最大线程数时,线程池会再创建新的线程执行任务。

  3. keepAliveTime(存活时间):
    非核心线程(救急线程)的空闲时间超过这个参数值时,会被回收。如果设置allowCoreThreadTimeout=true,该参数也同样会作用于核心线程。

  4. unit(时间单位):
    keepAliveTime参数的时间单位,常用的有TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)等。

  5. workQueue(工作队列):
    存储待执行的任务队列。常见的工作队列类型有:ArrayBlockingQueueLinkedBlockingQueueSynchronousQueuePriorityBlockingQueue等。

  6. threadFactory(线程工厂):
    用于创建新线程。可以通过自定义的线程工厂为线程设置更合理的名字、是否为守护线程以及优先级等属性。

  7. handler(拒绝策略):
    当所有线程都在繁忙,且工作队列也已满时,线程池会触发拒绝策略。常见的拒绝策略有:ThreadPoolExecutor.AbortPolicy(直接抛出异常)、ThreadPoolExecutor.CallerRunsPolicy(由调用线程执行任务)、ThreadPoolExecutor.DiscardPolicy(不处理,丢弃掉该任务)和ThreadPoolExecutor.DiscardOldestPolicy(丢弃队列里最老的任务,然后重新尝试执行任务)等。

5 线程池工作队列对比

  1. ArrayBlockingQueue

    • 基于数组结构的有界阻塞队列。
    • 按FIFO(先进先出)原则对元素进行排序。
    • 由于是数组结构,它在内存使用上可能更为紧凑,但在高并发场景下,其性能可能略低于其他队列。
  2. LinkedBlockingQueue

    • 基于链表结构的阻塞队列。
    • 同样按FIFO原则对元素进行排序。
    • 通常吞吐量要高于ArrayBlockingQueue,因为它在并发环境下的存取效率更高。
    • 静态工厂方法Executors.newFixedThreadPool()默认使用了这个队列。
  3. SynchronousQueue

    • 一个不存储元素的阻塞队列。
    • 每个插入操作必须等待另一个线程调用移除操作,否则插入操作将一直阻塞。
    • 吞吐量通常要高于LinkedBlockingQueue
    • 适用于传递性场景,即任务的生产者和消费者速率大致相当的情况。
    • 静态工厂方法Executors.newCachedThreadPool()默认使用了这个队列。
  4. PriorityBlockingQueue

    • 一个具有优先级的无界阻塞队列。
    • 任务根据优先级进行排序,优先级高的任务将优先被执行。
    • 如果多个任务具有相同的优先级,则按照它们到达队列的顺序执行。

6 线程池四种拒绝策略及其对比

  1. AbortPolicy(中止策略):

    • 当线程池无法处理新任务时,直接抛出一个RejectedExecutionException异常。
    • 这是线程池默认的拒绝策略。
    • 适用于对任务丢失敏感的场景,因为它会明确地告知任务被拒绝。
  2. CallerRunsPolicy(调用者运行策略):

    • 当线程池无法处理新任务时,不直接抛出异常,而是将任务交给调用者(提交任务的线程)来执行。
    • 这样做的好处是可以降低新任务的提交速度,从而减轻线程池的压力。
    • 适用于非关键性任务或允许延迟执行的任务。
  3. DiscardPolicy(丢弃策略):

    • 当线程池无法处理新任务时,默默地丢弃该任务,不抛出任何异常或给出任何提示。
    • 适用于对任务丢失不敏感的场景,或者允许任务被静默丢弃的场景。
  4. DiscardOldestPolicy(丢弃最旧任务策略):

    • 当线程池无法处理新任务时,丢弃队列中最旧(存活时间最长)的任务,并尝试重新提交新任务。
    • 这样做可能会导致队列中等待时间较长的任务被丢弃,因此可能不适合对任务顺序有严格要求的场景。
    • 适用于对任务执行顺序不敏感,但需要确保新任务得到执行的场景。

在选择拒绝策略时,需要根据具体的业务需求和场景来决定。例如,如果对任务丢失非常敏感,可以选择AbortPolicy以便及时发现问题;如果允许任务被静默丢弃,可以选择DiscardPolicy以减少不必要的异常处理;如果希望减轻线程池的压力并允许任务延迟执行,可以选择CallerRunsPolicy;如果希望确保新任务得到执行且不关心旧任务的丢失,可以选择DiscardOldestPolicy

7 Synchronized锁升级的条件

  1. 偏向锁升级为轻量级锁
    • 当一个线程已经持有了偏向锁,但此时有另一个线程尝试访问同步代码块时,偏向锁就会升级为轻量级锁。具体来说,当线程A已经持有偏向锁,线程B尝试访问时,它会发现对象头的Mark Word中的线程ID不是自己的,于是会尝试通过CAS操作获取锁。如果失败,偏向锁就会升级为轻量级锁。
  2. 轻量级锁升级为重量级锁
    • 当线程通过自旋等待获取轻量级锁超过一定的次数(这个次数不是固定的,会根据上一次自旋的时间等因素进行调整)或者自旋的线程数超过CPU核数的一半时,轻量级锁就会升级为重量级锁。升级为重量级锁后,未获取到锁的线程会被阻塞,进入等待状态。

8 偏向锁是什么?

偏向锁是Java 6之后加入的一种锁优化机制,它是一种针对加锁操作的优化手段。偏向锁的核心思想是,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。因此,为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。

偏向锁的执行逻辑是,当一个线程首次访问同步块并获取锁时,会在Java对象头中的Mark Word里记录下当前线程的ID。这样,在下次该线程再次访问这个同步块时,JVM就可以检查Mark Word中是否存储着当前线程的ID。如果是,表示该线程已经获得了偏向锁,可以直接进入同步块执行代码,无需再次进行锁的获取操作。

偏向锁的主要优点是,在没有竞争的情况下,它可以提供非常快的响应速度,因为线程无需进行任何额外的操作就可以直接获取锁。然而,偏向锁也有其局限性。当存在其他线程尝试访问同步块时,偏向锁就会升级为轻量级锁或重量级锁,以适应多线程并发的情况。

9 轻量级锁是什么?

轻量级锁是JDK 1.6之后加入的新型锁机制,其本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁并不是用来代替重量级锁的,而是在没有多线程竞争或竞争不激烈的情况下,提供一种性能更好的锁机制。

轻量级锁的实现方式是,在锁被占用时,尝试通过自旋等待的方式获取锁。如果自旋等待的次数超过了预定的上限,或者存在其他线程持有该锁的时间过长,那么轻量级锁就会升级为重量级锁,未获取到锁的线程会被阻塞。

与重量级锁相比,轻量级锁的优势在于它不会阻塞线程,而是通过让线程空循环等待(自旋)的方式来获取锁。这种方式适用于那些同步代码块执行时间很短、线程竞争不激烈的场景。在这些场景下,线程原地等待很短的时间就能够获得锁,从而避免了线程阻塞和唤醒的开销。

10 重量级锁是什么?

重量级锁是Java中最基本的锁机制之一,它依赖于底层操作系统的Mutex(互斥锁)实现。当一个线程尝试获取已经被其他线程持有的重量级锁时,该线程会被阻塞,直到持有锁的线程释放锁为止。在等待锁的过程中,线程会进入阻塞状态,不会消耗CPU资源,但会消耗一定的内存资源来记录线程的状态信息。

重量级锁的开销相对较大,因为每次线程的阻塞和唤醒都需要操作系统的介入,这涉及到用户态和内核态的切换,因此性能成本较高。但是,重量级锁的优点在于它能够保证同一时间只有一个线程访问共享资源,从而避免了多线程并发访问可能导致的数据不一致问题。

11 轻量级锁为啥比重量级锁轻?

重量级锁依赖于底层操作系统的互斥量(Mutex)实现,当线程尝试获取锁失败时,会被阻塞并放入锁等待队列中,由操作系统进行调度。这种方式的开销相对较大,因为涉及到用户态和内核态的切换,以及线程的阻塞和唤醒等操作,都需要消耗一定的CPU和内存资源。

而轻量级锁则是为了在无竞争或低竞争情况下减少这种开销而设计的。轻量级锁在执行过程中,会先尝试通过CAS(Compare-and-Swap)操作来获取锁,如果成功则直接进入同步代码块执行,无需进行线程阻塞和唤醒。这种方式避免了用户态和内核态的切换,减少了线程的阻塞和唤醒开销,因此相对于重量级锁来说,轻量级锁的开销更小。

但是,需要注意的是,轻量级锁并不总是比重量级锁更优。在高竞争情况下,由于轻量级锁需要不断尝试CAS操作来获取锁,这可能会导致大量的CPU资源消耗在无谓的自旋等待上。此时,重量级锁可能会表现得更好,因为它可以让等待锁的线程进入阻塞状态,从而释放CPU资源供其他线程使用。

因此,在选择使用轻量级锁还是重量级锁时,需要根据具体的场景和需求进行权衡。在并发访问较低或竞争不激烈的情况下,轻量级锁可能是一个更好的选择;而在并发访问较高或竞争激烈的情况下,重量级锁可能更为合适。

12 ReentrantLock和Synchronized区别

ReentrantLock和Synchronized都是Java中用于处理多线程并发访问共享资源的锁机制,但它们之间存在一些关键的区别:

  1. 锁的实现方式:

    • Synchronized是Java语言内置的关键词,是依赖于JVM的解释器来锁定,其锁的释放和获取都是由JVM控制,因此其使用上更加便捷,但也因此其机制比较单一,不够灵活。
    • ReentrantLock是JDK提供的一个接口的实现类,是一种依赖于API的锁机制。ReentrantLock需要手动获取和释放锁,且必须在finally块中释放锁,否则可能导致死锁。虽然使用上相对复杂,但ReentrantLock提供了更多的灵活性,例如可以实现公平锁、非公平锁,以及尝试获取锁等。
  2. 等待可中断性:

    • Synchronized是不可中断的,除非加锁的代码执行完毕或者发生异常,否则等待的线程会一直等待下去。
    • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lockInterruptibly()方法来实现这个机制。
  3. 锁状态的查询:

    • Synchronized并没有提供任何机制来查询锁的状态,我们无法知道一个线程是否持有一个对象的锁。
    • ReentrantLock提供了一些方法来查询锁的状态,例如isLocked()方法可以用来查询锁是否被任何线程持有,isHeldByCurrentThread()方法可以用来查询锁是否被当前线程持有。
  4. 锁的申请方式:

    • Synchronized是非公平锁,它无法控制线程获取锁的顺序,容易导致某些线程长时间得不到锁。
    • ReentrantLock默认是非公平锁,但可以通过构造方法指定为公平锁。公平锁会按照线程请求锁的顺序来获取锁,从而避免某些线程长时间得不到锁的情况。
  5. 锁绑定的条件(Condition):

    • Synchronized没有提供与锁绑定的条件(Condition),需要自行实现等待/通知机制,使用起来不够灵活且容易出错。
    • ReentrantLock提供了与锁绑定的条件(Condition),可以更加灵活地实现等待/通知机制,且使用起来更加简单、安全。

总的来说,ReentrantLock提供了比Synchronized更丰富的特性和更灵活的使用方式,但也需要更复杂的操作和管理。在选择使用哪种锁机制时,需要根据具体的场景和需求进行权衡。

13 ThreadLocal的原理

ThreadLocal是Java中的一个类,它提供了线程本地变量的功能。每个线程都可以独立地访问自己的ThreadLocal变量,而不会受到其他线程的干扰。ThreadLocal的原理主要是通过使用一个ThreadLocalMap来存储每个线程的变量副本。

具体来说,每个线程内部都维护了一个ThreadLocalMap数据结构,这个Map的key是ThreadLocal对象,value是线程要存储的变量。ThreadLocalMap是ThreadLocal的一个静态内部类,它实现了键值对的设置和获取功能。每个线程中的ThreadLocalMap都是独立的,互不影响。

当我们在一个线程中调用ThreadLocal的set方法时,实际上是在当前线程的ThreadLocalMap中以ThreadLocal对象为键,将要设置的变量作为值进行存储。而调用get方法时,则是从当前线程的ThreadLocalMap中根据ThreadLocal对象作为键来获取对应的变量值。

由于每个线程都有自己的ThreadLocalMap,并且这个Map只能被当前线程访问,因此ThreadLocal能够实现线程之间的数据隔离。每个线程都可以独立地访问和操作自己的ThreadLocal变量,而不会影响到其他线程。

需要注意的是,ThreadLocal中的变量并不是共享变量,而是每个线程都有自己的一个副本。因此,在使用ThreadLocal时,需要保证每个线程中的变量都被正确地初始化和清理,以避免出现内存泄漏等问题。

另外,ThreadLocalMap中的key使用了弱引用(WeakReference),这是为了避免内存泄漏。当ThreadLocal对象不再被强引用时,它可能会被垃圾回收器回收。由于ThreadLocalMap中的key是弱引用,因此在ThreadLocal对象被回收后,对应的键值对也就能够被清理掉,从而避免内存泄漏的问题。

总之,ThreadLocal的原理是通过在每个线程内部维护一个独立的ThreadLocalMap来实现线程本地变量的存储和访问。每个线程都可以独立地访问和操作自己的ThreadLocal变量,而不会受到其他线程的干扰。同时,为了避免内存泄漏等问题,ThreadLocalMap中的key使用了弱引用。

14 volatile的作用

volatile是Java中的一个关键字,它主要用于修饰变量,并且具有以下作用:

  1. 保证可见性:当一个线程修改了一个volatile变量的值,其他线程能够立即看到这个修改。这是因为volatile关键字会告诉编译器不要对这个变量进行优化,确保变量的读写操作都直接从内存中进行,而不是使用寄存器或缓存。这避免了在多线程环境下,由于每个线程可能拥有自己的本地缓存或寄存器副本,导致变量值在不同线程之间不可见的问题。
  2. 禁止指令重排:编译器在编译代码时会进行优化,包括将一些看似无用的变量读写操作删除或重新排序,以提高代码执行效率。但在多线程环境下,这种优化可能会导致问题,因为一个线程修改了变量的值,但其他线程可能不会立即看到这个变化。使用volatile关键字可以禁止这种指令重排优化,确保变量的读写操作按照代码的顺序执行。

然而,需要注意的是,volatile并不能保证原子性。尽管它可以确保一个线程对变量的修改对其他线程可见,但如果多个线程同时修改同一个volatile变量,仍然可能导致数据不一致的问题。因此,在需要保证原子性的情况下,通常需要使用其他同步机制,如synchronized关键字或java.util.concurrent.atomic包中的原子变量。

15 java锁的分类

  1. 自旋锁:自旋锁是一种采用让当前线程不断在循环体内执行实现的锁。当循环的条件被其他线程改变时,才能进入临界区。如果一直不能访问到资源,就会一直占用CPU资源,所以它会循环一段时间后进入阻塞状态。
  2. 重量级锁:synchronized就是重量级锁的实现机制。当线程尝试获取锁失败时,会被阻塞,等待唤醒。重量级锁会涉及到用户态和内核态的切换,因此性能开销相对较大。
  3. 偏向锁:偏向锁会偏向于第一个访问资源的线程。如果只有一个线程执行同步代码块,那么就会上偏向锁。如果有其他线程抢占资源,那么就会升级为轻量级锁。偏向锁主要目的是减少无竞争情况下的解锁重加锁操作,提高性能。
  4. 轻量级锁:轻量级锁是偏向锁升级后的锁。当线程尝试获取偏向锁失败时,会尝试获取轻量级锁。轻量级锁中的其他线程会进入自旋状态,如果自旋失败,就会升级为重量级锁。轻量级锁主要目的是减少线程挂起的几率,提高性能。
  5. 公平锁和非公平锁:公平锁是指线程按照先来后到的顺序获取锁,而非公平锁则不保证线程获取锁的顺序。synchronized是非公平的,而ReentrantLock默认是非公平的,但可以设置为公平锁。
  6. 可重入锁:可重入锁是指一个线程可以多次获取同一把锁而不会发生死锁。ReentrantLock和synchronized都是可重入锁的实现。
  7. 独享锁和共享锁:独享锁是指同一时间只能有一个线程持有锁,而共享锁则允许多个线程同时持有锁。ReentrantReadWriteLock就是共享锁和独享锁的实现。
  8. 乐观锁和悲观锁:乐观锁和悲观锁并不是Java中具体的锁实现,而是一种锁的设计思想。乐观锁认为数据被其他线程修改的概率很小,因此不会事先加锁,只是在更新数据时判断之前是否有其他线程修改过数据。而悲观锁则认为数据被其他线程修改的概率很大,因此在访问数据时总是先加锁。

16 AQS是什么?

AQS,全称AbstractQueuedSynchronizer,是Java并发包java.util.concurrent.locks下的一个用于构建锁和同步器的框架。它是一个底层同步工具类,使用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。AQS的主要使用方式是继承,子类通过继承AQS并实现其抽象方法来管理同步状态。AQS没有实现任何同步接口,它只定义了若干个acquire和release的抽象方法,这些方法需要被子类去实现。

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

此外,AQS支持两种同步方式:独占式和共享式。这样方便使用者实现不同类型的同步组件,如独占式的ReentrantLock,共享式的Semaphore和CountDownLatch,以及组合式的ReentrantReadWriteLock等。

总的来说,AQS是Java并发编程中非常重要的一个基础组件,它提供了一种构建锁和其他同步组件的通用框架,使得开发者能够更加灵活地处理多线程同步问题。

17 ConcurrentHashMap的面试题

  1. ConcurrentHashMap与HashMap的主要区别是什么?

    • ConcurrentHashMap是线程安全的,而HashMap不是。ConcurrentHashMap在JDK1.7中采用分段锁机制实现并发安全,而在JDK1.8中则采用CAS+Synchronized保证并发安全。
    • ConcurrentHashMap的并发性能通常优于同步的HashMap,因为它允许多个线程同时访问不同的数据段。
  2. ConcurrentHashMap是如何实现线程安全的?

    • 在JDK1.7中,ConcurrentHashMap使用分段锁机制。它将内部数据分为多个段(Segment),每个段都是一个小的哈希表,并有自己的锁。这样,不同的线程可以同时访问不同的小哈希表,从而避免了多个线程同时竞争同一个锁的情况,提高了并发性能。
    • 在JDK1.8中,ConcurrentHashMap选择了与HashMap相同的Node数组+链表+红黑树结构,但采用了CAS+Synchronized来保证并发安全性。当线程尝试修改数据时,首先会使用CAS操作尝试修改,如果失败则使用synchronized关键字加锁,保证只有一个线程能修改数据。
  3. ConcurrentHashMap的get操作需要加锁吗?为什么?

    • ConcurrentHashMap的get操作不需要加锁。在JDK1.7中,由于采用了分段锁机制,不同的线程可以同时访问不同的小哈希表,因此get操作不需要加锁。在JDK1.8中,虽然使用了CAS+Synchronized来保证并发安全性,但get操作只读取数据而不修改数据,因此也不需要加锁。此外,ConcurrentHashMap还使用了volatile关键字和内存屏障等技术来确保数据的可见性和一致性。
  4. ConcurrentHashMap的扩容机制是怎样的?在扩容过程中如何保证线程安全?

    • ConcurrentHashMap的扩容机制与HashMap类似,它会在哈希表的负载因子达到阈值时进行扩容。扩容的过程中,ConcurrentHashMap会将原来的小哈希表逐一复制到新的大哈希表中。
    • 在JDK1.7中,由于采用了分段锁机制,每个小哈希表的扩容是独立的,因此可以并行进行。同时,ConcurrentHashMap在扩容过程中会使用锁来确保数据的一致性。具体来说,当某个线程正在对某个小哈希表进行扩容时,其他线程需要等待该线程完成扩容后才能继续访问该小哈希表。
    • 在JDK1.8中,由于使用了CAS+Synchronized来保证并发安全性,扩容过程中的数据迁移也是线程安全的。当一个线程正在迁移某个节点的数据时,其他线程需要等待该线程完成数据迁移后才能继续访问该节点。同时,ConcurrentHashMap还使用了“高低位”策略来减少扩容过程中的锁竞争,提高了并发性能。
  5. 你能简要描述一下ConcurrentHashMap在JDK1.8中的内部结构吗?

    • 在JDK1.8中,ConcurrentHashMap采用了Node数组+链表+红黑树的结构。其中,Node数组用于存储键值对数据,链表用于解决哈希冲突,红黑树则用于优化查询性能。当链表的长度超过一定阈值时(默认为8),ConcurrentHashMap会将链表转换为红黑树以提高查询效率。此外,ConcurrentHashMap还使用了volatile关键字和CAS操作等机制来保证并发安全性。每个Node节点都包含了一个key、value、hash值和next指针等字段。其中,key和value分别表示键值对的键和值;hash值表示该键值对的哈希值;next指针则指向下一个Node节点(如果存在哈希冲突的话)。同时,每个Node节点还包含了一个锁状态字段和线程拥有者字段等用于实现并发控制的字段。但是需要注意的是,在JDK1.8的ConcurrentHashMap实现中并没有直接使用这些字段来实现锁机制;而是通过CAS+Synchronized等机制来保证并发安全性。因此这些字段更多地是用于辅助实现并发控制而非直接作为锁使用。
  6. ConcurrentHashMap中的put操作是如何进行的?如果发生哈希冲突怎么办?

    • ConcurrentHashMap中的put操作会首先根据key的hash值计算出在Node数组中的索引位置,然后检查该位置上的Node节点是否已经存在。如果存在且key相同,则直接更新value值;如果存在但key不同(即发生哈希冲突),则将新键值对添加到链表或红黑树中;如果不存在,则创建一个新的Node节点并将其添加到数组中。在添加新节点时,如果链表长度超过阈值,则将其转换为红黑树以提高查询效率。整个过程需要保证线程安全性,因此会使用CAS+Synchronized等机制来确保只有一个线程能修改数据。具体来说,在添加新节点或更新已有节点时,会使用CAS操作尝试修改数据;如果失败则使用synchronized关键字加锁并重新尝试修改数据。同时,在遍历链表或红黑树时也需要考虑线程安全性问题,以避免出现并发修改导致的数据不一致问题。因此ConcurrentHashMap在遍历过程中也使用了相应的锁机制来确保数据一致性。但是需要注意的是,在JDK1.8中由于使用了红黑树来优化查询性能,因此在处理哈希冲突时相比JDK1.7有了更大的改进和提升。
  7. 你能谈一下ConcurrentHashMap与Hashtable的主要区别吗?除了线程安全性之外它们还有哪些不同点?

    • 除了线程安全性之外,ConcurrentHashMap与Hashtable还有以下主要区别:首先,在数据结构方面,Hashtable使用单一的数据结构(即数组+链表),而ConcurrentHashMap在JDK1.8中采用了更复杂的数据结构(即数组+链表+红黑树)。这种数据结构的变化使得ConcurrentHashMap在查询性能上有了显著的提升。其次,在锁机制方面,Hashtable使用单一的全局锁来控制并发访问,这意味着当一个线程正在访问Hashtable时,其他线程必须等待该线程完成访问后才能继续访问Hashtable。而ConcurrentHashMap则使用了更细粒度的锁机制(如分段锁或CAS+Synchronized)来控制并发访问,这使得多个线程可以同时访问ConcurrentHashMap的不同部分,从而提高了并发性能。此外,在扩容机制、哈希算法以及其他方面也存在一些细微的差异。例如,在扩容机制方面,Hashtable在扩容时会创建一个新的数组并将原数组中的所有元素重新计算哈希值后放入新数组中;而ConcurrentHashMap则采用了更复杂的扩容机制来减少数据迁移过程中的锁竞争和性能开销。在哈希算法方面,两者也可能使用不同的哈希算法来计算键值对的哈希值。这些差异都可能导致两者在实际应用中的性能表现有所不同。因此,在选择使用哪种哈希表实现时,需要根据具体的应用场景和需求进行权衡和选择。
  8. HashMap

    • 除了线程安全性之外,ConcurrentHashMap与Hashtable还有以下主要区别:首先,在数据结构方面,Hashtable使用单一的数据结构(即数组+链表),而ConcurrentHashMap在JDK1.8中采用了更复杂的数据结构(即数组+链表+红黑树)。这种数据结构的变化使得ConcurrentHashMap在查询性能上有了显著的提升。其次,在锁机制方面,Hashtable使用单一的全局锁来控制并发访问,这意味着当一个线程正在访问Hashtable时,其他线程必须等待该线程完成访问后才能继续访问Hashtable。而ConcurrentHashMap则使用了更细粒度的锁机制(如分段锁或CAS+Synchronized)来控制并发访问,这使得多个线程可以同时访问ConcurrentHashMap的不同部分,从而提高了并发性能。此外,在扩容机制、哈希算法以及其他方面也存在一些细微的差异。例如,在扩容机制方面,Hashtable在扩容时会创建一个新的数组并将原数组中的所有元素重新计算哈希值后放入新数组中;而ConcurrentHashMap则采用了更复杂的扩容机制来减少数据迁移过程中的锁竞争和性能开销。在哈希算法方面,两者也可能使用不同的哈希算法来计算键值对的哈希值。这些差异都可能导致两者在实际应用中的性能表现有所不同。因此,在选择使用哪种哈希表实现时,需要根据具体的应用场景和需求进行权衡和选择。
  9. 你能谈一下ConcurrentHashMap与Hashtable的主要区别吗?除了线程安全性之外它们还有哪些不同点?

    • 除了线程安全性之外,ConcurrentHashMap与Hashtable还有以下主要区别:首先,在数据结构方面,Hashtable使用单一的数据结构(即数组+链表),而ConcurrentHashMap在JDK1.8中采用了更复杂的数据结构(即数组+链表+红黑树)。这种数据结构的变化使得ConcurrentHashMap在查询性能上有了显著的提升。其次,在锁机制方面,Hashtable使用单一的全局锁来控制并发访问,这意味着当一个线程正在访问Hashtable时,其他线程必须等待该线程完成访问后才能继续访问Hashtable。而ConcurrentHashMap则使用了更细粒度的锁机制(如分段锁或CAS+Synchronized)来控制并发访问,这使得多个线程可以同时访问ConcurrentHashMap的不同部分,从而提高了并发性能。此外,在扩容机制、哈希算法以及其他方面也存在一些细微的差异。例如,在扩容机制方面,Hashtable在扩容时会创建一个新的数组并将原数组中的所有元素重新计算哈希值后放入新数组中;而ConcurrentHashMap则采用了更复杂的扩容机制来减少数据迁移过程中的锁竞争和性能开销。在哈希算法方面,两者也可能使用不同的哈希算法来计算键值对的哈希值。这些差异都可能导致两者在实际应用中的性能表现有所不同。因此,在选择使用哪种哈希表实现时,需要根据具体的应用场景和需求进行权衡和选择。

18 JUC工具:Condition、CountDownLatch、CyclicBarrier、阻塞队列简介

JUC(java.util.concurrent)是Java提供的一个并发工具包,其中包含了许多用于多线程编程的工具类,如Condition、CountDownLatch、CyclicBarrier和阻塞队列等:

  1. Condition

Condition是一个多线程协调通信的工具类,它可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒。Condition是AQS(AbstractQueuedSynchronizer)的内部类,每个Condition对象中都保存了一个FIFO的等待队列。线程通过调用Condition的await()方法进入等待状态,并被加入到等待队列中,直到其他线程调用Condition的signal()或signalAll()方法来唤醒它们。这种方法可以实现更精细化的线程同步控制。

  1. CountDownLatch

CountDownLatch是一个同步辅助工具,它允许一个或多个线程等待其他线程完成操作。CountDownLatch内部有一个计数器,初始值为线程的数量。每当一个线程完成操作后,计数器的值就会减1。当计数器的值变为0时,表示所有线程都已经完成操作,此时在CountDownLatch上等待的线程就会被唤醒并继续执行。这种机制可以用于实现多线程的并行处理和同步等待。

  1. CyclicBarrier

CyclicBarrier也是一个同步辅助工具,它可以让一组线程互相等待,直到所有线程都到达某个公共的屏障点(barrier point)后再继续执行。CyclicBarrier内部也维护了一个计数器,用于记录已经到达屏障点的线程数量。当计数器的值达到设定的线程数量时,表示所有线程都已经到达屏障点,此时CyclicBarrier会唤醒所有等待的线程并让它们继续执行。与CountDownLatch不同的是,CyclicBarrier是可以重复使用的,即当所有线程都通过屏障点后,可以重新设置屏障点并再次使用。

  1. 阻塞队列

阻塞队列是一种特殊的队列,它支持在队列为空时阻塞取队列元素的线程,直到队列中有新的元素可供取出;同时也支持在队列已满时阻塞向队列中添加元素的线程,直到队列中有空闲位置可供添加新元素。Java中的阻塞队列都实现了BlockingQueue接口,常见的实现类有ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue等。阻塞队列在多线程编程中非常有用,可以用于实现生产者-消费者模型、线程池等场景。

以上这些JUC工具都是基于Java的内存模型和并发控制机制实现的,它们提供了丰富的功能和灵活的用法,可以帮助开发者更好地编写高效、安全的多线程程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值