java并发包与线程池
java.util.concurrent
- 并发是伴随着多核处理器的诞生而产生的,为了充分利用硬件资源,诞生了多线程技术。但是多线程又存在资源竞争的问题,引发了同步和互斥的问题
- JDK 1.5推出的java.util.concurrent(并发工具包)可以解决这些问题。
new Thread的弊端
- new Thread()新建对象,性能差
- 线程缺乏统一管理,可能无限制的新建线程,相互竞争,严重时会占用过多系统资源导致死机或OOM
线程池ThreadPool
- 重用存在的线程,减少对象对象、消亡的开销
- 线程总数可控,提高资源的利用率
- 避免过多资源竞争,避免阻塞
- 提供额外功能,定时执行、定期执行、监控等
线程池的种类
在java.util.concurrent中,提供了工具类Executors(调度器)对象来创建线程池,可创建的线程池有四种:
- CachedThreadPool - 可缓存线程池
- FixedThreadPool - 定长线程池
- SingleThreadExecutor - 单线程池
- ScheduledThreadPool - 调度线程池
可缓存线程池示例:
package threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolSample1 {
public static void main(String[] args) {
//调度器对象
//ExecutorService用于管理线程池
ExecutorService threadPool = Executors.newCachedThreadPool();//创建一个可缓存线程池
//可缓存线醒池的特点是,无限大,如果线程池中没有可用的线理则创建。有空闲线程则利用起来
for (int i = 1; i <= 1000; i++) {
final int index = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + index);
}
});
}
//threadPool.shutdownNow();立即终止线程池
threadPool.shutdown();
}
}
定长线程池的示例:
package threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolSample1 {
public static void main(String[] args) {
//调度器对象
//ExecutorService用于管理线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);//创建一个定长线程池
for (int i = 1; i <= 1000; i++) {
final int index = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + index);
}
});
}
//threadPool.shutdownNow();立即终止线程池
threadPool.shutdown();
}
}
单线程线程池示例:
单线程是指程序中只有一个线程在执行任务,执行完一个任务后才会执行下一个任务。单线程适用于一些简单的任务,例如顺序执行一些代码片段。
单线程池是单线程的一种实现方式,它将任务放入任务队列中,单个线程按照队列中的顺序依次执行任务。与单线程相比,单线程池可以更好地控制任务的执行顺序和频率,避免了线程启动和销毁的开销。但是,由于只有一个线程在执行任务,如果任务量过大或者某个任务阻塞,会导致整个线程池的执行性能降低。
package threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolSample1 {
public static void main(String[] args) {
//调度器对象
//ExecutorService用于管理线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();//创建一个单线程池
for (int i = 1; i <= 1000; i++) {
final int index = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + index);
}
});
}
//threadPool.shutdownNow();立即终止线程池
threadPool.shutdown();
}
}
CountDownLatch - 倒计时锁
- CountDownLatch倒计时锁特别适合“总-分任务”,例如多线程计算后的数据汇总
- CountDownLatch类位于java.util.concurrent (J.U.C) 包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了
执行原理
CountDownLatch是Java中一种同步工具,它可以使线程等待其他线程完成操作后再执行。CountDownLatch的原理是通过一个计数器来实现的,计数器初始值为n,当每个线程完成任务后,计数器的值减1。当计数器的值为0时,代表所有线程都已经完成任务,等待的线程就可以继续执行了。
在具体实现时,可以调用CountDownLatch的await()方法来阻塞当前线程,直到计数器的值为0才会继续执行。而其他线程完成任务后,调用CountDownLatch的countDown()方法来使计数器的值减1。当所有线程都完成任务后,计数器的值会变为0,此时等待的线程就可以继续执行了。
例如,假设有三个线程需要执行任务,可以用如下方式实现:
CountDownLatch latch = new CountDownLatch(3);
Thread worker1 = new Thread(new Worker(latch));
Thread worker2 = new Thread(new Worker(latch));
Thread worker3 = new Thread(new Worker(latch));
worker1.start();
worker2.start();
worker3.start();
latch.await();
System.out.println("All workers have completed their tasks.");
在这个例子中,CountDownLatch的初始值为3,代表有三个线程需要完成任务。每个线程完成任务后,都会调用latch.countDown()方法来使计数器的值减1。当计数器的值变为0时,latch.await()方法会返回,等待的主线程就可以继续执行。
示例
package threadPool;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownSample {
private static int count = 0;
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(100);
CountDownLatch countDownLatch = new CountDownLatch(10000);//总数和操作数一致
for (int i = 1; i <= 10000; i++) {
final int index = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
synchronized (CountDownSample.class) {
try {
count = count + index;
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();//计数器减1
}
}
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(count);
threadPool.shutdown();
}
}
Semephore信号量
Semephore信号量的作用
- Semaphore信号量经常用于限制获取某种资源的线程数量。
- 举个例子,比如说操场上有5个跑道,一个跑道一次只能有一个学生在上面跑步,一旦所有跑道在使用,那么后面的学生就需要等待,直到有一个学生不跑了
示例
package threadPool;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreSample {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
Semaphore semaphore = new Semaphore(5);//定义5个信号量,也就是说假务器只充许5个人在坚面究
for(int i=0;i<20;i++){
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();//获取一个信号量
play();
semaphore.release();//执行后释放
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
threadPool.shutdown();
}
public static void play(){
try {
System.out.println(new Date()+" "+Thread.currentThread().getName()+"获取资格");
Thread.sleep(1000);
System.out.println(new Date()+" "+Thread.currentThread().getName()+"退出");
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
semaphore.tryAcquire(2, TimeUnit.SECONDS)//尝试获取信号量,尝试等待2秒
package threadPool;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreSample {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
Semaphore semaphore = new Semaphore(5);//定义5个信号量,也就是说假务器只充许5个人在坚面究
for(int i=0;i<20;i++){
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
if(semaphore.tryAcquire(2, TimeUnit.SECONDS)){//尝试获取信号量,尝试等待2秒
play();
semaphore.release();//执行后释放
}else {
System.out.println(new Date()+" "+Thread.currentThread().getName()+"无空闲线程");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
threadPool.shutdown();
}
public static void play(){
try {
System.out.println(new Date()+" "+Thread.currentThread().getName()+"获取资格");
Thread.sleep(1000);
System.out.println(new Date()+" "+Thread.currentThread().getName()+"退出");
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
CyclicBarrier循环屏障
- CyclicBarrier是一个同步工具类,它允许一组线程互相等待,直到到达某个公共屏障点。
- 与CountDownLatch不同的是,barrier在释放等待线程后可以重用,所以称它为循环(Cyclic)的屏障(Barrier)
示例
package threadPool;
import java.util.Date;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CyclicBarrierSample {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 1; i <= 20; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
executorService.execute(new Runnable() {
@Override
public void run() {
go();
}
});
}
executorService.shutdown();
}
private static void go() {
System.out.println(new Date() + " " + Thread.currentThread().getName() + "准备就绪");
try {
cyclicBarrier.await();
System.out.println(new Date() + " " + Thread.currentThread().getName() + "开始就绪");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
}
重入锁
- 重入锁是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞
- ReentrantLock设计的目标是替代synchronized关键字
与synchronized区别
示例
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
public class test {
private static int count = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(100);
CountDownLatch countDownLatch = new CountDownLatch(10000);//总数和操作数一致
for (int i = 0; i <= 10000; i++) {
int finalI = i;
executorService.execute(() -> {
lock.lock();
count = count + finalI;
countDownLatch.countDown();//计数器减1
lock.unlock();
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(count);
executorService.shutdown();
}
}
Condition条件唤醒
- 在并行程序中,某些线程要预先规定顺序执行,例如先新增再修改、先买后卖、先进后出…对于这类场景,使用JUC的Condition对象再合适不过了。
JUC中提供了Condition对象,用于让指定线程等待与唤醒,按预期顺序执行。
它必须和ReentrantLock重入锁配合使用。 - Condition用于替代wait()/notify()方法
- notify只能随机唤醒等待的线程,而Condition可以唤醒指定的线程,这有利于更好的控制并发程序
Condition核心方法
- await() - 阻塞当前线程,直到singal唤醒
- signal() - 唤醒被await的线程,从中断处继续执行
- signalAll() - 唤醒所有被await()阻塞的线程
示例
package threadPool;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionSamole {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
condition1.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("花落知多少");
lock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
condition2.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("夜来风雨声");
condition1.signal();
lock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
condition3.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("处处闻啼鸟");
condition2.signal();
lock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("春眠不觉晓");
condition3.signal();
lock.unlock();
}
}).start();
}
}
Callable与Future
- Callable和Runnable一样代表着任务,区别在于Callable有返回值并且可以抛出异常。
- Future是一个接口,用于表示异步计算的结果。提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果
示例
package threadPool;
import java.util.concurrent.*;
public class FutureSample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 2; i <= 10000; i++) {
Computor c = new Computor();
c.setNum(i);
//future用于计算线程的监听,因为计算在其他线程执行,返回是异步的
Future<Boolean> future = executorService.submit(c);
try {
Boolean result = future.get();//获取返回值,没有则等待
if(result){
System.out.println(c.getNum());
}
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}
}
class Computor implements Callable<Boolean> {
private Integer num;
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
@Override
public Boolean call() {
boolean isprime = true;
for (int i = 2; i < num; i++) {
if (num % i == 0) {
isprime = false;
break;
}
}
return isprime;
}
}
并发容器
线程安全的类
- Vector是线程安全的,ArrayList、LinkedList是线程不安全的
- Properties是线程安全的,HashSet、TreeSet是不安全的
- StringBuffer是线程安全的,StringBuilder是线程不安全的
- HashTable是线程安全的,HashMap是线程不安全的
线程安全-并发容器
- ArrayList -> CopyOnWriteArrayList —写复制列表
- HashSet -> CopyOnWriteArraySet ——写复制集合
- HashMap -> ConcurrentHashMap ——分段锁映射
示例
CopyOnWriteArrayList
package threadPool;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListSample {
public static void main(String[] args) {
//List<Integer> list = new ArrayList<>();//如果用普通arraylist,会报读写并发错误
List<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
list.add(i);
}
for (Integer i : list) {
list.remove(i);
}
System.out.println(list);
}
}
CopyOnWriteArrayList是一种线程安全的List,它的读操作不需要进行同步,而写操作则需要进行同步。它的原理是,写操作时先将List复制一份,然后在新复制的List上进行修改,修改完成后再将原List替换为新List。这种写操作虽然需要进行复制操作,但由于读操作不需要同步,因此读操作的性能非常高。
以下是CopyOnWriteArrayList的实现原理:
-
CopyOnWriteArrayList内部维护了一个数组,读操作直接返回该数组,不需要进行同步。
-
写操作时,先将数组复制一份,然后在新数组上进行修改。这样,读操作可以继续在旧数组上进行,不会受到写操作的影响。
-
修改完成后,再将原数组替换为新数组。这样,读操作就能看到最新的修改结果了。
-
在复制数组时,CopyOnWriteArrayList使用了可重入锁来保证线程安全。
由于CopyOnWriteArrayList需要进行数组复制,因此写操作的性能比较低。因此,它适用于读操作比写操作多得多的场景,比如缓存。
ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author user
*/
public class test {
private static ConcurrentHashMap count = new ConcurrentHashMap();
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
int finalI = i;
executorService.execute(() -> {
count.put(finalI, finalI);
});
}
executorService.shutdown();
System.out.println(count.size());
}
}
ConcurrentHashMap 是 Java 中的一个高效的并发 Hash 表,它支持同时多个线程访问它的查找与更新操作,并且不会阻塞其他操作。ConcurrentHashMap 的实现原理主要包括以下两个方面:
- 分段锁机制
ConcurrentHashMap 内部维护了多个 Segment,每个 Segment 都是一个 Hash 表,它们是相对独立的,也就是说每个 Segment 都可以被不同的线程访问,不同的线程可以同时访问不同的 Segment,这样可以显著提高并发性能。在 ConcurrentHashMap 中,锁的粒度被降低到了 Segment 级别,而不是对整个 Hash 表加锁。
- CAS 操作
ConcurrentHashMap 在执行插入和删除操作时,使用了 CAS(Compare-And-Swap)操作来保证数据的一致性。在插入或者删除一个元素时,如果该元素所在的位置已经被其他线程更新了,那么 CAS 会失败,防止多个线程同时修改同一个元素引起的数据不一致问题。
综上所述,ConcurrentHashMap 采用了分段锁机制和 CAS 操作来保证并发安全性,同时支持高效的并发访问。
Atomic与CAS算法(乐观锁)
原子性
指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行。
Atomic包
- Atomic包是java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类。
- Atomic常用类
- AtomicInteger
- AtomicIntegerArray
- AtomicBoolean
- AtomicLong
- AtomicLongArray
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class test {
private static AtomicInteger count = new AtomicInteger();
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
executorService.execute(() -> {
count.getAndIncrement();//count++
});
}
executorService.shutdown();
System.out.println(count);
}
}
CAS算法
- 锁是用来做并发最简单的方式,当然其代价也是最高的。
- 独占锁是一种悲观锁。synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
- 乐观锁,就是每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。其中CAS (Compare AndSwap,比较与交换)是一种有名的无锁算法。
Atomic的应用场景
- 虽然基于CAS的线程安全机制很好很高效,
- 但要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小型,例如计数器等需求,否则也不会有锁的存在了。