Java多线程
线程池的类型
- Executors.newCachedThreadPool:
- 解释:创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种线程池适合执行很多短期异步的小程序或者负载较轻的服务器。
- 应用场景:Executors.newCachedThreadPool:这种线程池可以创建无限多个线程,适合执行很多短期异步的小任务,或者是负载较轻的服务器。例如,一个 Web 服务器接收到大量并发请求时,可以使用这种线程池来处理每个请求。
Java
- 代码解读
- 复制代码
// 创建一个固定大小为 5 的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 向线程池提交 10 个任务
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
System.out.println("CurrentThread name:" + Thread.currentThread().getName());
});
}
// 关闭线程池
executorService.shutdown();
- Executors.newFixedThreadPool:
- 解释:创建一个固定大小的线程池,每次提交一个任务就创建一个线程,直到达到线程池的最大大小。这种线程池可以控制并发的线程数,超出的线程会在队列中等待。这种线程池适合执行长期的稳定和固定的任务。
- 应用场景:这种线程池可以创建固定数量的线程,适合执行长期的稳定和固定的任务,或者是有资源限制的场景。例如,一个数据库服务器需要控制并发访问数时,可以使用这种线程池来分配连接。
- Executors.newScheduledThreadPool:
- 解释:创建一个定长的线程池,支持定时及周期性任务执行。这种线程池适合执行延迟或者定时的任务。
- 应用场景:这种线程池可以创建固定数量的线程,并支持定时和周期性任务执行。适合执行一些需要在指定时间或者周期性地执行的任务。例如,一个定时任务调度器需要按照时间表执行不同的任务时,可以使用这种线程池来安排任务。
- Executors.newSingleThreadExecutor:
- 解释:创建单个后台线程来执行任务,如果该后台进程异常结束会有另一个取代它。这种线程池保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。这种线程池适合需要按顺序执行各个任务,并且在任意时间点只能有一个任务被执行。
- 应用场景:这种线程池只有一个工作线程,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。适合执行一些需要按顺序执行各个任务,并且在任意时间点只能有一个任务被执行的场景。例如,一个消息队列需要按照消息到达顺序处理消息时,可以使用这种线程池来消费消息12。
Java锁
Java synchronized 示例
java
代码解读
复制代码
public class Counter {
private int counter;
public synchronized void increment() {
counter++;
}
public int read() {
return counter;
}
}
Java CAS 实现:
CAS(Compare And Swap)是一种利用处理器提供的特殊指令来实现对内存地址上的值进行比较和替换的操作12。CAS指令接收三个参数:内存地址(M)、期望值(A)和更新值(B)。它会将内存地址上的当前值与期望值进行比较,如果相等,则将内存地址上的值替换为更新值,并返回true;如果不相等,则不做任何修改,并返回false12。
Java中没有直接实现CAS,而是通过sun.misc.Unsafe类提供了一些底层方法来调用CAS指令34。Unsafe类是一个非常危险的类,它可以直接操作内存地址和数据,绕过Java的安全机制和访问控制。因此,它只能在受信任的代码中使用,一般用户无法直接获取Unsafe类的实例34。
Unsafe类提供了多个方法来执行CAS操作,例如compareAndSwapInt()、compareAndSwapLong()、compareAndSwapObject()等。这些方法都需要传入一个对象、一个偏移量和两个期望值或更新值作为参数,并尝试将对象中偏移量位置上的值从期望值改为更新值。如果成功,则返回true;否则返回false。
例如,在AtomicInteger类中,就使用了Unsafe类提供的compareAndSwapInt()方法来实现原子地增加或减少变量值:
Java
代码解读
复制代码
public class AtomicInteger {
private volatile int value;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final int decrementAndGet() {
for (;;) {
int current = get();
int next = current - 1;
if (compareAndSet(current, next))
return next;
}
}
}
在这个例子中,首先使用反射获取value变量在AtomicInteger对象中的偏移量,并保存在valueOffset属性中。然后在compareAndSet()方法中,调用unsafe.compareAndSwapInt(this, valueOffset, expect, update)方法来执行CAS操作。在incrementAndGet()和decrementAndGet()方法中,使用循环不断地尝试获取当前值、计算新值并调用compareAndSet()方法来更新变量值。
Java锁的类型:
1.可重入锁 java.util.concurrent.locks.ReentrantLock
可重入锁是一种锁,它允许一个线程多次获取同一个锁,而不会造成死锁。每次获取锁时,锁的持有计数会加一,每次释放锁时,持有计数会减一。当持有计数为零时,锁被完全释放。可重入锁可以实现线程对共享资源的独占访问,同时也支持线程在同一个方法或者不同方法中递归地获取同一个锁。
Java中提供了ReentrantLock类来实现可重入的互斥锁24。ReentrantLock类实现了Lock接口,并提供了一些扩展功能,比如公平性、可中断性、条件变量等。使用ReentrantLock类的一般步骤如下:
创建一个ReentrantLock实例,根据需要选择不同的构造参数 在访问共享资源之前,调用lock()方法获取锁,如果锁不可用,则线程会阻塞直到获取到锁 在try-finally块中访问共享资源,并在finally块中调用unlock()方法释放锁 根据需要使用其他ReentrantLock提供的方法和特性 例如:
Java
代码解读
复制代码
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++; // 访问共享资源
} finally {
lock.unlock(); // 释放锁
}
}
public int read() {
return count;
}
}
这个例子中,使用了ReentrantLock作为锁对象,它是一个可重入的互斥锁。在increment()方法中,在对count变量进行加一操作之前,先调用lock.lock()方法获取锁。如果其他线程已经持有了这个锁,则当前线程会阻塞直到获得这个锁。然后在try-finally块中访问count变量,并在finally块中调用lock.unlock()方法释放锁。这样可以保证只有一个线程能够修改count的值。
2.读写锁 java.util.concurrent.locks.ReadWriteLock
- Java读写锁是一种可以实现读写分离的同步机制,它允许多个线程同时获取读锁,但只能有一个线程获取写锁1。当有线程持有写锁时,其他线程不能获取读锁或写锁。
- Java读写锁的一个实现类是 ReentrantReadWriteLock,它基于AQS(队列同步器)的独占和共享模式来完成功能。它使用一个int变量来表示同步状态,将高16位用于共享状态(读状态),低16位用于独占状态(写状态)。
- ReentrantReadWriteLock支持公平和非公平两种模式,在非公平模式下,允许写锁插队,也允许读锁插队,但是读锁插队的前提是队列中的头节点不能是想获取写锁的线程。在公平模式下,都是严格按照请求锁顺序进行的。
- ReentrantReadWriteLock提供了ReadLock和WriteLock两个内部类,分别代表读锁和写锁对象。它们都有lock()和unlock()方法来加解锁。在加解锁过程中,会通过CAS操作更新同步状态,并维护相关的计数器来记录重入次数和持有线程信息。
Java
代码解读
复制代码
// 导入相关类
import java.util.concurrent.locks.ReentrantReadWriteLock;
// 创建一个ReentrantReadWriteLock对象
ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
// 获取读锁和写锁对象
ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();
// 定义一个方法用于读操作
public static void read() {
// 尝试获取读锁
readLock.lock();
try {
// 执行读操作,打印线程名和信息
System.out.println(Thread.currentThread().getName() + "获取读锁,开始执行");
// 模拟耗时操作,睡眠1秒
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放读锁,并打印线程名和信息
readLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放读锁");
}
}
// 定义一个方法用于写操作
public static void write() {
// 尝试获取写锁
writeLock.lock();
try {
// 执行写操作,打印线程名和信息
System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");
// 模拟耗时操作,睡眠1秒
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放写锁,并打印线程名和信息
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放写锁");
}
}
// 在main方法中创建四个线程分别进行读和写操作,并启动它们
public static void main(String[] args) {
new Thread(() -> read(), "Thread1").start();
new Thread(() -> read(), "Thread2").start();
new Thread(() -> write(), "Thread3").start();
new Thread(() -> write(), "Thread4").start();
}
输出结果如下,可以看到线程1和线程2可以同时获取读锁,而线程3和线程4只能依次获取写锁,因为线程4必须等待线程3释放写锁后才能获取到锁:
代码解读
复制代码
Thread1获取读锁,开始执行
Thread2获取读锁,开始执行
Thread1释放读锁
Thread2释放读锁
Thread3获取写锁,开始执行
Thread3释放写锁
Thread4获取写锁,开始执行
Thread4释放写锁
`