1. 简介
JUC
java.util.concurrent工具包的简称。一个处理线程的工具包。
进程与线程
进程
- 是系统进行资源分配和调度的基本单位
- 是操作系统结构的基础。
- 是线程的容器
- 是程序的实体
线程
- 操作系统能够进行运算调度的最小单位
- 是进程实际运作单位
- 一条线程指的是进程中一个单一顺序的控制流
- 一个进程可以并发多个线程,每条线程并行不同任务
并发与并行
并发
多个线程在访问同一资源
并行
多个线程执行不同任务,然后汇总
线程的状态
- new
- runnable
- blocked
- waiting(不见不散)
- timed_waiting(过时不候)
- terminated(翻译:结束)
wait 与 sleep
-
sleep是Thread的静态方法,wait是Object的方法(任何对象实例都能调用)
-
sleep不会释放锁,它也不需要占用锁.
-
public static void main(String[] args) { Object obj = new Object(); new Thread(()->{ synchronized (obj) { try { Thread.sleep(5000);//不会释放 // obj.wait(); System.out.println("1释放锁了"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(()->{ synchronized (obj) { System.out.println("2获取到锁了"); // obj.notify(); } }).start(); } /* 1释放锁了 2获取到锁了 ------------ 2获取到锁了 1释放锁了 */
-
-
wait会释放锁,调用它的前提是当前线程占有锁。(即代码在synchronized中)
-
它们都可以被interrupted方法中断。
2. 概述1
管程
管程 => Monitor(监视器) => 锁
- 是一种同步机制,保证同一时间,只有一个线程可以访问被保护的数据或者代码。
JVM同步基于进入和退出,使用管程对象来实现。
3. 概述2
用户线程 与 守护线程
用户线程
- 自定义线程
- 主线程和用户线程如果还没结束,JVM就继续存活
- isDaemon():判断为false,is deamon: 是否为守护线程
守护线程
-
比如说垃圾回收的那个执行线程
-
我们自己也可以设置守护线程
-
守护线程在JVM结束(用户线程和主线都结束)的时候就结束了
-
设置守护线程的方法:setDeamon(true);
4. synchronized
简介
-
修饰一个代码块
-
synchronized (对象) { //作用范围 }
-
作用的对象是调用这个代码块的对象(疑惑)
-
-
修饰一个方法
-
synchronized void test01() { //作用范围 }
-
作用的对象是调用这个方法的对象
-
-
修饰一个静态方法
-
static synchronized void test02() { //作用范围 }
-
作用的对象这个类的所有对象
-
-
修饰一个类
-
作用的对象是这个类的所有对象
-
class ClassName { public void method() { synchronized(ClassName.class) { // todo //作用范围 } } }
-
多线程编程步骤
第一步: 创建资源类,在资源类创建属性和操作方法
第二步: 创建多个线程,调用资源类的操作方法
例子演示:
–买票
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();//资源对象
new Thread(()->{
while(true) {
if(ticket.sellTicket()==0) break;
}
},"name_aaa").start();
new Thread(()->{
while(true) {
if(ticket.sellTicket()==0) break;
}
},"name_bbb").start();
new Thread(()->{
while(true) {
if(ticket.sellTicket()==0) break;
}
},"name_ccc").start();
}
}
class Ticket {
private int ticketNums = 30;
public synchronized int sellTicket() {
if (ticketNums > 0) {
System.out.println(Thread.currentThread().getName() + "买了第" + ticketNums-- + "张票,还剩" + ticketNums + "张。");
return ticketNums;
}
return 0;
}
}
5. Lock接口
- Lock接口的实现类:ReentrantLock(可重复锁)
买票例子展示:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicketLock {
public static void main(String[] args) {
TicketLock ticket = new TicketLock();
new Thread(()->{
while (true) {
ticket.sellTicket();
if (!ticket.getFlag()) break;
}
},"name_aaa").start();
new Thread(()->{
while (true) {
ticket.sellTicket();
if (!ticket.getFlag()) break;
}
},"name_bbb").start();
new Thread(()->{
while (true) {
ticket.sellTicket();
if (!ticket.getFlag()) break;
}
},"name_ccc").start();
}
}
class TicketLock {
private int ticketNums;
private boolean flag;
private final Lock lock;
public TicketLock() {
this.ticketNums = 30;
this.flag = true;
this.lock = new ReentrantLock();
}
public boolean getFlag() {
return flag;
}
public void sellTicket() {
lock.lock();//锁上
try {
if (ticketNums > 0) {
System.out.println(Thread.currentThread().getName() + "买了第" + ticketNums-- + "张票,还剩" + ticketNums + "张。");
}else {
flag = false;
}
}finally {
lock.unlock();//解锁
}
}
}
Lock VS synchronized
- Lock不是Java语言内置的,通过Lock实现类可以实现同步访问
- synchronized系统自动让线程释放对锁的占用;Lock需要手动。
- Lock可以让等待锁响应中断,synchronized不行。
- Lock可以知道有没有获取到锁,synchronized不行
- Lock可以提高多个线程进行读操作过程。当竞争资源相当激烈时,Lock的性能远远优于synchronized
补充
- 调用start()方法线程会不会马上被创建,这个不一定,最后靠被native修饰的start0()方法告诉操作系统,由操作系统决定。
6. 线程间的通信
多线程编程步骤
-
第一步: 创建资源类,在类中创建属性和操作方法
-
第二步: 在资源类操作方法中的操作:
- 判断
- 干活
- 通知
-
第三步: 创建多个线程,调用资源类的操作方法
例子测试:
synchronized实现
//第一步:创建资源类
class Share {
private int num = 1;
public synchronized void incr() {
//第二步:判断 干活 通知
//判断
while (num != 0) { //不用if是怕虚假通知(如果到时候忘了,换成if跑一下)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//干活
System.out.println(Thread.currentThread().getName()+" :: "+ ++num);
//通知
notifyAll();
}
public synchronized void decr() {
//判断
while (num != 1) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//干活
System.out.println(Thread.currentThread().getName()+" :: "+ --num);
//通知
notifyAll();
}
}
public class ThreadDemo01 {
public static void main(String[] args) {
Share share = new Share();
//希望:name_aaa: 1 , name_bbb: 0 , name_ccc: 1 , name_ddd: 0
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.incr();
}
},"name_aaa").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.decr();
}
},"name_bbb").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.incr();
}
},"name_ccc").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.decr();
}
},"name_ddd").start();
}
}
虚假唤醒问题
ps: wait()方法中断和虚假唤醒是可能的,并且该方法应该始终在循环中使用
//就是我们用while,不用if
if(/*判断条件*/) {
wait();//存在虚假唤醒的可能
}
//=========
while(/*判断条件*/) {
wait();
}
//看上面的例子就好理解了
上面假如换成if就没办法达到这种预期了,虽然只有两个线程是问题不大,打算多了就存在虚假唤醒问题了。
Lock实现
知识补充
- 通过Lock接口里面的方法实现上述操作,我们需要通过Lock接口的newCondition()方法获取Condition对象,使用里面的await和signalAll
代码展示
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//1. 创建资源类
class Share {
private int num;
private final Lock lock;
private final Condition condition;
Share() {
this.num = 1;
this.lock = new ReentrantLock();
this.condition = lock.newCondition();
}
public void incr() {
//方法三部曲:判断=>干活=>通知
lock.lock();
try {
//判断(防止虚假唤醒)
while (num != 0) {
condition.await();
}
//干活
++num;
System.out.println(Thread.currentThread().getName()+" :: "+num);
//唤醒
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decr() {
lock.lock();
try {
//判断
while (num != 1) {
condition.await();
}
//干活
--num;
System.out.println(Thread.currentThread().getName()+" :: "+num);
//通知
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo02 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.incr();
}
},"AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.decr();
}
},"BBB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.incr();
}
},"CCC").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
share.decr();
}
},"DDD").start();
}
}
7. 线程间的定制化通信
例子:实现顺序打印:5个A、10个B、15个C,循环打印10遍
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//1. 创建资源类,创建属性和操作方法
class ShareResource {
//标志位
private int flag = 1;
private final Lock lock;
private final Condition c1;
private final Condition c2;
private final Condition c3;
public ShareResource() {
lock = new ReentrantLock();//可重复锁
c1 = lock.newCondition();
c2 = lock.newCondition();
c3 = lock.newCondition();
}
public void print05() {
//三部曲:判断 干活 通知
lock.lock();
try {
//判断
while (flag != 1) {
c1.await();
}
//干活
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+"A");
}
flag = 2;
//通知
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
//判断
while (flag != 2) {
c2.await();
}
//干活
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+"B");
}
flag = 3;
//通知
c3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
//判断
while (flag != 3) {
c3.await();
}
//干活
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+"C");
}
flag = 1;
//通知
c1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ThreadDemo03 {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareResource.print05();
}
},"AAA").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareResource.print10();
}
},"BBB").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareResource.print15();
}
},"CCC").start();
}
}
关于这个线程等待和唤醒,我们可以揣测一下实现方法:
上面那个例子:使用了signalAll可以是因为两个玩意使用同一个condition实现类的await方法,相当于绑定在一起了。
这个例子就好解释了,就是通过绑定不同的condition实现类来实现。
8. List的线程安全问题
线程不安全演示
public static void main(String[] args) {
//线程不安全演示
//会出现并发修改问题:ConcurrentModificationException
List<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,6));
System.out.println(list);
},String.valueOf(i)).start();
}
}
解决方案
- 使用Vector,很老了
- 使用Collections.synchronizedList(对象)获取
- 对象是ArrayList或者LinkedList,其他也行吧
- 使用CopyOnWriteArrayList
9.CopyOnWriteArrayList
使用了写时复制技术
- 读支持并发读
- 写操作先复制一份,写入新内容之后再赋给现在的数组
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();
}
}
final void setArray(Object[] a) {
array = a;
}
final Object[] getArray() {
return array;
}
- 原来是上锁了,所以可以保证并发。
10. Set与Map线程安全问题
Set线程不安全演示
public static void main(String[] args) {
//Exception in thread "7"
// Exception in thread "9"
// java.util.ConcurrentModificationException
Set<String> set = new HashSet<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,3));
System.out.println(set);
},String.valueOf(i)).start();
}
}
Set的解决方案
- 使用CopyOnWriteSet类
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,3));
System.out.println(set);
},String.valueOf(i)).start();
}
}
Map线程不安全演示
//Exception in thread "2" Exception in thread "3" Exception in thread "7"
// java.util.ConcurrentModificationException
public static void main(String[] args) {
Map<String,String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
String key = String.valueOf(i);
new Thread(()->{
map.put(key, UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
ps:variable used in lambda expression should be final or effectively final
翻译:在lambda表达式中使用的变量应该是final或有效的final
for (int i = 0; i < 10; i++) {
new Thread(()->{
map.put(String.valueOf(i), UUID.randomUUID().toString().substring(0,5));
},String.valueOf(i)).start();
}
不是很懂上面这个bug
Map的解决方案
使用ConcurrentMap类
public static void main(String[] args) {
Map<String,String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 10; i++) {
String key = String.valueOf(i);
new Thread(()->{
map.put(key, UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
11. 多线程锁
Synchronized锁
实现同步基础: Java中的每一个对象都可以作为锁。
三种具体表现形式:
- 对应普通同步方法,锁是当前实例对象。
- 对应静态同步方法,锁是当前类的Class对象
- 对应同步方法块,锁是Synchronized括号里配置的对象。
我们分析运行结果,就可以根据锁是否一样来判断运行结果。
12. 公平/非公平锁
ReentrantLock
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁:效率低,照顾到每个线程
非公平锁:效率低,会出现线程饿死的情况。
默认情况下是非公平锁,公平锁每次会判断一下是否有锁在等待,非公平锁就不会了。
13.可重入锁
synchronized(隐式)和Lock(显式)都是可重入锁。
- 例子:你进家门,需要开锁,之后到房间和开房间的背包都不需要开锁了
代码展示:
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
System.out.println("外部");
synchronized (o) {
System.out.println("中部");
synchronized (o) {
System.out.println("内部");
}
}
}
}
public synchronized void add() {
add();
}
ReentrantLock的可重用锁证明
Lock lock = new ReentrantLock();
new Thread(()->{
lock.lock();//上锁
try {
System.out.println("外层");
lock.lock();//上锁
try {
System.out.println("中层");
lock.lock();//上锁
try {
System.out.println("内层");
}finally {
lock.unlock();
}
}finally {
lock.unlock();
}
}finally {
lock.unlock();
}
}).start();
Lock上锁了就记得解锁,不然就会坑了别的线程。
14. 死锁
两个或者两个以上线程在执行过程中,因为争夺资源而造成一种互相等待的想象。如果没有外力干涉,它们无法再执行下去。
产生死锁的原因:
- 第一:系统资源不足
- 第二:进程运行推进顺序不合适
- 第三:资源分配不当
代码演示
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
new Thread(()->{
synchronized(a) {
sout("获取到a的锁,试图获取b的锁");//dddd
synchronized(b) {
sout("获取b的锁");
}
}
}).start();
Thread.sleep(1000);//会抛出异常
new Thread(()->{
synchronized(b) {
sout("获取到b的锁,试图获取a的锁");//dddd
synchronized(a) {
sout("获取a的锁");
}
}
}).start();
}
//这样就会出现死锁。通过jps jstack查看
验证是否产生死锁
===================================================
"B线程":
at JUC.demo2.Demo03.lambda$main$1(Demo03.java:26)
- waiting to lock <0x000000076b8166c8> (a java.lang.Object)
- locked <0x000000076b8166d8> (a java.lang.Object)
at JUC.demo2.Demo03$$Lambda$2/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"A线程":
at JUC.demo2.Demo03.lambda$main$0(Demo03.java:18)
- waiting to lock <0x000000076b8166d8> (a java.lang.Object)
- locked <0x000000076b8166c8> (a java.lang.Object)
at JUC.demo2.Demo03$$Lambda$1/2003749087.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
15. Callable接口
创建线程方式
- 继承Thread,重写run
- 实现Runnable接口,放到Thread代理
- Callable接口
- 线程池方式
Runnable接口与Callable接口的区别
- 是否有返回值
- 是否抛出异常
- 一个实现run()方法,一个实现call()方法
Callable接口开启线程方法
- 找到一个实现了Runnable接口的中介才行:FutureTask类
FutureTask实现了RunnableFuture接口,RunnableFuture接口继承了Runnable接口还要Future接口,Future接口里面的方法的实现是为了操作Callable接口的call()方法的。
这算是静态代理的设计模式吧。
泛型V,就是Callable接口的泛型
实现:lambda表达式
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<>(()->{
return 1024;
});
}
FutureTask原理:未来任务
例子引入:安排4个同学计算:A:1+2+3 、B: 1+2+3+…+100、C: 100+200、D:300+400
B同学计算任务重,FutureTask开启线程给同学计算,先汇总A、B、D,最后再B
一次汇总,B计算了一次,下一处要直接拿就行,不需要再计算一遍了。
代码演示:
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(()->{
return 1024;
});
new Thread(futureTask,"ThreadAAA").start();
while (!futureTask.isDone()) {
System.out.println(Thread.currentThread().getName()+": wait....");
}
System.out.println(futureTask.get());
System.out.println(futureTask.get());
}
- 一次计算,再次获取无需计算。演示不出,看源码可以看出。
16. JUC辅助类
CountDownLatch类
latch: 锁存器
- 可以设置一个计数器
- 通过countDown方法来进行减1的操作
- 使用await方法等待计数器不大于0,然后执行await方法之后的语句
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"号同学离开教室了!");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
try {
countDownLatch.await();
System.out.println("六人离开教室,班长锁门。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
CyclicBarrier
循环栅栏
//模拟例子:当且仅当七颗龙珠被收集完成之后就可以召集神龙
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("七龙珠收集完毕,可以召集神龙了。");
});
for (int i = 1; i <= 7; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"龙珠被收集到了!");
try {
cyclicBarrier.await();//当cyclicBarrier等待的数量等于parties时开放
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
简述作用:当调用await的等待线程等于传入的parties时,线程就可以被唤醒了。如果有传入Runnable接口,可以执行里面的内容。
Semaphore
翻译:信号标
初始值,表示有多少个许可证。
通过acquire可以获取一个,releave归还一个
//模拟6辆车,三个车位
//初始化,表示有3个许可证,通过acquire抢,通过release放弃
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
try {
semaphore.acquire();
int stayTime = new Random().nextInt(5);
System.out.println("第"+Thread.currentThread().getName()+"号车获取到车位了。停留时间: "+stayTime+"。");
TimeUnit.SECONDS.sleep(stayTime);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
17.读写锁
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
线程进入读锁的前提条件:
• 没有其他线程的写锁
• 没有写请求,或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)。
线程进入写锁的前提条件:
• 没有其他线程的读锁
• 没有其他线程的写锁
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
悲观锁
每次操作先上锁,搞完之后再开锁。
能解决并发中的各种问题,不支持并发操作,只能一个人一个人操作
效率很低
乐观锁
通过版本号进行控制,当前版本是1,操作完之后变成2了,如果有一个来时3,那么直接失败。
表锁
直接锁住表
行锁
锁住表中某行,能发生死锁
读锁
共享锁,能发生死锁(读锁,不表示只读)
写锁
占有锁,能发生死锁
18. 读写锁案例
volatile,标记属性为一个不断变化的属性
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;
class ResourceClass {
private volatile Map<String,String> map = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key,String value) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"正在进行写操作:key="+key);
TimeUnit.MILLISECONDS.sleep(100);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写操作完成: value="+value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
public void get(String key) {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"正在进行读操作:key="+key);
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName()+"读取数据成功:value: "+ map.get(key));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
ResourceClass resource = new ResourceClass();
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
resource.put(String.valueOf(num),num+"");
},String.valueOf(i)).start();
}
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
resource.get(String.valueOf(num));
},String.valueOf(i)).start();
}
}
}
0正在进行写操作:key=0
0写操作完成: value=0
1正在进行写操作:key=1
1写操作完成: value=1
2正在进行写操作:key=2
2写操作完成: value=2
3正在进行写操作:key=3
3写操作完成: value=3
4正在进行写操作:key=4
4写操作完成: value=4
1正在进行读操作:key=1
0正在进行读操作:key=0
2正在进行读操作:key=2
3正在进行读操作:key=3
4正在进行读操作:key=4
3读取数据成功:value: 3
0读取数据成功:value: 0
1读取数据成功:value: 1
2读取数据成功:value: 2
4读取数据成功:value: 4
还是不懂,为什么读锁发生死锁的那种情况,为什么读锁还可以有修改操作
19. 读写锁深入理解
读写锁: 一个资源可以被多个线程访问,或者可以被一个线程访问,但是不能同时存在读写线程,读写互斥,读读共享
- 无锁情况,多个线程抢夺资源,乱
- 添加锁,使用synchronized或者ReentrantLock,都是独占的,每次只能来一个操作
- 读写锁,ReentrantReadWriteLock,读读共享,提升性能,同时多人进行读操作
- 缺点:造成饥饿
- 缺点:读的时候不能写,只有读完了之后才能写,写操作可以读。
20.读写锁的降级
写锁降级为读锁
JDK8说明:[获取写锁] => [获取读锁] => [释放写锁] => [释放读锁]
ps: 读锁不能升级为写锁
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
//1. 获取写锁
rwLock.writeLock().lock();
System.out.println("获取到写锁");
rwLock.readLock().lock();
System.out.println("获取到读锁");
rwLock.writeLock().unLock();
rwLock.readLock().unLock();
}
增删改:写锁
查:读锁
21. 阻塞队列概述和架构
BlockingQueue
当队列是空的,从队列中获取元素操作将会被阻塞
。。。。满的,。。。。添加元素操作将会被阻塞
22. 常见阻塞队列
ArrayBlockingQueue、PriorityBlockingQueue、LinkedBlockingQueue、LinkedTransferQueue、SynchronousQueue
抛出异常:add、remove、element
特殊值:offer、poll、peek
阻塞:put、take
超时:offer、poll
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
// test01(blockingQueue);
// test02(blockingQueue);
// test03(blockingQueue);
test04(blockingQueue);
}
static void test04(BlockingQueue<String> blockingQueue) {
try {
System.out.println(blockingQueue.offer("随风的林子", 3, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("随风的叶子", 3, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("随风的润子", 3, TimeUnit.SECONDS));
System.out.println(blockingQueue.offer("阻塞3秒后失败", 3, TimeUnit.SECONDS));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll(3L,TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static void test03(BlockingQueue<String> blockingQueue) {
try {
blockingQueue.put("随风的林子");
blockingQueue.put("随风的叶子");
blockingQueue.put("随风的润子");
// blockingQueue.put("随风的阻塞");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());//阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static void test02(BlockingQueue<String> blockingQueue) {
//第二组
System.out.println(blockingQueue.offer("随风的林子"));
System.out.println(blockingQueue.offer("随风的叶子"));
System.out.println(blockingQueue.offer("随风的润子"));
System.out.println(blockingQueue.offer("失败不报错"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.peek());
}
static void test01(BlockingQueue<String> blockingQueue) {
//第一组测试
System.out.println(blockingQueue.add("随风"));
System.out.println(blockingQueue.add("叶子"));
System.out.println(blockingQueue.add("叶润"));
// System.out.println(blockingQueue.add("夜晚"));
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.element());
// ----------------------------------------------
}
23. 线程池概述
线程池: 一种线程使用模式。线程池维护多个线程,等待监督管理者分配可并发执行的任务。
- 好处:避免了在处理短时间任务时创建和销毁线程的代价。
- 好处:保证内核的充分利用,还防止过分调度。
特点:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁的消耗
- 提高响应速度:当任务到达时,任务无需创建线程就可理解执行。
- 提高线程的可管行:线程池可以进行统一的分配,调优和监控。
- Java线程池通过Executor框架实现。
24. 线程池使用方式
Executors工具类
- newFixedThreadPool
- newSingleThreadPool
- newCachedThreadPool
代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo04 {
public static void main(String[] args) {
//1. 创建线程池,线程数N
ExecutorService executorService01 = Executors.newFixedThreadPool(5);
//2. 创建线程池,线程数1
ExecutorService executorService02 = Executors.newSingleThreadExecutor();
//3. 创建线程池,线程数可扩容
ExecutorService executorService03 = Executors.newCachedThreadPool();
//运行
// test(executorService01);
// test(executorService02);
test(executorService03);
}
static void test(ExecutorService executorService) {
try {
for (int i = 0; i < 10; i++) {
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"运行中");
});
}
}finally {
executorService.shutdown();
}
}
}
25. 线程池底层原理
都涉及ThreadPoolExecutor
7个参数解释说明
public ThreadPoolExecutor(
int corePoolSize,//常驻线程数量
int maximumPoolSize,//最大线程数量
long keepAliveTime,//保存时间多少
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
RejectedExecutionHandler handler){//拒绝策略
/*
银行为例,平时业务办理窗口就只有(corePoolSize)个,忙的时候最大只有(maximumPoolSize)个,(maximumPoolSize-corePoolSize)个窗口保存时间为(数字:keepAliveTime)(单位:unit),大家领到号就形成workQueue,号拿完了,就会有拒接策略handler。
*/
}
• corePoolSize线程池的核心线程数
• maximumPoolSize能容纳的最大线程数
• keepAliveTime空闲线程存活时间
• unit 存活的时间单位
• workQueue 存放提交但未执行任务的队列
• threadFactory 创建线程的工厂类
• handler 等待队列满后的拒绝策略
26. 工作流程和拒绝策略
工作流程
我要求自己要背下这段话。
- 在创建了线程池后,线程池中的线程数为零
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
- 2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
- 4.1 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
- 4.2 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
拒绝策略
四种:
- AbortPolicy:丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
- CallerRunsPolicy:只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。(读得有点迷)
- 将某些任务退回给调用者,从而降低新任务的流量。
- DiscardOldestPolicy:只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
- DiscardPolicy:直接丢弃,其他啥都没有
创建方式:new ThreadPoolExecutor.AbortPolicy()
27. 自定义线程
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)
FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)
CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
ps:什么是OOM?
代码:
public static ExecutorService getExecutor(int corePoolSize,int maximumPoolSize) {
return new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>((corePoolSize+maximumPoolSize)/2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
}
28. Fork/Join简介
- 分支 = > 合并
- Fork:把一个复杂任务进行分拆,大事化小
- Join:把分拆的结果进行合并
Fork/Join它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。
任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割
执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
这部分源码自己还没看过,标记一下,以后去理清一下思路。
29. Fork/Join使用演示
例子:完成1++2+…+100
拆分条件[begin,end],begin-end<=10;
不满足,拆成:[begin,begin+(end-begin)/2]+[begin+(end-begin)/2+1,end]
拆分操作继承:RecursiveTask: 返回类型。fork、join、return
分支合并使用:ForkJoinPool:submit->get
ps:好奇,get时,是像前面未来任务那样需要等待的吗?和未来任务一样,我们get的时候还没计算出结果会进行线程等待吗?
- 好像这是异步的,监听。。。结果获取到了。没法自己演示,以后再看吧。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
class Calculator extends RecursiveTask<Integer> {
private int begin;
private int end;
private int result;
private static final int VALUE = 10;
public Calculator(int begin,int end) {
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
if (end-begin>VALUE) { //拆分
int tmp = begin + (end-begin)/2;
Calculator task01 = new Calculator(begin,tmp);
Calculator task02 = new Calculator(tmp+1,end);
//分支
task01.fork();
task02.fork();
//合并
result = task01.join()+task02.join();
}else {
for (int i=begin;i<=end;i++) result += i;
}
return result;
}
}
public class Demo02 {
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(new Calculator(1, 100));
try {
Integer res = forkJoinTask.get();
System.out.println(res);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}finally {
forkJoinPool.shutdown();
}
}
}
30. 异步回调
CompletableFuture
Futrue在Java里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个Futrue,在Future里面有isDone方法来 判断任务是否处理结束,还有get方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成。
Future的主要缺点如下:
(1)不支持手动完成
我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果,现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直等待它执行完成
(2)不支持进一步的非阻塞调用
通过Future的get方法会一直阻塞到任务完成,但是想在获取任务之后执行额外的任务,因为Future不支持回调函数,所以无法实现这个功能
(3)不支持链式调用
对于Future的执行结果,我们想继续传到下一个Future处理使用,从而形成一个链式的pipline调用,这在Future中是没法实现的。
(4)不支持多个Future合并
比如我们有10个Future并行执行,我们想在所有的Future运行完毕之后,执行某些函数,是没法通过Future实现的。
(5)不支持异常处理
Future的API没有任何的异常处理的api,所以在异步运行时,如果出了问题是不好定位的。
没返回值的
CompletableFuture<Void>
有返回值的
使用
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> completableFutureVoid = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName()+":Void异步方法");
});
completableFutureVoid.get();
CompletableFuture<Integer> completableFutureObject = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+": 有返回值异步方法");
return 1024;
});
completableFutureObject.whenComplete((returnVal,throwable)->{
System.out.println("返回值:"+returnVal);
System.out.println("抛出异常:"+throwable);
});
}
/*
ForkJoinPool.commonPool-worker-9:Void异步方法
ForkJoinPool.commonPool-worker-9: 有返回值异步方法
返回值:1024
抛出异常:null
*/
上面说的问题,到这里应该可以比较明确了。