Java并发编程是指在多个线程同时执行的情况下,协调和管理这些线程的过程。在现代计算机系统中,使用多线程并发编程可以显著提高应用程序的性能和响应速度。Java作为一门流行的编程语言,具有强大的并发编程能力。
本文将介绍Java并发编程的基本原理和实践技巧,帮助读者更好地掌握这一领域的知识。
🏅 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
目录
一、Java并发编程的基本原理1.1 线程和进程1.2 同步和互斥1.3 线程间的通信1.3.1 wait()/notify()/notifyAll()方法1.3.2 Condition接口1.3.3 CountDownLatch类1.3.4 CyclicBarrier类1.3.5 Semaphore类 1.4 线程池 二、Java并发编程的实践技巧2.1 使用volatile关键字2.1.1 使用 volatile 关键字来保证多线程间的可见性示例代码 2.2 使用synchronized关键字和锁机制2.2.1 synchronized关键字和锁机制实现线程同步的示例代码 2.3 使用Atomic类2.3.1 Atomic类实现线程同步的示例代码 2.4 使用线程池2.4.1 Java线程池实现多线程的示例代码 2.5 避免死锁2.5.1 如何避免死锁示例代码 三、使用ThreadLocal类3.1 使用Concurrent包3.1.1 代码示例 3.2 使用信号量和倒计时门闩(chuan)3.1.2 Java中使用信号量和倒计时门闩的示例代码3.1.2.2 信号量代码示例3.1.2.2 倒计时门闩代码示例 四、总结一、Java并发编程的基本原理
1.1 线程和进程
线程是一种轻量级的执行单元,它可以在进程中创建和撤销,且可以与同一进程中的其他线程共享数据和资源。Java程序的所有线程都是运行在进程中。每一个Java线程都有其自己的执行堆栈和指令指针。当一个线程调用一个方法时,该方法的参数值、局部变量和返回值都保存在执行堆栈中。线程之间可以通过共享内存来通信。进程是程序在执行时占用资源和运行的基本单位,一个进程可以包含多个线程。每个进程都有一个程序计数器(PC)、内存空间、文件资源和系统信号。Java程序的每个线程都在一个进程中执行。进程是操作系统中的一个概念,它可以分配CPU时间、内存空间、文件资源和系统信号。在操作系统中,每个进程都有唯一的进程标识符(PID),用来标识该进程。Java程序通过Java虚拟机(JVM)运行,每个Java程序都有一个独立的JVM实例作为进程。在JVM中运行的Java线程共享该JVM的内存区域,从而实现了线程之间共享数据的目的。在Java中,可以使用ProcessBuilder类创建子进程,并通过标准输入/输出流进行通信。子进程可以通过exitValue()方法获取其执行状态,以判断是否执行成功。1.2 同步和互斥
1. 同步
同步是指多个线程在访问共享资源时,按照一定的顺序和时间互相通知,以保证数据同步和正确性。在Java中,同步可以通过以下机制来实现:
synchronized关键字:用于控制对共享资源的访问,从而避免多个线程同时访问同一个共享资源。wait()/notify()方法:用于线程间的通信,当一个线程等待某个条件时,可以调用wait()方法阻塞自己,当另一个线程满足条件时,可以调用notify()方法唤醒等待线程继续执行。Lock/Condition机制:与synchronized关键字类似,用于控制对共享资源的访问,但更加灵活和高效。
同步机制能够保证多个线程访问共享资源时的正确性、安全性和一致性,但会影响并发性能,降低程序的效率。
2. 互斥
互斥是指多个线程在访问共享资源时,使用某种机制来防止共享资源被多个线程同时访问和修改。在Java中,互斥可以通过以下机制来实现:
synchronized关键字:同步机制中的synchronized关键字也能够实现互斥,当一个线程进入synchronized块时,会对共享资源加锁,防止其他线程同时访问。Lock接口:Lock接口提供了与synchronized关键字类似的锁定机制,但更加灵活,支持公平锁和非公平锁、可重入锁、带条件的锁等。
互斥机制能够保证多个线程访问共享资源时的排它性,避免竞争和冲突,提高程序的可靠性和安全性。但如果使用不当,互斥机制也可能导致死锁和饥饿等问题。
总之,同步和互斥是Java并发编程的基本原理之一,能够有效地保证程序的正确性、安全性和一致性。Java提供了多种机制来实现同步和互斥,可以根据具体场景选择最适合的机制来实现。
1.3 线程间的通信
Java提供了多种机制来实现线程间的通信,包括wait/notify机制、join方法、volatile关键字等。wait/notify机制可以让一个线程等待另一个线程的通知,join方法可以让一个线程等待另一个线程的结束,volatile关键字可以保证多个线程之间共享变量的可见性。
1.3.1 wait()/notify()/notifyAll()方法
这三个方法既可以用于同步机制中,也可以用于线程间通信。wait方法使当前线程进入阻塞状态,并释放同步锁,直到其他线程调用notify或notifyAll方法来唤醒它。notify方法随机唤醒一个等待该对象锁的线程,而notifyAll方法会唤醒所有等待该对象锁的线程。使用wait()/notify()/notifyAll()时,需要注意以下几点:
必须在同步代码块或同步方法中调用wait()/notify()/notifyAll()方法;必须在获取对象锁后调用wait()方法;必须在notify()/notifyAll()方法调用前释放对象锁。
1.3.2 Condition接口
Condition接口是JDK 5.0引入的新特性,提供了类似wait()/notify()的机制,但更加高级和灵活。在Condition中,线程可以等待指定的条件变量并等待通知,以实现线程间的通信。Condition中包含了await()、signal()、signalAll()方法。
1.3.3 CountDownLatch类
CountDownLatch计数器是Java并发包中提供的一个同步工具,它允许在一个或多个线程中等待一组事件发生。CountDownLatch类包含一组await()和countDown()方法。await()方法会导致当前线程等待,直到计数器的值为0,而countDown()方法会减少计数器的值。
1.3.4 CyclicBarrier类
CyclicBarrier类提供了一种同步机制,允许一组线程等待彼此到达某个公共障碍点。它包含await()和reset()方法,await()方法会使当前线程等待,直到其他线程的到达,而reset()方法会重置障碍的计数器。
1.3.5 Semaphore类
Semaphore类是一种基于计数的信号量,用于控制多个线程对共享资源的访问。它包含acquire()和release()方法,acquire()方法会尝试获取信号量的许可证,如果许可证不可用则阻塞线程,而release()方法会释放信号量的许可证。
1.4 线程池
线程池是Java并发编程中一种重要的技术,它可以提高线程的使用效率、减少创建和销毁线程的开销,同时也能控制并发线程数量,从而避免线程资源的浪费和系统的负担。Java线程池的实现基于两种机制:ThreadPoolExecutor类和Executors工具类。
ThreadPoolExecutor类是Java中提供的原始的线程池实现方式,它包含了线程池的核心参数和方法,可以支持自定义线程池的各种配置。ThreadPoolExecutor的构造方法参数包括线程池的核心线程数、最大线程数、线程空闲时间、等待队列大小,还可以自定义拒绝策略等。它的主要方法包括execute()、shutdown()、shutdownNow()、awaitTermination()等。
Executors工具类是Java中提供的封装线程池的类集合,它提供了多种预定义的线程池,如newFixedThreadPool()、newCachedThreadPool()、newSingleThreadExecutor()等,可以根据不同的需求快速创建线程池,而无需关心核心参数的配置。这些方法的实现都是基于ThreadPoolExecutor的,同时提供了一些默认的配置参数和线程池特性。
Java线程池的优点:
提高程序性能:线程池可以重复利用已创建的线程,从而减少创建线程和销毁线程的开销。提高程序可靠性:线程池可以控制并发线程的数量,避免资源被浪费和服务器崩溃的风险。提高程序可读性:使用Java线程池可以方便地管理线程,降低程序员的耦合度。
Java线程池的缺点:
需要适当的配置:需要根据实际情况配置线程池的核心参数,如果配置不当,可能会影响整个系统的并发性能。可能会增加程序复杂度:线程池中可能存在线程安全问题,比如死锁和竞争条件等,需要程序员善于处理。
二、Java并发编程的实践技巧
2.1 使用volatile关键字
在多线程环境下,使用volatile关键字可以保证共享变量的可见性。volatile关键字可以防止指令重排,保证写操作的原子性,从而避免多线程操作时的数据冲突问题。
2.1.1 使用 volatile 关键字来保证多线程间的可见性示例代码
public class SharedObject {private volatile int count;public SharedObject(int initialCount) {this.count = initialCount;}public int getCount() {return count;}public void incrementCount() {count++;}}public class Main {public static void main(String[] args) {SharedObject sharedObject = new SharedObject(0);Thread thread1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {sharedObject.incrementCount();}});Thread thread2 = new Thread(() -> {while (sharedObject.getCount() < 10000) {// busy-waiting}System.out.println("Count has reached 10000");});thread1.start();thread2.start();}}
代码说明
这段代码展示了一个使用Java多线程的例子,其中包含一个线程安全的共享对象SharedObject和两个线程thread1和thread2。
SharedObject类有一个私有的volatile整型成员变量count,和三个公共方法。构造函数初始化count的值,getCount()方法返回当前count的值,incrementCount()方法使count的值加1。由于多个线程都可以访问count,volatile关键字确保count的可见性。
在Main类的main()方法中,创建了一个共享对象sharedObject,然后创建两个线程thread1和thread2。thread1执行incrementCount()方法将count的值递增10000次,而thread2执行busy-waiting等待count的值达到10000,一旦达到则打印出一条消息。
在多线程环境中,由于线程之间的执行顺序和时间不确定,可能会导致一些并发问题,例如竞争条件和死锁。在本例中,线程1和线程2都在访问和修改共享对象sharedObject的count变量,需要确保线程安全性。由于volatile关键字确保了可见性,但不保证原子性,因此可能会存在并发问题。
2.2 使用synchronized关键字和锁机制
synchronized关键字和锁机制可以保证多个线程之间的同步和互斥访问,避免数据冲突问题。在使用synchronized关键字时,需要注意锁的范围和粒度,避免锁定过多的代码。
2.2.1 synchronized关键字和锁机制实现线程同步的示例代码
public class SynchronizedDemo {private int num = 0;/*** 定义一个同步方法,用于加1操作*/public synchronized void increment() {num++;System.out.println(Thread.currentThread().getName() + "输出:" + num);// 通知另一个线程继续执行notifyAll();}/*** 定义一个同步方法,用于减1操作*/public synchronized void decrement() {while (num == 0) {// 只有当num不等于0时,才执行下面的代码try {// 当前线程进入等待状态wait();} catch (InterruptedException e) {e.printStackTrace();}}num--;System.out.println(Thread.currentThread().getName() + "输出:" + num);// 通知另一个线程继续执行notifyAll();}}public class SynchronizedTest {public static void main(String[] args) {SynchronizedDemo synchronizedDemo = new SynchronizedDemo();// 定义一个线程,用于执行递增操作new Thread(() -> {for (int i = 0; i < 5; i++) {synchronizedDemo.increment();}}, "线程1").start();// 定义一个线程,用于执行递减操作new Thread(() -> {for (int j = 0; j < 5; j++) {synchronizedDemo.decrement();}}, "线程2").start();}}
代码说明:
在上面的代码中,SynchronizedDemo类中的increment()和decrement()方法都被声明为synchronized方法,并使用关键字synchronized实现了线程同步。其中,使用wait()方法和notifyAll()方法实现线程间的通信,当num为0时,当前线程会进入等待状态,直到被其他线程唤醒。在主函数中,创建两个线程分别执行加1和减1操作,通过synchronized关键字和锁机制保证了两个线程的输出结果的正确性。
2.3 使用Atomic类
Java提供了Atomic类来保证多个线程之间对共享变量的原子性访问。Atomic类提供了一系列的原子操作,包括增加、减少、比较和交换等操作,可以避免数据冲突问题,并且具有更好的性能表现。
2.3.1 Atomic类实现线程同步的示例代码
import java.util.concurrent.atomic.AtomicInteger;public class AtomicDemo {private AtomicInteger num = new AtomicInteger(0);/*** 定义一个方法,用于加1操作*/public void increment() {num.incrementAndGet();System.out.println(Thread.currentThread().getName() + "输出:" + num.get());}/*** 定义一个方法,用于减1操作*/public void decrement() {num.decrementAndGet();System.out.println(Thread.currentThread().getName() + "输出:" + num.get());}}public class AtomicTest {public static void main(String[] args) {AtomicDemo atomicDemo = new AtomicDemo();// 定义一个线程,用于执行递增操作new Thread(() -> {for (int i = 0; i < 5; i++) {atomicDemo.increment();}}, "线程1").start();// 定义一个线程,用于执行递减操作new Thread(() -> {for (int j = 0; j < 5; j++) {atomicDemo.decrement();}}, "线程2").start();}}
代码说明:
在上面的代码中,AtomicDemo类中的num字段也被定义为常用的AtomicInteger类型,它可以实现对整数类型的原子更新,避免多线程操作的安全问题。在increment()和decrement()方法中,使用num.incrementAndGet()和num.decrementAndGet()方法实现对num字段的原子操作。在主函数中,创建两个线程分别执行加1和减1操作,通过Atomic类实现线程的安全同步,并保证输出结果的正确性。
2.4 使用线程池
在Java中,使用线程池可以避免线程的创建和销毁造成的性能损失。线程池可以根据需要调整线程的数量和优先级,提高程序的并发性能。在使用线程池时,需要注意线程池的大小和任务队列的大小,避免线程池的饱和和任务队列的溢出。
2.4.1 Java线程池实现多线程的示例代码
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ThreadPoolExample {public static void main(String[] args) {// 创建线程池,其中包含10个线程ExecutorService executorService = Executors.newFixedThreadPool(10);// 提交任务给线程池执行for (int i = 0; i < 100; i++) {Runnable task = new Task(i);executorService.submit(task);}// 关闭线程池executorService.shutdown();}static class Task implements Runnable {private int taskId;public Task(int id) {this.taskId = id;}@Overridepublic void run() {System.out.println("Task " + taskId + " is running.");}}}
代码说明
该示例代码创建一个包含10个线程的线程池,并提交100个任务给线程池执行。每个任务都是一个实现了Runnable接口的Task对象,当线程池执行该任务时,打印该任务的ID。最后,线程池被关闭。
2.5 避免死锁
死锁是指多个线程之间互相等待对方释放资源的情况,从而导致程序无法继续执行的问题。在Java中,可以使用锁的顺序来避免死锁问题。锁的顺序要保持一致,并且尽量减少锁的粒度和范围,避免锁定过多的代码。
2.5.1 如何避免死锁示例代码
public class DeadlockAvoidanceExample {private static Object lock1 = new Object();private static Object lock2 = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (lock1) {System.out.println("Thread 1 acquired lock 1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2) {System.out.println("Thread 1 acquired lock 2");}}});Thread thread2 = new Thread(() -> {synchronized (lock1) {System.out.println("Thread 2 acquired lock 1");synchronized (lock2) {System.out.println("Thread 2 acquired lock 2");}}});// 设置线程优先级,使其更有可能出现死锁thread1.setPriority(Thread.MAX_PRIORITY);thread2.setPriority(Thread.MIN_PRIORITY);thread1.start();thread2.start();}}
代码说明
在上面的示例代码中,有两个线程,它们尝试获取两个不同的锁(lock1和lock2)。如果两个线程在执行时交替获取锁,就不会发生死锁。但是,如果两个线程分别获取一个锁,然后尝试获取另一个锁,就会出现死锁
三、使用ThreadLocal类
在Java中,ThreadLocal类可以用来实现线程间的数据隔离,避免线程间的数据冲突问题。ThreadLocal类可以将数据绑定到线程上,保证每个线程都有自己独立的数据副本,避免多个线程之间共享数据造成的问题。
3.1 使用Concurrent包
Java的Concurrent包提供了多种并发容器和工具类,可以方便地实现多线程编程。例如,使用ConcurrentHashMap类可以实现多线程安全的HashMap,使用ConcurrentLinkedQueue类可以实现多线程安全的队列等。
3.1.1 代码示例
import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentMap;public class ConcurrentExample {public static void main(String[] args) {ConcurrentMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();// 将键值对添加到ConcurrentMap中concurrentMap.put("A", 1);concurrentMap.put("B", 2);concurrentMap.put("C", 3);// 获取并打印所有键值对for (Map.Entry<String, Integer> entry : concurrentMap.entrySet()) {String key = entry.getKey();Integer value = entry.getValue();System.out.println(key + " : " + value);}// 如果键“A”对应的值为1,则将它的值改为100concurrentMap.replace("A", 1, 100);// 获取键“A”对应的值,并打印输出Integer value = concurrentMap.get("A");System.out.println("Value of key A is: " + value);}}
代码说明
该示例代码创建了一个ConcurrentMap,并将几个键值对添加到其中。然后遍历并打印所有的键值对。接着,如果键“A”的值为1,则将其值改为100。最后,获取键“A”的值并打印输出。
ConcurrentHashMap是ConcurrentMap接口的一个实现类,它是线程安全的,可以被多个线程同时访问和修改。在并发环境下,使用ConcurrentHashMap可以避免由于并发修改导致的数据不一致的问题。ConcurrentHashMap支持基本的Map操作,如put、get、remove等,并且还提供了一些增强功能,如replace、putIfAbsent等。
3.2 使用信号量和倒计时门闩(chuan)
在Java中,可以使用信号量和倒计时门闩来实现线程间的同步和互斥操作。信号量可以用来控制线程的访问数量,倒计时门闩可以用来控制线程的等待时间。这些机制可以用来协调多个线程的执行顺序,避免数据冲突和死锁问题。
3.1.2 Java中使用信号量和倒计时门闩的示例代码
3.1.2.2 信号量代码示例
import java.util.concurrent.Semaphore;public class SemaphoreExample {public static void main(String[] args) {Semaphore semaphore = new Semaphore(2); // 初始化信号量数量为2for (int i = 1; i <= 5; i++) {new Thread(() -> {try {semaphore.acquire(); // 获取信号量System.out.println("Thread " + Thread.currentThread().getId() + " acquired semaphore");Thread.sleep(1000);System.out.println("Thread " + Thread.currentThread().getId() + " releasing semaphore");semaphore.release(); // 释放信号量} catch (InterruptedException e) {e.printStackTrace();}}).start();}}}
代码说明
在上面的示例代码中,创建了一个Semaphore对象,初始值为2,表示最多有两个线程可以同时执行。然后,启动了5个线程,每个线程获取信号量,执行一些任务,然后释放信号量。
3.1.2.2 倒计时门闩代码示例
import java.util.concurrent.CountDownLatch;public class CountdownLatchExample {public static void main(String[] args) throws InterruptedException {// 创建倒计时门闩,计数器初始值为3CountDownLatch latch = new CountDownLatch(3);// 创建3个线程并启动for (int i = 0; i < 3; i++) {Thread thread = new Thread(new Worker(i, latch));thread.start();}// 等待倒计时门闩的计数器为0latch.await();// 所有线程都执行完毕,打印提示信息System.out.println("All workers have finished.");}static class Worker implements Runnable {private int id;private CountDownLatch latch;public Worker(int id, CountDownLatch latch) {this.id = id;this.latch = latch;}@Overridepublic void run() {System.out.println("Worker " + id + " is working...");try {// 模拟每个线程工作的时间Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Worker " + id + " has finished.");// 计数器减1latch.countDown();}}}
代码说明
该示例代码创建了一个倒计时门闩,并将计数器初始值设置为3。然后,创建3个工作者线程并启动,每个线程会睡眠1秒钟以模拟工作的时间。在每个工作者线程执行完毕后,会调用latch.countDown()将计数器减1。当计数器减为0时,主线程才能继续执行。
CountDownLatch是Java中的一个工具类,它可以让一个或多个线程等待一个或多个事件的发生。CountDownLatch有一个计数器,当计数器为0时,会释放所有等待线程。可以通过countDown()方法将计数器的值减1,也可以通过await()方法等待计数器的值为0。
四、总结
Java并发编程是一项非常重要的技术,在现代计算机系统中广泛应用。掌握Java并发编程的基本原理和实践技巧,可以帮助我们更好地编写高性能的多线程程序。本文介绍了Java并发编程的基本原理和实践技巧,包括线程和进程、同步和互斥、线程间的通信、线程池等方面的内容。希望读者通过本文的介绍,可以更好地理解Java并发编程,并能够在实践中应用这些知识。