juc并发知识

一、什么java线程

Java线程是Java虚拟机中的基本执行单元,它是程序执行的路径。Java线程可以同时执行多个任务,每个任务都可以在不同的时间段内执行。线程是Java多线程编程的核心,可以让程序同时执行多个任务,有效地提高系统的并发执行能力和响应速度。

在Java中,线程可以通过以下方式创建和启动:

  1. 继承Thread类并重写run()方法。
  2. 实现Runnable接口并重写run()方法,将实现Runnable接口的对象作为参数传递给Thread类的构造函数。
  3. 使用Executor框架来创建线程。

二、上下文切换

上下文切换是指操作系统在执行多任务时,从一个任务(进程或线程)切换到另一个任务的过程。在进行上下文切换时,操作系统需要保存当前任务的执行状态(包括寄存器的内容、程序计数器的值、内存分配等信息),将其存储在内存中,然后恢复另一个任务的状态,继续执行该任务。上下文切换是操作系统中非常重要的操作,因为它使得操作系统能够更好地利用计算机的硬件资源,使得多个任务得以同时运行。但是,上下文切换本身也是有开销的,因为需要进行存储和恢复任务状态的操作,所以需要尽量减少上下文切换的次数,以提高计算机的性能。

三、cas算法

CAS算法(Compare and Swap,比较并交换)是一种并发算法。在并发编程中,多个线程可能同时访问同一份数据,这就容易出现问题,如竞态条件等。CAS算法通过原子性操作来避免这些问题,确保多个线程访问同一份数据时能够正确地完成操作。

CAS算法基于三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相等,那么处理器会使用新值更新内存位置的值,否则不会进行任何操作。在Java语言中,CAS可以使用java.util.concurrent.atomic包中提供的类来实现。

CAS算法的优点在于其非阻塞性,即线程不会因为等待锁而挂起,从而提高了并发性能。但是,CAS也有一定的缺点,比如ABA问题——如果一个变量的值从A变为B,再从B变为A,那么CAS会认为这两个操作都成功了,但实际上中间可能存在修改操作,从而导致数据出错。针对这个问题,Java提供了AtomicStampedReference类来解决。同时,CAS如果经常失败,也会耗费系统资源,因此需要进行合理的调优。

四、volatile/synchronized

1、volatile

在Java中,volatile是一种关键字,用于修饰变量。它可以保证多个线程操作同一变量时的可见性、有序性和禁止指令重排序。

具体来说,volatile关键字的作用有以下几个方面:

  • 可见性:使用volatile关键字修饰的变量,在多个线程中进行操作时,每个线程都能及时看到其他线程对该变量最新的修改。这是因为Java虚拟机使用内存屏障机制,使得变量的写操作先于后面的读操作执行,从而保证了可见性。
  • 有序性:实现指令重排序的优化过程可以提高计算机程序的运行效率。但对于涉及多线程的程序来说,指令重排序可能会影响程序的正确性。使用volatile关键字修饰的变量,在赋值和读取时都会插入内存屏障,禁止指令重排,从而保证了操作的有序性。
  • 禁止指令重排序:在涉及多线程的程序中,有一些指令可能会被重排序,使得程序执行顺序与预期不一致。使用volatile关键字修饰的变量,可以禁止特定指令的重排,从而保证程序的正确性。

需要注意的是,volatile关键字不保证原子性。如果需要保证变量操作的原子性,可以选择使用synchronized关键字或者Atomic类。

总之,使用volatile关键字可以在多线程编程中保证变量的可见性和操作的有序性,避免指令重排序带来的问题,提高程序的稳定性和可靠性。

2、synchronized介绍

在Java中,synchronized是一种关键字,用于实现线程同步。synchronized关键字可以用于两个方面:

  • Synchronized修饰方法:当一个方法被synchronized修饰时,该方法在被调用时会自动获取对象的锁,使得该方法的执行过程中只能有一个线程进入,其他线程需要等待锁被释放后才能进入该方法。方法执行完成后,该方法会自动释放对象锁。为了保证锁的释放,synchronized修饰的方法必须正常完成,不能抛出异常或死锁等错误。
  • Synchronized修饰代码块:在代码块中使用synchronized关键字时,需要手动指定锁对象,即在括号内写明锁定的对象。当一个线程进入该代码块时,会自动获取锁对象,其他线程需要等待该线程执行完该代码块中的内容后才能进入,执行程序的下一步操作。同样,在代码块执行完后,该线程会自动释放掉锁。

使用synchronized关键字可以有效地避免多线程并发访问时出现的数据不一致、同步问题等问题,提高程序的稳定性和可靠性。同时,需要注意使用synchronized时需要考虑一些线程安全问题,比如死锁的发生和性能问题等。

总之,synchronized关键字是Java中实现线程同步的基础机制,可以用于方法和代码块等地方。它可以保证同一时间只有一个线程访问共享变量或资源,避免了多线程带来的数据不一致性问题。

3、synchronized实现原理

