目 录CONTENT

文章目录

Java核心卷(一)并发

Jinty
2023-12-15 / 0 评论 / 0 点赞 / 17 阅读 / 30147 字

并发

线程与进程

进程和线程是操作系统中用于执行任务的两种不同的执行单元。

  1. 进程:进程是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位。每个进程都有自己的地址空间、内存、数据栈以及其他用于跟踪进程执行的辅助数据。进程之间是相互独立的,每个进程都在自己的地址空间中运行,彼此之间不能直接访问对方的内存数据。

  2. 线程:线程是进程中的一个实体,是CPU调度和分派的基本单位。一个进程可以包含多个线程,它们共享进程的地址空间和资源,包括内存和文件句柄等。线程之间可以方便地进行通信和数据共享,因此线程间的切换开销相对较小。

在实际编程中,多线程通常用于提高程序的并发性能,例如在图形界面应用中,一个线程负责用户界面的响应,另一个线程负责后台数据的处理;而多进程通常用于实现系统级的并发,例如在服务器应用中,每个连接可以由一个独立的进程来处理。

总结来说,进程是操作系统资源分配的基本单位,每个进程拥有独立的地址空间;而线程是进程中的执行单元,多个线程可以共享进程的资源。

同步与异步

同步和异步是用来描述程序执行方式的两种不同模式。

  1. 同步:在同步模式下,当一个任务或操作被触发时,调用者需要等待这个任务或操作完成后才能继续执行后续的操作。在同步模式中,调用者会被阻塞,直到被调用的任务或操作完成。同步通常是指在一个线程中按顺序执行任务,一个任务执行完毕后再执行下一个任务。

  2. 异步:相反,在异步模式下,当一个任务或操作被触发时,调用者不需要等待这个任务或操作完成,而是可以继续执行后续的操作。在异步模式中,调用者不会被阻塞,而是可以立即得到一个中间结果或通知,然后继续执行其他操作。异步通常是指任务的执行不受调用者的控制,可以在另一个线程中执行,执行完毕后通知调用者。

在实际编程中,同步和异步通常与多线程、网络通信、I/O操作等相关。例如,一个使用同步方式的网络请求会在发送请求后阻塞当前线程直到收到响应;而一个使用异步方式的网络请求会在发送请求后立即返回,然后在收到响应时通过回调或其他方式通知调用者。

阻塞与非阻塞

阻塞与非阻塞是关于程序或操作在等待某个事件完成时的两种不同的处理方式。

  1. 阻塞:当一个线程或进程在执行某个操作时,如果需要等待一个事件完成,它会暂时停止执行,直到事件完成为止。在这个等待的过程中,线程或进程会被挂起,无法执行其他任务。这种等待方式被称为阻塞。阻塞是一种同步的方式,因为线程或进程需要等待某个事件完成后才能继续执行。

  2. 非阻塞:相反,当一个线程或进程在执行某个操作时,如果需要等待一个事件完成,它不会暂停执行,而是会不断地轮询或询问事件是否完成,然后根据事件的状态来决定下一步的操作。在这种方式下,线程或进程可以继续执行其他任务,不会被挂起。这种等待方式被称为非阻塞。非阻塞是一种异步的方式,因为线程或进程可以继续执行其他任务而不需要等待事件完成。

在实际编程中,阻塞和非阻塞通常与输入输出(I/O)操作相关。例如,当一个线程从网络或磁盘读取数据时,如果采用阻塞方式,它会等待数据读取完成后再继续执行;而如果采用非阻塞方式,它会不断地询问数据是否已经准备好,然后根据数据的状态来决定下一步的操作。

操作系统线程

  • 线程是计算机操作系统任务调度的最小单位

定义:在操作系统中,进程是程序在执行过程中分配和管理资源的基本单位,而线程是进程中的一个执行流程,是程序执行的最小单位

资源分配:进程拥有独立的地址空间、文件描述符、系统资源等,每个进程都有自己的一套资源;而线程与所属进程共享相同的地址空间和系统资源,包括文件描述符、信号处理等。

调度:进程是操作系统进行资源分配和调度的基本单位,操作系统可以同时运行多个进程,并通过进程调度算法来决定哪个进程获得CPU时间片;线程是进程中的一个执行流程,由操作系统的线程调度器来决定线程的执行顺序

通信和同步:进程之间的通信需要使用进程间通信机制,如管道、消息队列、共享内存等;而线程之间可以通过共享内存的方式进行通信,也可以使用线程同步机制来保证共享数据的一致性,如互斥锁、条件变量等。

创建和销毁:创建和销毁进程的代价比较大,需要分配和回收资源,而创建和销毁线程的代价相对较小,一般只需要分配和回收一些线程私有的数据结构。

