Java核心技术之- 多线程并发-JUC总结-并发编程

JUC并发包结构图:

java.util.concurrent:

JUC是Java.util.concurrent的缩写,指的是Java中的并发工具包,包含了大量的并发编程工具和类,例如线程池、锁、队列等,是Java并发编程中不可或缺的一部分。
在这里插入图片描述
java.util.concurrent.Atomic:

java.util.concurrent.atomic包提供了一组原子操作类,用于利用CPU指令提供更快速的线程安全数据处理。这些类支持在单个原子操作中读取和写入变量的值,从而避免了线程交错或另一个线程执行会干扰到正在执行的方法的可能性。该包中的原子变量类型包括原始类型(如int和long),以及引用类型(如AtomicReference)。Java中原子操作的实现是依靠CPU指令级别的CAS操作,即比较并交换操作,来保证多线程环境下的线程安全。使用Atomic包中的原子操作进行线程间同步,可以避免传统的synchronized锁操作带来的性能损失和线程阻塞问题,提高并发性能。
在这里插入图片描述
java.util.concurrent.Lock:

java.util.concurrent.Lock是Java.util.concurrent并发包中的一种锁机制,是Java中的一种可重入的互斥锁。和Java中的synchronized关键字相比,Lock接口提供了更灵活的锁定方式,支持可重入锁、公平锁、读写锁等。Lock接口中定义了lock()和unlock()方法来实现加锁和解锁的功能。和synchronized关键字不同的是,当一个线程获取到锁后,其它线程不能直接抢占该锁,而只能等待该线程释放锁或超时。而且可以有多个条件变量,一个锁可以支持多种条件变量,从而实现更灵活的线程同步机制。Lock接口的实现类有ReentrantLock、ReentrantReadWriteLock等。

另外AQS和AQLS都是Java并发编程中的重要概念,它们之间的区别如下:

  1. AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个基础框架,它是Java.util.concurrent包的核心之一,提供了基于FIFO等待队列的同步机制,可以用来实现多种同步器,如CountDownLatch、ReentrantLock、Semaphore等。

  2. AQLS(AbstractQueuedLongSynchronizer)是AQS的一个具体实现,主要用于实现锁或其他同步器的基础框架。与AQS不同的是,AQLS维护的等待队列中的节点存储的是long类型的状态值,而不是AQS中的Node节点。

  3. 在实现具体同步器时,需要继承AQS或AQLS,并重写其中的一些方法。对于AQS而言,需要重写tryAcquire()/tryRelease()方法,对于AQLS而言,需要重写tryAcquire()/tryReleaseShared()方法。

  4. 从设计思想上来说,AQS更侧重于同步器的实现原理和基础架构,而AQLS更侧重于提供一种基于long型状态值的同步机制来支持锁的实现。

总之,AQS和AQLS都是Java并发编程中重要的同步机制基础框架,AQLS是AQS的一种具体实现,主要用于实现锁或其他同步器的基础框架。
在这里插入图片描述

1. 线程基础

- 线程的创建与启动方式

线程的创建与启动方式可以通过以下两种途径实现:

  1. 继承 Thread 类
    定义一个类,继承自 Thread 类,并重写 run() 方法来定义线程的执行逻辑。然后创建该类的实例对象并调用 start() 方法来启动线程,例如:
public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行逻辑
    }
}
MyThread thread = new MyThread();
thread.start();
  1. 实现 Runnable 接口
    定义一个类,实现 Runnable 接口,并实现 run() 方法来定义线程的执行逻辑。然后创建该类的实例对象并将其传递给 Thread 类的构造函数中,最后调用 start() 方法来启动线程,例如:
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行逻辑
    }
}
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();

这两种方式都可以实现线程的创建和启动,但一般而言,推荐使用第二种方式,因为可以避免由于 Java 中的单继承问题而导致的类继承关系过于复杂的情况。

- 线程生命周期管理

线程的生命周期可以分为以下几个阶段:

  1. 新建状态:线程被创建时,进入新建状态。此时线程没有被启动,也没有开始执行任何任务。
  2. 就绪状态:调用线程的 start() 方法后,线程进入就绪状态。此时线程已准备好运行,等待 CPU 分配时间片。
  3. 运行状态:在就绪状态时,如果 CPU 分配到了时间片,线程就进入运行状态。此时线程开始执行其 run() 方法中的任务代码。
  4. 阻塞状态:线程可能进入阻塞状态,例如等待 I/O 操作完成、等待其他线程执行完毕等。当线程进入阻塞状态时,它会暂时停止运行,直到满足某些条件才会被唤醒。
  5. 终止状态:线程完成了它的任务代码执行或者因异常或中断而被迫终止时,进入终止状态。此时线程的执行已经结束,它将永远不会再被启用。

线程的生命周期管理包括:

  1. 线程状态的监控和判定:可以使用 getState() 方法获取线程的状态,也可以使用 isAlive() 方法来判定线程是否处于运行状态。
  2. 线程状态的转换和控制:可以使用 wait()、notify()、notifyAll() 方法来实现线程状态的转换和控制,实现线程通信和协作。
  3. 线程中断和异常处理:可以使用 interrupt() 方法中断线程的执行,也可以在 run() 方法中使用 try-catch 块来处理异常,保证线程的正常终止。

