JUC
1. JUC是什么
java.util.concurrent在并发编程中使用的工具类
线程/进程回顾
-
进程/线程是什么
进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
-
wait/sleep的区别
功能都是当前线程暂停,有什么区别?
wait放开手去睡,放开手里的锁
sleep握紧手去睡,醒了手里还有锁 -
并发/并行
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
并行:多项工作一起执行,之后再汇总
2. Lock接口
2.1 synchronized
卖票例子:
老版写法:
package com.y.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-04 16:14
**/
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket=new Ticket();
new Thread(new Runnable() {
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.saleTicket();
}
}
},"阿明").start();
new Thread(new Runnable() {
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.saleTicket();
}
}
},"阿伟").start();
new Thread(new Runnable() {
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.saleTicket();
}
}
},"阿亮").start();
}
}
class Ticket{
private int number=30;
public synchronized void saleTicket(){
if (number>0){
System.out.println(Thread.currentThread().getName()+"\t卖出第:"+(number--)+"\t还剩下:"+number);
}
}
}
新版写法:
package com.y.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-04 16:14
**/
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket=new Ticket();
new Thread(()->{for (int i=1;i<=40;i++) ticket.saleTicket();},"啊明").start();
new Thread(()->{for (int i=1;i<=40;i++) ticket.saleTicket();},"啊伟").start();
new Thread(()->{for (int i=1;i<=40;i++) ticket.saleTicket();},"啊亮").start();
}
}
class Ticket{
private int number=30;
Lock lock=new ReentrantLock();
public void saleTicket(){
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "\t卖出第:" + (number--) + "\t还剩下:" + number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
2.2 Lock
2.2.1 是什么
2.2.2 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()
}
}
}
2.3 synchronized与Lock的区别
两者区别:
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锁适合代码少量的同步问题。
3. 线程间通信
线程间通信:1、 生产者+消费者 2、通知等待唤醒机制
3.1 synchronized实现
package com.y.demo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-05 10:23
**/
public class ProdConsumerDemo {
public static void main(String[] args) {
Aircondition aircondition=new Aircondition();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 1; i < 10; i++) {
try {
aircondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 1; i < 10; i++) {
try {
aircondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 1; i < 10; i++) {
try {
aircondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
老版写法
class Aircondition{
private int number=0;
public synchronized void increment() throws InterruptedException {
if (number!=0){
this.wait();
}
//while (number!=0){
//this.wait();
//}
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number==0){
this.wait();
}
//while (number==0){
// this.wait();
//}
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}
}
换成4个线程会导致错误,虚假唤醒
原因:在java多线程判断时,不能用if,程序出错出在了判断上面,
突然有一添加的线程进到if了,突然中断了交出控制权,
没有进行验证,而是直接走下去了,加了两次,甚至多次
解决办法:
中断和虚假唤醒是可能产生的,所以要用loop循环,if只判断一次,while是只要唤醒就要拉回来再判断一次。if换成while
class Aircondition{
private int number=0;
public synchronized void increment() throws InterruptedException {
while (number!=0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
while (number==0){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}
}
3.2 java8新版实现
对标实现
Condition
package com.y.demo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-05 10:23
**/
public class ProdConsumerDemo {
public static void main(String[] args) {
Aircondition aircondition=new Aircondition();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
try {
aircondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 1; i < 10; i++) {
try {
aircondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 1; i < 10; i++) {
try {
aircondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 1; i < 10; i++) {
try {
aircondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//新版写法
class Aircondition{
private int number=0;
private Lock lock=new ReentrantLock();
private Condition condition=lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number!=0){
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number==0){
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
4. 线程间定制化调用通信
判断-干活-通知:
-
有顺序通知,需要有标识位
-
有一个锁Lock,3把钥匙Condition
-
判断标志位
-
输出线程名+第几次+第几轮
-
修改标志位,通知下一个
package com.y.demo;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-05 15:32
**/
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 备注:多线程之间按顺序调用,实现A->B->C
* 三个线程启动,要求如下:
* AA打印5次,BB打印10次,CC打印15次
* 接着
* AA打印5次,BB打印10次,CC打印15次
* 来10轮
* 1.高内聚低耦合前提下,线程操作资源类
* 2.判断/干活/通知
* 3.多线程交互中,防止虚假唤醒(判断只能用while,不能用if)
* 4.标志位
*/
public class ConditionDemo {
public static void main(String[] args) {
ShareData shareData=new ShareData();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareData.printc1();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareData.printc2();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareData.printc3();
}
},"C").start();
}
}
class ShareData{
private int number=1;
private Lock lock=new ReentrantLock();
private Condition condition1=lock.newCondition();
private Condition condition2=lock.newCondition();
private Condition condition3=lock.newCondition();
public void printc1(){
lock.lock();
try {
while (number!=1){
try {
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//2.干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3.通知
number = 2;
//通知第2个
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printc2(){
lock.lock();
try {
while (number!=2){
try {
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//2.干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3.通知
number = 3;
//通知第3个
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printc3(){
lock.lock();
try {
while (number!=3){
try {
condition3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//2.干活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3.通知
number = 1;
//通知第1个
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
5. 多线程锁
锁的8个问题:
- 标准访问,先打印短信还是邮件
- 停4秒在短信方法内,先打印短信还是邮件
- 普通的hello方法,是先打短信还是hello
- 现在有两部手机,先打印短信还是邮件
- 两个静态同步方法,1部手机,先打印短信还是邮件
- 两个静态同步方法,2部手机,先打印短信还是邮件
- 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
- 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
代码:
package com.y.lock;
import java.util.concurrent.TimeUnit;
/**
* @program: JUC
* @description: 多线程八锁现象
* @author: y
* @create: 2020-11-06 10:38
**/
public class lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
try {
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100);
new Thread(()->{
try {
// phone1.sendMs();
// phone1.sayHello();
phone2.sendMs();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
class Phone{
public static synchronized void sendEmail() throws Exception{
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("*******sendEmail");
}
public synchronized void sendMs() throws Exception{
// TimeUnit.SECONDS.sleep(2);
System.out.println("*******sendMs");
}
public void sayHello() throws Exception{
// TimeUnit.SECONDS.sleep(3);
System.out.println("*****sayHello");
}
}
分析:
-
标准访问,先打印邮件
-
邮件设置暂停4秒方法,先打印邮件
对象锁
一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其他的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法,
锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized方法 -
新增sayHello方法,先打印sayHello
加个普通方法后发现和同步锁无关 -
两部手机,先打印短信
换成两个对象后,不是同一把锁了,情况立刻变化 -
两个静态同步方法,同一部手机,先打印邮件
-
两个静态同步方法,同两部手机,先打印邮件,锁的同一个字节码对象
全局锁
synchronized实现同步的基础:java中的每一个对象都可以作为锁。
具体表现为一下3中形式。
对于普通同步方法,锁是当前实例对象,锁的是当前对象this,
对于同步方法块,锁的是synchronized括号里配置的对象。
对于静态同步方法,锁是当前类的class对象 -
一个静态同步方法,一个普通同步方法,同一部手机,先打印短信
-
一个静态同步方法,一个普通同步方法,同二部手机,先打印短信
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的普通同步方法因为跟该实例对象的普通同步方法用的是不同的锁,
所以无需等待该实例对象已获取锁的普通同步方法释放锁就可以获取他们自己的锁。 所有的静态同步方法用的也是同一把锁–类对象本身,
这两把锁(this/class)是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有静态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
而不管是同一个实例对象的静态同步方法之间,
还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象
6. NotSafeDemo
6.1 证明集合不安全
java.util.ConcurrentModificationException
ArrayList在迭代的时候如果同时对其进行修改就会
抛出java.util.ConcurrentModificationException异常
并发修改异常
原理
List<String> list = new ArrayList<>();
for (int i = 0; i <30 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
看ArrayList的源码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
没有synchronized线程不安全
6.2 解决方案
-
Vector
List<String> list = new Vector<>();
看Vector的源码
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
有synchronized线程安全
- Collections
-
写时复制
List<String> list = new CopyOnWriteArrayList<>();
6.3 写时复制
不加锁性能提升出错误,加锁数据一致性能下降
CopyOnWriteArrayList是arraylist的一种线程安全变体,
其中所有可变操作(add、set等)都是通过生成底层数组的新副本来实现的。
举例:名单签到
CopyOnWrite理论:
CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是现将当前容器Object[]进行Copy, 复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements);。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器
package com.y.notSafe;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-06 14:32
**/
public class NotSafeDemo {
public static void main(String[] args) {
mapNotSafe();
setNotSafe();
ListNotSafe();
}
private static void mapNotSafe() {
Map<String,String> map = new ConcurrentHashMap<String, String>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,8));
System.out.println(map);
},String.valueOf(i)).start();
}
}
private static void setNotSafe() {
Set<String> set= new CopyOnWriteArraySet();
for (int i = 0; i < 300; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(set);
},String.valueOf(i)).start();
}
}
private static void ListNotSafe() {
// List<String> list=new ArrayList<>();
// List<String>list= Collections.synchronizedList(new ArrayList<>());
List<String> list= new CopyOnWriteArrayList();
for (int i = 0; i < 300; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
7. Callable接口
与runnable对比
创建新类MyThread实现runnable接口
class MyThread implements Runnable{
@Override
public void run() {
}
}
新类MyThread2实现callable接口
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 200;
}
}
callable接口与runnable接口的区别?
- 是否有返回值
- 是否抛异常
- 落地方法不一样,一个是run,一个是call
7.1 怎么用
直接替换runnable是否可行
不可行,因为:thread类的构造方法根本没有Callable
java多态,一个类可以实现多个接口
FutureTask<Integer> ft = new FutureTask<Integer>(new MyThread());
new Thread(ft, "AA").start();
运行成功后如何获得返回值?
7.2 FutureTask
7.2.1 是什么
未来的任务,用它就干一件事,异步调用
main方法就像一个冰糖葫芦,一个个方法由main串起来。
但解决不了一个问题:正常调用挂起堵塞问题
7.2.2 原理
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,
就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
只计算一次,get方法放到最后
package com.y;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-12 16:48
**/
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask=new FutureTask<>(new MyThread());
new Thread(futureTask,"A").start();
Integer result=futureTask.get();
System.out.println(result);
}
}
class MyThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("******come in call method()");
return 1024;
}
}
8. JUC强大的辅助类
8.1 CountDownLatch减少计数
原理:
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
- 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
- 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
package com.y;
import java.util.concurrent.CountDownLatch;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-12 17:05
**/
/* 解释:6个同学陆续离开教室后值班同学才可以关门。
*
* main主线程必须要等前面6个线程完成全部工作后,自己才能开干
*/
public class CountDownLatchDemo8 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t离开教室");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t关门走人");
}
}
8.2 CyclicBarrier循环栅栏
原理:
- CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,
- 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
- 直到最后一个线程到达屏障时,屏障才会开门,所有
- 被屏障拦截的线程才会继续干活。
- 线程进入屏障通过CyclicBarrier的await()方法。
package com.y;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-12 17:12
**/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
System.out.println("*****召唤神龙");
});
for (int i = 1; i <=14; i++) {
final int tempInt=i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t收集到第:"+tempInt+"颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
8.3 Semaphore信号灯
原理:
在信号量上我们定义两种操作:
- acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),
- 要么一直等下去,直到有线程释放信号量,或超时。
- release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
- 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
package com.y;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-13 11:08
**/
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抢到了车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"\t离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
9. ReentrantReadWriteLock读写与锁
问题例子:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
public void put(String key,Object value){
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);
}
public Object get(String key){
Object result = null;
System.out.println(Thread.currentThread().getName()+"\t 正在读"+key);
try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace(); }
result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t 读完了"+result);
return result;
}
}
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();
}
}
}
加锁后:
package com.y;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-13 12:52
**/
class MyCache{
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.MICROSECONDS.sleep(300);
} catch (Exception e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t--写入完成");
} 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读取数据");
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=0;i<6;i++){
final int tempInt = i;
new Thread(()->{
myCache.put(tempInt+"",tempInt+"");
},String.valueOf(i)).start();
}
for (int i=0;i<6;i++){
final int tempInt = i;
new Thread(()->{
myCache.get(tempInt+"");
},String.valueOf(i)).start();
}
}
}
10. BlockingQueue
10.1 阻塞队列
阻塞:必须要阻塞/不得不阻塞
阻塞队列是一个队列,在数据结构中起的作用如下图:
线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素
-
当队列是空的,从队列中获取元素的操作将会被阻塞
-
当队列是满的,从队列中添加元素的操作将会被阻塞
-
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
-
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
10.2 阻塞队列的用处
- 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
为什么需要BlockingQueue
-
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
-
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
10.3 架构梳理、种类分析
种类分析:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
- LinkedTransferQueue:由链表组成的无界阻塞队列。
- LinkedBlockingDeque:由链表组成的双向阻塞队列。
10.4 核心方法
抛出异常 | 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException |
---|---|
特殊值 | 插入方法,成功ture失败false 移除方法,成功返回出队列的元素,队列里没有就返回null |
一直阻塞 | 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用 |
超时退出 | 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出 |
package com.y;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-16 08:18
**/
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String>blockingQueue=new ArrayBlockingQueue<>(3);
System.out.println( blockingQueue.add("a") );
System.out.println( blockingQueue.add("b") );
System.out.println( blockingQueue.add("c") );
// System.out.println( blockingQueue.add("d") );
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
}
}
超过容量时:
队空时:
11. ThreadPool线程池
11.1 为什么用线程池
线程池的优势:
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用;控制最大并发数;管理线程。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
11.2 线程池如何使用
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
11.2.1 编码实现
-
Executors.newFixedThreadPool(int);
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue
-
Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
ewSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue
-
Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
ewCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池
* Arrays
* Collections
* Executors
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
//List list = new ArrayList();
//List list = Arrays.asList("a","b");
//固定数的线程池,一池五线程
// ExecutorService threadPool = Executors.newFixedThreadPool(5); //一个银行网点,5个受理业务的窗口
// ExecutorService threadPool = Executors.newSingleThreadExecutor(); //一个银行网点,1个受理业务的窗口
ExecutorService threadPool = Executors.newCachedThreadPool(); //一个银行网点,可扩展受理业务的窗口
//10个顾客请求
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();
}
}
}
11.2.2 ThreadPoolExecutor底层原理
11.3 线程池几个重要参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize:线程池中的常驻核心线程数
- maximumPoolSize:线程池中能够容纳同时
执行的最大线程数,此值必须大于等于1 - keepAliveTime:多余的空闲线程的存活时间
当前池中线程数量超过corePoolSize时,当空闲时间
达到keepAliveTime时,多余线程会被销毁直到
只剩下corePoolSize个线程为止 - unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:表示生成线程池中工作线程的线程工厂,
用于创建线程,一般默认的即可 - handler:拒绝策略,表示当队列满了,并且工作线程大于
等于线程池的最大线程数(maximumPoolSize)时如何来拒绝
请求执行的runnable的策略
11.4 线程池底层工作原理
- 在创建了线程池后,开始等待请求。
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。 - 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
11.5 线程池用哪个?生产中如何设置合理参数
11.5.1 线程池的拒绝策略
等待队列已经排满了,再也塞不下新任务了。同时,线程池中的max线程也达到了,无法继续为新任务服务。
这个是时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略:
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不
会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。 - DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中
尝试再次提交当前任务。 - DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。
如果允许任务丢失,这是最好的一种策略。
以上内置拒绝策略均实现了RejectedExecutionHandle接口
11.5.2 在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?
一个都不用
11.5.3 在工作中如何使用线程池,是否自定义过线程池
package com.y;
import java.util.concurrent.*;
/**
* @program: JUC
* @description:
* @author: y
* @create: 2020-11-16 11:07
**/
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool=new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()
//new ThreadPoolExecutor.CallerRunsPolicy()
//new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardOldestPolicy());
try {
for (int i=1;i<=9;i++){
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
// TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}