Java并发编程是指在多个线程同时执行的情况下,协调和管理这些线程的过程。在现代计算机系统中,使用多线程并发编程可以显著提高应用程序的性能和响应速度。Java作为一门流行的编程语言,具有强大的并发编程能力。
本文将介绍Java并发编程的基本原理和实践技巧,帮助读者更好地掌握这一领域的知识。
欢迎点赞 收藏 ⭐留言 如有错误敬请指正!
1. 同步
同步是指多个线程在访问共享资源时,按照一定的顺序和时间互相通知,以保证数据同步和正确性。在Java中,同步可以通过以下机制来实现:
2. 互斥
互斥是指多个线程在访问共享资源时,使用某种机制来防止共享资源被多个线程同时访问和修改。在Java中,互斥可以通过以下机制来实现:
总之,同步和互斥是Java并发编程的基本原理之一,能够有效地保证程序的正确性、安全性和一致性。Java提供了多种机制来实现同步和互斥,可以根据具体场景选择最适合的机制来实现。
Java提供了多种机制来实现线程间的通信,包括wait/notify机制、join方法、volatile关键字等。wait/notify机制可以让一个线程等待另一个线程的通知,join方法可以让一个线程等待另一个线程的结束,volatile关键字可以保证多个线程之间共享变量的可见性。
这三个方法既可以用于同步机制中,也可以用于线程间通信。wait方法使当前线程进入阻塞状态,并释放同步锁,直到其他线程调用notify或notifyAll方法来唤醒它。notify方法随机唤醒一个等待该对象锁的线程,而notifyAll方法会唤醒所有等待该对象锁的线程。使用wait()/notify()/notifyAll()时,需要注意以下几点:
Condition接口是JDK 5.0引入的新特性,提供了类似wait()/notify()的机制,但更加高级和灵活。在Condition中,线程可以等待指定的条件变量并等待通知,以实现线程间的通信。Condition中包含了await()、signal()、signalAll()方法。
CountDownLatch计数器是Java并发包中提供的一个同步工具,它允许在一个或多个线程中等待一组事件发生。CountDownLatch类包含一组await()和countDown()方法。await()方法会导致当前线程等待,直到计数器的值为0,而countDown()方法会减少计数器的值。
CyclicBarrier类提供了一种同步机制,允许一组线程等待彼此到达某个公共障碍点。它包含await()和reset()方法,await()方法会使当前线程等待,直到其他线程的到达,而reset()方法会重置障碍的计数器。
Semaphore类是一种基于计数的信号量,用于控制多个线程对共享资源的访问。它包含acquire()和release()方法,acquire()方法会尝试获取信号量的许可证,如果许可证不可用则阻塞线程,而release()方法会释放信号量的许可证。
线程池是Java并发编程中一种重要的技术,它可以提高线程的使用效率、减少创建和销毁线程的开销,同时也能控制并发线程数量,从而避免线程资源的浪费和系统的负担。Java线程池的实现基于两种机制:ThreadPoolExecutor类和Executors工具类。
ThreadPoolExecutor类是Java中提供的原始的线程池实现方式,它包含了线程池的核心参数和方法,可以支持自定义线程池的各种配置。ThreadPoolExecutor的构造方法参数包括线程池的核心线程数、最大线程数、线程空闲时间、等待队列大小,还可以自定义拒绝策略等。它的主要方法包括execute()、shutdown()、shutdownNow()、awaitTermination()等。
Executors工具类是Java中提供的封装线程池的类集合,它提供了多种预定义的线程池,如newFixedThreadPool()、newCachedThreadPool()、newSingleThreadExecutor()等,可以根据不同的需求快速创建线程池,而无需关心核心参数的配置。这些方法的实现都是基于ThreadPoolExecutor的,同时提供了一些默认的配置参数和线程池特性。
Java线程池的优点:
Java线程池的缺点:
在多线程环境下,使用volatile关键字可以保证共享变量的可见性。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关键字确保了可见性,但不保证原子性,因此可能会存在并发问题。
synchronized关键字和锁机制可以保证多个线程之间的同步和互斥访问,避免数据冲突问题。在使用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关键字和锁机制保证了两个线程的输出结果的正确性。
Java提供了Atomic类来保证多个线程之间对共享变量的原子性访问。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类实现线程的安全同步,并保证输出结果的正确性。
在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;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running.");
}
}
}
代码说明
该示例代码创建一个包含10个线程的线程池,并提交100个任务给线程池执行。每个任务都是一个实现了Runnable接口的Task对象,当线程池执行该任务时,打印该任务的ID。最后,线程池被关闭。
死锁是指多个线程之间互相等待对方释放资源的情况,从而导致程序无法继续执行的问题。在Java中,可以使用锁的顺序来避免死锁问题。锁的顺序要保持一致,并且尽量减少锁的粒度和范围,避免锁定过多的代码。
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)。如果两个线程在执行时交替获取锁,就不会发生死锁。但是,如果两个线程分别获取一个锁,然后尝试获取另一个锁,就会出现死锁
在Java中,ThreadLocal类可以用来实现线程间的数据隔离,避免线程间的数据冲突问题。ThreadLocal类可以将数据绑定到线程上,保证每个线程都有自己独立的数据副本,避免多个线程之间共享数据造成的问题。
Java的Concurrent包提供了多种并发容器和工具类,可以方便地实现多线程编程。例如,使用ConcurrentHashMap类可以实现多线程安全的HashMap,使用ConcurrentLinkedQueue类可以实现多线程安全的队列等。
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,则将它的值改为100
concurrentMap.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等。
在Java中,可以使用信号量和倒计时门闩来实现线程间的同步和互斥操作。信号量可以用来控制线程的访问数量,倒计时门闩可以用来控制线程的等待时间。这些机制可以用来协调多个线程的执行顺序,避免数据冲突和死锁问题。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2); // 初始化信号量数量为2
for (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个线程,每个线程获取信号量,执行一些任务,然后释放信号量。
import java.util.concurrent.CountDownLatch;
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
// 创建倒计时门闩,计数器初始值为3
CountDownLatch latch = new CountDownLatch(3);
// 创建3个线程并启动
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(new Worker(i, latch));
thread.start();
}
// 等待倒计时门闩的计数器为0
latch.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;
}
@Override
public 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.");
// 计数器减1
latch.countDown();
}
}
}
代码说明
该示例代码创建了一个倒计时门闩,并将计数器初始值设置为3。然后,创建3个工作者线程并启动,每个线程会睡眠1秒钟以模拟工作的时间。在每个工作者线程执行完毕后,会调用latch.countDown()将计数器减1。当计数器减为0时,主线程才能继续执行。
CountDownLatch是Java中的一个工具类,它可以让一个或多个线程等待一个或多个事件的发生。CountDownLatch有一个计数器,当计数器为0时,会释放所有等待线程。可以通过countDown()方法将计数器的值减1,也可以通过await()方法等待计数器的值为0。
Java并发编程是一项非常重要的技术,在现代计算机系统中广泛应用。掌握Java并发编程的基本原理和实践技巧,可以帮助我们更好地编写高性能的多线程程序。本文介绍了Java并发编程的基本原理和实践技巧,包括线程和进程、同步和互斥、线程间的通信、线程池等方面的内容。希望读者通过本文的介绍,可以更好地理解Java并发编程,并能够在实践中应用这些知识。