- 线程的状态转换

线程的状态转换通常由线程自身或其他线程中的代码触发,主要包括以下几种情况:

  1. 线程启动:当调用线程的 start() 方法时,线程从新建状态转换到就绪状态。

  2. 线程阻塞:

    • 当线程执行 sleep() 方法时,线程进入阻塞状态,等待指定的时间后重新进入就绪状态。
    • 当线程执行 wait() 方法时,线程进入阻塞状态,等待其他线程调用 notify() 或 notifyAll() 方法进行唤醒。
    • 当线程调用 join() 方法等待其他线程完成时,该线程进入阻塞状态,等待线程执行完成后进入就绪状态。
    • 当线程获取不到共享资源的锁时,线程进入阻塞状态,等待其他线程释放锁。
  3. 线程恢复:

    • 当线程的 sleep() 时间到达后,线程恢复到就绪状态。
    • 当线程调用 wait() 方法等待其他线程唤醒时,其他线程调用对应的 notify() 或 notifyAll() 方法后,该线程重新进入就绪状态。
    • 当线程执行 join() 方法等待其他线程完成时,其他线程完成后,该线程重新进入就绪状态。
    • 当线程获取到共享资源的锁时,线程恢复到就绪状态。
  4. 线程终止:

    • 当线程执行完 run() 方法中的代码后,线程进入终止状态。
    • 当线程因为异常或其他原因被迫中断时,线程进入终止状态。

在多线程编程中,需要了解线程状态的转换,以便编写正确的线程控制代码和线程间协作代码。

- 线程同步与互斥

在多线程并发编程中,线程同步和互斥是非常重要的概念,可以使用以下几种方式来实现:

  1. 互斥锁(Mutex):互斥锁是一种最基本的线程同步机制,可以用来保证在任意时刻只有一个线程能够访问共享资源。当一个线程获取到互斥锁时,其他线程就不能获取该锁,只能等待当前线程释放锁后才能继续执行。互斥锁可以使用 synchronized 关键字来实现。
  2. 信号量(Semaphore):信号量是另一种线程同步机制,用于控制对共享资源的访问数量。当信号量为1时,多个线程就会竞争获取这个信号量,只有一个线程获取到后才能继续执行,其他线程则被阻塞。信号量可以使用 Semaphore 类来实现。
  3. 条件变量(Condition):条件变量是一种高级线程同步机制,在互斥锁机制的基础上扩展了等待/通知模型。当线程等待某个条件时,它会释放锁并进入等待状态,等待其他线程的通知;当其他线程满足了条件后,会通知等待线程,并重新获得锁。条件变量可以使用 Condition 类来实现。
  4. 线程局部变量(ThreadLocal):线程局部变量是一种特殊的变量,每个线程都有自己的一份副本,可以避免线程之间共享变量导致的并发问题。线程局部变量可以使用 ThreadLocal 类来实现。
  5. 原子操作(Atomic):原子操作是指一组操作中的所有操作要么全部执行成功,要么全部不执行。原子操作可以用来实现非阻塞算法,避免线程间的锁竞争。Java 提供了一些原子操作类,如 AtomicInteger、AtomicLong 等。

- 线程优先级设置

线程优先级是线程调度的一种方式,它指定了线程在竞争CPU资源时相对于其他线程的优先级。线程优先级可以使用以下几种方式来设置:

  1. setPriority() 方法:Java 线程类提供了 setPriority() 方法,可以用来设置线程的优先级,取值范围为 1~10,其中 1 表示最低优先级,10 表示最高优先级,默认为 5。

  2. Thread.MAX_PRIORITY、Thread.NORM_PRIORITY 和 Thread.MIN_PRIORITY 常量:Java 线程类也提供了常量用于表示最高、默认和最低优先级。

  3. 线程组(ThreadGroup):线程组可以作为一种逻辑上的组织方式,可以将一组相关的线程归为同一组,并通过设置组的优先级来影响其中所有线程的优先级。

在设置线程优先级时,需要注意以下几点:

  1. 线程优先级不是绝对的:虽然设置优先级可以影响线程的调度,但是具体执行时仍可能受到其他因素(如操作系统的调度算法、CPU资源的竞争等)的影响,因此不能完全依赖优先级来保证程序的正确性和性能。

  2. 不要滥用优先级:过高的优先级可能会导致低优先级的线程无法得到执行,进而导致程序的不稳定性和性能问题。

  3. 优先级设置应该合理:根据不同的业务需求和运行环境,合理设置线程优先级可以提高程序的性能和稳定性。一般来说,低优先级的线程适合用于占用资源较少、执行时间较长的任务,而高优先级的线程适合用于占用资源较多、执行时间较短的任务。

2. 线程间通信

- synchronized 关键字及其实现原理

