Java 并发编程

Java 并发编程

  • 一、线程创建
    • [1.1 继承 Thread 类](#1.1 继承 Thread 类)
    • [1.2 实现 Runnable 接口](#1.2 实现 Runnable 接口)
    • [1.3 实现 Callable 接口](#1.3 实现 Callable 接口)
  • 二、线程方法
  • 三、线程同步
    • [3.1 锁](#3.1 锁)
      • [3.1.1 synchronized](#3.1.1 synchronized)
      • [3.1.2 ReentrantLock](#3.1.2 ReentrantLock)
      • [3.1.3 StampedLock](#3.1.3 StampedLock)
    • [3.2 原子变量](#3.2 原子变量)
    • [3.3 Semaphore](#3.3 Semaphore)
    • [3.4 Condition](#3.4 Condition)
  • 四、线程池
    • [4.1 ThreadPoolExecutor](#4.1 ThreadPoolExecutor)
    • [4.2 Executors](#4.2 Executors)

一、线程创建

1.1 继承 Thread 类

通过继承 Thread 类来创建线程是最简单的方法之一。只需要创建一个继承自 Thread 的子类,并重写其 run() 方法,然后通过调用子类的 start() 方法来启动线程。如果 JVM 采用 1:1 的线程模型,start() 方法底层会通过 POSIX 线程库中的 pthread_create() 创建一个内核线程,并将 Java 中创建的线程映射到这个内核线程中,不过不同的 JVM 具有不同的映射方案。

这种方法的优点是简单易用,适用于简单的线程逻辑。不过由于 Java 不支持多重继承,因此通过继承 Thread 类来创建线程会限制类的继承关系。

通过继承 Thread 类来创建线程:

java 复制代码
package atreus.ink;

import java.lang.management.ManagementFactory;

public class MyThread extends Thread {
    @Override
    public void run() {
        long threadId = Thread.currentThread().getId();
        String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        System.out.println("My pid is " + processId + ", my tid is " + threadId + ".");
    }
}
java 复制代码
package atreus.ink;

import java.lang.management.ManagementFactory;

public class Main {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();

        long threadId = Thread.currentThread().getId();
        String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        System.out.println("My pid is " + processId + ", my tid is " + threadId + ".");
    }
}
My pid is 66201, my tid is 13.
My pid is 66201, my tid is 1.

注意事项 :主线程启动子线程需要调用 start() 方法而不是 run() 方法,调用 run() 方法会将线程对象当作普通的 Java 对象来进行方法调用,并不会向操作系统注册线程,实际还是单线程执行。


1.2 实现 Runnable 接口

通过实现 Runnable 接口来创建线程是更加灵活的方法。通过这种方式,一个类既可以实现其他接口,又可以创建线程。

通过实现 Runnable 接口来创建线程:

java 复制代码
package atreus.ink;

import java.lang.management.ManagementFactory;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        long threadId = Thread.currentThread().getId();
        String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        System.out.println("My pid is " + processId + ", my tid is " + threadId + ".");
    }
}
java 复制代码
package atreus.ink;

import java.lang.management.ManagementFactory;

public class Main {
    public static void main(String[] args) {
        Runnable runnable = new MyRunnable();
        new Thread(runnable).start();

        long threadId = Thread.currentThread().getId();
        String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        System.out.println("My pid is " + processId + ", my tid is " + threadId + ".");
    }
}
My pid is 7388, my tid is 14.
My pid is 7388, my tid is 1.

此外,对于通过实现 Runnable 接口的创建方法,还可以通过匿名内部类和 Lambda 进行代码简化:

java 复制代码
package atreus.ink;

import java.lang.management.ManagementFactory;

public class Main {
    public static void main(String[] args) {
        // 1.通过匿名内部类进行代码简化
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                long threadId = Thread.currentThread().getId();
                String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
                System.out.println("My pid is " + processId + ", my tid is " + threadId + ".");
            }
        };
        new Thread(runnable).start();

        // 2.通过Lambda表达式进行简化
        new Thread(() -> {
            long threadId = Thread.currentThread().getId();
            String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
            System.out.println("My pid is " + processId + ", my tid is " + threadId + ".");
        }).start();

        long threadId = Thread.currentThread().getId();
        String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        System.out.println("My pid is " + processId + ", my tid is " + threadId + ".");
    }
}

1.3 实现 Callable 接口

当使用 Callable 接口来创建线程时,可以实现一些需要返回结果的异步操作。与使用 Runnable 不同,Callablecall() 方法可以返回一个值,也可以抛出异常。

通过实现 Callable 接口创建线程,同时获取返回值:

java 复制代码
package atreus.ink;

import java.lang.management.ManagementFactory;
import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        long threadId = Thread.currentThread().getId();
        String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        return "My pid is " + processId + ", my tid is " + threadId + ".";
    }
}
java 复制代码
package atreus.ink;

import java.lang.management.ManagementFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable = new MyCallable();
        FutureTask<String> task = new FutureTask<>(callable);
        new Thread(task).start();
        System.out.println(task.get()); // get方法会使主线程等待子线程执行完毕,然后获取结果

        long threadId = Thread.currentThread().getId();
        String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        System.out.println("My pid is " + processId + ", my tid is " + threadId + ".");
    }
}
My pid is 25580, my tid is 14.
My pid is 25580, my tid is 1.

当然,这种创建方法也可以通过匿名内部类和 Lambda 表达式简化。

java 复制代码
package atreus.ink;

import java.lang.management.ManagementFactory;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> task = new FutureTask<>(() -> {
            long threadId = Thread.currentThread().getId();
            String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
            return "My pid is " + processId + ", my tid is " + threadId + ".";
        });
        new Thread(task).start();
        System.out.println(task.get());

        long threadId = Thread.currentThread().getId();
        String processId = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
        System.out.println("My pid is " + processId + ", my tid is " + threadId + ".");
    }
}

二、线程方法

Thread 类提供了很多与线程操作相关的方法:

常用方法 说明
public static Thread currentThread() 获取当前执行的线程对象
public void run() 线程的任务方法
public void start() 启动线程
public String getName() 获取当前线程的名称,线程名称默认是 Thread-索引
public void setName(String name) 为线程设置名称
public static void sleep(long time) 让当前执行的线程休眠
public final void join() 调用这个方法的线程将等待被调用的线程执行完成,然后再继续执行,类似于 POSIX 线程库中的 pthread_join

join() 的基本使用示例:

java 复制代码
package atreus.ink;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
        });

        thread.start();
        thread.join(); // 主线程会在此处等待子线程执行完毕

        System.out.println(Thread.currentThread().getName());
    }
}

