JUC线程并发
JUC介绍: java.util.concurrent在并发编程中使用的工具类
进程:一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
线程状态:6种
新建 | 准备就绪 | 阻塞 | 不见不散 | 过时不候 | 终结 |
---|---|---|---|---|---|
NEW | RUNNABLE | BLOCKED | WAITING | TIMED_WAITING | TERMINATED |
lock锁
锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。
使用方法:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
lock锁于synchronized锁的区别:
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
生产者与消费者问题
8锁问题
/**
1 标准访问,先打印短信还是邮件
2 停4秒在短信方法内,先打印短信还是邮件
3 普通的hello方法,是先打短信还是hello
4 现在有两部手机,先打印短信还是邮件
5 两个静态同步方法,1部手机,先打印短信还是邮件
6 两个静态同步方法,2部手机,先打印短信还是邮件
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
运行答案:
1、短信
2、短信
3、Hello
4、邮件
5、短信
6、短信
7、邮件
8、邮件
*/
注意:集合使用多态写法,方便后期维护(只改变集合类型),除非调用本集合特有方法。
集合不安全
**原因:**读写不一致(读快写慢)
抛出异常:
//高并发异常(线程不安全)
java.util.ConcurrentModificationException
CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,
而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容器Object[] newElements里添加元素。
添加元素后,再将原容器的引用指向新的容器setArray(newElements)。
这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
Arraylist不安全
**底层:**数组(【索引】查询快,增删改慢)
-
vector
-
Collections工具类
List<Object> alist = Collections.synchronizedList(new ArrayList<>());
-
CopyOnWriteArrayList(写时复制一个,读写分离)
List<String> list = new CopyOnWriteArrayList<>();
Hashset不安全
底层(Hashmap):
//源码
public HashSet() {
map = new HashMap<>();
}
//add方法添加的是hashmap的key,value永远为固定的常量
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
解决办法:
- Collections工具类
Collections.synchronizedSet(new HashSet<>());
-
CopyOnWriteArraySet
Set<String> set = new CopyOnWriteArraySet<>();
Hashmap不安全
底层:Node(键值对)节点类型的单向链表+红黑树(hash冲突【8】)
初始值16(可能被改),负载因子0.75,等于16*0.75=12时扩容一倍16->32->64…
优化:增大初始值,避免频繁扩容。
-
ConcurrentHashMap();
// 线程安全的hashmap Map<String,String> map = new ConcurrentHashMap();
-
Collections工具类
Collections.synchronizedMap(new HashMap<String,String>());
BlockingQueueDemo阻塞队列
种类分析
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
BlockingQueue核心方法
抛出异常当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
特殊值插入方法,成功ture失败false移除方法,成功返回出队列的元素,
队列里没有就返回null一直阻塞当阻塞队列满时,生产者线程继续往队列里put元素,
队列会一直阻塞生产者线程直到put数据or响应中断退出当阻塞队列空时,消费者线程试图从队列里take元素,
队列会一直阻塞消费者线程直到队列可用超时退出当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出
JUC强大辅助工具类
CountDownLatch 减少计数
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
- 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
例子:最后一个人关门案例
CyclicBarrier 循环栅栏
字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,
- 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
- 直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
- 线程进入屏障通过CyclicBarrier的await()方法。
例子:七颗龙珠召唤神龙!
Semaphore 信号灯
在信号量上我们定义两种操作:
- acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),
-
要么一直等下去,直到有线程释放信号量,或超时。
- release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
- 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
例子:占用空车位,位置有限
传值练习
public class TestTransferValue {
public void changeValue(int age){
age=30;
}
public void changeValue1(Person person){
person.setName("xxx");
}
public void changeValue2( String S){
S="新址";
}
public static void main(String[] args) {
TestTransferValue test =new TestTransferValue();
int age=20;
test.changeValue(age);
System.out.println("age------>"+age);
System.out.println("========================");
Person person =new Person("abc");
test.changeValue1(person);
System.out.println(person.getName());
System.out.println("========================");
String S ="旧址";
test.changeValue2(S);
System.out.println(S);
}
}
//答案:
age------>20
========================
xxx
========================
旧址
线程池
优势:
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
主要特点:
线程复用;控制最大并发数;管理线程。
用处:
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
架构:
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
七大参数
核心线程数 | 最大线程数 | 工作队列 | 活跃时间 | 线程工厂(默认) | 活跃时间单位 | 拒绝策略 |
---|---|---|---|---|---|---|
corePoolSize | maximumPoolSize | workQueue | keepAliveTime | threadFactory | unit | handler |
底层原理
1、在创建了线程池后,开始等待请求。
2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
最大线程容量=队列数+池中最大线程数
代码实现
public class threadpool_test {
public static void main(String[] args) {
// ExecutorService threadpool = Executors.newFixedThreadPool(5);//一池固定5个线程
// ExecutorService threadpool = Executors.newSingleThreadExecutor();//一池个工作线程
ExecutorService threadpool = Executors.newCachedThreadPool();//可扩容。可伸缩,一池n的线程
try {
for (int i = 0; i < 10; i++) {
threadpool.execute(()-> System.out.println(Thread.currentThread().getName()+"执行任务!"));
}
}
catch (Exception e){
e.printStackTrace();
}finally {
threadpool.shutdown();
}
}
}
分支合并框架
相关类
ForkJoinPool
ForkJoinTask
RecursiveTask
Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并
四大函数式接口
Consumer<T()> 消费型接口
只有输入参数,无返回值
Supplier <T()> 供给型接口
没有参数,只有返回值
Function<T,R>函数型接口
传入参数T,返回类型R
Predicate<T()>断定型接口
有一个输入参数,返回值只能是布尔值!