synchronized 是 Java 中用于实现同步的关键字,它可以修饰代码块或方法,用于限制多个线程同时访问共享资源的情况,达到线程安全的目的。

synchronized 的实现原理是通过 Java 对象头中的 monitor 实现的。每个 Java 对象都有一个与之关联的 monitor,它就是用来实现 synchronized 的锁机制的。当一个线程想要获取一个对象的 monitor,它就需要先尝试获得 monitor 对应的锁(也称为对象锁或内部锁)。如果该对象没有被其他线程锁定,则当前线程会获得该对象的锁并进入 synchronized 块或方法;否则,当前线程就会进入阻塞状态,等待其他线程释放该对象锁后才能进入 synchronized 块或方法。

我们可以使用 JConsole 或 JVisualVM 等工具分析 Java 进程中的对象信息,包括对象头中的 monitor 对象。在这些工具中,可以查看对象的详细信息,包括对象头中的所有字段信息。通常情况下,我们建议使用 JVisualVM 工具来监控 Java 应用程序的内存和线程情况,可以通过该工具查看对象的详细信息,包括 monitor 对象。

锁升级过程是指在执行 synchronized 代码块或方法时,根据不同的情况将锁的级别从偏向锁、轻量级锁、重量级锁依次升级。这样做的目的是为了尽可能地减少锁竞争的情况,提高并发访问共享资源的效率。

偏向锁是指在仅有一个线程访问锁的情况下,为了减少获取锁的时间和代价,JVM 会对锁对象做升级处理。偏向锁会将锁对象的对象头中的标志位设置为偏向锁,并将当前线程 ID 记录在对象头中,表示该对象处于偏向模式。如果其他线程也想要访问该对象的锁,就需要先撤销该对象的偏向状态,将锁升级为轻量级锁。

轻量级锁是指在有多个线程竞争同一个锁时,为了减少锁竞争的代价,JVM 会将锁对象的对象头中的标志位设置为轻量级锁,并将当前线程在栈中的锁记录拷贝到锁对象的 Mark Word 字段中。如果此时仍有其他线程竞争该锁,就需要将锁升级为重量级锁。

重量级锁是指在有多个线程竞争同一个锁,并且这种竞争情况比较激烈时,JVM 会将锁升级为重量级锁,这时锁的状态就不再保存在锁对象的 Mark Word 字段中,而是由操作系统使用互斥量来实现锁的同步。由于重量级锁需要进行用户态和内核态之间的切换,所以效率比偏向锁和轻量级锁要低,应该尽量避免锁的升级过程。monitor对象的地址存储在Java对象头的Mark Word字段中。在重量级锁中,Mark Word字段的值指向一个C++对象,该C++对象包含了操作系统中互斥量的状态信息,以实现锁的同步。这个C++对象和Java对象之间的映射关系由JVM来管理和维护。因此,在重量级锁中,Java对象头中的Mark Word字段存储了一个指向C++对象的指针。

具体实现原理如下:

  1. synchronized 代码块:synchronized(this)代码块会获取当前对象的锁,当多个线程同时访问该对象时,只有获得锁的线程可以进入代码块执行,其他线程需要等待释放锁后才能继续执行,从而避免了线程之间的竞争。
  2. synchronized 方法:synchronized 修饰的方法会获取当前对象的锁,当多个线程同时访问该对象的同步方法时,只有获得锁的线程可以执行方法,其他线程需要等待释放锁后才能执行方法。
  3. 静态 synchronized 方法:synchronized 修饰的静态方法会获取当前类的类锁,当多个线程同时访问该类的静态同步方法时,只有获得类锁的线程可以执行方法,其他线程需要等待释放锁后才能执行方法。
  4. synchronized 对象锁:在 Java 中,每个对象都有一个与之关联的对象锁,当线程访问一个对象时,需要先获得该对象的锁,才能执行 synchronized 块或方法。当线程执行完 synchronized 块或方法后,会释放对象锁,其他线程才能获得该对象的锁并执行。
    使用 synchronized 关键字能够有效避免多线程访问共享资源时引起的并发问题,但同时也可能影响程序的性能和效率,应该根据实际情况进行使用。

- wait() , notify() ,notifyAll(),join()方法的使用

  1. wait()方法是Object类中的方法,用于将当前线程放入等待状态,直到另一个线程调用notify()或notifyAll()方法唤醒它。wait()方法需要在synchronized代码块或方法中使用,否则会抛出IllegalMonitorStateException异常。

  2. notify()方法用于唤醒在同一个对象上调用wait()方法而进入等待状态的单个线程。如果有多个线程在等待,那么只会唤醒其中一个线程,并且无法确定唤醒哪个线程。notify()方法也需要在synchronized代码块或方法中使用。

  3. notifyAll()方法用于唤醒在同一个对象上调用wait()方法而进入等待状态的所有线程。notifyAll()方法也需要在synchronized代码块或方法中使用。

  4. join()方法是Thread类中的方法,用于等待调用该方法的线程执行完毕。如果在另一个线程中调用join()方法,则当前线程会进入等待状态,直到该线程执行完毕。join()方法也可以有超时时间的重载方法来避免线程无限等待。

  5. 这些方法都需要在synchronized的代码块或方法中使用,以确保线程间通信的正确性和同步性。在使用wait()和notify()/notifyAll()方法时,要注意在调用wait()方法前,要获取该对象的锁,并在调用wait()后,释放该对象的锁,否则会抛出异常或导致死锁的情况。

