本文主要介绍了Java中的并发编程模型和常用工具类,首先阐述了并发编程的概念及其重要性,然后详细介绍了线程的基本概念、生命周期和状态转换、同步与互斥、死锁问题以及线程池的使用和实现原理。接着介绍了synchronized关键字和Lock接口的使用、原子变量和原子操作类的使用、Condition接口和ReentrantLock类的使用、CountDownLatch类和CyclicBarrier类的使用、Semaphore类和Exchanger类的使用。最后,提出了并发编程的性能优化和注意事项。
并发编程指的是在多核心、多线程、多任务的操作系统中,同时执行多个任务和线程。在计算机领域中,如今大多数系统都支持并发编程,因为并发编程可以大大提高系统的吞吐量和响应速度,提升系统的性能和可用性。Java作为一门面向对象的编程语言,也提供了一套完善的并发编程模型和工具类库,为Java开发者提供了便捷的并发编程解决方案。
Java中的并发编程模型基于线程和锁机制。Java中的线程是轻量级的进程,每个线程都有自己的执行路径,可以独立执行任务。Java中的锁机制可以保证多个线程之间的同步和互斥,避免资源竞争和冲突。Java中的线程和锁机制是Java并发编程的基础。
进程和线程是计算机操作系统中的两个基本概念,它们都可以执行任务和程序,但是在实现方式、资源占用、通信方式等方面有所不同。
1. 进程
进程是计算机中执行任务的基本单位,它是操作系统中的一个独立的运行实例。每个进程都有自己的独立地址空间、代码段、数据段、堆栈、文件描述符等系统资源。进程之间相互独立,互不干扰。进程是资源分配的最小单位,它可以分配和使用计算机中的资源,如CPU、内存、文件、网络等。
2. 线程
线程是进程中的一个执行单元,它是操作系统中调度的最小单位。一个进程中可以包含多个线程,这些线程共享进程的地址空间和系统资源,如文件、网络等。线程之间可以通过共享内存的方式进行通信,但是也可能出现竞争和冲突的情况。线程是轻量级的进程,创建和销毁线程的开销相对较小,因此可以更加高效地利用计算机的资源。
3. 进程和线程的区别
进程和线程的主要区别在于资源占用、调度和通信方式等方面。进程是资源分配的最小单位,它可以分配和使用计算机中的资源,但是进程之间的通信需要通过IPC(Inter-Process Communication)机制,开销相对较大。线程是轻量级的进程,它共享进程的地址空间和系统资源,因此线程之间的通信和调度开销相对较小。但是线程之间可能出现竞争和冲突的情况,需要进行同步和互斥操作。
线程的生命周期包括五种状态:新建状态、就绪状态、运行状态、阻塞状态和死亡状态。这些状态可以通过不同的方法进行转换,下面是每个状态的含义、转换条件:
参考文章:https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.State.html
线程同步是指多个线程在共享数据时按照一定的规则进行访问和操作,以避免数据的混乱和错误。线程互斥是指多个线程在对共享数据进行访问时,通过一些机制防止多个线程同时对数据进行操作,从而避免数据的冲突和错误。
在Java中,线程同步和互斥是通过锁机制实现的。锁是一个标识,用来保护共享资源,只有持有锁的线程才能访问共享资源。Java提供了两种锁机制:synchronized关键字和Lock接口。
synchronized关键字是Java中最基本的锁机制,它是一种隐式锁,只需要在方法或者代码块前加上synchronized关键字即可实现同步和互斥。synchronized关键字的作用是确保同一时间只有一个线程可以进入同步代码块或方法,并且在执行完同步代码块或方法后会自动释放锁。
Lock接口是Java中的另一种锁机制,它是一种显示锁,需要手动加锁和释放锁。Lock接口提供了更加灵活和精细的锁控制,比如可以设置超时时间、多个条件变量等。
线程同步和互斥可以有效避免多个线程对共享数据的冲突和错误,保证程序的正确性和稳定性。但是如果同步和互斥使用不当,也会带来一定的性能问题,因此需要在使用时考虑好锁的粒度、锁的持有时间和锁的竞争情况等因素。
线程死锁是指在多线程并发的程序中,两个或多个线程因为相互等待对方释放资源而陷入一种无限等待的状态,从而导致程序无法继续执行的问题。简单来说,就是两个或多个线程互相持有对方需要的资源,并且都在等待对方释放资源,从而导致程序无法继续执行。
下面是一个简单的死锁示例:
public class DeadlockExample {
private static Object resource1 = new Object();
private static Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Holding resource 1...");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for resource 2...");
synchronized (resource2) {
System.out.println("Thread 1: Holding resource 1 and 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 2...");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for resource 1...");
synchronized (resource1) {
System.out.println("Thread 2: Holding resource 1 and 2...");
}
}
});
thread1.start();
thread2.start();
}
}
在这个例子中,有两个线程分别持有resource1和resource2两个资源,并且都在等待对方释放资源。如果运行这个程序,就会陷入死锁状态,程序无法继续执行。
为了避免死锁问题,我们需要遵循以下原则:
synchronized关键字是Java中最基本的同步机制,可以用于实现线程的同步和互斥。synchronized关键字可以应用于方法和代码块两种形式。
synchronized方法可以用于保证同一时刻只有一个线程可以访问该方法。当一个线程访问synchronized方法时,其他线程必须等待该线程执行完毕后才能访问该方法。
public synchronized void synchronizedMethod() {
// do something
}
synchronized代码块可以用于保证同一时刻只有一个线程可以访问该代码块。synchronized代码块需要指定一个锁对象,只有获取该锁对象的线程才能访问该代码块。
Object lock = new Object();
public void synchronizedBlock() {
synchronized(lock) {
// do something
}
}
Lock接口是Java中提供的另一种同步机制,相比于synchronized关键字,Lock接口具有更加灵活的控制能力。Lock接口定义了加锁和释放锁的方法,可以手动控制线程的同步和互斥。
Lock接口定义了两个核心方法:lock()和unlock()。lock()方法用于加锁,只有获取锁的线程才能进入临界区执行代码。unlock()方法用于释放锁,使得其他线程可以获取该锁。
Lock lock = new ReentrantLock();
public void lockMethod() {
lock.lock();
try {
// do something
} finally {
lock.unlock();
}
}
与synchronized关键字不同,Lock接口可以支持锁的重入。当一个线程已经获取了锁,并且在临界区内嵌套了另一个加锁操作时,该线程仍然可以正常执行。
Lock lock = new ReentrantLock();
public void reentrantLock() {
lock.lock();
try {
// do something
lock.lock();
try {
// do something
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}
Java提供了一些原子变量类型,例如AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等,可以直接在多线程环境下使用,保证操作的原子性。
下面是AtomicInteger的使用方式:
AtomicInteger count = new AtomicInteger(0);
count.getAndIncrement(); // 原子地增加计数器
需要注意的是,虽然原子变量能够保证操作的原子性,但并不能保证线程安全,因此需要考虑其他的同步机制。
除了原子变量类型之外,Java还提供了一些原子操作类,例如AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray等,可以对数组中的元素进行原子操作。
下面是AtomicIntegerArray的使用方式:
AtomicIntegerArray array = new AtomicIntegerArray(10);
array.getAndIncrement(0); // 原子地对数组元素增加1
需要注意的是,原子操作类的使用方式和原子变量类型类似,同样需要考虑其他的同步机制。
总的来说,原子变量和原子操作类是Java提供的保证操作原子性的机制,可以有效避免竞态条件问题,提高多线程程序的性能和正确性。但需要注意的是,它们并不能完全保证线程安全,仍然需要考虑其他的同步机制。
ReentrantLock类是一个可重入的互斥锁,它提供了与synchronized关键字类似的功能,但相比synchronized关键字更灵活,能够实现更加复杂的锁定操作。同时,ReentrantLock类还提供了一些高级功能,例如Condition接口,能够实现更加灵活的线程间通信。
ReentrantLock类的基本用法与synchronized关键字类似,可以用来实现互斥锁,保护共享资源。
下面是ReentrantLock的基本使用方式:
// 定义一个ReentrantLock对象
ReentrantLock lock = new ReentrantLock();
// 获取锁
lock.lock();
try {
// 访问共享资源
} finally {
// 释放锁
lock.unlock();
}
需要注意的是,和synchronized关键字一样,ReentrantLock也需要在finally块中释放锁,以确保能够释放锁资源。
Condition接口是ReentrantLock的一个高级功能,能够实现更加灵活的线程间通信。Condition接口可以用来实现等待/通知模式,使得线程能够更加精确地控制等待和唤醒的条件。
下面是Condition接口的基本使用方式:
// 定义一个Condition对象
Condition condition = lock.newCondition();
// 等待条件
condition.await();
// 唤醒等待条件的线程
condition.signal();
需要注意的是,Condition接口的等待和唤醒操作必须在ReentrantLock的锁保护下进行,否则会抛出IllegalMonitorStateException异常。
下面是一个使用ReentrantLock和Condition接口实现生产者消费者模式的示例代码:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerExample {
private static final int MAX_CAPACITY = 10;
private ReentrantLock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
private int count = 0;
public void produce() throws InterruptedException {
lock.lock();
try {
while (count == MAX_CAPACITY) {
notFull.await();
}
count++;
System.out.println("Produced, count = " + count);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
count--;
System.out.println("Consumed, count = " + count);
notFull.signal();
} finally {
lock.unlock();
}
}
}
CountDownLatch是一个计数器,它的作用是允许一个或多个线程等待一组事件的完成。CountDownLatch有一个计数器,计数器的初始值为一个正整数,每当一个线程完成了一个事件,计数器的值就减一,当计数器的值为0时,表示所有事件都已经完成,此时所有等待该事件的线程就可以继续执行。
使用CountDownLatch的步骤如下:
(1)创建一个CountDownLatch对象,并指定计数器的初始值。
(2)在等待事件的线程中,调用CountDownLatch对象的await()方法进行等待,直到计数器的值变为0。
(3)在完成事件的线程中,完成事件后调用CountDownLatch对象的countDown()方法,计数器的值减一。
示例代码如下:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(3); // 3个事件需要等待
new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("事件1完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 计数器减一
}
}).start();
new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("事件2完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 计数器减一
}
}).start();
new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println("事件3完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 计数器减一
}
}).start();
try {
latch.await(); // 等待所有事件完成
System.out.println("所有事件已完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
下面是一个简单的示例,演示如何使用CyclicBarrier:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
int n = 3;
CyclicBarrier barrier = new CyclicBarrier(n, () -> {
System.out.println("所有线程已到达屏障点,开始执行任务!");
});
for (int i = 0; i < n; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 到达屏障点");
barrier.await();
System.out.println(Thread.currentThread().getName() + " 继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
在上面的示例中,我们创建了一个CyclicBarrier对象,指定了屏障点的数量为3,并且指定了所有线程到达屏障点后要执行的任务。然后我们创建了3个线程,每个线程在到达屏障点后调用了await()方法等待其他线程到达屏障点。当3个线程都到达屏障点后,CyclicBarrier会执行指定的任务,然后释放所有等待的线程继续执行。
Semaphore类是一个计数信号量,用于控制同时访问某个资源的线程数量。Semaphore维护了一个可配置的许可证数量,线程可以通过acquire()方法获取许可证,release()方法释放许可证。当许可证被全部占用时,后续线程需要等待其他线程释放许可证后才能获取到许可证。
Semaphore类的常用方法:
Semaphore类的使用示例:
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
private static Semaphore semaphore = new Semaphore(10); // 设置最多允许10个线程同时访问
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
threadPool.execute(() -> {
try {
semaphore.acquire(); // 获取许可证
System.out.println(Thread.currentThread().getName() + "获取到许可证,开始执行任务");
Thread.sleep(5000); // 模拟任务执行时间
System.out.println(Thread.currentThread().getName() + "执行任务完毕,释放许可证");
semaphore.release(); // 释放许可证
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
}
Exchanger是Java中的一个线程同步工具,它允许两个线程在同一个时刻交换数据。每个线程通过调用exchange()方法来向对方交换数据,当两个线程都调用了该方法后,它们会交换数据并继续执行。
Exchanger的主要方法是exchange()方法,它有两个重载的版本:
public V exchange(V x) throws InterruptedException;
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException;
其中,第一个方法将指定的数据对象与另一个线程交换,如果另一个线程在同一时刻调用了exchange()方法,则它的数据对象也会被返回。如果另一个线程还没有调用exchange()方法,则当前线程会阻塞等待,直到另一个线程调用exchange()方法为止。
第二个方法与第一个方法类似,但是增加了一个超时参数。如果在指定的超时时间内没有另一个线程调用了exchange()方法,则当前线程会抛出TimeoutException异常。
下面是一个简单的示例程序,展示了如何使用Exchanger来交换两个线程之间的数据:
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger exchanger = new Exchanger<>();
new Thread(() -> {
String data1 = "Hello";
System.out.println("Thread1: send " + data1);
try {
String data2 = exchanger.exchange(data1);
System.out.println("Thread1: received " + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
String data1 = "World";
System.out.println("Thread2: send " + data1);
try {
String data2 = exchanger.exchange(data1);
System.out.println("Thread2: received " + data2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}