在Java中,synchronized关键字的实现是基于对象的互斥锁机制,主要有以下两个步骤:

  • 获取锁:当一个线程尝试获取对象的锁时,如果该锁没有被其他线程持有,该线程通过CAS操作将该对象标识为“持有锁”的状态,然后进入临界区继续执行。如果该锁已经被其他线程持有,则该线程进入阻塞状态,等待对象的锁释放,然后再次尝试获取锁。
  • 释放锁:当一个线程执行完临界区代码后,会自动释放该对象的锁。此时,该对象的状态被标识为“无锁”的状态,其他线程可以通过CAS操作再次获取该对象的锁。

具体实现过程中,Java虚拟机(JVM)为每个对象维护一个锁记录(Lock Record),用于存储锁的状态和持有锁的线程信息。当一个线程尝试获取对象的锁时,JVM会查询锁记录,根据锁的状态判断是否可以获取该对象的锁。如果可以获取锁,则修改锁记录的状态为“持有锁”的状态,同时将持有锁的线程信息存储到锁记录中;否则,将线程阻塞。

需要注意的是,在Java中锁的实现是基于对象的,每个对象都有一把锁,只有该对象的锁被释放后,其他线程才能获取该对象的锁。而且,锁只能保证线程安全,不能保证数据安全,因此,在使用synchronized关键字时,我们需要注意线程安全问题和数据一致性问题。

总之,synchronized关键字的实现是基于对象的互斥锁机制,通过锁的获取和释放实现线程同步。了解synchronized的实现原理有助于我们更好地使用synchronized,编写高效、可靠的多线程程序。

4、synchronized锁升级流程

在Java中,synchronized关键字通过锁的升级来实现线程同步,锁的升级过程分为三个阶段:无锁状态、偏向锁状态和轻量级锁状态、重量级锁状态。

  • 首先,对象在刚被创建时,处于无锁状态。当多个线程同时尝试访问该对象时,会发生争夺,需要将该对象升级为更高级的锁状态。
  • 第二,偏向锁状态与轻量级锁状态。在多线程并发下,通常只有一个线程对数据进行更新操作,其他线程只能对数据进行读取操作。此时引入偏向锁,将一个线程对数据的使用权赋予偏向线程,使得其他线程可以直接使用该数据,从而提高程序的运行效率。当多个线程同时访问锁时,偏向锁状态升级为轻量级锁状态,通过CAS(Compare and Swap,比较并交换)方式实现加锁操作。
  • 最后,重量级锁状态。在并发访问激烈的情况下,使用轻量级锁状态可能会导致性能下降、CPU占用率增加等问题。此时,锁状态升级为重量级锁状态,使用操作系统级别的synchronized来保证线程同步,即使多个线程竞争同一把锁也能保证稳定高效的工作。

总之,Java中的synchronized关键字通过锁的升级来实现线程同步,锁的升级过程分为无锁状态、偏向锁状态和轻量级锁状态、重量级锁状态,逐渐提升锁的级别以保证线程同步和数据的正确性。理解锁升级流程有助于我们更好地使用synchronized关键字,提高程序的性能和稳定性。

五、AQS框架

1、框架介绍

AQS(AbstractQueuedSynchronizer)是Java中用于实现锁和其他同步器的基础框架,是一种队列式同步器。它提供了管理线程和资源获取的抽象队列,并定义了基本的获取和释放资源的方法,比如acquire和release等方法,子类可以通过实现这些方法来实现自定义锁和其他同步机制。

AQS的核心数据结构是一个双向链表,用于保存等待AQS资源的线程。线程首先会入队列,等待AQS资源释放后再从队列中获取资源。在AQS中,线程的行为由其在队列中的位置决定,队列中位于前面的线程有优先权。

同时,AQS还提供了一些高级同步机制的实现,比如ReentrantLock、Semaphore、CountDownLatch等。这些同步器的实现都是基于AQS框架提供的抽象方法和队列数据结构来完成的。在使用AQS框架实现锁和其他同步机制时,需要注意安全性和性能问题,并进行测试和调优。

总之,AQS框架是Java中实现锁和其他同步机制的基础类,提供了抽象的队列和基本的获取、释放资源方法,使得开发者能够更加方便、安全和高效地实现自定义的同步机制。

2、AQS的实现类

在Java中,AQS(AbstractQueuedSynchronizer)是实现锁和其他同步器的基础类。AQS提供了一个抽象的队列,用于管理线程和资源的获取,并定义了获取和释放资源的基本方法(acquire和release方法)。同时,AQS也提供了一些常用的同步工具类的实现,如ReentrantLock和Semaphore等。

常见的AQS实现类包括:

  • ReentrantLock:重入锁,可重复进入的互斥锁,使用AQS的子类实现。
  • Semaphore: 信号量,可用来控制线程并发执行的数量,同样使用AQS的子类实现。
  • CountDownLatch: 倒计时器,用于等待一组线程执行完毕后再继续执行任务。
  • CyclicBarrier: 循环栅栏,多个线程可等待到达一个共同的屏障点。
  • ReentrantReadWriteLock: 可重入读写锁,用于控制读和写的并发执行,使用AQS的子类实现。
  • Condition: 条件对象,用于在多个线程间同步操作,实现类似Object.wait和Object.notify的功能。