总之,wait()、notify()、notifyAll()和join()方法是Java多线程编程中常用的线程间通信和控制方法。在使用这些方法时,需要注意线程的同步和互斥,以保证线程安全和稳定性。

- Lock 和 Condition 的使用

  1. Lock 是Java并发包中提供的一种替代 synchronized 关键字的机制。使用Lock能够更精细地控制多线程之间的并发访问,提高并发性能。Lock接口有多种实现类,比如ReentrantLock、ReentrantReadWriteLock等。
  2. Condition 是Lock接口提供的增强版Object类中的wait()、notify()和notifyAll()方法的替代品,用于线程间的通信和协作。Condition接口只能通过Lock实例的方法来获取,比如newCondition()方法。
  3. Lock接口中最常用的方法是 lock() 和 unlock()。在使用 Lock 时,需要在try-finally块中将unlock()方法放在finally块中来确保锁的释放,以避免死锁的发生。
  4. Condition接口中最常用的方法是await()、signal()和signalAll()。await()方法使当前线程等待,直到其他线程调用signal()或signalAll()方法将其唤醒;signal()方法唤醒一个等待在该条件上的线程;signalAll()方法唤醒所有等待在该条件上的线程。
  5. 在使用Condition时,必须先获取与该条件相关的锁才能进行等待或唤醒。也可以使用await(long time, TimeUnit unit)方法来指定等待的时间。
  6. 与 synchronized 关键字相比,Lock和Condition更加灵活,可以实现更复杂的线程同步和控制,但也更加复杂和容易出错。
  7. 在使用锁和条件变量时,可以通过synchronized和wait()、notify()、notifyAll()方法的对应关系来理解其基本概念和使用方法。理解了基本概念和使用方法后,再结合具体应用场景,选择适合的锁和条件变量来实现线程同步和控制。

- volatile 关键字的使用

  1. volatile关键字用于修饰Java变量,表示该变量是“易变的”(即可能被其他线程修改)。使用volatile可以确保多线程之间对该变量的读写操作的可见性和顺序性。
  2. volatile关键字可以保证变量的可见性,但不能保证原子性。对于一些需要原子操作的变量,比如计数器等,需要使用synchronized、Lock或Atomic类等机制来保证其原子性。
  3. volatile关键字可以被用来实现一些特定的多线程编程场景,比如控制共享变量的可见性、控制线程的停止、触发线程的中断等。
  4. 在多线程中使用volatile关键字时,要注意以下几点:(1)对volatile变量的读写操作不能被指令重排;(2)volatile变量的操作不具有原子性;(3)volatile关键字不能保证多线程之间的同步,需要结合其他机制来实现线程同步。
  5. 相比于synchronized和Lock等机制,volatile关键字的使用更加轻量级,适用于一些简单的共享变量场景。但是,对于更加复杂的多线程编程场景,需要使用更加复杂和灵活的机制来实现线程同步和控制。
  6. volatile 适合一写多读的场景,如果是多谢多读场景会存在i++问题(线程A和线程B从主内存获取i的值,同时进行i++再写到主内存中,此时有第二次同步的线程会覆盖掉第一次同步的线程同步的值,两次更新之后i的值只++了一次),所以说无法保证原子性,我们可以通过下面的原子类的CAS来解决i++问题。
    总之,volatile关键字是Java多线程编程中的重要机制之一,可以保证共享变量的可见性和顺序性。在使用volatile关键字时,需要注意其使用场景和使用方法,以确保多线程编程的正确性和稳定性。

3. 并发工具类

- 原子类的使用

  1. 原子类是Java.util.concurrent包中提供的一组线程安全的类,用于实现对共享变量的原子操作。原子类的操作是不可分割的,不受线程调度或中断的影响。
  2. 原子类包括AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference等多种类型,可以实现对基本类型和对象类型的原子操作。
  3. 原子类的实现原理基于Java内存模型(Java Memory Model),利用了CPU提供的CAS(Compare And Swap)指令。CAS指令可以在一次原子操作中,比较某个内存地址的值和一个预期值,如果相等,则将该内存地址的值设置为一个新值。由于CAS操作是原子的,因此可以实现对共享变量的原子操作。
  4. 原子类的使用可以避免使用synchronized关键字或Lock等机制,从而提高程序的并发性能和可伸缩性。
  5. 在使用原子类时,要注意以下几点:(1)原子类的操作是原子的,但是不能保证一系列操作的原子性,因此需要结合其他机制来实现复杂的线程安全性;(2)原子类的操作开销比普通变量的操作开销大,因此适用于一些需要频繁修改的共享变量场景;(3)原子类的操作是基于内存地址的,因此适用于一些单个变量的操作,对于复杂的数据结构,需要使用其他机制来实现线程安全性。
    总之,原子类是Java多线程编程中的重要机制之一,可以实现对共享变量的高效原子操作,提高程序的并发性能和可伸缩性。在使用原子类时,需要注意其使用场景和使用方法,以确保多线程编程的正确性和稳定性。
    原子类可以避免volatile关键字的i++问题,但是有部分类也存在线程安全问题(例:线程A和线程B从主内存获取i的值,同此时线程A对主内存的值++了一次,并–了一次,这种时候线程B修改完之后同步时CAS也会通过,但此时这个值已经被其它线程改动过),可以使用带有版本号的原子类来避免这种问题。

