Java多线程并发编程技术详解
引言
在现代软件开发中,多线程并发编程是提升系统性能和效率的关键技术之一。Java语言自1.5版本开始引入了强大的并发工具类库,并提供了丰富的API来支持多线程编程。本文将深入探讨Java中的多线程并发编程概念、原理以及实战技巧。
一、线程创建
在Java中,创建线程主要有两种方式:
1. 继承Thread类
示例代码:
public class MyThread extends Thread { private String threadName; public MyThread(String name) { this.threadName = name; } @Override public void run() { // 在这里定义线程执行的任务 System.out.println("线程 " + threadName + " 正在运行..."); performTask(); } private void performTask() { // 具体的线程任务逻辑 for (int i = 0; i详细解释:
- 我们通过创建一个继承自java.lang.Thread的子类MyThread来实现一个新的线程类型。
- 在MyThread类中重写了run()方法,这个方法包含了线程要执行的任务逻辑。
- main方法中创建了两个MyThread对象,并调用它们的start()方法启动新线程。当调用start()方法时,Java虚拟机会为每个线程创建新的执行路径,并调用该线程对应的run()方法。
2. 实现Runnable接口
示例代码:
public class RunnableTask implements Runnable { private String taskName; public RunnableTask(String name) { this.taskName = name; } @Override public void run() { // 线程执行的任务 System.out.println("任务 " + taskName + " 正在运行..."); executeTask(); } private void executeTask() { for (int i = 0; i
详细解释:
- 这次我们创建了一个实现了java.lang.Runnable接口的类RunnableTask,并在其中实现了run()方法。
- Runnable接口仅包含一个run()方法,这意味着它允许我们将任务与线程的具体实现(即Thread类)解耦。
- 在main方法中,我们创建了RunnableTask实例并将其传递给Thread构造函数,然后启动这两个线程。即使不直接扩展Thread类,由于Thread类可以接受Runnable类型的参数,因此仍然能够执行并发任务。
二、线程同步
Java多线程并发编程中,线程同步是一种控制多个线程对共享资源的访问,以确保线程安全的方法。下面通过一个示例代码来详细解释线程同步的使用。
假设有一个银行账户类BankAccount,其中包含一个共享资源(账户余额),需要多个线程进行操作(存款和取款)。为了确保线程安全,我们需要对共享资源进行同步控制。
public class BankAccount { private int balance; public BankAccount(int initialBalance) { this.balance = initialBalance; } // 存款方法 public synchronized void deposit(int amount) { this.balance += amount; System.out.println(Thread.currentThread().getName() + " 存款成功,当前余额:" + this.balance); } // 取款方法 public synchronized void withdraw(int amount) { if (this.balance >= amount) { this.balance -= amount; System.out.println(Thread.currentThread().getName() + " 取款成功,当前余额:" + this.balance); } else { System.out.println(Thread.currentThread().getName() + " 取款失败,余额不足"); } } }
在上述示例代码中,我们通过在deposit()和withdraw()方法上添加synchronized关键字来实现线程同步。这意味着在同一时刻,只有一个线程可以执行这两个方法中的任意一个。
接下来,我们创建一个测试类BankAccountTest,用于模拟多个线程对账户进行并发操作。
public class BankAccountTest { public static void main(String[] args) { BankAccount account = new BankAccount(1000); Thread t1 = new Thread(() -> account.deposit(500)); Thread t2 = new Thread(() -> account.withdraw(200)); Thread t3 = new Thread(() -> account.withdraw(300)); t1.start(); t2.start(); t3.start(); } }
在这个测试类中,我们创建了一个BankAccount对象,并启动了三个线程,分别执行存款、取款操作。由于我们在线程同步中使用了synchronized关键字,因此这些操作将会按顺序执行,确保了线程安全。
三、等待/通知机制
Java中的等待/通知机制是线程间通信的一种方式,通过调用Object类的wait()、notify()和notifyAll()方法来实现。下面是一个使用这些方法进行线程同步的示例代码,并对其进行详细解释
public class BoundedBuffer { private final Object lock = new Object(); private final int capacity; private int count = 0; private int in = 0, out = 0; private final Object[] buffer; public BoundedBuffer(int capacity) { this.capacity = capacity; this.buffer = new Object[capacity]; } // 生产者放入元素 public void put(Object item) throws InterruptedException { synchronized (lock) { while (count == capacity) { // 如果缓冲区已满 lock.wait(); // 生产者线程进入等待状态 } buffer[in] = item; // 将元素放入缓冲区 in = (in + 1) % capacity; // 更新入指针 count++; // 增加计数 lock.notifyAll(); // 唤醒所有等待的消费者线程 } } // 消费者取出元素 public Object take() throws InterruptedException { synchronized (lock) { while (count == 0) { // 如果缓冲区为空 lock.wait(); // 消费者线程进入等待状态 } Object item = buffer[out]; // 取出缓冲区中的元素 buffer[out] = null; // 清除缓冲区位置 out = (out + 1) % capacity; // 更新出指针 count--; // 减少计数 lock.notifyAll(); // 唤醒所有等待的生产者线程 return item; } } } // 使用示例: public class Main { public static void main(String[] args) { BoundedBuffer buffer = new BoundedBuffer(3); Thread producer = new Thread(() -> { for (int i = 0; i { for (int i = 0; i
详细解释:
在这个BoundedBuffer类中,我们模拟了一个容量有限的缓冲区,它可以由多个生产者线程向其中添加元素,同时也可以由多个消费者线程从中移除元素。
- put()方法用于将元素放入缓冲区,当缓冲区已满时,生产者线程会调用lock.wait(),使自己进入等待状态,释放锁。
- 当消费者从缓冲区取走一个元素后,在take()方法中会调用lock.notifyAll()唤醒所有等待在该锁上的生产者线程,使其重新尝试执行操作。
反之,当缓冲区为空时,消费者线程会在take()方法中调用lock.wait()进入等待状态,而生产者线程在添加新元素后则会调用lock.notifyAll()唤醒所有等待的消费者线程。
这样就实现了生产者线程和消费者线程之间的协调工作,确保了多线程环境下的正确数据交换,避免了竞态条件和死锁等问题。
四、并发工具类
Java并发工具类库java.util.concurrent(JUC)提供了一系列高效的线程同步和并发控制工具,下面列举几个常用的并发工具类并给出示例代码及详细解释:
1. CountDownLatch**作用:**允许一个或多个线程等待其他线程完成操作。
示例代码:
import java.util.concurrent.CountDownLatch; public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { int threadCount = 5; final CountDownLatch startSignal = new CountDownLatch(1); final CountDownLatch doneSignal = new CountDownLatch(threadCount); for (int i = 0; i
详细解释:
- CountDownLatch的构造函数接收一个整数值作为参数,表示需要计数到零的次数。
- 当通过countDown()方法减少计数到0时,那些在await()上等待的线程将被释放,可以继续执行。
2. Semaphore
**作用:**限制同时访问特定资源的线程数量。
示例代码:
import java.util.concurrent.Semaphore; public class SemaphoreExample { private static final Semaphore semaphore = new Semaphore(3); // 只允许3个线程同时访问资源 public static void main(String[] args) { for (int i = 0; i { try { semaphore.acquire(); // 获取许可 accessResource(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); // 释放许可 } }).start(); } } private static void accessResource() { System.out.println(Thread.currentThread().getName() + " 正在访问共享资源..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 访问结束."); } }
详细解释:
- Semaphore对象用于维护一组许可证,通过acquire()获取许可证,如果没有可用的许可证则会阻塞当前线程。
- 当线程完成对共享资源的访问时,通过release()方法归还许可证,这样其他等待的线程就可以继续执行。
3. CyclicBarrier
**作用:**多个线程到达某个屏障点时会被阻塞,直到最后一个线程到达,然后所有线程才会一起继续执行。
示例代码:
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierExample { private static final int THREAD_COUNT = 5; private static final CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT); public static void main(String[] args) { for (int i = 0; i { try { processTask(); barrier.await(); // 等待所有线程都到达障碍点 System.out.println("所有线程都已经到达,现在一起继续执行"); } catch (BrokenBarrierException | InterruptedException e) { e.printStackTrace(); } }).start(); } } private static void processTask() { System.out.println(Thread.currentThread().getName() + " 开始处理任务..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 完成任务..."); } }
详细解释:
- CyclicBarrier的构造函数接收一个整数参数,表示需要等待的线程数。
- 当每个线程执行完自己的任务后,调用await()方法,这时线程将会阻塞,直到所有线程都调用了await()方法。当所有线程都到达这个障碍点时,它们将被同时释放,可以继续后续的操作
五、原子类
在Java并发编程中,原子类(Atomic Classes)是位于java.util.concurrent.atomic包下的一个重要的工具集,它们提供了对基本数据类型和引用类型的原子操作。这些类的实例可以在高并发环境下保证线程安全地更新变量值,而不需要使用synchronized关键字或锁。
以下是一些常用的原子类及其示例:
1.AtomicIntegerimport java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerExample { private AtomicInteger counter = new AtomicInteger(0); public void incrementCounter() { // 使用原子操作自增 counter.incrementAndGet(); } public int getCounter() { return counter.get(); } public static void main(String[] args) throws InterruptedException { AtomicIntegerExample example = new AtomicIntegerExample(); Thread t1 = new Thread(() -> { for (int i = 0; i { for (int i = 0; i
详细解释:
- AtomicInteger提供了一种线程安全的方式来增加或减少整数值。
- 在上述示例中,我们创建了一个AtomicInteger对象作为计数器,并启动了两个线程分别对其增加1000次。
- 即使两个线程同时尝试修改这个共享的计数器,由于incrementAndGet()方法内部实现了CAS(Compare and Swap)算法,确保了即使在多线程环境下,计数器的值也能准确无误地增加2000,不会出现线程安全问题。
2.AtomicReference
import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceExample { private AtomicReference valueHolder = new AtomicReference("初始值"); public void updateValue(String newValue) { // 使用原子方式设置新值 valueHolder.set(newValue); } public String getValue() { return valueHolder.get(); } public static void main(String[] args) { AtomicReferenceExample example = new AtomicReferenceExample(); example.updateValue("新的值"); System.out.println("获取到的最新值为: " + example.getValue()); } }
详细解释:
- AtomicReference用于存储并原子地更新引用类型(如对象引用)的值。
- 在这个例子中,我们创建了一个AtomicReference来保存字符串值,并通过set()方法在多线程环境下安全地更新它的值。
六、线程局部变量
Java中的线程局部变量是通过java.lang.ThreadLocal类来实现的。它为每个线程创建一个单独的变量副本,使得在并发执行时,每个线程可以拥有独立的变量值,从而避免了线程间共享资源带来的同步问题。
示例代码:import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadLocalExample { // 创建一个ThreadLocal实例 public static final ThreadLocal THREAD_LOCAL = new ThreadLocal(); public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); // 提交任务到线程池 for (int i = 0; i { // 设置当前线程对应的ThreadLocal变量 THREAD_LOCAL.set("Value from Thread " + threadId); System.out.println("In Thread " + Thread.currentThread().getName() + ": " + THREAD_LOCAL.get()); // 执行一些操作... // 清除当前线程对应的ThreadLocal变量(可选) THREAD_LOCAL.remove(); }); } // 关闭线程池 executor.shutdown(); } }
详细解释:
- ThreadLocal是一个泛型类,其声明和使用方式如上所示。
- 在main方法中,我们创建了一个固定大小的线程池,并提交了两个任务。
- 每个任务内部调用THREAD_LOCAL.set()方法设置线程局部变量的值,这里将线程ID作为变量值的一部分。
- 当我们在任务内部获取这个变量值时,即使两个线程同时运行,由于每个线程都有自己的ThreadLocal副本,所以它们不会互相影响,输出各自的线程ID相关的值。
- 使用完ThreadLocal后,可以通过调用remove()方法删除当前线程关联的变量,以防止内存泄漏。如果线程结束后不再需要该变量,这是个好习惯。
这样,每个线程都有自己独立的ThreadLocal存储空间,当在一个线程中修改ThreadLocal的值时,其他线程的ThreadLocal值不受影响,这在处理每个线程特有的状态信息、或者在多线程环境中需要隔离数据的情况下非常有用。例如,在Web应用中,ThreadLocal经常用于存储与请求相关的会话信息,确保不同请求之间的数据互不干扰。
七、线程状态管理
Java线程有五种基本状态,分别是:新建(New)、运行(Runnable)、阻塞(Blocked)、等待(Waiting)和终止(Terminated)。下面通过示例代码来说明如何管理线程状态以及状态之间的转换。
1.线程状态转换 - Runnable到Blockedimport java.util.concurrent.locks.LockSupport; public class ThreadStateExample { public static void main(String[] args) { Thread t = new Thread(() -> { System.out.println("线程开始执行"); // 模拟进入Blocked状态,调用park方法让线程挂起 LockSupport.park(); System.out.println("线程恢复执行"); }); t.start(); try { // 线程启动后,主线程休眠2秒,确保t线程有机会开始执行并被挂起 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 唤醒t线程 LockSupport.unpark(t); try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
详细解释:
- 在这个例子中,我们创建了一个新的线程t。当线程开始执行时,它调用了LockSupport.park()方法,这使得线程进入了Blocked状态,即线程在等待某个条件变为真或获得某种资源之前无法继续执行。然后主线程通过LockSupport.unpark(t)唤醒了这个被挂起的线程,使其重新回到Runnable状态,并最终完成执行。
2.线程状态转换 - Runnable到Waitingimport java.util.concurrent.TimeUnit; public class ThreadStateExample2 { private final Object lock = new Object(); public static void main(String[] args) { ThreadStateExample2 example = new ThreadStateExample2(); Thread t = new Thread(example::waitMethod); t.start(); try { TimeUnit.SECONDS.sleep(1); // 主线程等待1秒 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (example.lock) { // 通知等待的线程 example.lock.notifyAll(); } } public void waitMethod() { synchronized (lock) { System.out.println("线程进入等待状态"); try { lock.wait(); // 进入Waiting状态 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程从等待状态恢复"); } } }
详细解释:
- 在本例中,我们创建了一个线程t,它在其run方法内调用lock.wait()进入Waiting状态。这意味着线程会释放锁并暂停执行,直到其他线程在同一把锁上调用notifyAll()或者notify()方法。在主线程中,我们在一定延迟后调用notifyAll()方法,将等待中的线程唤醒,使其从Waiting状态转换回Runnable状态。
八、线程调度策略
在Java中,线程调度主要由Java虚拟机(JVM)和底层操作系统共同完成。Java提供了两种不同的优先级设置来影响线程调度,但请注意,尽管设置了线程优先级,具体的调度策略仍然很大程度上取决于操作系统的实现。
示例代码:线程优先级public class ThreadPriorityExample { public static void main(String[] args) { Thread thread1 = new Thread(() -> { System.out.println("Thread 1 started, priority: " + Thread.currentThread().getPriority()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 1 finished"); }); // 设置线程优先级,范围是1-10,默认为5 thread1.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级 Thread thread2 = new Thread(() -> { System.out.println("Thread 2 started, priority: " + Thread.currentThread().getPriority()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 2 finished"); }); thread2.start(); thread1.start(); } }
详细解释: 在这个示例中,我们创建了两个线程thread1和thread2。其中thread1的优先级被设置为最大值(Thread.MAX_PRIORITY),这意味着在所有条件相同的情况下,操作系统将更倾向于调度优先级高的线程执行。
然而,实际的调度结果可能会受到很多因素的影响,包括但不限于:
- 时间片轮转:操作系统通常使用时间片轮转的方式进行线程调度,即使高优先级线程也可能需要等待其时间片。
- 抢占式调度:某些情况下,高优先级线程可以在低优先级线程正在运行时被抢占并获得CPU资源。
- 线程状态:线程必须处于可运行状态(Runnable)才能被调度,阻塞或等待状态的线程不会参与调度。
- 操作系统的调度策略:Java的线程优先级映射到操作系统的优先级可能并不直接对应,且不同操作系统的调度机制各异。
因此,虽然可以设置线程优先级,但在编写多线程程序时,应尽量避免依赖于优先级来进行同步控制,而是更多地依靠并发工具类如synchronized、volatile关键字、java.util.concurrent包下的工具类等来进行正确的并发控制和通信。
九、死锁检测与避免
死锁是指两个或多个线程相互等待对方持有的资源而造成的僵局。在Java多线程并发编程中,死锁是一个需要特别注意的问题。下面通过一个示例来演示死锁的发生以及如何检测和避免。
示例代码:死锁的演示public class DeadlockExample { static Object resource1 = new Object(); static Object resource2 = new Object(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { synchronized (resource1) { System.out.println("Thread 1: Acquired resource 1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resource2) { System.out.println("Thread 1: Acquired resource 2"); } } }); Thread thread2 = new Thread(() -> { synchronized (resource2) { System.out.println("Thread 2: Acquired resource 2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resource1) { System.out.println("Thread 2: Acquired resource 1"); } } }); thread1.start(); thread2.start(); } }
详细解释:
在这个示例中,我们定义了两个线程thread1和thread2,它们分别试图获取两个资源resource1和resource2的锁。线程thread1首先尝试获取resource1的锁,然后获取resource2的锁;而线程thread2的顺序相反。如果线程thread1在获取resource1的锁之后,线程thread2立即获取了resource2的锁,然后线程thread1尝试获取resource2的锁时将被阻塞,线程thread2尝试获取resource1的锁时也将被阻塞,这时就形成了一个死锁。
死锁的检测Java提供了一个工具来检测死锁,即jstack命令。通过运行jstack命令并指定进程ID,我们可以获得线程的堆栈信息,从而查看是否有线程处于死锁状态。
死锁的避免
以下是一些常见的策略来避免死锁的发生:
- 资源的有序获取:确保所有线程按照相同的顺序获取资源锁,可以避免死锁的发生。
- 设置超时:在尝试获取锁时设置一个超时时间,如果在指定时间内无法获取锁,则线程放弃并重试或执行其他操作。
- 锁的粒度控制:尽量使用细粒度的锁,而不是大粒度的锁,以减少锁竞争和死锁的可能性。
- 避免嵌套锁:尽量避免使用嵌套锁,因为这可能导致锁的获取顺序变得复杂,从而增加死锁的风险。
- 检测锁的依赖关系:在设计多线程程序时,通过分析锁的依赖关系图,可以提前发现潜在的死锁风险
十、非阻塞同步机制
在Java中,非阻塞同步机制主要包括原子变量(Atomic Variables)和循环CAS(Compare and Swap)等技术。其中,java.util.concurrent.atomic包下的原子类就是实现非阻塞同步的一种重要手段。
示例代码:使用AtomicInteger进行非阻塞同步import java.util.concurrent.atomic.AtomicInteger; public class NonBlockingSyncExample { // 使用AtomicInteger作为共享计数器 private final AtomicInteger counter = new AtomicInteger(0); public void increment() { // 使用compareAndSet方法实现原子性的递增操作 while (true) { int current = counter.get(); int next = current + 1; // 如果当前值等于预期的旧值,则更新为新值 if (counter.compareAndSet(current, next)) { break; // 更新成功,退出循环 } } } public static void main(String[] args) { NonBlockingSyncExample example = new NonBlockingSyncExample(); Thread t1 = new Thread(example::increment); Thread t2 = new Thread(example::increment); Thread t3 = new Thread(example::increment); t1.start(); t2.start(); t3.start(); // 等待所有线程完成任务 try { t1.join(); t2.join(); t3.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最终计数值: " + example.counter.get()); } }
详细解释:
- 在上述示例中,我们使用了AtomicInteger来代替传统的整型变量,并通过其提供的compareAndSet方法实现了线程安全的递增操作。
- compareAndSet方法是基于CAS算法的,它会比较当前值是否与预期的旧值相等,如果相等则将值设置为新的值。这个过程是一个原子操作,意味着在多线程环境下不会出现数据竞争的问题。
- 当多个线程同时调用increment方法时,每个线程都会尝试去更新计数器的值。由于compareAndSet方法的特性,只有一个线程能成功地更新计数器,其他线程在发现当前值已改变后会继续下一轮循环尝试。
- 这种非阻塞同步机制的优点在于避免了传统锁机制带来的上下文切换和线程阻塞,从而提高了并发性能。
总结
Java多线程并发编程是一项需要深入理解和熟练掌握的技术,只有合理地设计和使用多线程,才能真正发挥其在提升系统性能方面的巨大潜力。随着对Java并发机制及工具类库的深入学习和应用,开发者可以更好地构建高效、稳定的并发系统。
- 在本例中,我们创建了一个线程t,它在其run方法内调用lock.wait()进入Waiting状态。这意味着线程会释放锁并暂停执行,直到其他线程在同一把锁上调用notifyAll()或者notify()方法。在主线程中,我们在一定延迟后调用notifyAll()方法,将等待中的线程唤醒,使其从Waiting状态转换回Runnable状态。
- 在这个例子中,我们创建了一个新的线程t。当线程开始执行时,它调用了LockSupport.park()方法,这使得线程进入了Blocked状态,即线程在等待某个条件变为真或获得某种资源之前无法继续执行。然后主线程通过LockSupport.unpark(t)唤醒了这个被挂起的线程,使其重新回到Runnable状态,并最终完成执行。