以上是一些常见的AQS实现类,它们都基于AQS的抽象队列来实现,使得Java的同步工具类更加高效、安全和易用。

3、ReentrantLock

ReentrantLock是Java中的一个可重入锁实现,它提供了与synchronized关键字类似的功能,但更加灵活和高级。下面是对ReentrantLock的实现原理和源码分析:

  •  实现原理:

ReentrantLock是通过一个内部类Sync来实现锁的控制。Sync内部类继承自AQS(AbstractQueuedSynchronizer),AQS是Java并发包中实现锁和同步器的基础框架。ReentrantLock利用了AQS的模板方法来实现自己的具体逻辑,其中的核心操作是获取锁和释放锁。

  • 主要方法:

ReentrantLock提供了以下常用的方法来控制锁的获取和释放:
- lock():获取锁,如果锁已经被其他线程持有,则当前线程会被阻塞,直到获取到锁为止。
- unlock():释放锁,如果当前线程持有锁,则释放锁,如果有其他线程在等待获取锁,则唤醒其中一个线程继续执行。

  • 源码解析:

ReentrantLock的源码比较复杂,涉及到了很多内部类和细节,下面是对关键部分的源码解析:

- lock()方法:

public void lock() {
  sync.acquire(1);
}

在lock()方法中,会调用Sync类的acquire()方法来获取锁。在acquire()方法中,会使用AQS提供的模板方法来实现获取锁的逻辑。

- tryLock()方法:

public boolean tryLock() {
  return sync.nonfairTryAcquire(1);
}

tryLock()方法会调用Sync类的nonfairTryAcquire()方法来尝试获取锁,如果成功获取到锁则返回true,否则返回false。

- unlock()方法:

public void unlock() {
  sync.release(1);
}

在unlock()方法中,会调用Sync类的release()方法来释放锁。在release()方法中,会使用AQS提供的模板方法来实现释放锁的逻辑。

总之,ReentrantLock通过Sync类内部继承AQS并借助AQS的模板方法来实现锁的控制,提供了lock()、unlock()等方法来控制锁的获取和释放。理解ReentrantLock的实现原理和源码解析有助于我们更好地应用和理解Java并发编程中的锁机制。

4、ReentrantReadWriteLock

ReentrantReadWriteLock是Java中的可重入读写锁实现,它允许多个线程同时获取读锁,但只允许一个线程获取写锁。下面是对ReentrantReadWriteLock的实现原理和源码解析:

  • 实现原理:

ReentrantReadWriteLock是通过一个内部类Sync来实现读写锁的控制。Sync内部类继承自AbstractQueuedSynchronizer(AQS),它利用了AQS的模板方法来实现自己的具体逻辑,其中的核心操作是获取读锁和写锁。

  • 主要方法:

ReentrantReadWriteLock提供了以下常用的方法来控制读锁和写锁的获取和释放:
- readLock():获取读锁,允许多个线程同时获取读锁。
- writeLock():获取写锁,只允许一个线程获取写锁。
- unlock():释放读锁或写锁。

  • 源码解析:

ReentrantReadWriteLock的源码相对复杂,涉及到了Sync、ReadLock、WriteLock等内部类和方法,下面是对关键部分的源码解析:

- readLock()方法:

public ReentrantReadWriteLock.ReadLock readLock() {
  return readerLock;
}

在readLock()方法中,会返回ReentrantReadWriteLock的内部类ReadLock实例,用于获取读锁。

- readLock()详解和源码:

ReentrantReadWriteLock的readerLock是ReentrantReadWriteLock类内部的一个内部类ReadLock的实例,用于获取读锁。下面是关于readerLock的原理和源码解析:

  • 原理:

ReentrantReadWriteLock的读锁(readerLock)的实现是基于AQS(AbstractQueuedSynchronizer)的共享模式。多个线程可以同时获取读锁,只要没有线程持有写锁。当有线程持有写锁时,其他线程无法获取读锁。

  • 源码解析:

readerLock是ReentrantReadWriteLock的内部类ReadLock的一个实例,下面是对它的关键方法的源码解析:

- lock()方法:

public void lock() {
    sync.acquireShared(1);
}

在lock()方法中,会调用sync(对应的是Sync类)的acquireShared()方法来获取读锁。在sync的acquireShared()方法内部,会调用AQS提供的doAcquireShared()方法来实现获取锁的逻辑。

- unlock()方法:

public void unlock() {
    sync.releaseShared(1);
}

在unlock()方法中,会调用sync(对应的是Sync类)的releaseShared()方法来释放读锁。在sync的releaseShared()方法内部,会调用AQS提供的doReleaseShared()方法来实现释放锁的逻辑。

总之,通过sync类继承自AQS并借助AQS的模板方法来实现读锁的控制。ReentrantReadWriteLock的readerLock实际上是对sync的封装,通过调用sync的acquireShared()和releaseShared()方法来获取和释放读锁。理解ReentrantReadWriteLock的readerLock的实现原理和源码解析有助于我们更好地应用和理解Java并发编程中的读写锁机制。

- writeLock()方法:

public ReentrantReadWriteLock.WriteLock writeLock() {
  return writerLock;
}

