java多线程框架有哪些-关于JavaUtilConcurrent的几个常见问题解析
文章目录
1. JUC是什么
JUC是Java并发编程中的一个重要模块,全称为Java Util Concurrent(Java并发工具包),它提供了一组用于多线程编程的工具类和框架java多线程框架有哪些,帮助开发者更方便地编写线程安全的并发代码。
本文主要介绍Java Util Concurrent下的一些常用接口和类
2. Callable接口
Callable接口类似于Runnable. 有一点区别就是Runable描述的任务没有返回值,而Callable接口是带有返回值的
示例:
Callable<返回值类型> callable = new Callable<Integer>() {
@Override
public 返回值类型 call() throws Exception {
// 执行的任务
}
};
Callable接口定义了一个call()方法,因此在创建实例的时要实现这个方法. 该方法在任务执行完成后返回一个结果,并且可以抛出异常。
与Runnable不同,Callable描述的任务不能直接传给线程去执行. 因此需要借助FutureTask这个类
FutureTask<返回值类型> futureTask = new FutureTask<>(callable);
获取上述任务的返回值可以使用 FuturTask提供的get方法.
示例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int ret = 0;
for (int i = 1; i <= 10; i++) {
ret += i;
}
return ret;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t1 = new Thread(futureTask);
t1.start();
System.out.println(futureTask.get());
}
运行结果:
3. ReentrantLock
ReentrantLock:ReentrantLock是Lock接口的一个实现类,它实现了Lock接口的所有方法。ReentrantLock支持重入性,也就是说同一个线程可以多次获取同一个锁,而不会产生死锁。这种特性使得ReentrantLock可以用于更复杂的线程同步场景。
在ReentrantLock中,有三个十分重要的方法:
lock():加锁unlock():解锁.tryLock(): 用于尝试获取锁,如果锁是可用的,就立即获取并返回true,如果锁不可用,就立即返回false,而不会阻塞当前线程。还可以指定获取锁的最大等待时间.
与synchronized不同,它的加锁和解锁操作时分开的,需要自己去添加.
这也可能会导致如果在加锁之后,代码出现异常,则有可能执行不到unlock方法.这也是ReentranLock的一个小弊端.但我们可以通过使用try finally来避免.
tryLock方法有两个版本:
在实际开发中, 使用这种"死等的策略"往往要慎重,tryLock()让我们面对这种情况有更多的选择
ReentrantLock可以实现公平锁. 默认是非公平的.
但当我们创建实例时,传入参数true时.就变成公平锁了
ReentrantLock reentrantLock = new ReentrantLock(true);
synchronize搭配wait/notify方法来实现线程的等待通知的,唤醒的线程是随机的
ReentrantLock搭配Condition类实现线程等待通知的.可以指定线程来进行唤醒
synchronized是Java中的关键字,底层是JVM实现的(C++)
ReentranLock 是标准库的一个类,底层是基于Java实现的
4. 原子类
原子类是为了解决多线程环境下的竞态条件(Race Condition)和数据不一致的问题。在多线程环境下,如果多个线程同时对一个共享变量进行读取和写入操作,可能会导致数据的不一致性,从而产生错误的结果。
原子类是基于CAS实现的
Java提供了多种原子类,常用的原子类有以下四个:
AtomicInteger:用于对int类型的变量进行原子操作。AtomicLong:用于对long类型的变量进行原子操作。AtomicBoolean:用于对boolean类型的变量进行原子操作。AtomicReference:用于对引用类型的变量进行原子操作。
接下来使用原子类AtomicInteger来实现两个线程针对同一个变量自增50000次的操作.
因为是类的实例对象,我们不能直接对类的实例对象进行++操作. 只能借助类提供的一些方法
AtomicInteger的一些方法:
AtomicInteger atomicInteger = new AtomicInteger();
// atomicInteger++
atomicInteger.getAndIncrement();
// ++atomicInteger
atomicInteger.incrementAndGet();
// atomicInteger--
atomicInteger.getAndDecrement();
// --atomicInteger
atomicInteger.decrementAndGet();
public class Demo23 {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(atomicInteger);
}
}
运行结果:
如果不用原子类,就需要使用synchronized来实现.
5. 线程池
线程池在我之前的文章中详细介绍过,这里就不再这里进行赘述了. 感兴趣的小伙伴可以看这篇文章: 【Java|多线程与高并发】线程池详解
6. 信号量
信号量(Semaphore)维护了一个许可计数器,表示可用的许可数量。当一个线程需要访问共享资源时,它必须先获取一个许可,如果许可数量为0,则线程将被阻塞,直到有可用的许可。当线程使用完共享资源后,它必须释放许可,以便其他线程可以获取许可并访问资源。
信号量的许可数量可以在创建信号量实例时进行设置
// 设置信号量的许可数量为 5
Semaphore semaphore = new Semaphore(5);
信号量中提供了两个主要操作:P(等待)和V(释放)。
P操作对应的方法为acquire()
V操作对应的方法为release()
例如:
public class Demo24 {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(5);
semaphore.acquire();
semaphore.acquire();
semaphore.acquire();
semaphore.acquire();
semaphore.acquire();
System.out.println("此时信号量的许可数量为0");
semaphore.acquire();
semaphore.release();
}
}
运行结果:
信号量可以通过控制许可的数量,可以限制同时访问共享资源的线程数量,从而避免竞争条件和数据不一致性。
7. CoutDownLatch
CountDownLatch(倒计时门闩)是Java并发编程中的一种同步工具,用于等待一组线程完成某个任务。
通过CountDownLatch的构造方法,指定等待线程的数量(计数器).
// 设置等待线程的数量为 5
CountDownLatch countDownLatch = new CountDownLatch(5);
当一个线程完成了自己的任务后,可以调用CountDownLatch的countDown()方法将计数器减1。其他线程可以通过调用CountDownLatch的await()方法来等待计数器变为0。
示例:
public class Demo25 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0;i < 5;i++){
Thread t = new Thread(() ->{
System.out.println(Thread.currentThread().getName()+" 执行任务");
countDownLatch.countDown();
});
t.start();
}
countDownLatch.await();
}
}
指定CoutDownLatch等待线程的数量为5,并创建5个线程. 线程执行完后执行countDown()方法. 并调用await()等待计数器变为0.
运行结果:
如果计数器的初始值大于等于等待的线程数量,会进入阻塞等待状态。
更改计数器的值为6,运行结果:
为了避免上述情况,可以使用await的一个重载版本来设置最大等待时间
8. 线程安全的集合类
Hashtable和ConcurrentHashMap:线程安全的哈希表实现,支持高并发的读写操作。
CopyOnWriteArrayList:线程安全的动态数组实现,适用于读多写少的场景
CopyOnWriteArraySet:线程安全的集合实现,基于CopyOnWriteArrayList,适用于读多写少的场景。
ConcurrentLinkedQueue:线程安全的无界队列实现java多线程框架有哪些,支持高并发的入队和出队操作。
BlockingQueue接口的实现类有: ArrayBlockingQueue、LinkedBlockingQueue、LinkedTransferQueue等,用于实现线程安全的阻塞队列。
ConcurrentSkipListMap:线程安全的跳表实现的有序映射表,支持高并发的读写操作。
ConcurrentSkipListSet:线程安全的跳表实现的有序集合,支持高并发的读写操作。
对于Hashtable和ConcurrentHashMap:
Hashtable并不建议使用. 它是用synchronized修饰方法.相当于对this进行加锁. 一个哈希表只有一个锁.
推荐使用ConcurrentHashMap. 这个类背后做了很多优化策略.
HashMap,Hashtable和ConcurrentHashMap的区别,这也是一个常见面试题.
回答这个问题. 可以从线程安全方面,HashMap是线程不安全的.Hashtable和ConcurrentHashMap是线程安全的,然后回答Hashtable和ConcurrentHashMap的区别. ConcurrentHashMap与Hashtable相比做了哪些改进等.
CopyOnWriteArrayList适用于读多写少的场景.
一般情况下,如果有的线程在进行写作操(修改),优点线程在读,很可能会读到修改了一半的数据.因此CopyOnWriteArrayList为了解决这个问题,就会把原来的数据复制一份,写操作就会在这个拷贝的数据上进行
但如果数据特别多/修改特别频繁,就不适合使用了
感谢你的观看!希望这篇文章能帮到你!
专栏: 《从零开始的Java学习之旅》在不断更新中,欢迎订阅!
“愿与君共勉,携手共进!”