**
一、Concurrent概述
**
Concurrent包是jdk5中开始提供的一套并发编程包,其中包含了大量和多线程开发相关的工具类,大大的简化了java的多线程开发,在高并发及分布式场景下应用广泛。Concurrent是java.util下的包。
二、BlockingQueue 阻塞式队列
1、概述
阻塞式队列是一种队列数据结构,和其他队列比起来,多了阻塞机制,从而可以在多个线程之间进行存取队列的操作,而不会有线程并发安全问题.所以称之为阻塞式队列。
可以简单的理解为,阻塞式队列是专门用来在多个线程间通过队列共享数据。
` 在阻塞式队列中,(1)如果队列满了,仍然有线程向其中写入数据,则这次写入操作会被阻塞住,直到有另外的线程从队列中消费了数据,队列有了空间,阻塞才会被放开,写入操作才可以执行;(2)同理,如果队列是空的,仍然有线程从队列中获取数据,则读取操作将会被阻塞住,直到有另外的线程向队列中写入了数据,队列不再为空,阻塞才会被放开,读取操作才可以执行。
可以发现:阻塞式队列通过阻塞机制,协调了多个线程在一个队列上的读写操作。
2、继承结构
|java.util.concurrent
|接口 BlockingQueue
|java.util.concurrent
|类 ArrayBlockingQueue
|java.util.concurrent
|类 LinkedBlockingQueue
其中,
ArrayBlockingQueue是一个有界的阻塞队列,底层是数组。有界也就意味着,它不能存储无限多的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了。其内部以FIFO(先进先出)的顺序对元素进行存储。
LinkedBlockingQueue 底层是链表,可以在创建时指定大小,也可以不指定,则将使用Integer.MAX_VALUE作为上限。
DelayQueue对元素进行持有直到一个特定的延迟到期,注意其中的元素必须实现Delayed接口。DelayQueue将会在每个元素的getDelay()方法返回的值的时间段之后才释放掉该元素。如果返回的是0或者负数,延迟将被认为过期,该元素将会在DelayQueue的下一次take被调用时被释放。
SynchronousQueue是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。
3、重要方法
抛出异常 | 特殊值 | 阻塞 | 超时 | |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查 | element() | peek() | 不可用 | 不可用 |
上表解释:
在BlockingQueue中的插入、移除和检查数据有四组方法,这四组方法在处理队列满时的插入操作和队列空时的获取操作会有不同的处理机制。
分别为:
抛出异常
返回特殊值
产生阻塞 (阻塞具有超时时间,一旦超过指定时间,阻塞自动放开)
在使用BlockingQueue过程中 可以根据需要选择对应的方法。
4、以put()、take()为例:
public class BlockingQueue1 {
public static void main(String[] args) throws Exception {
//1.创建阻塞式队列
BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
//2.存入数据
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
}
}
//消费者
class Consumer implements Runnable{
private BlockingQueue<String> queue = null;
public Consumer(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while(true){
Thread.sleep(2000);//2秒
String s = queue.take();//移除
System.out.println("消费者消费了"+s);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//生产者
class Producer implements Runnable{
private BlockingQueue<String> queue = null;
public Producer(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
int i = 0;
while(true){
Thread.sleep(5000);//5秒
int n = ++i;
queue.put("a"+n);//插入
System.out.println("生产者生产了a"+n);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、ConcurrentMap 同步map
1、ConcurrentMap概述
ConcurrentMap是一个线程安全的Map,可以防止多线程并发安全问题。
2、ConcurrentMap与HashTable的比较:
HashTable也是线程安全的,但是ConcurrentMap性能要比HashTable好的多,所以推荐使用ConcurrentMap。原因如下:
(1)ConCurrentMap的锁更加精细
HashTable加锁是锁在整个HashTable上,一个线程操作时其他线程无法操作,性能比较低;ConcurrentMap加锁是将锁加在数据分段(桶)上,只锁正在操作的部分数据,所以效率高。
(2)ConCurrentMap引入了读写锁机制
在多线程并发操作的过程中,多个并发的读其实不需要隔离,但只要有任意一个写操作,就必须隔离。HashTable没有考虑一点,无论什么类型的操作,直接在整个HashTable上加锁。ConcurrentMap则区分了读写操作,读的时候加读锁,写的时候加写锁,读锁和读锁可以共存,写锁和任意锁都不能共存,从而实现了在多个读的过程中不会隔离 提高了效率。
补充:
Hashtable的最大特点是散列,注意他不是唯一的,只是重复的概率极低。
3、ConcurrentMap的继承结构
java.util.concurrent
接口 ConcurrentMap<K,V>
java.util.concurrent
类 ConcurrentHashMap<K,V>
4、涉及代码
ConcurrentMap<String,String>map = new ConcurrentHashMap<>();//创建concurrentMap
map.put("name", "zs");//存入数据
map.put("addr","bj");
System.out.println(map.get("name"));//取出数据
System.out.println(map.get("addr"));
四、CountDownLatch 闭锁
1、闭锁概述
java.util.concurrent.CountDownLatch是一个并发构造,实现协调某个线程阻塞直到其他若干线程执行达到一定条件才放开阻塞继续执行的效果。
2、重要的API
(1)构造方法CountDownLatch(int count) 在构造的过程中直接传入一个数字作为闭锁的计数器的初始值。在闭锁上调用此方法,可以阻塞当前线程,阻塞到CountDownLatch中的计数器count值变为0,自动放开阻塞。
(2).await() 调用此方法可以将闭锁中的计数器数值-1,如果减到零,await的阻塞会自动放开
(3)countDown() 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
3、代码
以做饭为例,需要先执行完买米买菜的线程才能执行做饭的线程。
public class CountDownLatch{
public static void main(String[] args) {
//1.创建闭锁 计数器初始值设置为3
CountDownLatch cdl = new CountDownLatch(2);
//2.创建线程传入闭锁,并执行线程
new Thread(new MaiMi(cdl)).start();
new Thread(new MaiCai(cdl)).start();
new Thread(new ZuoFan(cdl)).start();
}
}
class ZuoFan implements Runnable{
private CountDownLatch cdl = null;
public ZuoFan(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
try {
//--调用await等待 达到执行条件
cdl.await();
System.out.println("开始做饭...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MaiMi implements Runnable{
private CountDownLatch cdl = null;
public MaiMi(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("米买回来了...");
//--在闭锁上-1
cdl.countDown();
}
}
class MaiCai implements Runnable{
private CountDownLatch cdl = null;
public MaiCai(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("菜买回来了...");
cdl.countDown();
}
}
五、ExecutorServcie 执行器服务
1、ExecutorServcie 概述
ExecutorService是Concurrent包下提供的一个接口,ExecutorService实现就是一个线程池实现。
所谓的池就是用来重用对象的一个集合,可以减少对象的创建和销毁,提高效率。
而线程本身就是一个重量级的对象,线程的创建和销毁都是非常耗费资源和时间,所以如果需要频繁使用大量线程,不建议每次都创建线程销毁线程,所以利用线程池的机制,实现线程对象的共享,提升程序的效率。
2、ExecutorService用法—通过ThreadPoolExecutor实现类创建线程池
(1)创建
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
用给定的初始参数和默认的线程工厂创建新的 ThreadPoolExecutor。
(2)各个参数的意义
int corePoolSize—核心池的大小,就是正式线程的数量和
int maximumPoolSize—最大池大小,正式线程数量和临时线程的总数
long keepAliveTime—空闲的多余线程保持时间,临时线程不执行能够存活的时间
TimeUnit unit,—时间单位
BlockingQueue workQueue—WQ(n)—阻塞式队列,正式线程都忙时将线程放到WQ中,WQ大小是n。
RejectedExecutionHandler handler—拒绝服务助手,当正式线程和临时线程都在执行,而且WQ中也是满的时候,就由他来处理后续来的请求。
(3)线程池的工作方式
a.在线程池刚创建出来时,线程池中没有任何线程,当有任务提交过来时,如果线程池中管理的线程的数量小于corePoolSize,则无论是否有闲置的线程都会创建新的线程来使用.而当线程池中管理的线程的数量达到了corePoolSize,再有新任务过来时,会复用闲置的线程.
b.当所有的核心池大小中的线程都在忙碌,则再有任务提交,会存入workQueue中,进行排队,当核心池大小中的线程闲置后,会自动从workQueue获取任务执行
c.而当所有的核心池大小中的线程都在忙碌,workQueue也满了,则会再去创建新的临时线程来处理提交的任务,但是,无论如何,总的线程数量,不允许超过maximumPoolSize
d.而当所有的核心池大小中的线程都在忙碌,workQueue也满了,也创建了达到了maximumPoolSize的临时线程,再有任务提交,此时会交予RJHandler来拒绝该任务
e.当任务高峰过去,workQueue中的任务也都执行完成,线程也依次闲置了下来,则在此时,会将闲置时间超过keepAliveTime(单位为unit)时长的线程关闭掉,但是关闭时会至少保证线程池中管理的线程的数量 不少于corePoolSize个
3、ExecutorService用法—通过Executors工具类的静态方法创建线程池
(1)重要的三种API
a. newCachedThreadPool()
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
corePoolSize=0
maximumPoolSize = Integer.MaxValue
keepAliveTime = 60
TimeUnit = Seconds
特点:擅长处理大量短任务的线程池。
b.newCachedThreadPool()
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
corePoolSize=1
maximumPoolSize = 1
workQueue = new LinkedBlockingQueue<Runnable>())
特点:实现使用单一线程处理任务,多个任务在无界的阻塞式队列中排队等待处理
c.newFixedThreadPool(int nThreads)
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运
行这些线程
corePoolSize=nTHreads
maximumPoolSize = nTHreads
workQueue = new LinkedBlockingQueue<Runnable>())
特点:可以实现使用指定数量的线程处理任务,多个任务在无界的阻塞式队列中排队等待处理。
4、创造线程池代码
public class ExecutorService {
public static void main(String[] args) {
//1.手动创建线程池
ExecutorService s1 = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5)
, new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("拒绝了..["+r+"]");
}
});
//2.利用工具类的静态方法快速创建线程池
ExecutorService s2 = Executors.newCachedThreadPool();
ExecutorService s3 = Executors.newSingleThreadExecutor();
ExecutorService s4 = Executors.newFixedThreadPool(5);
}
}
手动创建线程池的好坏:
可以自定义,但是设置的参数不一定是运行效果最优的。
5、向线程池中提交任务
(1)execute(Runnable)
最普通的提交任务的方法,直接传入一个Runnable接口的实现类对象,即可要求线程池取执行这个任务,这种方式提交的任务无法监控线程的执行 也无法在线程内向调用者返回返回值.
实例代码:
s.execute(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("run.."+i);
}
}
});
(2)submit(Runnable)
向线程池提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future,可以通过Future对象的get()方法来检测线程是否执行结束,如果线程未执行结束get()方法将会阻塞。
Future<?> future = s.submit(new Runnable() {
@Override
public void run() {
try {
for(int i=0;i<10;i++){
System.out.println("run.."+i);
}
Thread.sleep(5000);
System.out.println("Runnable结束了..");
} catch (Exception e) {
e.printStackTrace();
}
}
});
future.get();
System.out.println("main线程结束...");
(3)submit(Callable)
和上面submit(Runnable)方法非常类似,只不过这个方法传入的是Callable接口的实现类,Callable接口功能和Runnable接口基本一致,唯一的不同在于,内部的方法叫call,且可以返回返回值,这个返回值可以通过Future对象通过get()方法得到
Future<String> future = s.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("子线程开始执行了..");
Thread.sleep(2000);
System.out.println("子线程执行结束了..");
return "111";
}
});
String str = future.get();
System.out.println(str);
(4)invokeAny(Collection<? extends Callable<T>> tasks)
可以接收若干Callable组成的集合,此方法将会自动从中选择任意一个执行
List<Callable> list = new ArrayList<>();
list.add(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "111";
}
});
list.add(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "222";
}
});
String str = s.invokeAny((Collection<? extends Callable<String>>) list);
System.out.println(str);
(5)invokeAll(Collection<? extends Callable<T>> tasks)
可以接收若干Callable组成的集合,此方法将会执行所有的Callable将结果组成集合返回
List<Callable> list = new ArrayList<>();
list.add(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "111";
}
});
list.add(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "222";
}
});
List<Future<String>> rs = s.invokeAll((Collection<? extends Callable<String>>) list);
for(Future<String> f : rs){
System.out.println(f.get());
}
6、关闭线程池
线程池中维护了大量线程,很耗费资源,所以当使用线程池结束时,应该手动关闭线程池,释放资源。虽然放在main函数中的线程运行完了,但是new的线程运行完放在线程池中了,并没有销毁,所以进程并不会停止。
重要的API:
(1)shutdown()
即使调用也不会立即关闭所有线程,而是不再接收新的任务,之前已经提交但尚未完成执行的线程仍然会继续执行,直到所有的任务都执行完,线程池关闭所有线程,退出.
(2)shotdownNow()
调用此方法时,会立即关闭所有线程,退出线程池,这种方式虽然可以立即推出线程池,但是正在执行的线程有可能被意外的中断,造成意想不到的问题。
六、Lock
1、Lock锁概述
java.util.concurrent.locks.Lock 是一个类似于synchronized 块的线程同步机制.但是 Lock比 synchronized 块更加灵活。Lock是个接口,有个实现类是ReentrantLock
2、重要方法
方法 | 功能 |
---|---|
ReentrantLock() | 创建一个 ReentrantLock 的实例。 |
ReentrantLock(boolean fair) //先来的先得资源 | 创建一个具有给定公平策略的 ReentrantLock。 |
lock() | 获取锁 |
unlock() | 试图释放锁 |
3、lock和syncronized的区别
(1)lock本身就是锁,不需要syncronized寻找指定锁对象。
(2)lock可以配置公平策略,实现线程按照先后顺序获取锁。
(3)提供了trylock方法 可以试图获取锁,获取到或获取不到时,返回不同的返回值 让程序可以灵活处理。
(4)lock()和unlock()可以在不同的方法中执行,可以实现同一个线程在上一个方法中lock()在后续的其他方法中unlock(),比syncronized灵活的多。
4、读写锁
对于多线程并发安全问题,其实只在涉及到并发写的时候才会发生,多个并发的读并不会有线程安全问题,所以在Concurrent包中提供了读写锁的机制,可以实现,分读锁和写锁来进行并发控制.多个读锁可以共存,而写锁和任意锁都不可共存,从而实现多个并发读并行执行提升效率,而任意时刻写都进行隔离,保证安全.这是一种非常高效而精细的锁机制。
(1)继承结构
java.util.concurrent.locks
接口 ReadWriteLock
java.util.concurrent.locks
类 ReentrantReadWriteLock
(2)重要的API
方法 | 功能 |
---|---|
ReentrantReadWriteLock() | 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock |
ReentrantReadWriteLock(boolean fair) | 使用给定的公平策略创建一个新的 ReentrantReadWriteLock |
ReentrantReadWriteLock.ReadLock readLock() | 返回用于读取操作的锁。 |
ReentrantReadWriteLock.WriteLock writeLock() | 返回用于写入操作的锁。 |
补充:
并发安全问题的三个条件:共享资源+写操作+多线程操作