在writeLock()方法中,会返回ReentrantReadWriteLock的内部类WriteLock实例,用于获取写锁。

- writeLock()详解和源码

ReentrantReadWriteLock的writeLock()方法是ReentrantReadWriteLock类内部的一个内部类WriteLock的实例,用于获取写锁。下面是关于writeLock()的原理和源码解析:

  • 原理:

ReentrantReadWriteLock的写锁(writeLock)的实现是基于AQS(AbstractQueuedSynchronizer)的独占模式。只有一个线程能够获取写锁,当有线程持有写锁或读锁时,其他线程无法获取写锁。

  • 源码解析:

writeLock()方法返回的是ReentrantReadWriteLock的内部类WriteLock的一个实例,下面是对它的关键方法的源码解析:

- lock()方法:

public void lock() {
    sync.acquire(1);
}

在lock()方法中,会调用sync(对应的是Sync类)的acquire()方法来获取写锁。在sync的acquire()方法内部,会调用AQS提供的doAcquire()方法来实现获取锁的逻辑。

- unlock()方法:

public void unlock() {
    sync.release(1);
}

在unlock()方法中,会调用sync(对应的是Sync类)的release()方法来释放写锁。在sync的release()方法内部,会调用AQS提供的doRelease()方法来实现释放锁的逻辑。

总之,ReentrantReadWriteLock的writeLock()方法内部实际上是对sync的封装,通过调用sync的acquire()和release()方法来获取和释放写锁。通过sync类继承自AQS并借助AQS的模板方法来实现写锁的控制。理解ReentrantReadWriteLock的writeLock()方法的实现原理和源码解析有助于我们更好地应用和理解Java并发编程中的读写锁机制。

- unlock()方法:

public void unlock() {
  sync.releaseShared(1);
}

在unlock()方法中,会调用Sync类的releaseShared()方法来释放读锁或写锁。在releaseShared()方法中,会使用AQS提供的模板方法来实现释放锁的逻辑。

总之,ReentrantReadWriteLock通过Sync类内部继承AQS并借助AQS的模板方法来实现读写锁的控制。它提供了readLock()、writeLock()等方法来控制读锁和写锁的获取,以及unlock()方法来释放锁。理解ReentrantReadWriteLock的实现原理和源码解析有助于我们更好地应用和理解Java并发编程中的读写锁机制。

六、线程池、同步队列

1、定义

Java线程池是一种管理和复用线程的机制,它利用一个线程集合(线程池),来管理同时运行的多个线程,以及满足多任务并行执行的需求。其中,线程池的任务是将多个任务分配到固定数量的线程中去执行,从而避免过多的线程竞争资源,提高应用程序的并发能力和响应速度。Java线程池的优点包括:节省了线程的创建和销毁等开销,避免了过多的线程竞争资源和导致性能下降等问题,更好地支持并发执行多个任务。

2、工作原理

 具体可参考这篇文章:Java中的线程池使用及原理icon-default.png?t=N7T8https://www.cnblogs.com/yuyiming/p/17592483.html3、队列

Java同步队列的工作原理主要涉及以下几个关键点:

  • 同步队列的定义:同步队列是一个具备阻塞功能的队列,它遵循先进先出(FIFO)的原则,可以实现多线程间的数据同步和通信。
  • 队列操作:同步队列提供了阻塞的入队和出队操作。当队列为空时,尝试出队操作的线程会进入阻塞状态,直到有新的元素入队为止。当队列满时,尝试入队操作的线程会进入阻塞状态,直到有空闲位置为止。
  • 线程协作:同步队列通过内部实现的锁、条件变量等机制,实现了线程的等待和唤醒。当队列为空时,出队操作的线程会等待直到队列非空;当队列满时,入队操作的线程会等待直到队列有空闲位置。
  • 线程安全:同步队列的内部实现采用了线程安全的机制,确保多个线程之间的操作不会出现竞争和数据不一致的问题。例如,在入队和出队操作时,会对共享的队列对象进行加锁,保证操作的原子性。

总而言之,Java同步队列通过阻塞操作和线程协作的方式,实现了多线程间的数据同步和通信,并且保证了线程安全。它在并发编程中起到了重要的作用。

在Java中,常见的队列有以下几种:

  • LinkedList:LinkedList是Java中基本的双向链表实现,也可以作为队列使用。它实现了Queue接口,因此可以使用队列的相关方法,如add、offer、poll、peek等。LinkedList是一个非线程安全的队列。
  • ArrayDeque:ArrayDeque是一个基于数组的双向队列实现,可以作为队列或栈使用。它也实现了Queue接口,并提供了相应的方法。ArrayDeque在添加和删除元素时具有高效性能,并且没有容量限制。它也是一个非线程安全的队列。
  • ArrayBlockingQueue:ArrayBlockingQueue是一个基于数组的阻塞队列实现,它具有一个固定的容量。它实现了BlockingQueue接口,可以安全地用于多线程环境。ArrayBlockingQueue在添加和删除元素时是线程安全的,并且支持阻塞操作,在队列满或空时,相关方法会阻塞线程。
  • LinkedBlockingQueue:LinkedBlockingQueue是一个基于链表的阻塞队列实现,它可以具有可选的容量限制。它也实现了BlockingQueue接口,可以用于多线程环境。LinkedBlockingQueue在添加和删除元素时是线程安全的,并且支持阻塞操作,在队列满或空时,相关方法会阻塞线程。
  • PriorityBlockingQueue:PriorityBlockingQueue是一个基于优先级的无界阻塞队列实现,可以用于多线程环境。它根据元素的优先级进行排序,并使用Comparable或Comparator接口来确定优先级。PriorityBlockingQueue在添加和删除元素时是线程安全的,并且支持阻塞操作。

