- 1. 线程与进程
- 2. 创建和管理线程
- 2.1. 继承Thread类
- 2.2. 实现Runnable接口
- 2.3 利用Callable、FutureTask接口实现。
- 2.4 Thread的常用方法
- 3. 线程同步
- 3.1. synchronized关键字
- 3.1.1同步代码块:
- 3.1.2 同步方法:
- 3.2. Lock接口
- 4. 线程间通信
- 5. 线程池
- 5.1 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
- 5.2 使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
- 6. Java并发工具类
- 6.1. CountDownLatch
- 6.2. CyclicBarrier
- 6.3. Semaphore
- 6.4. ConcurrentHashMap
- 6.5. BlockingQueue
- 7. 总结
- 9. 线程优先级
- 10. 守护线程
- 11. 线程局部变量
- 12. 线程异常处理
- 13. 实践建议
Java多线程编程是Java程序员必备的技能之一,因为它可以帮助我们编写更高效、更快速的程序。在本文中,我们将深入探讨Java多线程的基本概念、线程的创建和管理、同步、线程池以及Java并发工具类。
1. 线程与进程
进程是操作系统进行资源分配的基本单位,它是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,它是系统进行资源分配和调度的一个独立单位。
线程是进程中的一个实体,它是被操作系统独立调度和执行的基本单位。一个进程中可以有多个线程,这些线程共享进程的资源,如内存、文件句柄等。线程之间的切换比进程切换要快得多,因为线程间通信成本更低。
2. 创建和管理线程
在Java中,有两种方法可以创建线程:(还有JDK5新增了一种方法:实现Callable接口)
2.1. 继承Thread类
步骤:
①定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
②创建MyThread类的对象
③调用线程对象的start()方法启动线程(启动后还是执行run方法的)
创建一个新类,该类继承自Thread
类,然后覆盖run
方法:
class MyThread extends Thread {
@Override
public void run() {
// 你的线程代码
}
}
// 在主程序中创建并启动线程:
MyThread myThread = new MyThread();
myThread.start();
优缺点:
- 优点:编码简单
- 缺点:存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展
注意:
1、为什么不直接调用了run方法,而是调用start启动线程。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
- 只有调用start方法才是启动一个新的线程执行。
2、把主线程任务放在子线程之前了。
-
这样主线程一直是先跑完的,相当于是一个单线程的效果了。
2.2. 实现Runnable接口
步骤:
①定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
②创建MyRunnable任务对象
③把MyRunnable任务对象交给Thread处理。
④调用线程对象的start()方法启动线程
创建一个新类,实现Runnable
接口,然后实现run
方法:
class MyRunnable implements Runnable {
@Override
public void run() {
// 你的线程代码
}
}
// 在主程序中创建并启动线程:
Thread thread = new Thread(new MyRunnable());
thread.start();
实现Runnable
接口通常是更好的选择,因为它允许你的类继承其他类,而Java不支持多继承。
优缺点:
- 优点:线程任务类只是实现了Runnale接口,可以继续继承类和实现接口,扩展性强。
- 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
Thread的构造器
构造器 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
public Thread(Runnable target ,String name ) | 封装Runnable对象成为线程对象,并指定线程名称 |
2.3 利用Callable、FutureTask接口实现。
前言:
1、前2种线程创建方式都存在一个问题:
- 他们重写的run方法均不能直接返回结果。
- 不适合需要返回线程执行结果的业务场景。
2、怎么解决这个问题呢?
- JDK 5.0提供了Callable和FutureTask来实现。
- 这种方式的优点是:可以得到线程执行的结果。
步骤:
①、得到任务对象
1.定义类实现Callable接口,重写call方法,封装要做的事情。
2.用FutureTask把Callable对象封装成线程任务对象。
②、把线程任务对象交给Thread处理。
③、调用Thread的start方法启动线程,执行任务
④、线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
FutureTask的API
方法名称 | 说明 |
---|---|
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象。 |
public V get() throws Exception | 获取线程执行call方法返回的结果。 |
优缺点:
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
- 可以在线程执行完毕后去获取线程执行的结果。
- 缺点:编码复杂一点。
总结:
方式 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。 | 编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 | 编程相对复杂 |
2.4 Thread的常用方法
Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
- 当有很多线程在执行的时候,我们怎么去区分这些线程呢?
此时需要使用Thread的常用方法:getName()、setName()、currentThread()等。
Thread获取和设置线程名称
方法名称 | 说明 |
---|---|
String getName() | 获取当前线程的名称,默认线程名称是Thread-索引 |
void setName(String name) | 将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称 |
Thread类获得当前线程的对象
方法名称 | 说明 |
---|---|
public static Thread currentThread(): | 返回对当前正在执行的线程对象的引用 |
注意:
1、此方法是Thread类的静态方法,可以直接使用Thread类调用。 2、这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。
Thread类的线程休眠方法
方法名称 | 说明 |
---|---|
public static void sleep(long time) | 让当前线程休眠指定的时间后再继续执行,单位为毫秒。 |
3. 线程同步
在多线程环境下,如果多个线程同时访问共享资源,可能会导致数据不一致的问题。为了解决这个问题,我们需要对共享资源进行同步。
线程同步的核心思想
- 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
3.1. synchronized关键字
在Java中,我们可以使用synchronized
关键字来实现同步。synchronized
可以修饰方法或者代码块。当一个线程进入synchronized
修饰的方法或代码块时,其他线程将被阻塞,直到该线程离开。
以下是一个使用synchronized
关键字的示例:
class Counter {
private int count;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
3.1.1同步代码块:
作用:把出现线程安全问题的核心代码给上锁。
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
锁对象要求
- 理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。
锁对象的规范要求
规范上:建议使用共享资源作为锁对象。
对于实例方法建议使用this作为锁对象。
对于静态方法建议使用字节码(类名.class)对象作为锁对象。
3.1.2 同步方法:
作用:把出现线程安全问题的核心方法给上锁。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
原理:
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
3.2. Lock接口
除了使用synchronized
关键字外,Java还提供了java.util.concurrent.locks
包中的Lock
接口,该接口允许我们实现更灵活、更细粒度的同步。ReentrantLock
是Lock
接口的一个实现,它允许线程以可重入的方式获取锁。
以下是一个使用ReentrantLock
的示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
count--;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
4. 线程间通信
线程间通信是指多个线程之间互相发送消息或数据的过程。在Java中,我们可以使用wait
、notify
和notifyAll
方法实现线程间通信。
线程通信常见形式
- 通过共享一个数据的方式实现。
- 根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
应用场景:
生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。
方法名称 | 说明 |
---|---|
void wait() | 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待的单个线程 |
void notifyAll() | 唤醒正在等待的所有线程 |
以下是一个使用wait
和notify
实现生产者消费者模型的示例:
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
int maxSize = 10;
Thread producer = new Thread(new Producer(queue, maxSize), "Producer");
Thread consumer = new Thread(new Consumer(queue), "Consumer");
producer.start();
consumer.start();
}
}
class Producer implements Runnable {
private Queue<Integer> queue;
private int maxSize;
public Producer(Queue<Integer> queue, int maxSize) {
this.queue = queue;
this.maxSize = maxSize;
}
@Override
public void run() {
int value = 0;
while (true) {
synchronized (queue) {
while (queue.size() == maxSize) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Producing " + value);
queue.add(value++);
queue.notifyAll();
}
}
}
}
class Consumer implements Runnable {
private Queue<Integer> queue;
public Consumer(Queue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int value = queue.poll();
System.out.println("Consuming " + value);
queue.notifyAll();
}
}
}
}
5. 线程池
线程池是一种管理线程的机制,它允许我们重用线程以减少创建和销毁线程的开销。在Java中,我们可以使用ExecutorService
接口和Executors
类来创建和管理线程池。
5.1 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
ThreadPoolExecutor构造器的参数说明
注:
临时线程什么时候创建啊?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
ExecutorService的常用方法
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行 Runnable 任务 |
Future submit(Callable task) | 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务 |
void shutdown() | 等任务执行完毕后关闭线程池 |
List<Runnable]> shutdownNow() | 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
5.2 使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
Executors得到线程池对象的常用方法
- Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
方法名称 | 说明 |
---|---|
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
public static ExecutorService newSingleThreadExecutor () | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
以下是一个使用线程池的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executorService.execute(worker);
}
executorService.shutdown();
while (!executorService.isTerminated()) {
}
System.out.println("All threads finished");
}
}
class WorkerThread implements Runnable {
private String message;
public WorkerThread(String message) {
this.message = message;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " processing message: " + message);
processMessage();
}
private void processMessage() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6. Java并发工具类
Java并发包java.util.concurrent
提供了许多并发工具类,它们可以帮助我们简化多线程编程。以下是一些常用的并发工具类:
6.1. CountDownLatch
CountDownLatch
允许一个或多个线程等待其他线程完成操作。它包含一个初始计数值,任何调用await
方法的线程都将被阻塞,直到计数值为0。
6.2. CyclicBarrier
CyclicBarrier
允许一组线程相互等待,直到所有线程都到达某个屏障点。当最后一个线程到达屏障点时,所有等待的线程将被释放。
6.3. Semaphore
Semaphore
用于限制可以访问某个资源的线程数量。当一个线程要访问受限资源时,需要先获得信号量;当线程完成操作时,释放信号量。如果没有可用的信号量,线程将阻塞,直到有线程释放信号量。
6.4. ConcurrentHashMap
ConcurrentHashMap
是一个线程安全的HashMap实现,它允许多个线程并发访问。与使用synchronized
关键字的HashMap相比,它具有更好的性能。
6.5. BlockingQueue
BlockingQueue
是一个阻塞队列,它支持线程安全的插入和删除操作。当队列为空时,删除操作将阻塞;当队列满时,插入操作将阻塞。BlockingQueue
在生产者-消费者模型中非常有用。
7. 总结
本文深入探讨了Java多线程编程的基本概念、线程的创建和管理、同步、线程池以及Java并发工具类。了解这些概念和技术对于编写高效、高性能的Java程序至关重要。希望本文能为你的Java多线程编程之旅提供有用的指导!## 8. 线程的状态和生命周期
在Java中,线程具有以下状态:
- 新建(New):当我们创建了一个线程对象,但还没有调用
start()
方法时,线程处于新建状态。 - 就绪(Runnable):当线程对象调用了
start()
方法后,线程进入就绪状态。在这个状态下,线程已经准备好运行,等待操作系统分配时间片。 - 运行(Running):线程获得时间片并开始执行
run()
方法中的代码时,线程处于运行状态。 - 阻塞(Blocked):线程在等待获取锁以进入同步代码块或方法时,线程进入阻塞状态。
- 等待(Waiting):线程在等待其他线程执行特定操作(如调用
notify()
或notifyAll()
方法)时,线程进入等待状态。例如,线程调用了Object.wait()
、Thread.join()
或LockSupport.park()
方法。 - 超时等待(Timed Waiting):线程在等待其他线程执行特定操作,但最多等待指定的时间。例如,线程调用了
Thread.sleep()
、Object.wait(long timeout)
或LockSupport.parkNanos()
方法。 - 终止(Terminated):线程完成了
run()
方法的执行或因异常而终止,线程进入终止状态。
要获取线程的当前状态,可以调用Thread.getState()
方法。
9. 线程优先级
在Java中,线程具有优先级。优先级较高的线程可能会比优先级较低的线程获得更多的执行时间。线程优先级是通过Thread.setPriority(int)
方法设置的,取值范围为1到10,其中1表示最低优先级,10表示最高优先级。可以使用Thread.getPriority()
方法获取线程的当前优先级。
需要注意的是,线程优先级并不能保证线程执行的顺序,它只是影响线程获得执行时间的可能性。因此,在编写多线程程序时,不应依赖线程优先级来实现正确的同步或顺序。
10. 守护线程
守护线程(Daemon Thread)是一种特殊类型的线程,主要用于在后台为其他线程提供服务。当所有非守护线程结束时,守护线程会自动结束。垃圾回收线程就是一个典型的守护线程。
要将线程设置为守护线程,可以在调用start()
方法之前调用Thread.setDaemon(true)
方法。可以使用Thread.isDaemon()
方法检查线程是否为守护线程。
注意:不要将可能与其他线程进行交互的线程设置为守护线程,因为守护线程可能会在任何时候终止。如果守护线程在与其他线程交互时突然终止,可能会导致不可预测的行为或数据不一致。
11. 线程局部变量
线程局部变量(Thread-Local Variables)是一种特殊类型的变量,它为每个线程提供独立的变量副本。这意味着每个线程可以访问和修改它自己的变量副本,而不会影响其他线程的副本。
Java中提供了ThreadLocal
类来实现线程局部变量。要创建一个线程局部变量,可以创建一个ThreadLocal
对象,并调用get()
和set(T value)
方法来获取和设置线程的变量副本。以下是一个使用ThreadLocal
的示例:
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
int counter = threadLocalCounter.get();
threadLocalCounter.set(counter + 1);
System.out.println("Thread-1: " + threadLocalCounter.get());
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
int counter = threadLocalCounter.get();
threadLocalCounter.set(counter + 1);
System.out.println("Thread-2: " + threadLocalCounter.get());
}
}
});
t1.start();
t2.start();
}
}
在这个示例中,threadLocalCounter
是一个线程局部变量。我们为每个线程提供了初始值0。当每个线程执行时,它们都会访问和修改自己的计数器副本,而不会影响其他线程的副本。
12. 线程异常处理
当线程抛出未捕获的异常时,Java提供了一种机制来处理这些异常。可以通过实现Thread.UncaughtExceptionHandler
接口并调用Thread.setDefaultUncaughtExceptionHandler()
方法来设置全局的未捕获异常处理器。此外,还可以通过调用Thread.setUncaughtExceptionHandler()
方法为单个线程设置未捕获异常处理器。
以下是一个设置未捕获异常处理器的示例:
public class ThreadExceptionHandlingExample {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + " throws exception: " + e);
}
});
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(1 / 0);
}
}, "Thread-1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
throw new RuntimeException("An exception occurred in Thread-2");
}
}, "Thread-2");
t1.start();
t2.start();
}
}
在这个示例中,我们设置了一个全局的未捕获异常处理器,用于处理所有线程抛出的未捕获异常。当Thread-1
和Thread-2
抛出未捕获的异常时,异常处理器会捕获并处理这些异常。
13. 实践建议
在进行多线程编程时,以下是一些建议和注意事项:
- 尽量使用
java.util.concurrent
包中提供的高级并发工具类,而不是直接使用Thread
和synchronized
关键字。这可以简化代码并提高程序的可维护性和性能。 - 在编写多线程程序时,避免使用全局变量或共享状态,以降低同步的复杂性和可能出现的错误。
- 要充分利用Java的内存模型,确保线程之间正确地共享和同步数据。
- 使用线程池来管理和调度线程,以提高程序性能。
- 避免线程优先级来实现正确的同步或顺序,因为它不能保证线程执行的顺序。
- 使用守护线程为其他线程提供后台服务,但不要将可能与其他线程进行交互的线程设置为守护线程。
- 当需要为每个线程提供独立的变量副本时,使用线程局部变量。
- 为线程设置未捕获异常处理器,以便在线程抛出未捕获的异常时进行处理。