三、线程同步

3.1 锁

3.1.1 synchronized

synchronized 是 Java 中用于实现线程同步的关键字,它主要用于创建同步代码块同步方法 ,以确保在多线程环境下对共享资源的访问是安全的。通过使用 synchronized 可以避免多个线程同时访问共享资源而引发的并发问题,如竞态条件和数据不一致等。

在 Java 6 之前,synchronized 只有传统的重量级锁 机制,直接通过对象内部的监视器(monitor)实现,存在性能上的瓶颈。在 Java 6 后,为了提高锁的获取与释放效率,JVM 引入了偏向锁轻量级锁两种新的锁机制。它们解决了在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。不过这些锁机制由 JVM 根据实际情况进行自动选择,在实际编程中,我们通常无需显式地操作这些锁。一般情况下 JVM 会优先考虑偏向锁和轻量级锁,然后才会考虑是重量级锁。

重量级锁依靠 monitor 机制实现,monitor 机制是操作系统提出来的一种高级原语,但属于语言范畴,由不同语言提供不同的实现。Java 中 monitor 机制的实现依赖于 ObjectMonitor,ObjectMonitor 是 JVM 内部基于 C++ 实现的一套机制,它为每个对象中都内置了一个 ObjectMonitor 对象,保证同一时刻只有一个线程能够获得指定对象的监视器(底层通过操作系统中的 mutex 实现)。因此一个对象可以作为 monitor object 被 synchronzied 关联,而访问同步方法或同步代码块的本质就是获取关联对象的监视器。