JVM线程

JVM线程和操作系统线程是两种不同的线程概念,它们之间存在一定的关系。

  1. JVM线程:在Java虚拟机(JVM)中,线程是由JVM自己的线程调度器(Thread Scheduler)进行调度和管理的。JVM线程是由Java程序直接创建和操作的,它们是Java语言级别的线程。在Java中,可以通过java.lang.Thread类或者java.util.concurrent包中的线程池等方式来创建和管理线程。

  2. 操作系统线程:操作系统线程是由操作系统内核进行调度和管理的,它们是操作系统级别的线程。操作系统线程的创建、调度和销毁等操作都由操作系统内核负责。不同的操作系统有不同的线程调度算法和线程管理机制。

JVM线程和操作系统线程之间存在一定的关系。在JVM中,每个Java线程都会映射到一个操作系统线程上。这个映射关系可以是一对一的,也可以是多对一的,具体取决于JVM的实现和操作系统的支持。在一些JVM实现中,会有一个线程池来复用操作系统线程,从而减少线程创建和销毁的开销。

Java线程模型

Java线程模型是指Java语言中对线程的支持和管理机制。Java提供了丰富的线程相关的类和接口,使得开发者可以方便地创建和管理线程,并实现多线程的并发编程。

Java线程模型的主要特点如下:

  1. 线程类:Java提供了Thread类,用于表示一个线程。开发者可以通过继承Thread类或者实现Runnable接口来创建自定义的线程类。

  2. 线程生命周期:Java线程具有多个生命周期状态,包括新建、就绪、运行、阻塞和终止等状态。开发者可以通过调用Thread类的方法来控制线程的状态转换。

  3. 线程调度:Java线程调度器负责决定哪个线程获得CPU时间片并执行。线程调度器根据线程的优先级和调度策略来进行调度。开发者可以通过设置线程的优先级来影响线程的调度顺序。

  4. 线程同步:Java提供了多种机制来实现线程之间的同步,包括synchronized关键字、锁(Lock)和条件(Condition)等。这些机制可以保证线程的互斥访问和协调执行,避免竞态条件和死锁等问题。

  5. 线程通信:Java提供了wait()、notify()和notifyAll()等方法,用于实现线程之间的通信和协作。通过这些方法,线程可以等待某个条件的满足,并在条件满足时被唤醒继续执行。

  6. 线程池:Java提供了线程池(ThreadPoolExecutor)来管理和复用线程。线程池可以提高线程的创建和销毁效率,同时控制并发线程的数量,避免资源过度消耗。

Java线程模型的设计使得开发者可以方便地实现多线程的并发编程,提高程序的性能和响应能力。同时,开发者也需要注意线程安全和线程间的协作,避免出现并发问题和死锁等情况。

Thread类

线程属性

  • 线程优先级:priority、 Thread.yield

  • 线程类型:daemon

  • 未捕获异常处理器

线程方法

Thread类提供了一系列方法来创建和管理线程。以下是Thread类的一些常用方法:

  1. 构造方法:

  • Thread():创建一个新的线程对象。

  • Thread(String name):创建一个新的线程对象,并指定线程的名称。

  • Thread(Runnable target):创建一个新的线程对象,并指定线程的执行任务。

  1. start()方法:启动线程,使其进入就绪状态,并自动调用线程的run()方法。

  2. run()方法:定义线程的执行逻辑,线程在启动后会执行run()方法中的代码。

  3. sleep(long millis)方法:使线程睡眠指定的时间,单位为毫秒。

  4. yield()方法:暂停当前正在执行的线程,让出CPU资源给其他线程。

  5. join()方法:等待其他线程执行完毕后再继续执行当前线程。

  6. interrupt()方法:中断线程,向线程发送中断信号。

  7. isAlive()方法:判断线程是否处于活动状态

创建线程的方式

yuque_diagram.png继承Thread类重写run方法

public class MyThread extends Thread {
    private static volatile int var = 0;

    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        while (var < 100) {
            System.out.println("MyThread"+getName()+":var=" + var);
            var++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new MyThread("A").start();
        new MyThread("B").start();
    }
}

实现Runnable接口的run方法

public class MyRunnable implements Runnable {
    private static volatile int var = 0;

    String name;
    public MyRunnable(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        while (var < 100) {
            System.out.println("MyThread"+name+":var=" + var);
            var++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new MyRunnable("A")).start();
        new Thread(new MyRunnable("B")).start();
        new Thread(new MyRunnable("C")).start();
    }
}

实现Callable类的call方法

public class MyCallable implements Callable<String> {

    private static volatile int var = 0;
    private String name;