这些队列的区别主要在于底层实现和特性。LinkedList和ArrayDeque是非阻塞队列,性能较高但不支持阻塞操作;ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue都是阻塞队列,支持阻塞操作并具有线程安全性。此外,ArrayBlockingQueue和LinkedBlockingQueue具有固定或可选的容量限制,而PriorityBlockingQueue是无界的。

4、java并发容器

Java并发容器是一类可以在多线程环境下安全使用的数据结构。Java并发容器提供了多线程环境下的安全操作,而不需要开发者进行额外的同步控制。以下是几种常用的Java并发容器:

  • ConcurrentHashMap

ConcurrentHashMap是一种线程安全的Map实现,支持高并发的读写操作。ConcurrentHashMap内部采用分段锁技术,将数据划分为若干个Segment,每个Segment内部都是一个HashMap实例,任何时候只有一个Segment被修改,其它Segment可以被并发访问,从而提高了Map的并发性能。

  • CopyOnWriteArrayList

CopyOnWriteArrayList是一种线程安全的List实现,内部采用读写分离的策略,即读取数据时不需要进行同步控制,而写入数据时则进行复制,保证读写之间互不干扰。这种策略可以极大地提高List的读取性能,适用于数据读取远远大于写入的场景。

  • ConcurrentLinkedQueue

ConcurrentLinkedQueue是一种线程安全的队列实现,内部采用非阻塞算法,通过CAS(Compare and Swap)操作实现线程安全,适用于高并发的生产者-消费者模型。ConcurrentLinkedQueue的性能优于BlockingQueue,尤其是在生产者比消费者多的场景。

  • BlockingQueue

BlockingQueue是一种阻塞队列,支持阻塞式的入队和出队操作。当队列为空时,消费者线程会被阻塞,直到有数据被加入到队列中;当队列已满时,生产者线程会被阻塞,直到有空闲位置可以存储数据。BlockingQueue是实现生产者-消费者模型的一个重要工具。常用的BlockingQueue有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。

七、java并发工具

1、CountDownLatch

CountDownLatch是Java提供的一种线程同步工具类,用于等待多个线程完成任务后再继续执行。CountDownLatch的构造方法接受一个int类型的参数,表示需要等待的线程数。CountDownLatch通过一个计数器来实现,初始值为参数指定的数值,当一个线程完成任务后,计数器的值会减一,当计数器的值变为0时,处于等待状态的线程将被唤醒。

在使用CountDownLatch时,通常会创建一个主线程和多个工作线程,主线程会调用CountDownLatch.await()方法进入等待状态,等待所有工作线程完成任务后再继续执行。每个工作线程完成任务后,会调用CountDownLatch.countDown()方法将计数器减一。

具体的工作流程如下:

  • 创建CountDownLatch对象,并指定需要等待的线程数,通常要等待的线程数是在主线程中指定的。

  • 创建工作线程,并将CountDownLatch对象传递给它们。每个工作线程完成任务后,会调用CountDownLatch.countDown()方法将计数器减一。

  • 主线程调用CountDownLatch.await()方法,等待所有工作线程完成任务。

  • 当所有工作线程都完成了任务后,计数器的值变为0,主线程从CountDownLatch.await()方法中返回,继续执行后续操作。

在使用CountDownLatch时,需要注意以下几点:

  • CountDownLatch只能使用一次,一旦计数器的初始值变为0,它将不能重置。

  • 如果在计数器减到0之前,主线程被中断了,那么await()方法将抛出InterruptedException异常。因此,需要正确处理InterruptedException异常。

  • CountDownLatch只能保证主线程在所有工作线程完成任务后才继续执行,但无法保证这些工作线程执行完成的顺序。

总之,CountDownLatch是Java中非常常用的线程同步工具,可以帮助我们实现多线程之间的同步。

用法详解可参考CountDownLatch用法详解

2、CyclicBarrier

CyclicBarrier是Java提供的一种线程同步工具类,与CountDownLatch类似,也可以用于多个线程之间的同步。与CountDownLatch不同的是,CyclicBarrier可以重复使用,而且可以在所有线程都到达屏障后执行一些特定的操作。

CyclicBarrier的工作原理可以用以下步骤来描述:

  • 创建CyclicBarrier对象,并指定需要等待的线程数以及到达屏障时需要执行的操作。

  • 创建多个工作线程,并将CyclicBarrier对象传递给它们。这些工作线程会执行一些耗时的操作,然后通过调用CyclicBarrier.await()方法等待其他线程到达屏障。

  • 当指定数量的工作线程都到达屏障时,CyclicBarrier会执行指定的操作,并将计数器重置为初始值。屏障释放后,所有线程都可以继续执行任务。

  • 如果多个线程到达屏障的时间不同,早到的线程将进入等待状态,等待其他线程到达屏障。只有在所有线程都到达屏障后,才会继续执行后续操作。