偏向锁和轻量级锁采用乐观的 CAS 无锁竞争的方式获取锁,CAS 由 Unsafe 类中 compareAndSwap 方法实现,它会通过 JNI 调用 C++ 方法以内联汇编的形式完成相关操作,同时 compareAndSwap 方法还会通过检查引用和标志来避免 ABA 问题。


同步代码块

通过在代码块内使用 synchronized 关键字来创建同步代码块,它可以用来保护代码块,确保在同一时刻只有一个线程能够进入同步代码块。一个典型的用法是将需要同步的代码放在同步代码块中,并指定一个锁对象作为同步的依据。

虽然任意一个唯一的对象(比如一个字符串)都可以作为同步代码块的锁对象,但锁的粒度过大会导致并发安全问题,粒度过小会导致性能下降。类比同步方法,一般情况下,对于实例方法 ,通常使用 this 作为锁对象,对于静态方法 ,通常使用类的字节码对象 类名.class 作为锁对象。

同步代码块的基本使用示例:

java 复制代码
package atreus.ink;

public class Main {
    private static int counter;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Runnable incrementTask = () -> {
            for (int i = 0; i < 10000; i++) {
                // 同步代码块
                synchronized (lock) {
                    counter++;
                }
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + counter);
    }
}
Final counter value: 20000

此外,在同步代码块中还可以通过 wait() 方法让当前线程进入等待状态,直到其他线程调用相同对象的 notify()notifyAll() 方法来唤醒它。这三个方法的实现也同样依赖于 monitor 机制,因此需要被绑定到指定的锁对象上。

wait()notify() 的基本使用示例:

java 复制代码
package atreus.ink;

public class Main {
    public static void main(String[] args) {
        final Object lock = new Object();

        // 等待线程
        Thread waiter = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Waiter: Waiting for a notification...");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Waiter: Got a notification!");
            }
        });

        // 通知线程
        Thread notifier = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Notifier: Performing some work...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Notifier: Work done, notifying the waiter...");
                lock.notify();
            }
        });

        waiter.start();
        notifier.start();
    }
}
Waiter: Waiting for a notification...
Notifier: Performing some work...
Notifier: Work done, notifying the waiter...
Waiter: Got a notification!

同步方法

通过在方法定义处使用 synchronized 关键字来创建同步方法,它可以将整个方法体都变成一个同步代码块。同步方法底层通过隐式锁对象实现,只是锁的范围是整个方法代码。如果方法是实例方法 ,同步方法默认用 this 作为的锁对象。如果方法是静态方法 ,同步方法默认用 类名.class 作为的锁对象。

同步方法的优点是简单,可以很方便地实现线程同步。不过锁的范围较大,可能影响性能,因为其他不需要同步的代码也会被锁住。

同步方法的基本使用示例:

java 复制代码
package atreus.ink;

public class Main {
    private static int counter;

    public static synchronized void increment() {
        for (int i = 0; i < 10000; i++) {
            counter++;
        }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(Main::increment);
        Thread thread2 = new Thread(Main::increment);

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + counter);
    }
}
Final counter value: 20000

3.1.2 ReentrantLock

ReentrantLock 是 Java 提供的一个可重入锁 ,默认为非公平锁 ,它相比于使用 synchronized 关键字具有更大的灵活性。通过 ReentrantLock,你可以显式地获取锁和释放锁,从而精确地控制同步范围。

ReentrantLock 提供了更多的功能,比如可重入性可定时的锁等待公平性设置 等。但需要注意,使用 ReentrantLock 需要手动释放锁,因此务必在 finally 块中释放锁,以防止死锁情况的发生。

ReentrantLock 的基本使用示例:

java 复制代码
package atreus.ink;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    private static final Lock lock = new ReentrantLock();
    private static int counter;

    public static void main(String[] args) {
        Runnable incrementTask = () -> {
            for (int i = 0; i < 10000; i++) {
                lock.lock();
                try {
                    counter++;
                } finally {
                    // 放在finally块中保证锁一定能被释放
                    lock.unlock();
                }
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + counter);
    }
}
Final counter value: 20000

3.1.3 StampedLock

StampedLock 是 Java 提供的一个支持乐观读、悲观读和写操作的锁机制。它在 Java 8 中引入,通过使用乐观读锁来提供更高的并发性,同时支持升级为悲观读锁或写锁。不过它不可重入不支持条件变量 Conditon

StampedLock 提供了三种读写控制模式:

  1. 乐观读锁 :乐观读锁是一种无锁操作,它假设没有写操作会发生。线程可以直接读取数据而无需获取锁,读取完成后通过校验版本信息来判断数据是否有效。如果数据有效,操作成功;如果数据无效,需要尝试其他方式来获取锁。乐观读锁适用于读多写少的场景。
  2. 悲观读锁 :悲观读锁是常规的读锁,它会阻塞写操作,但不会阻塞其他读操作。悲观读锁适用于读多写多的场景,可以保证读操作之间的数据一致性。
  3. 写锁:写锁会阻塞其他的读操作和写操作,用于保护共享资源的写操作。

StampedLock 的基本使用示例:

java 复制代码
package atreus.ink;

import java.util.concurrent.locks.StampedLock;

public class Main {
    private static final StampedLock lock = new StampedLock();
    private static int counter;

    public static void main(String[] args) {
        Runnable incrementTask = () -> {
            for (int i = 0; i < 10000; i++) {
                long stamp = lock.writeLock(); // 获取写锁
                try {
                    counter++;
                } finally {
                    lock.unlockWrite(stamp); // 释放写锁
                }
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + counter);
    }
}

StampedLock 在某些场景下可以提供更好的并发性能,但也需要注意合理的使用,以避免过于复杂的代码结构和潜在的死锁情况。


3.2 原子变量

synchronizedReentrantLock 都是基于悲观锁思想实现的,意味着它们假定在执行临界区代码期间会发生并发冲突。在高并发场景下,由于激烈的锁竞争,可能会导致线程阻塞,从而降低性能。特别是在多读场景下,悲观锁可能引入大量的额外并发开销,因为每个读操作都需要获得独占锁。

相比之下,StampedLock乐观锁思想更适合多读场景。乐观锁假定数据操作不存在并发冲突,因此不会引起锁竞争,也不会导致线程阻塞和死锁。乐观锁通常在提交修改时才验证资源是否被其他线程修改。然而,在多写场景下,乐观锁可能会由于频繁的冲突而引起失败和重试,这可能会对性能产生一定的影响。不过在多写场景下乐观锁会频繁失败和重试,这同样会对性能造成一定影响。

除了 StampedLock 中的乐观读锁,java.util.concurrent.atomic 包下面 AtomicIntegerAtomicLongAtomicIntegerArrayAtomicReference 等原子变量类也是基于乐观锁的思想实现的。

AtomicInteger 的基本使用示例:

java 复制代码
package atreus.ink;

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    private static final AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) {
        Runnable incrementTask = () -> {
            for (int i = 0; i < 10000; i++) {
                counter.incrementAndGet();
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + counter.get());
    }
}

AtomicReference 的基本使用示例:

java 复制代码
package atreus.ink;

import java.util.concurrent.atomic.AtomicReference;

public class Main {
    private static final AtomicReference<Integer> counterRef = new AtomicReference<>(0);

