【JavaEE】多线程进阶(2)
- 一、JUC(java.util.concurrent) 的常⻅类
- 1.1 Callable 接⼝
- 1.2 ReentrantLock
- 1.3 原子类
- 原子类的特性:
- 常见原子类:
- 原子类的实例:
- 1.4 线程池
- 1.5 信号量 Semaphore
- 代码实例
- 1.6 CountDownLatch
- 代码实例
- 1.7 线程安全的集合类
- 多线程环境使⽤ ArrayList
- 多线程环境使⽤哈希表
- Hashtable
- ConcurrentHashMap
- 1.8 死锁
一、JUC(java.util.concurrent) 的常⻅类
博客结尾附有此篇博客的全部代码!!!
1.1 Callable 接⼝
Callable 接口是 Java 中用于定义可以返回结果的任务的接口,它位于 java.util.concurrent 包中。
public interface Callable<V> {
V call() throws Exception;
}
实例应用:计算1+2+…+100的值,使用Callable接口
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Integer> callable = new Callable<Integer>() {
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
};
Thread thread = new Thread(callable);
thread.start();
}
原因:Thread本身不提供接受结果的方法,需要FutureTask对象来拿到结果(Thread不提供接受结果是为了更好的解耦合,将任务和线程分离开)
- FutureTask:FutureTask 实现了 Runnable 接口,因此可以被 Thread 接受。
- Thread类的构造函数可以接受一个 Runnable 对象,但不能接受其他类型的对象,因为 Thread 的内部逻辑是基于 Runnable 的 run() 方法实现的。
修改:
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<Integer> callable=new Callable<Integer>() {
public Integer call() throws Exception {
int sum=0;
for (int i = 0; i <= 100; i++) {
sum+=i;
}
return sum;
}
};
FutureTask<Integer> futureTask=new FutureTask<>(callable);
Thread thread=new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
通过Runnable接口计算1+2+…+100的值:
public class RunnableDemo {
private static int total=0;
public static void main(String[] args) throws InterruptedException {
Runnable r = new Runnable(){
int sum=0;
public void run() {
for (int i = 0; i <=100 ; i++) {
sum+=i;
}
total=sum;
}
};
Thread t1 = new Thread(r);
t1.start();
t1.join();
System.out.println(total);
}
}
1.2 ReentrantLock
可重⼊互斥锁. 和 synchronized 定位类似, 都是⽤来实现互斥效果, 保证线程安全
ReentrantLock 的核心功能是通过 Lock 接口实现的,它提供了以下方法:
- lock():获取锁,如果锁已经被其他线程占用,则当前线程会阻塞,直到获取锁。
- unlock():释放锁。
- tryLock():尝试获取锁,如果锁可用则立即获取,否则返回 false,不会阻塞。
- tryLock(long timeout, TimeUnit unit):尝试获取锁,如果在指定时间内无法获取锁,则返回 false。
- isHeldByCurrentThread():判断当前线程是否持有该锁。
- isLocked():判断锁是否被任何线程持有。
public class ReentrantLockDemo1 {
private static int total = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock locker = new ReentrantLock();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
locker.lock();
total++;
locker.unlock();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
locker.lock();
total++;
locker.unlock();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(total);
}
}
运行结果:total=100000
这里需要注意的:
因为这里解锁需要自己手动解锁,但是不可避免的抛出异常而导致代码运行终止,有可能就执行不到 locker.lock();
改进:将unlocker.lock();放入finally代码块中
ReentrantLock和synchronized对比:
- synchronized是关键字,ReentrantLock是Java的标准库中的类
- synchronized是通过代码块执行加锁解锁,而ReentrantLock是通过lock()和unlock()加锁解锁,需要注意的是unlock()不调用问题
- ReentrantLock提供的tryLock(),如果成功加锁,返回true;反之,加锁失败,返回false,不会出现阻塞;而且还可以设置等待时长,在这段时间后再尝试加锁,返回true/false。
- synchronized是非公平锁,ReentrantLock默认是非公平锁,但是可以设置为公平锁
ReentrantLock lock = new ReentrantLock(true);
- 更强⼤的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程. ReentrantLock 搭配Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定线程。
1.3 原子类
原子类通过提供一系列线程安全的变量操作方法,确保在多线程环境下对变量的读写操作是不可分割的(即原子的)。它们利用了底层硬件的原子操作指令(如 CAS),从而避免了锁的开销,提高了性能。
原子类的特性:
- 无锁并发:原子类通过 CAS 机制实现线程安全,无需使用重量级的锁(如 synchronized 或 ReentrantLock)。
- 高性能:由于避免了锁的开销,原子类在高并发场景下通常比传统同步机制性能更高。
- 线程安全:原子类保证了对变量的操作是原子的,即使在多线程环境下也不会出现竞态条件。
常见原子类:
(1)基本类型原子类:
AtomicInteger:用于原子操作的整数。
AtomicLong:用于原子操作的长整型。
AtomicBoolean:用于原子操作的布尔值。
(2)引用类型原子类:
AtomicReference:用于原子操作的对象引用。
AtomicStampedReference:用于原子操作的对象引用,同时带有版本号(用于解决 ABA 问题)。
AtomicMarkableReference:用于原子操作的对象引用,同时带有布尔标记。
(3)数组类型原子类:
AtomicIntegerArray:用于原子操作整型数组。
AtomicLongArray:用于原子操作长整型数组。
AtomicReferenceArray:用于原子操作对象引用数组。
原子类的实例:
基本类型原子类:AtomicInteger:用于原子操作的整数
public class AtomicIntegerArrayDemo1 {
public static void main(String[] args) {
AtomicInteger atomicInt = new AtomicInteger(2);
atomicInt.incrementAndGet(); // 增加 1
atomicInt.addAndGet(2); // 增加 5
atomicInt.compareAndSet(5, 10); // 如果当前值为 5,则设置为 10
System.out.println(atomicInt.get());//这里获取的是10
}
}
public class AtomicIntegerArrayDemo {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInt = new AtomicInteger(0);
Thread t1 = new Thread(() -> {
for(int i = 0; i < 5000;i++ ){
atomicInt.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for(int i = 0; i < 5000;i++ ){
atomicInt.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(atomicInt.get());//获取的是10000
}
}
引用类型原子类:AtomicStampedReference:用于原子操作的对象引用,同时带有版本号。
public class AtomicStampedReferenceDemo1 {
public static void main(String[] args) {
AtomicStampedReference<String> ref = new AtomicStampedReference<>("Hello", 0);
ref.compareAndSet("Hello", "World",
0, 1); // 更新引用和版本号
System.out.println(ref.getReference());//expectedStamp和initialStamp相等,
// 则更新initialRef引用值为newReference,并且更新版本号
}
}
compareAndSet 方法的作用:
- 检查当前引用值是否为 “Hello”。
- 检查当前版本号是否为 0。
- 如果两个条件都满足,则将引用值更新为 “World”,版本号更新为 1
数组类型原子类:AtomicReferenceArray:用于原子操作对象引用数组。
public class AtomicReferenceArrayDemo {
public static void main(String[] args) {
AtomicReferenceArray<String> array = new AtomicReferenceArray<>(new String[]{"Hello", "World"});
array.set(1, "Java");//将索引为1的引用改为Java
System.out.println(array.get(1));
}
}
1.4 线程池
线程池
1.5 信号量 Semaphore
Semaphore 的核心思想是通过一组许可证(permits)来控制对资源的访问。每个线程在访问资源之前,必须先获取一个许可证;访问完成后,释放许可证。许可证的数量是有限的,当许可证用完时,后续的线程将被阻塞,直到有许可证被释放。
代码实例
public class SemaphoreDemo {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(5);
System.out.println("使用第一个许可证");
semaphore.acquire();
System.out.println("使用第二个许可证");
semaphore.acquire();
System.out.println("使用第三个许可证");
semaphore.acquire();
System.out.println("使用第四个许可证");
semaphore.acquire();
// semaphore.release();
semaphore.acquire();
System.out.println("使用第五个许可证");
}
}
将许可证改为4张,任务还是5个:
这里可以通过jconsole.exe来调试看下运行结果:
还是四张许可证,但是这里释放了一张许可证:
1.6 CountDownLatch
使用多线程,经常将一个大的任务分成多个子任务,使用多线程执行子任务,提高执行效率。
怎么判断子任务全部执行完毕呢?
这里就可以用CountDownLatch来记录各个任务完成。
- 构造 CountDownLatch 实例, 初始化 10 表⽰有 10 个任务需要完成.
- 每个任务执⾏完毕, 都调⽤ latch.countDown() . 在 CountDownLatch 内部的计数器同时⾃减.
- 主线程中使⽤ latch.await(); 阻塞等待所有任务执⾏完毕. 相当于计数器为 0 了
代码实例
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
Thread t1 = new Thread(()->{
for(int i=0;i<3;i++){
try {
Thread.sleep((long) (Math.random() * 2000));
latch.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
latch.await(); // 阻塞主线程,直到计数器为 0
System.out.println("所有任务执行完毕");
}
}
1.7 线程安全的集合类
Vector, Stack, HashTable, 是线程安全的(不建议⽤), 其他的集合类不是线程安全的
多线程环境使⽤ ArrayList
让ArrayList变成线程安全:
- ⾃⼰使⽤同步机制 (synchronized 或者 ReentrantLock)
- Collections.synchronizedList(new ArrayList);
返回List的各种关键方法都带synchronized,这种做法类似于Vector, Stack - 使⽤ CopyOnWriteArrayList
读操作:读操作直接访问底层数组,不需要加锁,因此性能很高。
写操作:
- 创建底层数组的完整副本。
- 在副本上进行修改操作。
- 将副本替换为原始数组。
这种操作的效率相对低效,因为每次都需要复制整个数组。
多线程环境使⽤哈希表
HashMap 本⾝不是线程安全的.
在多线程环境下使⽤哈希表可以使⽤:
• Hashtable
• ConcurrentHashMap
Hashtable
- 使用全局锁(synchronized)保护整个哈希表(这意味着在任何时刻,只有一个线程可以修改哈希表,其他线程必须等待),所有操作(包括读写)都会锁住整个表。
- 这种机制简单但效率低下,尤其是在高并发场景下,容易导致线程阻塞。
存在缺点:
- 如果多线程访问同⼀个 Hashtable 就会直接造成锁冲突.
- size 属性也是通过 synchronized 来控制同步, 也是⽐较慢的.
- ⼀旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到⼤量的元素拷⻉, 效率会⾮常低.
ConcurrentHashMap
- 使用分段锁(Segment)机制,将哈希表分为多个段,每个段有自己的锁。
- JDK 1.8 以后,进一步优化为基于 CAS 和 synchronized 的锁机制,结合数组 + 链表 + 红黑树的数据结构。
- 读操作通常不需要加锁,写操作的锁粒度更细,大大减少了锁竞争。
优化扩容:
- 发现需要扩容的线程, 只需要创建⼀个新的数组, 同时只搬⼏个元素过去.
- 扩容期间, 新⽼数组同时存在.
- 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运⼀⼩部分元素.
- 搬完最后⼀个元素再把⽼数组删掉.
- 这个期间, 插⼊只往新数组加.
- 这个期间, 查找需要同时查新数组和⽼数组
1.8 死锁
线程安全
此篇博客的全部代码!!!