并发编程是指在一个程序中同时执行多个任务或线程。这通常涉及到多线程编程、线程同步、并发容器等技术。这些技术可以用来解决多线程环境中的问题,如线程安全、资源竞争、死锁等问题。在实际的Java并发编程中,还需要考虑到线程池、Future、Callable、ExecutorService等概念。
在另一篇文章介绍了多线程、进程、并发、并行等基本概念,并分析了线程安全问题产生的原因,同时也整理了线程实现的4种方式,并做了对比,请参考 java-06 多线程-4种实现方式
如果你觉得我分享的内容或者我的努力对你有帮助,或者你只是想表达对我的支持和鼓励,请考虑给我点赞、评论、收藏。您的鼓励是我前进的动力,让我感到非常感激。
文章目录
1 线程同步
1.1 锁
StampedLock 在某些场景下可以提供更好的并发性能,但也需要注意合理的使用,以避免过于复杂的代码结构和潜在的死锁情况。
synchronized 和 ReentrantLock 都是基于悲观锁思想实现的,意味着它们假定在执行临界区代码期间会发生并发冲突。在高并发场景下,激烈的锁竞争可能会导致线程阻塞,从而降低性能。特别是在多读场景下,悲观锁可能引入大量的额外并发开销,因为每个读操作都需要获得独占锁。
相比之下,StampedLock 的乐观锁思想更适合多读场景。乐观锁假定数据操作不存在并发冲突,因此不会引起锁竞争,也不会导致线程阻塞和死锁。乐观锁通常在提交修改时才验证资源是否被其他线程修改。不过在多写场景下乐观锁会频繁失败和重试,这同样会对性能造成一定影响。
1.1.1 synchronized
synchronized 是 Java 中用于实现线程同步的关键字,它主要用于创建同步代码块或同步方法,以确保在多线程环境下对共享资源的访问是安全的。通过使用 synchronized 可以避免多个线程同时访问共享资源而引发的并发问题,如竞态条件和数据不一致等。
1.1.1.1 同步代码块
通过在代码块内使用 synchronized 关键字来创建同步代码块,它可以用来保护代码块,确保在同一时刻只有一个线程能够进入同步代码块。一个典型的用法是将需要同步的代码放在同步代码块中,并指定一个锁对象作为同步的依据。
虽然任意一个唯一的对象(比如一个字符串)都可以作为同步代码块的锁对象,但锁的粒度过大会导致并发安全问题,粒度过小会导致性能下降。类比同步方法,一般情况下,对于实例方法,通常使用 this 作为锁对象,对于静态方法,通常使用类的字节码对象 类名.class 作为锁对象。
同步代码块的基本使用示例:
private static int counter;
private static final Object lock = new Object();
public static void main(String[] args) {
Runnable incrementTask = () -> {
for (int i = 0; i < 10000; i++) {
// 同步代码块
synchronized (lock) {
counter++;
}
}
};
Thread thread1 = new Thread(incrementTask);
Thread thread2 = new Thread(incrementTask);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final counter value: " + counter);
}
此外,在同步代码块中还可以通过 wait() 方法让当前线程进入等待状态,直到其他线程调用相同对象的 notify() 或 notifyAll() 方法来唤醒它。这三个方法的实现也同样依赖于 monitor 机制,因此需要被绑定到指定的锁对象上。
wait() 和 notify() 的基本使用示例:
public static void main(String[] args) {
final Object lock = new Object();
// 等待线程
Thread waiter = new Thread(() -> {
synchronized (lock) {
System.out.println("Waiter: Waiting for a notification...");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Waiter: Got a notification!");
}
});
// 通知线程
Thread notifier = new Thread(() -> {
synchronized (lock) {
System.out.println("Notifier: Performing some work...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Notifier: Work done, notifying the waiter...");
lock.notify();
}
});
waiter.start();
notifier.start();
}
1.1.1.2 同步方法
通过在方法定义处使用 synchronized 关键字来创建同步方法,它可以将整个方法体都变成一个同步代码块。同步方法底层通过隐式锁对象实现,只是锁的范围是整个方法代码。如果方法是实例方法,同步方法默认用 this 作为的锁对象。如果方法是静态方法,同步方法默认用 类名.class 作为的锁对象。
同步方法的优点是简单,可以很方便地实现线程同步。不过锁的范围较大,可能影响性能,因为其他不需要同步的代码也会被锁住。
同步方法的基本使用示例:
public class Test {
private static int counter;
public static synchronized void increment() {
for (int i = 0; i < 10000; i++) {
counter++;
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(Test::increment);
Thread thread2 = new Thread(Test::increment);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final counter value: " + counter);
}
}
1.1.2 ReentrantLock
ReentrantLock 是 Java 提供的一个可重入锁,默认为非公平锁,它相比于使用 synchronized 关键字具有更大的灵活性。通过 ReentrantLock,你可以显式地获取锁和释放锁,从而精确地控制同步范围。
ReentrantLock 提供了更多的功能,比如可重入性、可定时的锁等待、公平性设置等。但需要注意,使用 ReentrantLock 需要手动释放锁,因此务必在 finally 块中释放锁,以防止死锁情况的发生。
ReentrantLock 的基本使用示例:
public class Test {
private static final Lock lock = new ReentrantLock();
private static int counter;
public static void main(String[] args) {
Runnable incrementTask = () -> {
for (int i = 0; i < 10000; i++) {
lock.lock();
try {
counter++;
System.out.println(Thread.currentThread().getName());
} finally {
// 放在finally块中保证锁一定能被释放
lock.unlock();
}
}
};
Thread thread1 = new Thread(incrementTask);
Thread thread2 = new Thread(incrementTask);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final counter value: " + counter);
}
}
1.1.3 StampedLock
StampedLock 是 Java 提供的一个支持乐观读、悲观读和写操作的锁机制。它在 Java 8 中引入,通过使用乐观读锁来提供更高的并发性,同时支持升级为悲观读锁或写锁。不过它不可重入且不支持条件变量 Conditon。
StampedLock 提供了三种读写控制模式:
- 乐观读锁:乐观读锁是一种无锁操作,它假设没有写操作会发生。线程可以直接读取数据而无需获取锁,读取完成后通过校验版本信息来判断数据是否有效。如果数据有效,操作成功;如果数据无效,需要尝试其他方式来获取锁。乐观读锁适用于读多写少的场景。
- 悲观读锁:悲观读锁是常规的读锁,它会阻塞写操作,但不会阻塞其他读操作。悲观读锁适用于读多写多的场景,可以保证读操作之间的数据一致性。
- 写锁:写锁会阻塞其他的读操作和写操作,用于保护共享资源的写操作。
StampedLock 的基本使用示例:
public class Main {
private static final StampedLock lock = new StampedLock();
private static int counter;
public static void main(String[] args) {
Runnable incrementTask = () -> {
for (int i = 0; i < 10000; i++) {
long stamp = lock.writeLock(); // 获取写锁
try {
counter++;
} finally {
lock.unlockWrite(stamp); // 释放写锁
}
}
};
Thread thread1 = new Thread(incrementTask);
Thread thread2 = new Thread(incrementTask);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final counter value: " + counter);
}
}
1.2 原子变量
java-07 多线程-并发编程(原子变量、CountDownLatch、Future和CompletableFuture、volatile)
1.3 ThreadLocal
1.4 CountDownLatch
java-07 多线程-并发编程(原子变量、CountDownLatch、Future和CompletableFuture、volatile)
1.5 Future 和 CompletableFuture
java-07 多线程-并发编程(原子变量、CountDownLatch、Future和CompletableFuture、volatile)
1.6 volatile
java-07 多线程-并发编程(原子变量、CountDownLatch、Future和CompletableFuture、volatile)