- ConcurrentHashMap 和 ConcurrentLinkedQueue 的使用

ConcurrentHashMap:

  1. ConcurrentHashMap是Java.util.concurrent包中提供的一种线程安全的哈希表,用于实现高并发的键值对存储。
  2. ConcurrentHashMap采用分段锁(Segment)的方式实现线程安全性,不同的Segment具有独立的锁,因此可以实现高度的并发性能。
  3. ConcurrentHashMap提供了put、get、remove等基本操作,其操作的时间复杂度为O(1)。
  4. ConcurrentHashMap在操作过程中不会出现死锁的情况,因此可以避免使用synchronized关键字或Lock等机制,提高程序的并发性能和可伸缩性。
  5. 在使用ConcurrentHashMap时,需要注意以下几点:(1)ConcurrentHashMap不支持null值,如果需要使用null值,可以使用ConcurrentHashMap的子类ConcurrentHashMapV8;(2)ConcurrentHashMap的容量需要预估好,以避免rehash带来的性能损失;(3)ConcurrentHashMap的迭代器是弱一致性的,可能会返回过期或新增的元素,因此在迭代过程中需要做好处理。

ConcurrentLinkedQueue:

  1. ConcurrentLinkedQueue是Java.util.concurrent包中提供的一种线程安全的队列,用于实现高并发的元素存储。
  2. ConcurrentLinkedQueue采用无锁的方式实现线程安全性,使用CAS指令实现元素的插入和删除操作。
  3. ConcurrentLinkedQueue提供了offer、poll等基本操作,其操作的时间复杂度为O(1)。
  4. ConcurrentLinkedQueue在操作过程中不会出现死锁的情况,因此可以避免使用synchronized关键字或Lock等机制,提高程序的并发性能和可伸缩性。
  5. 在使用ConcurrentLinkedQueue时,需要注意以下几点:(1)ConcurrentLinkedQueue不支持null值,如果需要使用null值,可以使用ConcurrentLinkedDeque;(2)ConcurrentLinkedQueue的迭代器是弱一致性的,可能会返回过期或新增的元素,因此在迭代过程中需要做好处理。

总之,ConcurrentHashMap和ConcurrentLinkedQueue是Java多线程编程中的重要机制之一,可以实现高并发的数据存储和访问。在使用这些机制时,需要注意其使用场景和使用方法,以确保多线程编程的正确性和稳定性。

- CountDownLatch 和 CyclicBarrier 的使用

CountDownLatch:

  1. CountDownLatch是Java.util.concurrent包中提供的一种工具类,可以实现多个线程之间的协同操作。
  2. CountDownLatch通过计数器的方式实现线程的等待和通知,当计数器为0时,所有等待线程将被唤醒。
  3. CountDownLatch提供了await、countDown等基本操作,可用于等待一组线程完成任务或等待某个条件满足。
  4. CountDownLatch在操作过程中不能重置计数器,因此一般用于一次性的任务等待。
  5. 在使用CountDownLatch时,需要注意以下几点:(1)CountDownLatch的计数器必须大于等于0,否则会抛出异常;(2)CountDownLatch只能被等待一次,不能重复使用;(3)CountDownLatch的计数器一旦到达0,将不能再重新设置。

    CyclicBarrier:

  6. CyclicBarrier是Java.util.concurrent包中提供的一种工具类,可以实现多个线程之间的协同操作。
  7. CyclicBarrier通过可重用的屏障的方式实现多个线程的同步操作,当所有线程到达屏障时,触发执行屏障动作,然后所有线程继续执行。
  8. CyclicBarrier提供了await等基本操作,可用于等待一组线程到达屏障。
  9. CyclicBarrier在操作过程中可以多次使用,因此适用于循环任务的等待。
  10. 在使用CyclicBarrier时,需要注意以下几点:(1)CyclicBarrier的参与线程数必须大于等于2,否则会抛出异常;(2)CyclicBarrier的动作需要由最后一个到达屏障的线程执行;(3)CyclicBarrier的屏障可以被重复使用,因此不会抛出异常。
    总之,CountDownLatch和CyclicBarrier是Java多线程编程中的重要机制之一,可以实现线程之间的协同操作和同步等待。在使用这些机制时,需要注意其使用场景和使用方法,以确保多线程编程的正确性和稳定性。

- Semaphore 和 Exchanger 的使用