在使用CyclicBarrier时,需要注意以下几点:

  • CyclicBarrier的计数器值可以重置,可以被多次使用。

  • 如果在所有工作线程都到达屏障之前,任意一个线程被中断了,那么await()方法将抛出BrokenBarrierException异常。因此,需要正确处理BrokenBarrierException异常。

  • 如果指定的操作需要进行时间比较长,可能会导致所有线程都到达屏障之后,需要等待一段时间才能执行操作。

总之,CyclicBarrier是Java中另一个非常常用的线程同步工具,在多个线程之间协调执行任务方面提供了很大的便利性。

用法详解:可参考并发编程之CyclicBarrier详解

3、Exchanger

Exchanger是Java提供的一种并发工具类,用于两个线程之间交换数据。Exchanger允许两个线程在合适的时候相互交换对象。当第一个线程调用Exchanger.exchange()方法时,它会阻塞等待另一个线程到达同一行代码并调用同一个Exchanger对象的exchange()方法。当两个线程都达到这个点时,它们将交换数据,并继续执行后续操作。

Exchanger的工作原理可以用以下步骤来描述:

  1. 创建Exchanger对象。

  2. 创建两个线程,其中一个线程先进入代码区,调用Exchanger.exchange()方法并传递一个对象。该线程进入阻塞状态等待另一个线程到达,并传递另一个对象。

  3. 另一个线程进入代码区后,也调用Exchanger.exchange()方法并传递一个对象。它会阻塞等待另一个线程到达,并传递另一个对象。

  4. 当两个线程都到达代码区,并且传递了对象,Exchanger将交换这两个对象并返回,两个线程都可以继续执行后续操作。

在使用Exchanger时,需要注意以下几点:

  1. Exchanger.exchange()方法是一个阻塞方法,一定要确保有另一个线程到达,否则将一直处于等待状态。

  2. Exchanger.exchange()方法可以在任何位置调用,可以是在多个线程之间的任意位置,不一定要在线程的起点或终点。

  3. Exchanger只能交换两个线程之间的数据,不适用于多个线程之间的数据交换。

总之,Exchanger是Java中提供的又一种线程同步工具,可以帮助我们实现两个线程之间的数据交换操作。

用法代码示例:

import java.util.concurrent.Exchanger;

public class ExchangerDemo {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();
        
