JUC学习记录
JUC是什么
java.util.concurrent在并发编程中使用的工具类
atomic:原子
locks:锁
基本概念:
进程:
进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
并发
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并行
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生
高内聚低耦合:
高内聚就是说相关度比较高的部分尽可能的集中,不要分散
低耦合就是说两个相关的模块尽可以能把依赖的部分降低到最小,不要让两个系统产生强依赖
线程状态
新建,就绪,阻塞,等待,终止
wait()和sleep()的区别
sleep来自Thread类,和wait来自Object类。
最主要是sleep方法没有释放锁,而wait方法释放了锁。
wait() 方法必须在synchronized 同步代码中,sleep() 则不需要
wait() 方法要结合 notify() 方法或者 notifyAll() 方法一起使用,不然就会一直阻塞线程。
Lock接口
Synchronized
在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。
在不使用Lock的情况下实现同步,一般使用synchronized(同步锁)
高内聚低耦合的前提下,线程 操作(对外调用方法) 资源类
步骤:1、创建资源类 2、资源类里创建同步方法,同步代码块
lock(java.util.concurrent.locks)
锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。
Lock接口的实现ReentrantLock可重入锁
使用方式:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
创建线程的方式
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池例如用Executor框架
1. 继承Thread `public class SaleTicket extends Thread`
2. new Thread `Thread t1 = new Thread(); t1.start();`
3. **Thread(Runnable target, String name)**
### 实现runnable方法
4. 新建类实现runnable接口
```bash
class MyThread implements Runnable//新建类实现runnable接口
new Thread(new MyThread,...)
```
5. 匿名内部类
```bash
new Thread(new Runnable() {
@Override
public void run() {
}
}, "your thread name").start();
这种方法不需要创建新的类,可以new接口
```
**6. Lambda表达式**
```bash
new Thread(() -> {
}, "your thread name").start();
```
具体代码:
```bash
//资源类
class Ticket {
private int number = 100;
private Lock lock = new ReentrantLock();//使用重入锁
public void sale() {
lock.lock();
try {
if (number > 0 ) {
System.out.println(Thread.currentThread().getName()+"\t 卖出第:"+ (number--)+"\t 还有:"+number);
}else {
System.out.println("票售空");
}
}finally {
lock.unlock();
}
}
}
public class SaleTicket {
public static void main(String[] args) throws Exception {
Ticket ticket = new Ticket();
new Thread(() -> { for (int i = 0; i <= 101; i++) { ticket.sale(); } }, "A").start();
new Thread(() -> { for (int i = 0; i <= 101; i++) { ticket.sale(); } }, "B").start();
new Thread(() -> { for (int i = 0; i <= 101; i++) { ticket.sale(); } }, "C").start();
}
}
Java8之lambda表达式
lambda表达式是什么
Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或剪头操作符。它将 Lambda 分为两个部分:
左侧:指定了 Lambda 表达式需要的所有参数
右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能
interface Foo {
public void say();
}
public class LambdaExpress {
public static void main(String[] args) {
Foo foo = () -> {
System.out.println("1"); };
foo.say();
}
}
使用要求:
lambda表达式,如果一个接口只有一个方法,我可以把方法名省略。
写法:
拷贝小括号(形参类表),写死右箭头->,落地大括号{方法实现}
函数式接口
只包含一个抽象方法的接口,称为函数式接口
lambda表达式,必须是函数式接口,必须只有一个方法。如果接口只有一个方法java默认它为函数式接口。为了正确使用Lambda表达式,需要给接口加个注解:@FunctionalInterface如有两个方法,立刻报错。
Runnable接口为什么可以用lambda表达式?
接口里是否能有实现方法?
Java 8 以后,可以
- 使用default关键字定义方法
@FunctionalInterface interface Foo { public void say(); default int div(int a,int b) { return a+b; } }
接口中的default方法可以有多个
2. 静态方法实现
@FunctionalInterface
interface Foo {
public static int div2(int a, int b){
return a+b;
}
}
静态方法实现也可以有多个。注意调用的时候使用类名.方法名调用。
线程间通信
线程间通信:1、生产者+消费者 2、通知等待唤醒机制
口诀:
- 高内聚低耦合前提下,线程 操作 资源类
- 交互:判断/干活/通知
- 多线程交互中,必须要防止线程的虚假唤醒,即判断不能用if,只能用while
- 注意标志位的修改
1. 使用synchronized实现(对象监视器)
/**
* 现在有两个线程,可以操作初始值为零的一个变量,
* 实现一个线程对该变量+1,一个线程对该变量-1,
* 实现交替,10轮,变量初始值为0。
* 1. 高内聚低耦合前提下,线程 操作 资源类
* 2. 交互:判断/干活/通知
* 3. 多线程交互中,必须要防止线程的虚假唤醒,即判断不能用if,只能用while
*/
class Air {
private int num = 0;
public synchronized void incr() throws InterruptedException {
//+
//判断, 不是0,就++,是0,就等待
while (num != 0) {
this.wait();
}
//干活
num++;
System.out.println(Thread.currentThread().getName()+"\t"+num);
//通知
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//-
//判断, 是0,就--,不是0,就等待
while (num == 0) {
this.wait();
}
//干活
num--;
System.out.println(Thread.currentThread().getName()+"\t"+num);
//通知
this.notifyAll();
}
}
public class ThreadWaitNotifyDemo {
public static void main(String[] args) throws Exception {
Air air = new Air();
new Thread(()->{
for (int i = 0; i <10; i++) {
try {
air.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i <10; i++) {
try {
air.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
2. 使用Lock实现(Java8新)
Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法( wait , notify和notifyAll 的使用。
class Air {
private int num = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void incr() throws InterruptedException {
//+
lock.lock();
try {
while (num != 0) {
//判断
//this.wait();
condition.await();
}
//干活
num++;
System.out.println(Thread.currentThread().getName()+"\t"+num);
//通知
//this.notifyAll();
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}
finally {
lock.unlock();
}
}
public void decr() throws InterruptedException {
//+
lock.lock();
try {
while (num == 0) {
//判断
//this.wait();
condition.await();
}
//干活
num--;
System.out.println(Thread.currentThread().getName()+"\t"+num);
//通知
//this.notifyAll();
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
3. Condition精准通知
1、有顺序通知,需要有标识位
2、有一个锁Lock,3把钥匙Condition
3、判断标志位
4、输出线程名+第几次+第几轮
5、修改标志位,通知下一个
/**
* 精准通知
* 多线程之间按顺序调用,实现A->B->C
*
* AA打印5次,BB打印10次,CC打印15次
* 共10轮
* 1. 高内聚低耦合前提下,线程 操作 资源类
* 2. 交互:判断/干活/通知
* 3. 多线程交互中,必须要防止线程的虚假唤醒,即判断不能用if,只能用while
* 4. 注意标志位
*/
class ShareResource {
private int num = 1; //1:A 2:B 3:C
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5(){
lock.lock();
try {
//判断
while (num != 1) {
condition1.await();
}
//干活
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//通知
//修改标志位
num = 2; //B
condition2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print10(){
lock.lock();
try {
//判断
while (num != 2) {
condition2.await();
}
//干活
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//通知
//修改标志位
num = 3; //B
condition3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print15(){
lock.lock();
try {
//判断
while (num != 3) {
condition3.await();
}
//干活
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//通知
//修改标志位
num = 1; //B
condition1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class ThreadOrderAccess {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareResource.print5();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareResource.print10();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareResource.print15();
}
},"C").start();
}
}
多线程锁
问题:
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、邮件
解释说明:
1、2、一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法。
3、加个普通方法后发现和同步锁无关,
4、换成两个对象后,不是同一把锁了,情况立刻变化。
5、synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,
所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁——类对象本身,
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
而不管是同一个实例对象的静态同步方法之间,
还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
线程不安全Demo
ArrayList
- 故障:java.util.ConcurrentModificationException 并发修改异常
- 原因:ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常 并发修改异常
- 解决:
3.1 使用Vector 线程同步 有synchronized同步锁 new Vector<>(),但是效率低。
3.2 线程安全包装方法 Collections.synchronizedList(new ArrayList<>())
3.3 写时复制 CopyOnWriteArrayList<>()
CopyOnwriteArrayList:讲解
private static void listNotSafe() {
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(list);
},String.valueOf(i)).start();
}
}
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
Set
HashSet的底层是HashMap,它的add方法调用的是put方法,存放在key的位置,而value位置存放的是Object类型的常量。
使用CopyOnWriteArraySet
public HashSet() {
map = new HashMap<>();
}
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
举例:
private static void setNotSafe() {
/**
* HastSet底层是HashMap,add()方法调用的是put()方法,存在key的位置,而value处是一个object类型的常量
*/
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> { set.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(set);
},String.valueOf(i)).start();
}
}
Map
HashMap的底层是node数组,node单链表,红黑二叉树
使用ConcurrentHashMap
public static void main(String[] args) {
Map<String, String> map = new ConcurrentHashMap<>();
// Collections.synchronizedMap();
for (int i = 0; i < 30; i++) {
new Thread(() -> { map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(4,0));
System.out.println(map);
},String.valueOf(i)).start();
}
}
Callable
是获取线程的第三种方式,实现Callable;它是函数式接口,可以使用lambda表达式或方法引用的赋值对象。
之前学习中创建线程的两种方式:一种是直接继承Thread,另外一种就是实现Runnable接口。
这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
而Callable能调用get方法获取返回值。
与Runnable接口实现方法对比
class MyThread implements Runnable {
@Override
public void run() {
}
}
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 666;
}
}
面试题:callable接口与runnable接口的区别?
- 是否有返回值
callable 是
runnable 否 - 是否抛异常
callable 是
runnable 否 - 落地方法不一样
runnable 是run
callable 是call
使用
不可以直接替换Runnable,因为:thread类的构造方法根本没有Callable
错误:
new Thread(() -> {
},"callable thread").start();
正确:
使用FutureTask(未来任务) 异步调用,它实现了Runnable。
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
try {
TimeUnit.SECONDS.sleep(4);
}catch (Exception e) {
e.printStackTrace();
}
System.out.println("callable interface");
return 666;
}
}
/**
* java多线程中获得多线程的方式
* 1. 继承Thread
* 2. 实现Runnable
* 3. 实现Callable
* 3.1 get方法一般放在最后一行
* 4. 线程池
*/
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// java多态,一个类可以实现多个接口
// FutureTask(未来任务) 异步调用,它实现了Runnable ;FutureTask(Callable<V> callable)
FutureTask futureTask = new FutureTask(new MyThread2());
new Thread(futureTask,"A").start();
System.out.println(Thread.currentThread().getName()+"######");
System.out.println(futureTask.get());
}
}
FutureTask是什么
为什么使用FutureTask
RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
原理
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,
就不能再重新开始或取消计算。get方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态, 然后会返回结果或者抛出异常。
FutureTask在高并发环境下确保任务只执行一次
get方法放到最后调用。
JUC强大的辅助类讲解
CountDownLatch减少计数
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
- 其它线程调用countDown方***将计数器减1(调用countDown方法的线程不会阻塞),
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"走了");
countDownLatch.countDown();// 计数器-1
},String.valueOf(i)).start();
}
countDownLatch.await();// 阻塞
System.out.println(Thread.currentThread().getName()+"门卫走了");
}
}
CyclicBarrier循环栅栏
- CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是
- 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
- 线程进入屏障通过CyclicBarrier的await()方法。
public class CyclicBarrierDemo {
public static void main(String[] args) {
// CyclicBarrier(int parties, Runnable barrierAction)
CyclicBarrier cyclicBarrier = new CyclicBarrier(5,() -> {
System.out.println("gogo");
});
for (int i = 1; i <= 5; i++) {
final int T = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t "+T);
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
Semaphore信号灯
在信号量上我们定义两种操作:
- acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
- release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
- 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);//模拟空位
for (int i = 1; i <=6 ; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t占了车位");
try {
TimeUnit.SECONDS.sleep(3); }catch (Exception e) {
e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
ReentrantReadWriteLock 读写锁
- 读写锁 ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入
- 一次只有一个线程(写)可以修改共享数据,
- 在许多情况下,多个线程可以同时读取数据。
- 读–读 能共存
- 读–写 不能共存
- 写–写 不能共存
/**
* 读写锁 ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入
* 一次只有一个线程(写)可以修改共享数据,
* 在许多情况下,多个线程可以同时读取数据。
* 读--读 能共存
* 读--写 不能共存
* 写--写 不能共存
*/
class MyCache{
/**
* 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
* 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
* 2)禁止进行指令重排序。
*/
private volatile Map<String,Object> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
// 写锁
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"\t 正在写"+key);
//暂停一会儿线程
try {
TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {
e.printStackTrace(); }
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t 写完了"+key);
}catch (Exception e) {
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"\t 正在读"+key);
try {
TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {
e.printStackTrace(); }
Object result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t 读完了"+result);
}catch (Exception e) {
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
BlockingQueue阻塞队列
栈与队列
栈:先进后出
队列:先进先出
阻塞队列
阻塞:必须要阻塞/不得不阻塞
线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素
说明:
当队列是空的,从队列中获取元素的操作将会被阻塞,直到其他线程往空的队列插入新的元素
当队列是满的,从队列中添加元素的操作将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
用处
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。
为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
架构
种类:
BlockingQueue核心方法
所以在使用Queue时,尽量避免使用collection的add和remove方法。
线程池
线程池的优势
线程池的优势:
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用;控制最大并发数;管理线程
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的创建
架构
代码
private static void initPool() {
/**
* Arrays.copyOf(); 数组工具类
* Collections.synchronizedList(); 集合工具类
* Executors.newFixedThreadPool(); 也是工具类
*/
//ExecutorService threadPool = Executors.newFixedThreadPool(5);//1池固定数线程
//ExecutorService threadPool = Executors.newSingleThreadExecutor();//1池1线程
ExecutorService threadPool = Executors.newCachedThreadPool(); //可扩容
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
以上三个方法,都实现了ThreadPoolExecutor
线程池七大重要参数
最大线程数maximumPoolSize的规定方式:
- CPU密集型
对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:线程数= CPU核数+1 - IO密集型
对于IO密集型的应用,就很好理解了,我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待的这段时间内,线程可以去做其它事,提高并发处理效率。
对于IO密集型应用:线程数= CPU核心数 / (1-阻塞系数)
这个阻塞系数一般为0.8~0.9之间,也可以取0.8或者0.9。套用公式,对于双核CPU来说,它比较理想的线程数就是20,当然这都不是绝对的。
线程池的工作原理
流程:
- 在创建了线程池后,开始等待请求。
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
线程池的拒绝策略
等待队列已经排满了,再也塞不下新任务了,线程池中的max线程也达到了,无法继续为新任务服务。
这个是时候我们就需要拒绝策略机制合理的处理这个问题。
jdk内置策略
以上内置拒绝策略均实现了RejectedExecutionHandle接口
在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?
都不用,我们只能自己创建
工作中使用ThreadPoolExecutor创建线程池
Java流式编程
java内置核心四大函数式接口
java.util.function包下主要为函数接口,有主要的四种:
代码演示:
public class FunctionDemo {
public static void main(String[] args) {
// R apply(T t); 函数型接口 一个参数,一个返回值
Function<String,Integer> function = t -> {
return t.length();};
System.out.println(function.apply("abcde"));
//boolean test(T t); 断言型接口 一个参数,返回boolean
Predicate<String> predicate = p -> {
return p.startsWith("a");};
System.out.println(predicate.test("abcde"));
// void accept(T t); 消费型接口 一个参数,没有返回值
Consumer<String> consumer = c -> {
System.out.println(c); };
consumer.accept("消费我吧!");
// T get(); 供给型接口 无参数,有返回值
Supplier<String> supplier = () -> {
return UUID.randomUUID().toString().substring(0,3);};
System.out.println(supplier.get());
}
}
输出:
5
true
消费我吧!
1cc
Stream流
流(Stream) 到底是什么呢?
是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
“集合讲的是数据,流讲的是计算!”
特点:
阶段:源头=>中间流水线=>结果
举例代码:
要求:偶数ID 年龄大于24 用户名转为大写 用户名字母倒序 只输出一个用户名。
可以看出,流stream的操作像数据库的SQL语句一样。
@Data
@NoArgsConstructor
@AllArgsConstructor
class User{
private Integer id;
private String userName;
private int age;
}
/**
* 偶数ID 年龄大于24 用户名转为大写 用户名字母倒序 只输出一个用户名
*/
public class StreamDemo {
public static void main(String[] args) {
User u1 = new User(11,"a",23);
User u2 = new User(12,"b",23);
User u3 = new User(13,"c",22);
User u4 = new User(14,"d",28);
User u5 = new User(16,"e",26);
List<User> list = Arrays.asList(u1,u2,u3,u4,u5);
list.stream().filter(u -> {
return u.getId() % 2 ==0;
}).filter(u -> {
return u.getAge() > 24;
}).map(m -> {
return m.getUserName().toUpperCase();
}).sorted((o1,o2) -> {
return o2.compareTo(o1);
}).limit(1).forEach(System.out::println);
}
}