Semaphore:

  1. Semaphore是Java.util.concurrent包中提供的一种工具类,可以实现多个线程之间的协同操作。
  2. Semaphore通过可配置的许可数和阻塞等待机制实现资源的控制和访问,类似于操作系统中的信号量机制。
  3. Semaphore提供了acquire、release等基本操作,可用于限制并发访问特定资源,或控制并发线程的数量。
  4. Semaphore在操作过程中可以动态调整许可数,从而实现资源的动态控制。
  5. 在使用Semaphore时,需要注意以下几点:(1)Semaphore的许可数不能为负数,否则会抛出异常;(2)Semaphore的许可数可以被多个线程获取和释放,因此需要考虑线程安全问题;(3)Semaphore的acquire方法可以设置等待时间,以避免线程无限等待的情况。

Exchanger:

  1. Exchanger是Java.util.concurrent包中提供的一种工具类,可以实现多个线程之间的数据交换。
  2. Exchanger通过阻塞等待和交换操作实现两个线程之间的数据交换,用于实现协同操作和信息交流。
  3. Exchanger提供了exchange等基本操作,可用于等待一组线程到达交换点,然后进行数据交换。
  4. Exchanger在操作过程中只能交换两个线程的数据,因此适用于双方协作完成任务并交换数据的场景。
  5. 在使用Exchanger时,需要注意以下几点:(1)Exchanger的交换操作必须由两个线程同时调用,否则会一直等待;(2)Exchanger只能交换两个线程的数据,因此需要考虑线程的数据一致性问题。
    总之,Semaphore和Exchanger是Java多线程编程中的重要机制之一,可以实现线程之间的协同操作和数据交换。在使用这些机制时,需要注意其使用场景和使用方法,以确保多线程编程的正确性和稳定性。

4. 线程池

线程池是Java中重要的多线程编程机制之一,其可以提供线程复用、管理线程并发数、避免线程频繁创建销毁的问题,提高程序的性能和效率。

- 线程池的实现原理

  1. 线程池的基本组成:线程池由线程池管理器、工作线程、任务队列和任务等四部分组成。

  2. 线程池的工作流程:当线程池被创建时,线程池管理器会启动一定数量的工作线程,这些线程会从任务队列中获取任务执行。当线程执行完任务后,会自动从任务队列中获取下一个任务继续执行。当线程池没有可用的工作线程时,新加入的任务会被暂时存储在任务队列中,等待工作线程空闲后再进行执行。

  3. 线程池的运行原理:线程池的运行原理是基于线程的复用,它可以避免重复创建和销毁线程所带来的资源消耗和时间开销,从而提高程序的运行效率。当线程执行完任务后,线程并不会真正销毁,而是会返回到线程池中等待下一个任务的分配。

  4. 线程池的优点和缺点:线程池的优点是可以提供可复用的线程资源,避免线程的频繁创建和销毁,提高程序的运行效率,并且可以管理线程的并发数,避免线程的过度竞争。但是线程池的缺点是需要占用一定的内存资源,并且需要考虑线程的安全性和数据一致性问题。

  5. 线程池的实现方法:Java中提供了Executor框架来实现线程池,在Executor框架中,可以通过ThreadPoolExecutor类来实现线程池的创建和管理,包括线程池的大小、线程存活时间、任务队列大小等参数的配置。

总之,线程池是Java多线程编程中的重要机制之一,能够提高程序的运行效率和并发处理能力。在使用线程池时,需要注意线程池的运行原理、优缺点以及实现方法,以确保多线程编程的正确性和稳定性。

- 线程池的使用与配置

  1. 线程池的创建:可以通过ThreadPoolExecutor或Executors工具类来创建线程池,其中Executors提供了一些预定义的线程池类型方便使用。
  2. 线程池的参数配置:线程池的参数包括核心线程数、最大线程数、线程存活时间、任务队列大小等,需要根据实际业务需求进行配置。
  3. 线程池的拒绝策略:当线程池达到最大线程数并且任务队列也已满时,需要设置线程池的拒绝策略来处理无法处理的任务。
  4. 线程池的执行:线程池可以通过submit或execute方法来提交任务进行处理,其中submit方法可以返回Future对象用于获取任务执行的结果。
  5. 线程池的关闭:在程序结束或不需要使用线程池时,需要正确关闭线程池以释放资源和避免内存泄漏等问题。
    总之,线程池的使用与配置需要根据实际业务需求进行设置,其中包括线程池的参数配置、拒绝策略设置、任务提交和线程池的关闭等问题,需要合理使用和管理线程池以提高程序的性能和效率。