    public static void main(String[] args) {
        Runnable incrementTask = () -> {
            for (int i = 0; i < 10000; i++) {
                while (true) {
                    Integer current = counterRef.get();
                    Integer updated = current + 1;
                    if (counterRef.compareAndSet(current, updated)) {
                        break;
                    }
                }
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + counterRef.get());
    }
}
Final counter value: 20000

3.3 Semaphore

Semaphore 类似于 POSIX 线程库中的匿名信号量,它维护了一个可用许可证的数量,线程可以通过获取许可证来访问受限资源,使用完毕后释放许可证,以供其他线程使用。

Semaphore 的基本使用示例:

java 复制代码
package atreus.ink;

import java.util.concurrent.Semaphore;

public class Main {
    private static final int PERMITS = 1; // 设置许可证数量,控制并发访问的线程数量
    private static final Semaphore semaphore = new Semaphore(PERMITS);
    private static int counter;

    public static void main(String[] args) {
        Runnable incrementTask = () -> {
            try {
                semaphore.acquire(); // 获取许可证,如果没有许可证则阻塞
                for (int i = 0; i < 10000; i++) {
                    counter++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release(); // 释放许可证
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

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

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + counter);
    }
}

3.4 Condition

Condition 类似于 POSIX 线程库中的条件变量,它通常是与锁一起使用,用于实现线程间的等待和通知。条件变量允许线程在满足某个条件之前等待,然后在条件满足时被其他线程通知,从而继续执行。它可以用于替代传统的 wait()notify() 操作,提供了更灵活和精细的线程同步机制。

java 复制代码
package atreus.ink;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean conditionMet = false;

    public static void main(String[] args) throws InterruptedException {
        ConditionExample example = new ConditionExample();

        Thread waiterThread = new Thread(() -> {
            try {
                example.waitForCondition();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread signallerThread = new Thread(() -> {
            try {
                Thread.sleep(2000); // 模拟一段时间后满足条件
                example.signalCondition();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        waiterThread.start();
        signallerThread.start();

        waiterThread.join();
        signallerThread.join();
    }

    public void waitForCondition() throws InterruptedException {
        lock.lock();
        try {
            while (!conditionMet) {
                condition.await(); // 等待条件满足
            }
            // 执行条件满足后的操作
            System.out.println("Condition met, proceeding...");
        } finally {
            lock.unlock();
        }
    }

    public void signalCondition() {
        lock.lock();
        try {
            conditionMet = true;
            condition.signal(); // 通知等待的线程条件已满足
            System.out.println("Condition signaled");
        } finally {
            lock.unlock();
        }
    }
}

四、线程池

4.1 ThreadPoolExecutor

Java 中的线程池接口为 ExecutorService,一个常用的实现类为 ThreadPoolExecutor,其构造函数为:

java 复制代码
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                   long keepAliveTime, TimeUnit unit,
                   BlockingQueue<Runnable> workQueue,
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler)

参数说明

  • corePoolSize线程池的核心线程数,即任务队列未达到队列容量时,最大可以同时运行的线程数量。即使线程是空闲的,它们也不会被销毁,除非线程池被关闭。
  • maximumPoolSize线程池的最大线程数。在没有核心线程空闲的情况下,如果任务数量增加,线程池可以扩展到最大线程数。如果任务数量继续增加,超过线程池最大大小的任务将会被拒绝执行。
  • keepAliveTime非核心线程的最大空闲时间 。当线程池中的线程数量超过 corePoolSize,多余的非核心线程会在空闲时间超过 keepAliveTime 后被销毁,以减少资源占用。
  • unit时间单位 ,用于指定 keepAliveTime 的时间单位。
  • workQueue用于存储等待执行的任务的阻塞队列 。当所有核心线程都忙碌时,新任务将被放入队列等待执行。常用的队列类型包括 LinkedBlockingQueueArrayBlockingQueuePriorityBlockingQueue 等。
  • threadFactory用于创建线程的工厂 。可以通过提供自己实现的 ThreadFactory 自定义线程的创建过程。
  • handler拒绝策略,用于处理无法提交给线程池执行的任务 。当任务数量超过线程池最大大小且队列已满时,将使用拒绝策略处理任务。常见的策略有 AbortPolicyCallerRunsPolicyDiscardPolicy 等。

注意事项

  • 新任务提交时发现核心线程都在忙任务队列也满了 ,并且还可以创建临时线程 ,此时才会创建临时线程
  • 核心线程和临时线程都在忙任务队列也满了 ,新的任务过来的时候才会开始拒绝任务

常用方法

方法名称 说明
void execute(Runnable command) 执行 Runnable 任务
Future<T> submit(Callable<T> task) 执行 callable 任务,返回未来任务对象,用于获取线程返回的结果
void shutdown() 等全部任务执行完毕后,再关闭线程池
List<Runnable> shutdownNow() 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

ThreadPoolExecutor 的基本使用示例:

java 复制代码
package atreus.ink;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.printf("[%s] %s\n", Thread.currentThread().getName(),
                          LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
java 复制代码
package atreus.ink;

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        ExecutorService pool = new ThreadPoolExecutor(2, 3,
                                                      8, TimeUnit.SECONDS,
                                                      new ArrayBlockingQueue<>(2),
                                                      Executors.defaultThreadFactory(),
                                                      new ThreadPoolExecutor.AbortPolicy());

        Runnable target = new MyRunnable();

        pool.execute(target); // 核心线程
        pool.execute(target); // 核心线程
        pool.execute(target); // 任务队列等待
        pool.execute(target); // 任务队列等待
        pool.execute(target); // 任务队列满,启动一个临时线程
        pool.execute(target); // 核心线程和临时线程忙,同时任务队列已满,拒绝任务

        pool.shutdown();
    }
}
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task atreus.ink.MyRunnable@7a0ac6e3 rejected from java.util.concurrent.ThreadPoolExecutor@71be98f5[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]
	at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
	at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
	at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
	at atreus.ink.Main.main(Main.java:20)
[pool-1-thread-2] 2023-08-30 15:57:44
[pool-1-thread-1] 2023-08-30 15:57:44
[pool-1-thread-3] 2023-08-30 15:57:44
[pool-1-thread-1] 2023-08-30 15:57:47
[pool-1-thread-2] 2023-08-30 15:57:47

新任务拒绝策略

策略 详解
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出 RejectedExecutionException 异常,是默认的策略
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常,这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 由主线程负责调用任务的 run() 方法从而绕过线程池直接执行

4.2 Executors

Executors 是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

方法名称 说明
public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它
public static ExecutorService newSingleThreadExecutor() 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程
public static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了 60s 则会被回收掉。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务或者定期执行任务

newScheduledThreadPool 的基本使用示例:

java 复制代码
package atreus.ink;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.printf("[%s] %s\n", Thread.currentThread().getName(),
                          LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}
java 复制代码
package atreus.ink;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
        Runnable target = new MyRunnable();

        // 延迟1秒后执行target任务
        pool.schedule(target, 1, TimeUnit.SECONDS);

        // 延迟2秒后,每隔3秒执行一次target任务
        pool.scheduleAtFixedRate(target, 2, 3, TimeUnit.SECONDS);

        Thread.sleep(10 * 1000);
        pool.shutdown();
    }
}
[pool-1-thread-1] 2023-08-30 16:26:33
[pool-1-thread-2] 2023-08-30 16:26:34
[pool-1-thread-2] 2023-08-30 16:26:37
[pool-1-thread-2] 2023-08-30 16:26:40

参考:

https://www.bilibili.com/video/BV1Cv411372m/?p=175
https://javaguide.cn/java/concurrent/
https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/

相关推荐
y25085 分钟前
《Object类》
java·开发语言
曙曙学编程6 分钟前
初级数据结构——树
android·java·数据结构
BestandW1shEs12 分钟前
彻底理解消息队列的作用及如何选择
java·kafka·rabbitmq·rocketmq
爱吃烤鸡翅的酸菜鱼14 分钟前
Java算法OJ(8)随机选择算法
java·数据结构·算法·排序算法
码蜂窝编程官方18 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
Viktor_Ye34 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm36 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
一二小选手40 分钟前
【Maven】IDEA创建Maven项目 Maven配置
java·maven
J老熊1 小时前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
猿java1 小时前
什么是 Hystrix?它的工作原理是什么?
java·微服务·面试