        new Thread(() -> {
            String str1 = "Hello";
            try {
                System.out.println("Thread 1 initial string: " + str1);
                String str2 = exchanger.exchange(str1);
                System.out.println("Thread 1 exchanged string: " + str2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        
        new Thread(() -> {
            String str2 = "World";
            try {
                System.out.println("Thread 2 initial string: " + str2);
                String str1 = exchanger.exchange(str2);
                System.out.println("Thread 2 exchanged string: " + str1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

在这个例子中,我们创建了一个Exchanger对象来交换两个线程之间的字符串。当其中一个线程调用exchange方法并传递字符串时,它将等待另一个线程到达同一行代码并调用同一个Exchanger对象的exchange()方法。当两个线程都调用了exchange()方法后,它们将互换字符串,并继续执行后续操作。

输出结果如下:

Thread 1 initial string: Hello

Thread 2 initial string: World

Thread 1 exchanged string: World

Thread 2 exchanged string: Hello

可以看到,第一个线程交换字符串"Hello"和第二个线程传递的字符串"World",返回交换后的字符串"World",第二个线程也交换字符串,返回交换后的字符串"Hello"。

4、Semaphore

Semaphore是Java并发工具类之一,用于限制同时访问某个资源的线程数量。Semaphore内部维护了一个计数器,该计数器表示当前可用的许可数。

Semaphore的工作原理如下:

  • 初始化Semaphore对象时需要指定许可的数量,也就是初始的计数器值。

  • 当一个线程需要访问受Semaphore限制的资源时,它必须先通过acquire()方法获取一个许可。

  • 如果当前可用许可数大于0,线程将获得一个许可并继续执行。如果当前可用许可数为0,则线程将进入阻塞状态等待其他线程释放一个许可。

  • 当一个线程完成了对受Semaphore限制的资源的访问,它必须调用release()方法释放一个许可。这样就增加了计数器的值,其他等待许可的线程将被唤醒并有机会获得许可。

Semaphore的使用场景包括但不限于:

  • 控制同时访问某个特定资源的线程数量,例如连接池、数据库连接等。

  • 实现限流功能,限制一定数量的并发请求。

需要注意的是:

  • 在使用Semaphore时,需要根据实际情况合理设置初始许可数量,避免出现竞争问题或资源的浪费。

  • 在获取许可时,可以通过尝试获取(tryAcquire)或指定等待超时时间(tryAcquire(timeout, unit))来灵活控制。

下面是一个简单的Semaphore使用示例,模拟实现一个资源池:

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2); // 初始许可数为2
        
        // 创建线程模拟资源池的使用

        for (int i = 0; i < 5; i++) {
            final int threadId = i;
            new Thread(() -> {
                try {
                    System.out.println("Thread " + threadId + " try to acquire a permit.");
                    semaphore.acquire(); // 获取一个许可

                    System.out.println("Thread " + threadId + " acquired a permit.");
                    Thread.sleep(2000); // 模拟使用资源的操作

                    System.out.println("Thread " + threadId + " release the permit.");
                    semaphore.release(); // 释放一个许可

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在上面的例子中,我们使用Semaphore模拟了一个资源池,初始许可数为2。每个线程在执行一段模拟使用资源的代码之前,首先尝试获取一个许可。当有两个线程同时获取到许可时,它们可以继续执行,并在完成后释放许可,给其他等待许可的线程机会。

输出结果可能如下所示:

Thread 0 try to acquire a permit.
Thread 0 acquired a permit.
Thread 1 try to acquire a permit.
Thread 1 acquired a permit.
Thread 0 release the permit.
Thread 2 try to acquire a permit.
Thread 2 acquired a permit.
Thread 3 try to acquire a permit.
Thread 4 try to acquire a permit.
Thread 2 release the permit.
Thread 4 acquired a permit.
Thread 1 release the permit.
Thread 4 release the permit.
Thread 3 acquired a permit.
Thread 3 release the permit.
5、Phaser

Phaser是Java并发工具类之一,用于控制多个线程分阶段执行任务,并能够自动同步各个线程。Phaser类可以视为CyclicBarrier和CountDownLatch的增强版本,它能够更灵活地控制线程的协作。

Phaser的工作原理如下:

  • 初始化Phaser对象时需要指定参与的线程数量(也就是参与者数),并可指定一个可选的初始阶段数。

  • 每个线程在执行任务时都会调用Phaser的arriveAndAwaitAdvance()方法,这个方法会将当前线程注册为Phaser的参与者,并等待其他参与者到达同一个阶段。

  • 当所有参与者都调用arriveAndAwaitAdvance()方法后,Phaser对象就会进入下一个阶段,所有等待在此阶段的线程将被唤醒继续执行下一阶段的任务。

  • Phaser还提供了一些其他的方法,例如arrive()和arriveAndDeregister(),能够对参与者的状态和行为进行更精细的控制。

需要注意的是:

  • Phaser支持可变的参与者数量,可以在运行时动态地添加或移除参与者,这对于动态调整任务执行的数量或优化资源使用非常有用。

  • Phaser也可以用于实现多阶段的任务,并且能够自动处理不同时刻参与者的到达情况,简化并发编程的实现。

下面是一个简单的Phaser使用示例,模拟多个线程分阶段执行任务:

import java.util.concurrent.Phaser;

public class PhaserDemo {
    public static void main(String[] args) {
        final int threads = 3;
        final int phases = 3;
        
        Phaser phaser = new Phaser(threads); // 创建Phaser对象,指定参与者数量

        for (int i = 0; i < threads; i++) {
            final int threadId = i;
            new Thread(() -> {
                for (int j = 0; j < phases; j++) {
                    System.out.println("Thread " + threadId + " start phase " + (j + 1));
                    phaser.arriveAndAwaitAdvance(); // 等待其他线程到达同一阶段

                    System.out.println("Thread " + threadId + " end phase " + (j + 1));
                }
                phaser.arriveAndDeregister(); // 注销参与者

            }).start();
        }
    }
}

在上面的例子中,我们创建了一个Phaser对象,指定参与者数量为3。每个线程在执行任务时都会分3个阶段,每个阶段需要等待其他线程到达同一阶段才能继续执行。当所有参与者完成了所有阶段的任务后,它们会逐个注销,这样Phaser对象就可以被垃圾回收了。

输出结果可能如下所示:

Thread 0 start phase 1

Thread 1 start phase 1

Thread 2 start phase 1

Thread 1 end phase 1

Thread 2 end phase 1

Thread 0 end phase 1

Thread 2 start phase 2

Thread 1 start phase 2

Thread 0 start phase 2

Thread 2 end phase 2

Thread 1 end phase 2

Thread 0 end phase 2

Thread 1 start phase 3

Thread 2 start phase 3

Thread 0 start phase 3

Thread 1 end phase 3

Thread 2 end phase 3

Thread 0 end phase 3
6、CompletableFuture

CompletableFuture是Java 8引入的一个类,用于支持异步编程和并发操作。它提供了一种简洁的方式来处理异步任务的完成和组合,并可以灵活地操作多个CompletableFuture对象。

CompletableFuture的工作原理如下:

  • CompletableFuture类代表一个异步计算,可以看作是一个容器,用来保存某个计算任务的最终结果。

  • CompletableFuture对象可以通过调用静态方法完成(complete),将一个值或一个异常传递给CompletableFuture。

  • CompletableFuture对象可以通过调用静态方法supplyAsync或runAsync,传入一个任务,获取一个CompletableFuture对象,并启动任务的执行。

  • CompletableFuture对象可以通过调用方法thenApply、thenAccept、thenRun或者thenCompose等方法,定义任务的执行流程,组合多个CompletableFuture对象。

  • 当CompletableFuture的任务执行完成后,它可以触发一系列的回调函数,例如whenComplete、handle、thenApply、thenAccept、thenRun等,以便在任务完成时处理结果或发生异常时进行相应的处理。

需要注意的是:

  • CompletableFuture提供了一系列的方法,用于处理多个CompletableFuture对象的组合操作,例如thenCombine、thenAcceptBoth、runAfterBoth等。

  • CompletableFuture还支持异步执行的超时处理,例如completeOnTimeout和orTimeout等。

下面是一个简单的CompletableFuture使用示例,模拟异步计算和组合操作:

import java.util.concurrent.CompletableFuture;

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("Start async calculation...");
            sleep(2000);
            System.out.println("Async calculation completed.");
            return 100;
        });
        
        CompletableFuture<String> future2 = future1.thenApply(result -> {
            System.out.println("Performing transformation on result: " + result);
            return "Transformed result: " + result;
        });
        
        CompletableFuture<Void> future3 = future2.thenAccept(result -> {
            System.out.println("Got result: " + result);
        });
        
        future3.get(); // 等待所有任务完成

    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上面的例子中,我们通过CompletableFuture.supplyAsync方法创建了一个异步计算任务future1,模拟了一个耗时2秒的计算过程。然后通过future1调用方法thenApply定义了一个转换操作,创建了另一个CompletableFuture对象future2。最后,我们通过future2调用方法thenAccept定义了一个消费操作,创建了最后一个CompletableFuture对象future3。在所有任务完成后,我们使用get方法等待并获取最终结果。

输出结果可能如下所示:

Start async calculation...
Async calculation completed.
Performing transformation on result: 100

Got result: Transformed result: 100

在上面的例子中,async calculation和transformation操作都是在不同的线程中执行的,它们的执行顺序是不确定的。CompletableFuture提供了非常灵活的方式来组合异步任务,能够充分发挥多核处理器的性能,提高程序的并发性能。

7、fork/join框架

fork/join框架是Java SE 7中引入的一种并行计算框架,使用fork/join框架可以更加方便和高效地实现任务拆分和分配,从而更好地利用多核处理器的性能。在Java 8中,Stream API也是基于fork/join框架实现的。

fork/join框架的工作原理如下:

  • fork/join框架主要包含两个类:ForkJoinPool和ForkJoinTask。ForkJoinPool管理线程池和任务队列,ForkJoinTask代表了可分解的任务,并提供了一种递归式的分治算法。

  • 分割任务:当一个任务的大小超过了一个阈值时,它将被分割为多个较小的子任务,这个过程称为fork。

  • 执行任务:ForkJoinPool中的工作线程将异步执行这些任务,如果执行的任务是大任务(即!=基础任务),则将会被分割成较小的子任务并递归执行,直到分割出的所有子任务都不能再进一步分割为止。

  • 合并结果:所有任务的计算结果将汇总到原始任务中心,并为它们提供单一的结果。在计算任务完成后,当前执行的线程将会执行几个允许它帮助其他工作线程执行的任务(例如,使用fork方法启动的任务)。

需要注意的是:

  • ForkJoinTask是一个抽象类,在实际应用中可以通过继承RecursiveAction或RecursiveTask来完成自己的任务计算和分割。

  • 在使用fork/join框架时需要注意防止问题分割得过多,因为分割过多会导致任务队列繁忙而导致性能下降。

下面是一个简单的fork/join框架使用示例,模拟求取一个数组中所有元素的和:

import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class ForkJoinDemo extends RecursiveTask<Integer> {

    private static final int THRESHOLD = 100;
    private int[] arr;
    private int start;
    private int end;

    public ForkJoinDemo(int[] arr, int start, int end) {
        this.arr = arr;
        this.start = start;
        this.end = end;
    }

    @Override

    protected Integer compute() {
        if (end - start <= THRESHOLD) { // 如果任务足够小,直接计算

            int sum = 0;
            for (int i = start; i <= end; i++) {
                sum += arr[i];
            }
            return sum;
        } else { // 如果任务太大,就分裂成两个子任务计算

            int mid = start + (end - start) / 2;
            ForkJoinDemo left = new ForkJoinDemo(arr, start, mid);
            ForkJoinDemo right = new ForkJoinDemo(arr, mid + 1, end);
            left.fork(); // 执行子任务

            right.fork();
            return left.join() + right.join(); // 合并子任务

        }
    }

    public static void main(String[] args) {
        int n = 10_000_000;
        int[] arr = new int[n];
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            arr[i] = random.nextInt(100);
        }
        ForkJoinPool pool = new ForkJoinPool();
        int sum = pool.invoke(new ForkJoinDemo(arr, 0, n - 1));
        System.out.println("Sum of array elements: " + sum);
    }
}

在上面的例子中,我们创建了一个ForkJoinDemo类,并继承自RecursiveTask。在compute方法中,我们首先判断任务的大小是否超过一个阈值,如果超过了,就分裂成两个子任务,并递归执行。如果任务足够小,则直接计算结果。在主函数中,我们创建一个长度为1000万的数组,通过ForkJoinDemo

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值