- 线程池的监控和调优

  1. 线程池的监控:可以通过ThreadPoolExecutor中的各种get方法来获取线程池的运行状况,例如getActiveCount、getCompletedTaskCount、getTaskCount等方法。
  2. 线程池的监控工具:可以使用Java自带的JMX监控工具或者第三方工具来进行线程池的监控和统计,例如JConsole、VisualVM、Grafana等等。
  3. 线程池的调优:可以通过调整线程池的参数来达到最优的线程池性能和效率,包括核心线程数、最大线程数、线程活动时间、队列大小、拒绝策略等等。
  4. 线程池的问题:线程池的常见问题包括线程数过多、线程死锁、任务队列溢出等等,需要及时发现和解决。
  5. 线程池的优化:可以通过使用合适的任务队列、线程池的预热、线程池分级、任务拆分等方式来优化线程池的性能和效率。
    总之,线程池的监控和调优是Java多线程编程中必不可少的部分,需要合理设置线程池的参数,并通过监控工具和调优手段来发现和解决线程池中出现的问题和瓶颈,以提高程序的性能和效率。

5. 并发编程模型

- 生产者-消费者模型

生产者-消费者模型是多线程编程中的经典模型,主要用于描述生产者不断地向一个有限缓冲区中生产数据,而消费者从缓冲区中取出数据进行消费的过程。以下是生产者-消费者模型相关的知识点总结:

  1. 有限缓冲区:生产者和消费者之间共享一个有限大小的缓冲区,生产者将数据放入缓冲区,消费者从缓冲区取出数据进行消费。
  2. 生产者:不断地生产数据并将其放入缓冲区,当缓冲区已满时需要等待消费者从缓冲区取出数据并通知其继续生产。
  3. 消费者:不断地从缓冲区取出数据进行消费,当缓冲区为空时需要等待生产者向缓冲区中添加数据并通知其继续消费。
  4. 同步机制:生产者和消费者之间需要使用同步机制来保证线程之间的互斥和同步,避免数据竞争和死锁等问题。常用的同步机制包括synchronized关键字、锁、信号量、管程等等。
  5. 等待-通知机制:在Java中,可以使用wait()和notify()/notifyAll()方法来实现生产者和消费者之间的等待-通知机制。当缓冲区已满时,生产者调用wait()方法等待消费者通知继续生产;当缓冲区为空时,消费者调用wait()方法等待生产者通知继续消费,而生产者和消费者则可以调用notify()/notifyAll()方法来唤醒处于等待状态的线程。

总之,生产者-消费者模型是多线程编程中的重要模型,需要通过同步机制和等待-通知机制来实现线程之间的同步和互斥。在实现生产者-消费者模型时,需要注意避免死锁、数据竞争等问题,以保证程序的正确性和稳定性。

这段代码中,定义了一个PCBuffer类作为生产者和消费者之间共享的缓冲区,实现了produce()和consume()方法来实现生产和消费过程。在produce()和consume()方法中,使用了synchronized关键字和wait()/notify()方法来实现线程之间的同步和互斥。同时,在main()方法中使用了Thread类来创建生产者和消费者线程,并启动、等待线程执行完毕:
import java.util.LinkedList;

public class ProducerConsumerExample {
    public static void main(String[] args) throws InterruptedException {
        final PCBuffer buffer = new PCBuffer(3); // 缓冲区大小为3

        // 创建生产者和消费者线程
        Thread producerThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    buffer.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread consumerThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    buffer.consume();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 启动生产者和消费者线程
        producerThread.start();
        consumerThread.start();

        // 等待生产者和消费者线程执行完毕
        producerThread.join();
        consumerThread.join();
    }

    // 缓冲区类
    public static class PCBuffer {
        private final LinkedList<Integer> buffer; // 缓冲区
        private final int capacity; // 缓冲区容量

        public PCBuffer(int capacity) {
            this.buffer = new LinkedList<>();
            this.capacity = capacity;
        }

        public void produce() throws InterruptedException {
            int value = 0;
            while (true) {
                synchronized (this) {
                    while (buffer.size() == capacity) { // 缓冲区已满,等待
                        wait();
                    }

                    System.out.println("生产者生产了:" + value);
                    buffer.add(value++);

                    notify(); // 通知处于等待状态的消费者线程
                    Thread.sleep(1000); // 生产者线程等待一段时间
                }
            }
        }

        public void consume() throws InterruptedException {
            while (true) {
                synchronized (this) {
                    while (buffer.size() == 0) { // 缓冲区为空,等待
                        wait();
                    }

                    int value = buffer.removeFirst();
                    System.out.println("消费者消费了:" + value);

                    notify(); // 通知处于等待状态的生产者线程
                    Thread.sleep(1000); // 消费者线程等待一段时间
                }
            }
        }
    }
}

- 读写锁模式