    public MyCallable(String name) {
        this.name = name;
    }

    @Override
    public String call() throws Exception {
        while (var < 100) {
            System.out.println("MyThread" + name + ":var=" + var);
            var++;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return String.valueOf(var);
    }

    public static void main(String[] args) {
        FutureTask taskA = new FutureTask<String>(new MyCallable("A"));
        FutureTask taskB = new FutureTask<String>(new MyCallable("B"));
        new Thread(taskA).start();
        new Thread(taskB).start();
        try {
            System.out.println(taskA.get().toString());
            System.out.println(taskB.get().toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

创建有返回值线程的方式(步骤):

  1. 创建一个实现了Callable接口的类,并实现其call()方法来定义线程的执行逻辑。在call()方法中执行异步计算,并返回计算结果。

import java.util.concurrent.Callable;

class MyCallable implements Callable<String> {
    public String call() throws Exception {
        // 执行异步计算的逻辑,并返回计算结果
        return "Hello, World!";
    }
}
  1. 创建一个ExecutorService对象,通过调用Executors类的静态方法来创建一个线程池。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newSingleThreadExecutor();
  1. 创建Callable对象,并将其作为参数传递给ExecutorService的submit()方法来提交任务。submit()方法会返回一个Future对象,表示异步计算的结果。

Callable<String> callable = new MyCallable();
Future<String> future = executor.submit(callable);
  1. 使用Future对象的get()方法来获取异步计算的结果。get()方法会阻塞当前线程,直到异步计算完成并返回结果。

try {
    String result = future.get();
    System.out.println(result);
} catch (Exception e) {
    e.printStackTrace();
}
  1. 最后,记得关闭ExecutorService,释放资源。

executor.shutdown();

线程生命周期及状态

image-idlo.png

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

  1. 新建状态(New):当创建一个线程对象时,线程处于新建状态。此时线程对象已经被创建,但还没有调用start()方法启动线程。

  2. 就绪状态(Runnable):当调用线程的start()方法后,线程进入就绪状态。此时线程已经准备好执行任务,但还没有被调度器选中执行。

  3. 运行状态(Running):当线程被调度器选中执行时,线程进入运行状态。此时线程会执行run()方法中的代码,并开始执行任务。

  4. 阻塞状态(Blocked):在运行状态下,线程可能会被阻塞,暂停执行。有以下几种情况会导致线程进入阻塞状态:

  • 调用了sleep()方法,线程会暂停执行一段时间。

  • 等待某个对象的锁,线程会被阻塞,直到获取到锁。

  • 调用了wait()方法,线程会被阻塞,直到被唤醒。

  • 等待输入输出完成。

  • 等待其他线程完成。

  1. 等待状态(Waiting):线程进入等待状态,是因为调用了wait()、join()或LockSupport.park()方法。线程在等待状态下,会一直等待,直到被唤醒。

  2. 超时等待状态(Timed Waiting):线程进入超时等待状态,是因为调用了带有超时参数的sleep()、wait()、join()或LockSupport.parkNanos()、LockSupport.parkUntil()方法。线程在超时等待状态下,会在指定的时间内等待,超过时间后会自动唤醒。

  3. 终止状态(Terminated):线程执行完任务后,或者发生了异常导致线程终止,线程进入终止状态。

需要注意的是,线程状态之间的转换并不是线性的,而是可以根据具体的情况进行切换。例如,一个线程在运行状态下可能被阻塞,然后又恢复到运行状态。线程的状态转换是由Java虚拟机自动管理的,开发人员只需要关注线程的创建、启动和终止即可。

JUC - java.util.concurrent

JUC(Java Util Concurrent)是Java提供的用于多线程编程的工具包,它包含了多个工具类和数据结构,用于简化多线程编程的开发工作。以下是JUC中一些常用的类和接口的内容列表:

  1. Lock:提供了比传统的synchronized关键字更灵活和可扩展的锁机制,如ReentrantLock、StampedLock等。

  2. Condition:与Lock配合使用,提供了类似于Object的wait和notify方法的功能,如Condition接口。

  3. Atomic:提供了一系列的原子操作类,用于实现线程安全的原子操作,如AtomicInteger、AtomicLong、AtomicReference等。

  4. Executor:提供了用于管理和调度线程池的接口和类,如Executor、ExecutorService、ThreadPoolExecutor等。

  5. ConcurrentMap:提供了一系列的线程安全的Map实现,如ConcurrentHashMap。

  6. BlockingQueue:提供了一系列的阻塞队列,用于实现生产者-消费者模式,如ArrayBlockingQueue、LinkedBlockingQueue等。

  7. CountDownLatch:提供了一种倒计时门闩的机制,用于控制线程的执行顺序,如CountDownLatch类。

  8. CyclicBarrier:提供了一种循环屏障的机制,用于等待一组线程达到某个条件后再继续执行,如CyclicBarrier类。

  9. Semaphore:提供了一种信号量的机制,用于控制同时访问某个资源的线程数量,如Semaphore类。

  10. Future:提供了一种异步计算的机制,用于获取线程执行结果,如Future接口、CompletableFuture类等。

锁与同步

在Java中,锁机制用于多线程编程中的同步控制,以确保多个线程之间的数据访问和操作的正确性。Java提供了两种主要的锁机制:synchronized关键字和Lock接口。

  1. synchronized关键字:

  • synchronized关键字是Java中最基本的锁机制,它可以用于修饰方法或代码块。

  • synchronized关键字通过获取对象的内置锁(也称为监视器锁)来实现线程同步。

  • synchronized关键字会自动释放锁,可以确保在同一时间只有一个线程能够执行被修饰的代码块或方法。

  1. Lock接口:

  • Lock接口是Java.util.concurrent包中提供的锁机制,它提供了比synchronized更灵活的锁定操作。

  • Lock接口的主要实现类是ReentrantLock和ReentrantReadWriteLock。

  • Lock接口可以手动获取锁和释放锁,相比synchronized关键字,它提供了更多的功能,如可重入性、公平锁和条件等待/通知机制。

  • Lock接口还提供了tryLock方法,可以尝试获取锁而不阻塞线程,可以用于实现一些特殊的需求。

锁机制的目的是保护共享资源的访问,防止多个线程同时对其进行修改导致数据不一致或错误的结果。使用锁机制可以确保线程之间的有序执行,避免竞态条件和数据竞争的问题。

在使用锁机制时,需要注意以下几点:

  • 锁的范围应该尽量小,以避免不必要的竞争。

  • 需要确保锁的获取和释放是成对出现的,否则可能导致死锁。

  • 对于使用synchronized关键字的锁机制,需要注意锁的粒度,避免过度同步。

  • 对于使用Lock接口的锁机制,需要手动释放锁,否则可能导致死锁。

总的来说,锁机制是多线程编程中重要的同步工具,可以保证线程安全和数据一致性。在选择和使用锁机制时,需要根据具体的需求和场景来决定使用哪种锁机制以及如何正确使用。

锁竞争

锁竞争是指多个线程同时竞争同一个锁资源的情况。当多个线程同时请求获取同一个锁时,只有一个线程能够成功获取到锁资源,而其他线程则会被阻塞等待。

锁竞争可能导致以下几个问题:

  1. 线程饥饿:如果某个线程一直无法获取到锁资源,那么它将一直处于等待状态,无法继续执行。这种情况称为线程饥饿,可能导致程序性能下降。

  2. 死锁:当多个线程相互等待对方释放锁资源时,就会发生死锁。即使所有线程都在等待,也没有任何一个线程能够继续执行下去。死锁是一种严重的问题,会导致程序无法正常运行。

  3. 竞态条件:当多个线程同时访问和修改共享资源时,由于执行顺序的不确定性,可能会导致结果的不确定性或错误。这种情况称为竞态条件。

为了避免锁竞争的问题,需要进行合理的锁设计和使用。以下是一些减少锁竞争的方法:

  1. 减小锁的粒度:尽量将锁的范围缩小到最小,只锁定必要的代码块,以减少线程之间的竞争。

  2. 使用读写锁:如果共享资源的读操作远远多于写操作,可以考虑使用读写锁(ReentrantReadWriteLock),它可以允许多个线程同时读取共享资源,而写操作会独占锁。

  3. 使用乐观锁:乐观锁假设在大多数情况下没有冲突,因此不会阻塞线程,而是在提交时检查是否有冲突。如果有冲突,则需要回滚并重新尝试。

  4. 使用无锁数据结构:无锁数据结构(如ConcurrentHashMap、ConcurrentLinkedQueue)可以避免锁竞争,通过使用CAS(Compare and Swap)等原子操作来保证数据的一致性。

  5. 使用并发工具类:Java提供了一些并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,可以用于线程之间的协调和同步,减少锁竞争。

  6. 使用线程池:线程池可以有效地管理线程的创建和销毁,减少线程的竞争,提高程序的性能。

总之,合理设计和使用锁机制、减小锁的粒度、使用乐观锁和无锁数据结构,以及使用并发工具类和线程池等方法,都可以帮助我们减少锁竞争,提高多线程程序的性能和可靠性。

锁的分类

锁可以根据其特性和用途进行分类,常见的锁分类如下:

  1. 悲观锁(Pessimistic Locking):悲观锁认为在并发环境下,总是会有其他线程来竞争共享资源,因此在访问共享资源之前,会先获取锁来保证独占访问。悲观锁的代表是synchronized锁和ReentrantLock锁。

  2. 乐观锁(Optimistic Locking):乐观锁认为在并发环境下,竞争共享资源的概率较低,因此不需要立即获取锁,而是在更新共享资源时,检查是否有其他线程对资源进行了修改。如果没有冲突,则更新成功,否则需要进行回滚或重试。乐观锁的代表是CAS(Compare and Swap)操作和版本号机制。

  3. 共享锁(Shared Lock):共享锁允许多个线程同时获取锁,并发访问共享资源。共享锁适用于读多写少的场景,可以提高并发性能。共享锁的代表是ReadWriteLock锁。

  4. 排他锁(Exclusive Lock):排他锁只允许一个线程获取锁,其他线程需要等待。排他锁适用于写操作,保证只有一个线程能够修改共享资源。排他锁的代表是synchronized锁和ReentrantLock锁。

  5. 自旋锁(Spin Lock):自旋锁是一种忙等待的锁,线程在获取锁时,如果发现锁已经被其他线程占用,会一直循环检查锁的状态,直到获取到锁为止。自旋锁适用于锁占用时间较短的场景,可以避免线程切换的开销。自旋锁的代表是Atomic类和synchronized锁。

  6. 重入锁(Reentrant Lock):重入锁允许线程多次获取同一个锁,可以避免死锁的发生。重入锁的代表是ReentrantLock锁。

  7. 公平锁(Fair Lock):公平锁是指多个线程按照申请锁的顺序获取锁,保证锁的获取是公平的。公平锁的代表是ReentrantLock锁。

  8. 非公平锁(Non-Fair Lock):非公平锁是指多个线程获取锁的顺序是不确定的,可能会导致某些线程一直获取不到锁。非公平锁的代表是ReentrantLock锁。

不同类型的锁适用于不同的场景和需求,需要根据具体情况选择合适的锁机制来实现线程安全和并发性能。

锁升级

锁升级是指在多个层次的锁之间进行转换,以提高并发性能和减少资源消耗。

在Java中,锁升级通常指的是从无锁状态到偏向锁状态,再到轻量级锁状态,最后到重量级锁状态的转换过程。

  1. 无锁状态(No Locking):当多个线程同时访问同一个共享资源时,不需要进行任何锁操作,每个线程都可以直接访问共享资源,不会引发线程竞争和同步问题。

  2. 偏向锁状态(Biased Locking):当只有一个线程访问共享资源时,可以将该线程获取到的锁的对象头信息标记为偏向锁状态,以后该线程再次访问共享资源时,无需进行锁的竞争和同步,直接获取锁即可。偏向锁的目的是为了减少无竞争情况下的锁操作开销。

  3. 轻量级锁状态(Lightweight Locking):当多个线程同时访问同一个共享资源时,会进行轻量级锁的竞争。轻量级锁通过CAS(Compare and Swap)操作来尝试获取锁,如果成功则表示获取到锁,如果失败则表示发生了锁竞争,需要升级为重量级锁。

  4. 重量级锁状态(Heavyweight Locking):当多个线程同时访问同一个共享资源,并且轻量级锁竞争失败时,会升级为重量级锁。重量级锁使用操作系统的Mutex(互斥量)来实现锁的竞争和同步,需要进行线程阻塞和唤醒,会引入较大的开销。

锁升级的目的是为了在无竞争情况下减少锁操作的开销,提高并发性能;而在有竞争情况下,使用更重量级的锁来保证线程安全和正确性。锁升级的过程是由JVM自动完成的,开发者无需手动干预。

线程池

基本工作机制

线程池是一种线程管理的机制,通过预先创建一定数量的线程,维护一个线程队列,来有效地管理和调度线程的执行。

线程池的实现原理如下:

  1. 线程池的创建:在创建线程池时,会初始化一定数量的线程,这些线程被称为核心线程。同时,还可以设置线程池的最大线程数、线程存活时间、任务队列等参数。

  2. 任务提交:当有任务需要执行时,可以通过线程池的submit()或execute()方法将任务提交给线程池。提交的任务会被封装成一个Runnable或Callable对象。

  3. 任务队列:线程池维护一个任务队列,用于存储提交的任务。当线程池的核心线程都在执行任务时,新提交的任务会被存放在任务队列中。

  4. 线程调度:线程池会根据当前的线程池状态、核心线程数、任务队列的状态等因素来决定如何调度线程执行任务。通常情况下,线程池会优先使用核心线程执行任务,当核心线程都在执行任务且任务队列已满时,会创建新的线程来执行任务。如果线程池的线程数已经达到最大线程数,并且任务队列也已满,则根据线程池的拒绝策略来处理新提交的任务。

  5. 线程执行任务:线程池中的线程会从任务队列中取出任务,并执行任务的run()方法。执行完任务后,线程会继续从任务队列中获取下一个任务执行,直到线程池关闭或线程被回收。

  6. 线程回收:线程池中的线程在执行完任务后,如果空闲时间超过设定的线程存活时间,且当前线程池的线程数大于核心线程数,则会被回收,从而减少线程的资源消耗。

通过线程池,可以有效地管理和调度线程的执行,避免频繁地创建和销毁线程,提高系统的性能和资源利用率。同时,线程池还可以控制并发线程的数量,避免系统资源被过度占用,提高系统的稳定性。

常用的线程池

常用的线程池有以下几种:

  1. FixedThreadPool(固定线程池):该线程池的核心线程数固定,不会根据任务数量的增减而变化。适用于需要控制并发线程数量的场景,如服务器端的请求处理。

  2. CachedThreadPool(缓存线程池):该线程池的核心线程数为0,最大线程数为Integer.MAX_VALUE,适用于任务数量较多且执行时间较短的场景。当有新任务提交时,会创建新线程执行任务,如果线程空闲时间超过60秒,则会被回收。

以下是一个使用CachedThreadPool的示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {

    public static void main(String[] args) {
        // 创建一个CachedThreadPool线程池
        ExecutorService executor = Executors.newCachedThreadPool();

        // 提交任务给线程池执行
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is executing.");
                // 执行任务的逻辑代码
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

上述示例中,我们使用Executors类的newCachedThreadPool()方法创建了一个CachedThreadPool线程池。然后,我们使用submit()方法向线程池提交了10个任务,每个任务都是一个Runnable对象,其中包含了任务的逻辑代码。任务会被线程池中的线程异步执行。

最后,我们调用shutdown()方法关闭线程池,等待所有任务执行完毕。注意,CachedThreadPool线程池会根据任务的数量动态调整线程的数量,如果有新的任务提交,线程池会创建新的线程来执行任务;如果线程池中的线程空闲一段时间,超过了一定的时间阈值,线程池会自动回收空闲的线程。这样可以充分利用系统的资源,提高任务的并行度和执行效率。

CachedThreadPool的特点是适用于执行大量的短期任务,当任务的数量较多时,可以快速地创建新的线程来处理任务,提高任务的并行度和执行效率。但是,如果任务的数量过多,可能会导致线程数过多,消耗过多的系统资源。

如果你想要设置核心线程数,并且希望线程池中的线程保持一定数量的线程,可以考虑使用FixedThreadPool或者ThreadPoolExecutor来创建线程池。这些线程池可以通过设置核心线程数来控制线程的数量。

  1. SingleThreadPool(单线程池):该线程池只有一个核心线程,适用于需要顺序执行任务的场景。当有多个任务提交时,会按照任务的提交顺序依次执行。

  2. ScheduledThreadPool(定时线程池):该线程池可以定期执行任务或延迟执行任务。适用于需要定时执行或延迟执行任务的场景,如定时任务调度。

  3. WorkStealingPool(工作窃取线程池):该线程池使用Fork/Join框架实现,适用于需要执行大量耗时任务的场景。线程池中的线程可以窃取其他线程的任务来执行,提高任务的并行度和执行效率。

以下是一个使用工作窃取线程池的示例:

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

public class WorkStealingPoolExample {

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();

        // 创建一个可分解的任务
        RecursiveTask<Integer> task = new RecursiveTask<Integer>() {
            @Override
            protected Integer compute() {
                if (someCondition) {
                    // 如果满足条件,则执行任务并返回结果
                    return someComputation();
                } else {
                    // 如果不满足条件,则将任务分解成子任务并提交给线程池执行
                    RecursiveTask<Integer> subTask1 = new SomeRecursiveTask();
                    RecursiveTask<Integer> subTask2 = new SomeRecursiveTask();
                    subTask1.fork();
                    subTask2.fork();
                    // 等待子任务执行完毕并获取结果
                    int result1 = subTask1.join();
                    int result2 = subTask2.join();
                    // 合并子任务的结果并返回
                    return result1 + result2;
                }
            }
        };

        // 提交任务给线程池执行
        int result = pool.invoke(task);
        System.out.println("Result: " + result);

        // 关闭线程池
        pool.shutdown();
    }
}

上述示例中,我们首先创建了一个ForkJoinPool对象,然后创建了一个可分解的任务RecursiveTask。在任务的compute()方法中,我们首先判断是否满足执行任务的条件,如果满足则执行任务并返回结果;如果不满足,则将任务分解成两个子任务,并使用fork()方法将子任务提交给线程池执行。最后,使用join()方法等待子任务执行完毕并获取结果,然后合并子任务的结果并返回。

以上是常用的线程池类型,根据具体的业务需求和场景选择合适的线程池可以提高系统的性能和资源利用率。同时,还可以通过自定义线程池来满足特定的需求,如设置线程池的拒绝策略、线程池的名称、线程池的优雅关闭等。

线程池的拒绝策略

线程池的拒绝策略是在线程池无法接受新的任务时,决定如何处理新提交的任务。当线程池已经达到最大线程数,并且等待队列也已满时,线程池就无法接受新的任务了。这时,根据拒绝策略的配置,可以选择不同的处理方式。

Java提供了四种内置的拒绝策略:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止系统正常运行。

  2. CallerRunsPolicy:直接由提交任务的线程来执行新的任务。这种策略可以降低新任务的提交速度,但是会影响系统的响应性能。

  3. DiscardPolicy:直接丢弃新的任务,不做任何处理。

  4. DiscardOldestPolicy:丢弃等待队列中最老的任务,然后尝试再次提交新的任务。

除了以上四种内置的拒绝策略,你还可以自定义拒绝策略,实现RejectedExecutionHandler接口,并重写rejectedExecution()方法来定义自己的处理逻辑。

以下是一个示例,演示了如何设置线程池的拒绝策略:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class RejectedExecutionExample {

    public static void main(String[] args) {
        // 创建一个FixedThreadPool线程池,设置核心线程数为5,最大线程数为10,等待队列长度为100
        ExecutorService executor = new ThreadPoolExecutor(
                5, 10, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(100));

        // 设置拒绝策略为CallerRunsPolicy
        ((ThreadPoolExecutor) executor).setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        // 提交任务给线程池执行
        for (int i = 0; i < 20; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is executing.");
                // 执行任务的逻辑代码
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

在上述示例中,我们使用ThreadPoolExecutor来创建线程池,并设置核心线程数为5,最大线程数为10,等待队列长度为100。然后,我们通过setRejectedExecutionHandler()方法将拒绝策略设置为CallerRunsPolicy,即由提交任务的线程来执行新的任务。

在提交任务时,我们提交了20个任务,由于线程池的最大线程数为10,所以会有10个任务被放入等待队列中。当等待队列也满了之后,剩下的任务会被提交的线程直接执行。

你可以根据具体的业务需求和场景来选择合适的拒绝策略,或者自定义拒绝策略来处理无法接受的新任务。

下面是一个示例,演示了如何自定义拒绝策略:

import java.util.concurrent.*;

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义处理逻辑,例如将任务放入消息队列中等待处理
        // 这里只是简单地打印一条日志
        System.out.println("Task rejected: " + r.toString());
    }

    public static void main(String[] args) {
        // 创建一个FixedThreadPool线程池,设置核心线程数为5,最大线程数为10,等待队列长度为100
        ExecutorService executor = new ThreadPoolExecutor(
            5, 10, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(100));

        // 设置自定义的拒绝策略
        ((ThreadPoolExecutor) executor).setRejectedExecutionHandler(new CustomRejectedExecutionHandler());

        // 提交任务给线程池执行
        for (int i = 0; i < 20; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is executing.");
                // 执行任务的逻辑代码
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

synchronized关键字

synchronized是Java中的关键字,用于实现线程的同步和互斥。它可以用于修饰方法和代码块,具体作用如下:

  1. 修饰方法:当synchronized关键字修饰一个方法时,该方法称为同步方法。在该方法被调用时,会自动获取当前对象的锁,其他线程需要等待锁被释放后才能继续执行该方法。同步方法可以保证多个线程对该方法的访问是互斥的,即同一时间只有一个线程可以执行该方法。

  2. 修饰代码块:当synchronized关键字修饰一个代码块时,需要指定锁对象。在执行该代码块时,会自动获取指定锁对象的锁,其他线程需要等待该锁被释放后才能继续执行该代码块。同步代码块可以保证多个线程对该代码块的访问是互斥的,即同一时间只有一个线程可以执行该代码块。

synchronized关键字的作用是保证多个线程之间的同步和互斥,从而避免出现线程安全问题。需要注意的是,synchronized关键字的使用会降低程序的执行效率,因为它会引入线程的上下文切换和竞争锁等开销。因此,在使用synchronized关键字时,应尽量减少锁的粒度,避免出现锁竞争和死锁等问题。另外,可以考虑使用Lock接口或者java.util.concurrent包中的并发工具类来替代synchronized关键字,以提高程序的并发性能。

volatile关键字

在Java中,volatile关键字用于声明一个变量是"易变"的。它的主要作用有两个方面:

  1. 可见性:当一个变量被声明为volatile时,对这个变量的写操作会立即被刷新到主内存中,而读操作也会直接从主内存中读取,而不是从线程的本地内存中读取。这样可以保证多个线程之间对volatile变量的读写操作是可见的,即一个线程对变量的修改对其他线程是立即可见的。

  2. 禁止指令重排序:volatile关键字还可以禁止指令重排序优化。在Java内存模型中,对volatile变量的写操作之前的所有操作都将被排在写操作之前,而对volatile变量的读操作之后的所有操作都将被排在读操作之后。

需要注意的是,volatile关键字只能保证可见性和禁止指令重排序,但不能保证原子性。如果一个操作是原子性的,那么volatile关键字就无法保证其线程安全性,需要使用synchronized关键字或者java.util.concurrent包中的原子类来保证原子性操作。

AQS

AQS(AbstractQueuedSynchronizer)是Java并发包中的一个重要类,它提供了一种基于FIFO等待队列的同步机制。AQS的原理主要基于两个核心概念:state(状态)和队列。

  1. 状态(State):AQS内部维护了一个状态变量,用来表示共享资源的状态。这个状态变量可以被子类进行修改和管理,以实现不同的同步语义。

  2. 队列(Queue):AQS内部维护了一个FIFO的等待队列,用于管理等待获取共享资源的线程。当一个线程无法立即获取到所需的共享资源时,它会被放入等待队列中,等待获取资源的机会。

AQS的使用方式是通过继承AQS类来创建自定义的同步原语。通过重写AQS的方法,可以实现基于AQS的各种同步机制。

AQS的核心方法包括acquire、release、tryAcquire和tryRelease等。这些方法可以被子类重写,以实现特定的同步语义。当一个线程调用acquire方法尝试获取共享资源时,AQS会根据当前状态变量的取值决定是否允许线程获取资源。如果资源不可用,线程将被放入等待队列中;如果资源可用,线程将成功获取资源并继续执行。

AQS的设计使得它可以被用来构建各种同步机制,如独占锁、共享锁、信号量等。通过继承AQS并重写其中的方法,可以实现特定的同步语义,从而满足不同的并发编程需求。

CAS

CAS是一种原子操作,用于实现无锁算法。它通过比较内存中的值与预期值,如果相等则将新值写入内存,否则不做任何操作。CAS操作是并发编程中实现原子性操作的一种重要方式。

AQS内部使用CAS操作来实现对共享资源状态的修改和线程之间的竞争。在AQS的实现中,通过CAS操作来进行状态变量的原子性更新,从而实现对共享资源的安全访问和控制。

AQS中的acquire和release等方法通常会使用CAS操作来尝试修改状态变量,以实现对共享资源的获取和释放。通过CAS操作,AQS可以实现高效的并发同步机制,避免了传统锁的性能开销。

ThreadLocal类

ThreadLocal类是Java中的一个线程局部变量工具类,它提供了一种线程本地变量的解决方案,使得每个线程都可以拥有自己独立的变量副本,且互不干扰。

在多线程环境下,使用ThreadLocal可以避免共享变量的线程安全问题,每个线程都可以独立地访问自己的变量副本,而不需要担心线程间的竞争和同步。

ThreadLocal类提供了以下主要方法:

  1. set(T value):将当前线程的变量副本设置为指定的值。

  2. get():获取当前线程的变量副本。

  3. remove():移除当前线程的变量副本。

使用ThreadLocal时,每个线程可以通过get()和set()方法访问自己的变量副本,而不需要担心其他线程对其造成影响。这对于一些需要跨多个方法和类的数据共享而又不希望使用全局变量的场景非常有用。

例如,以下是一个使用ThreadLocal的示例:

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        threadLocal.set(1);

        Thread thread1 = new Thread(() -> {
            threadLocal.set(2);
            System.out.println(threadLocal.get()); // 输出2
        });

        Thread thread2 = new Thread(() -> {
            System.out.println(threadLocal.get()); // 输出1
        });

        thread1.start();
        thread2.start();
    }
}

在上面的示例中,我们创建了一个ThreadLocal对象,并使用set()方法设置了当前线程的变量副本。在不同的线程中,我们可以通过get()方法获取到不同的变量副本,互不干扰。

需要注意的是,使用ThreadLocal时要小心内存泄漏问题,因为ThreadLocal中的变量是与线程绑定的,如果没有适当地清理和释放资源,可能会导致内存泄漏。因此,使用ThreadLocal时应该在适当的时机调用remove()方法来清理不再需要的变量副本。

0

评论区