读写锁(Read-Write Lock)是一种高效的多线程同步机制,用于实现在多读单写场景下的同步。在读多写少的场景下,使用读写锁可以提高系统的并发性能。读写锁允许多个线程同时访问共享资源,但在写操作时需要独占访问,也就是说读操作不会被写操作所阻塞,但写操作会阻塞其他所有操作。
下面是几个相关知识点的总结:

  1. 读写锁的特点
  • 读写锁分为读锁和写锁两种类型,一个线程获取到读锁后可以多次读取共享资源,直到另一个线程获取到写锁;而一个线程获取到写锁后,其他线程无法再获取到读锁或写锁。
  1. 读写锁的使用场景
  • 适用于读多写少的场景,可以提高系统的并发性能。
  1. 读写锁的实现
  • Java提供了ReadWriteLock接口和ReentrantReadWriteLock类来实现读写锁。

  • 读写锁的基本操作包括获取读锁、释放读锁、获取写锁和释放写锁等。

     下面是一个简单的Java代码演示读写锁的使用:
     这个例子中,使用了ReentrantReadWriteLock类来实现读写锁,MyData类是共享资源,其中包含了读操作和写操作。在读操作中使用了readLock()方法获取读锁,而在写操作中使用了writeLock()方法获取写锁,这样就可以实现多个线程同时读取数据,但只允许一个线程写入数据。
    
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
    public static void main(String[] args) {
        final MyData data = new MyData(); // 共享资源
        // 创建10个读线程和一个写线程
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        data.read(); // 读操作
                    }
                }
            }).start();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    data.write(); // 写操作
                }
            }
        }).start();
    }
    // 共享资源类
    public static class MyData {
        private int data = 0; // 数据
        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); // 读写锁
        public void read() {
            lock.readLock().lock(); // 获取读锁
            try {
                System.out.println(Thread.currentThread().getName() + "开始读取数据");
                Thread.sleep(1000); // 模拟读取数据耗时
                System.out.println(Thread.currentThread().getName() + "读取到的数据为:" + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.readLock().unlock(); // 释放读锁
            }
        }
        public void write() {
            lock.writeLock().lock(); // 获取写锁
            try {
                System.out.println(Thread.currentThread().getName() + "开始写数据");
                Thread.sleep(1000); // 模拟写数据耗时
                data++;
                System.out.println(Thread.currentThread().getName() + "写入的数据为:" + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.writeLock().unlock(); // 释放写锁
            }
        }
    }
}

- 线程死锁的定位与解决

当两个或多个线程在等待其他线程释放锁,而又不主动释放自己持有的锁时,就会导致线程死锁。这种情况下,这些线程都不能继续执行,因为它们都在等待其他线程释放锁。

定位线程死锁可以通过线程转储(Thread Dump)来实现。线程转储可以显示当前Java虚拟机上所有线程的状态和调用堆栈。通过分析这些日志,可以查找到哪些线程正在等待其他线程释放锁。通常,我们可以使用JDK自带的jstack工具或使用可视化的工具,如VisualVM或Eclipse Memory Analyzer进行调试和分析。

解决线程死锁的方法通常有以下几种:

  • 避免无谓的锁竞争:尽量减小锁的竞争范围,避免竞争条件的产生,以提高程序的并发性能和稳定性。
  • 加锁的顺序:尽量以固定的顺序获取锁,避免同时使用多个锁而导致死锁。
  • 超时处理:在获取锁的时候,加入超时机制,一旦等待超过一定时间就放弃获取锁并进行其他处理。
  • 死锁检测:检测线程死锁的发生,并对死锁线程做出相应的处理,如强制中断、放弃锁等。
  • 其他方案:使用读写锁、分段

    - 并发编程中的陷阱和常见问题

  1. 竞争条件:当多个线程访问共享资源时,可能会出现竞争条件,导致数据不一致或程序错误。可以通过同步访问共享资源或使用原子操作来解决竞争条件问题。

  2. 死锁:当多个线程互相等待对方释放锁时,可能会出现死锁情况,导致线程阻塞。可以通过避免无谓的锁竞争、加锁顺序、超时处理、死锁检测等方式来解决死锁问题。

  3. 活锁:当多个线程在尝试解决死锁问题时,可能会出现活锁情况,导致线程一直在处理,无法继续执行。可以通过增加随机性或放弃一些操作来解决活锁问题。

  4. 共享资源争用:当多个线程同时竞争同一个共享资源时,可能会出现性能瓶颈和延迟问题。可以通过减小锁的粒度、使用读写锁或分段锁等方式来解决共享资源争用问题。

  5. 内存一致性错误:当多个线程在访问共享内存时,由于内存读写可能存在乱序执行的情况,可能会导致内存一致性错误。可以通过使用volatile关键字、synchronized关键字或使用原子操作来解决内存一致性问题。

  6. 线程泄漏:当线程不正常退出或销毁时,可能会导致线程资源泄漏,最终导致系统资源的枯竭。可以通过编写健壮的代码、使用线程池等方式来解决线程泄漏问题。

  7. 可见性问题:当多个线程在访问共享变量时,可能会存在可见性问题,导致一个线程修改了变量,但其他线程看不到变量的最新值。可以通过使用volatile关键字、synchronized关键字或使用原子操作来解决可见性问题。

  8. 上下文切换开销:当多个线程在竞争CPU的时间片时,可能会存在上下文切换开销过大的问题,降低程序的性能。可以通过减少线程数量、降低锁竞争范围等方式来减少上下文切换开销。

总之,并发编程中存在许多陷阱和常见问题,需要程序员在编写代码时注意这些问题,采取相应的措施来保证程序的可靠性、稳定性和性能。

总结

锁升级过程可以看这篇文档:https://blog.csdn.net/sinat_40572875/article/details/128110447

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值