目录
1. 什么是JUC
JUC实际是java.util.concurrent*工具包
2. 进程和线程
进程:
一个程序,QQ.exe Music.exe 程序的集合;
一个进程往往可以包含多个线程,至少包含一个!
Java默认有几个线程
2 个 mian、GC
线程:
开了一个进程 Typora,写字,自动保存(线程负责的)
对于Java而言:Thread、Runnable、Callable
并发
多线程操作同一个资源
CPU 一核 ,模拟出来多条线程,cpu快速交替执行
并行
多个核心一起执行
CPU 多核 ,多个线程可以同时执行; 线程池
/**
* 查看cpu核心数(包括虚拟)
* @author lane
* @date 2021年05月25日 下午3:46
*/
public class Test {
public static void main(String[] args) {
// 获取cpu的核数
// CPU 密集型,IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
并发编程的本质:
充分利用CPU的资源
线程有几个状态
public enum State {
// 新生
NEW,
// 运行
RUNNABLE,
// 阻塞
BLOCKED,
// 等待,死死地等
WAITING,
// 超时等待
TIMED_WAITING,
// 终止
TERMINATED;
}
Thread.States类中定义线程的状态:
- NEW:Thread对象已经创建,但是还没有开始执行。
- RUNNABLE:Thread对象正在Java虚拟机中运行。
- BLOCKED : Thread对象正在等待锁定。
- WAITING:Thread 对象正在等待另一个线程的动作。
- TIME_WAITING:Thread对象正在等待另一个线程的操作,但是有时间限制。
- TERMINATED:Thread对象已经完成了执行。
getState()方法获取Thread对象的状态,可以直接更改线程的状态。
在给定时间内, 线程只能处于一个状态。这些状态是JVM使用的状态,不能映射到操作系统的线程状态。
wait/sleep 区别
- 来自不同的类
wait => Object
sleep => Thread
- 关于锁的释放
wait 会释放锁,sleep 睡觉了,抱着锁睡觉,不会释放!
- 使用的范围是不同的
wait必须在同步代码块中
sleep 可以再任何地方睡
- 是否需要捕获异常
wait 不需要捕获异常
sleep 必须要捕获异常
3. synchronized关键字
锁的对象
实例方法的锁加在对象myClass上;静态方法的锁加在MyClass.class上。
synchronized关键字“给某个对象加锁”,示例代码:
package com.concurrent.demo1;
/**
* @author lane
* @date 2021年05月19日 下午4:37
*/
public class SynchornizedDemo {
public static final Object obj = new Object();
//method1 = 2
//method3 = 4
//synchronized关键字“给某个实例对象加锁”,示例代码:
public void method1(){
synchronized (this){
System.out.println("aaa");
}
synchronized (obj){
System.out.println("aaa");
}
}
//synchronized关键字“给某个实例对象加锁”,示例代码:
public synchronized void method2(){
System.out.println("bbb");
}
public synchronized static void method3(){
System.out.println("ccc");
}
public static void method4(){
synchronized (SynchornizedDemo.class){
System.out.println("ddd");
}
}
}
4. Lock锁(重点)
Lock三部曲
1、 new ReentrantLock();
2、 lock.lock(); // 加锁
3、 finally=> lock.unlock(); // 解锁
// Lock三部曲
// 1、 new ReentrantLock();
// 2、 lock.lock(); // 加锁
// 3、 finally=> lock.unlock(); // 解锁
class Ticket2 {
// 属性、方法
private int number = 30;
Lock lock = new ReentrantLock();
public void sale(){
lock.lock(); // 加锁
try {
// 业务代码
if (number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+
(number--)+"票,剩余:"+number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 解锁
}
}
}
//main
package com.kuang.demo01;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicketDemo02 {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类, 把资源类丢入线程
Ticket2 ticket = new Ticket2();
// @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
new Thread(()->{for (int i = 1; i < 40 ; i++)
ticket.sale();},"A").start();
new Thread(()->{for (int i = 1; i < 40 ; i++)
ticket.sale();},"B").start();
new Thread(()->{for (int i = 1; i < 40 ; i++)
ticket.sale();},"C").start();
}
}
公平锁:
十分公平:可以先来后到
非公平锁:
十分不公平:可以插队 (默认)
Synchronized 和 Lock 区别
1、Synchronized 内置的Java关键字, Lock 是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
4、Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
5、Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
5. 生产者和消费者问题
生产者和消费者问题 Synchronized 版
业务类
package com.concurrent.demo5;
/**
* @author lane
* @date 2021年05月21日 下午2:28
*/
public class Business {
private int number =0;
public Business(int number) {
this.number = number;
}
//1. 注意必须加sync才可以使用wait和notify
//2. 不能使用if,if醒来之后不会判断,while会判断
public synchronized void produce(){
while (number!=0){
System.out.println(Thread.currentThread().getName()+"number为"+number );
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number++;
notifyAll();
}
public synchronized void consume(){
while (number==0){
System.out.println(Thread.currentThread().getName()+"number为"+number );
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number--;
notifyAll();
}
}
测试类
package com.concurrent.demo5;
import java.sql.DataTruncation;
/**
* 实现+1 -1操作
* @author lane
* @date 2021年05月21日 下午2:28
*/
public class PSThread {
public static void main(String[] args) {
Business business = new Business(0);
business.produce();
Thread threadA= new Thread(()->{
for (int i = 0; i <10 ; i++) {
business.produce();
}
},"A");
Thread threadAA= new Thread(()->{
for (int i = 0; i <10 ; i++) {
business.produce();
}
},"AA");
Thread threadB = new Thread(()->{
for (int i = 0; i <10 ; i++) {
business.consume();
}
},"B");
Thread threadBB = new Thread(()->{
for (int i = 0; i <10 ; i++) {
business.consume();
}
},"BB");
threadA.start();
threadB.start();
threadAA.start();
threadBB.start();
}
}
测试效果
Anumber为1
BBnumber为0
AAnumber为1
Bnumber为0
if和while区别
if醒来之后不会继续判断,会造成虚假唤醒问题
while则会继续判断
JUC版的生产者和消费者问题
package com.concurrent.demo5;
import java.time.Clock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 单一的生产者消费者不会有问题
* 多个生产者和消费者就会出现问题
* @author lane
* @date 2021年05月21日 下午2:28
*/
public class BusinessLock {
private int number =0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public BusinessLock(int number) {
this.number = number;
}
//1. 注意必须加sync才可以使用wait和notify
//2. 不能使用if,if醒来之后不会判断,while会判断
public void produce(){
lock.lock();
while (number!=0){
System.out.println(Thread.currentThread().getName()+"number为"+number );
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
number++;
condition.signalAll();
}
public void consume(){
lock.lock();
while (number==0){
System.out.println(Thread.currentThread().getName()+"number为"+number );
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
number--;
condition.signalAll();
}
}
测试类
package com.concurrent.demo5;
/**
* @author lane
* @date 2021年05月21日 下午2:28
*/
public class PSThreadLock {
public static void main(String[] args) {
BusinessLock business = new BusinessLock(0);
Thread threadA= new Thread(()->{
for (int i = 0; i <10 ; i++) {
business.produce();
}
},"A");
Thread threadAA= new Thread(()->{
for (int i = 0; i <10 ; i++) {
business.produce();
}
},"AA");
Thread threadB = new Thread(()->{
for (int i = 0; i <10 ; i++) {
business.consume();
}
},"B");
Thread threadBB = new Thread(()->{
for (int i = 0; i <10 ; i++) {
business.consume();
}
},"BB");
threadA.start();
threadB.start();
threadAA.start();
threadBB.start();
}
}
//Exception in thread "B" java.lang.IllegalMonitorStateException
精确唤醒顺序执行
package com.concurrent.demo5;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 精确唤醒
* @author lane
* @date 2021年05月21日 下午3:53
*/
public class BusinessCondition {
private Lock lock = new ReentrantLock();
private int number =1;
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
List<String> list = new ArrayList<String>();
public void printA() throws InterruptedException {
lock.lock();
while (number!=1){
condition1.await();
}
number=2;
System.out.println(Thread.currentThread().getName()+"aaaaaa");
//先唤醒
condition2.signal();
//最后才释放锁
lock.unlock();
}
public void printB() throws InterruptedException {
lock.lock();
while (number!=2){
condition2.await();
}
number=3;
System.out.println(Thread.currentThread().getName()+"bbbbbb");
condition3.signal();
lock.unlock();
}
public void printC() throws InterruptedException {
lock.lock();
while (number!=3) {
condition3.await();
}
number=1;
System.out.println(Thread.currentThread().getName()+"cccccc");
condition1.signal();
lock.unlock();
}
}
测试类
package com.concurrent.demo5;
/**
* 顺序执行ABC
* @author lane
* @date 2021年05月21日 下午3:59
*/
public class PSThreadCondition {
public static void main(String[] args) {
BusinessCondition businessCondition = new BusinessCondition();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
try {
businessCondition.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
try {
businessCondition.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
try {
businessCondition.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
}
}
执行结果
Aaaaaaa
Bbbbbbb
Ccccccc
Aaaaaaa
6. 8锁现象
如何判断锁的是谁!永远的知道什么锁,锁到底锁的是谁!
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁,就是关于锁的8个问题
* 1、标准情况下,两个线程先打印 发短信还是 打电话? 1/发短信 2/打电话
* 1、sendSms延迟4秒,两个线程先打印 发短信还是 打电话? 1/发短信 2/打电话
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
//锁的存在
new Thread(()->{
phone.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone{
// synchronized 锁的对象是方法的调用者!、
// 两个方法用的是同一个锁,谁先拿到谁执行!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 3、 增加了一个普通方法后!先执行发短信还是Hello? 普通方法
* 4、 两个对象,两个同步方法, 发短信还是 打电话? // 打电话
*/
public class Test2 {
public static void main(String[] args) {
// 两个对象,两个调用者,两把锁!
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone2{
// synchronized 锁的对象是方法的调用者!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
// 这里没有锁!不是同步方法,不受锁的影响
public void hello(){
System.out.println("hello");
}
}
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 5、增加两个静态的同步方法,只有一个对象,先打印 发短信?打电话?
* 6、两个对象!增加两个静态的同步方法, 先打印 发短信?打电话?
*/
public class Test3 {
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
// Phone3唯一的一个 Class 对象
class Phone3{
// synchronized 锁的对象是方法的调用者!
// static 静态方法
// 类一加载就有了!锁的是Class
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 1、1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话?
* 2、1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话?
*/
public class Test4 {
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
// Phone3唯一的一个 Class 对象
class Phone4{
// 静态的同步方法 锁的是 Class 类模板
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 普通的同步方法 锁的调用者
public synchronized void call(){
System.out.println("打电话");
}
}
new this 具体的一个手机
static Class 唯一的一个模板
7. 线程安全的集合类
List不安全与安全类
package com.concurrent.demo6;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author lane
* @date 2021年05月21日 下午5:50
*/
public class ListConcurrent {
public static void main(String[] args) {
//线程不安全的
// List<String> list = new ArrayList<>();
//线程安全的
// List<String> list = new Vector<String>();
// List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list =new CopyOnWriteArrayList<>();
// Queue<String> list =new ConcurrentLinkedDeque<String>();
for (int i = 0; i <10 ; i++) {
new Thread(()->{
UUID uuid = UUID.randomUUID();
String s = uuid.toString().substring(0,2);
list.add((s) );
System.out.println(list);
}).start();
}
}
}
Set不安全与安全类
package com.concurrent.demo6;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @author lane
* @date 2021年05月21日 下午5:50
*/
public class SetConcurrent {
public SetConcurrent() {
}
public static void main(String[] args) {
//不安全的
// Set<String> set = new HashSet<>();
//安全的
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i <10 ; i++) {
new Thread(()->{
UUID uuid = UUID.randomUUID();
String s = uuid.toString().substring(0,2);
set.add((s) );
System.out.println(set);
}).start();
}
}
}
Map不安全与安全类
package com.concurrent.demo6;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author lane
* @date 2021年05月22日 上午11:19
*/
public class MapConcurrent {
public static void main(String[] args) {
//不安全的
// Map<String,Object> map = new HashMap<>();
//安全的
Map<String,Object> map = new ConcurrentHashMap<>();
for (int i = 0; i <10 ; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(1,3));
System.out.println(map);
}).start();
}
}
}
hashSet 底层是什么
public HashSet() {
map = new HashMap<>();
}
// add set 本质就是 map key是无法重复的!
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object(); // 不变得值!
8. Callable接口
与runnable很像的接口,下面是不同点
1、可以有返回值
2、可以抛出异常
3、方法不同,run()/ call()
代码实现
package com.concurrent.demo7;
import java.util.concurrent.Callable;
/**
* @author lane
* @date 2021年05月22日 上午11:39
*/
public class CallableThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName()+"callbale接口的call方法执行了。。。");
return "call的返回值";
}
}
测试类
package com.concurrent.demo7;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author lane
* @date 2021年05月22日 上午11:39
*/
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 同样是通过thread.start()方法启动线程
//2. thread构造只有runnbale
//3. runnable接口有一个实现类futureTask可以传入参数为callable;
//4. callable类似与runnable接口不过有返回值和缓存,可能会阻塞
CallableThread callableThread = new CallableThread();
//适配器
FutureTask futureTask = new FutureTask(callableThread);
Runnable runnable = futureTask;
Thread thread = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread.start();
thread2.start();
//这个get 方法可能会产生阻塞!把他放到最后
Object ret = futureTask.get();
System.out.println(ret);
}
}
//Thread-0callbale接口的call方法执行了。。。
//call的返回值
细节:
1、有缓存
2、结果可能需要等待,会阻塞!
9. 常用的辅助类(必会)
9.1 CountDownLatch
计数类线程工具
代码实现
package com.concurrent.demo8;
import java.util.concurrent.CountDownLatch;
/**
* @author lane
* @date 2021年05月22日 上午11:56
*/
public class CountdownlatchDemo {
public static void main(String[] args) throws InterruptedException {
//线程计数--类
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i <10 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"离开了");
//开始计数--
countDownLatch.countDown();
}).start();
}
//等到减少到0以后main线程再继续执行下去
countDownLatch.await();
System.out.println("可以关门了");
}
}
/*
Thread-1离开了
Thread-4离开了
Thread-2离开了
Thread-0离开了
Thread-8离开了
Thread-3离开了
Thread-9离开了
Thread-7离开了
Thread-6离开了
Thread-5离开了
可以关门了
*/
原理:
指定个数
//线程计数--类 CountDownLatch countDownLatch = new CountDownLatch(10);
/数量-1
//开始计数-- countDownLatch.countDown();
等到减少到0以后main线程再继续执行下去
countDownLatch.await();
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续执行!
如果指定的个数过大,线程没能执行完所有的数量,计数器的值大于0,此时将会一直countDownLatch.await();
阻塞等待
感觉就是确保所有线程执行完毕之后再执行main方法后面的内容,如果数量小于线程执行数,则无法保证其余线程顺序在main线程之前
9.2 CyclicBarrier
确保所有的线程执行完毕,才会执行屏障调用的方法,如七龙珠召唤神龙
代码实现
package com.concurrent.demo8;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* @author lane
* @date 2021年05月22日 下午12:12
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
//await线程数量到达之后之后会执行方法
System.out.println("召唤神龙成功!");
});
for (int i = 0; i < 7; i++) {
int finalI = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"成功集齐一颗龙珠编号为"+ finalI);
try {
//每一个线程都会等待在这,知道达到7个之后再继续下去
cyclicBarrier.await();
// System.out.println("abcd");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
/*
Thread-3成功集齐一颗龙珠编号为3
Thread-0成功集齐一颗龙珠编号为0
Thread-6成功集齐一颗龙珠编号为6
Thread-1成功集齐一颗龙珠编号为1
Thread-5成功集齐一颗龙珠编号为5
Thread-2成功集齐一颗龙珠编号为2
Thread-4成功集齐一颗龙珠编号为4
召唤神龙成功!
*/
9.3 Semphore
package com.concurrent.demo8;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* 限流,停车位有限
* @author lane
* @date 2021年05月25日 下午5:05
*/
public class SemaphoreDemo {
public static void main(String[] args) {
//指定线程的个数3,停车位
Semaphore semaphore = new Semaphore(3);
//进来了10辆车,3个停车位
for (int i = 0; i <10 ; i++) {
new Thread(()->{
try {
//占有得到
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"占用了一个停车位");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"离开了一个停车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放
semaphore.release();
}
},"车辆编号:"+i).start();
}
}
}
/*
车辆编号:3占用了一个停车位
车辆编号:1占用了一个停车位
车辆编号:0占用了一个停车位
车辆编号:3离开了一个停车位
车辆编号:0离开了一个停车位
车辆编号:1离开了一个停车位
*/
semaphore.acquire() 获得,假设如果已经满了,等待,等待被释放为止!
semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数!
可以这样理解这三个常用类
CountDownLantch
今天上课,如果来够10个人,我们的课程就开始讲解。如果不够10个就等到10个人,可能一下来了20个人,此时讲解的时候有20个人
CyclicBarrier
只有集齐七颗龙珠才能召唤神龙,不够就一直等着
Semphore
就是停车场只有三个停车位,走一辆,才能再进一辆
10. 读写锁
ReadWriteLock
代码实现之不加锁
package com.concurrent.demo9;
import java.util.HashMap;
import java.util.Map;
/**
* 独占锁(写锁)只能被一个线程占有
* 共享锁(读锁)可以被多个线程共有
* 读-读 可以同时多个线程、
* 读-写 只能同时存在一个线程
* 写-写 只能同时一个线程存在
* @author lane
* @date 2021年05月26日 上午10:45
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//5个线程同时写入缓存
for (int i = 0; i <5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.set(String.valueOf(temp),Thread.currentThread().getName());
}).start();
}
//5个线程同时读取缓存
for (int i = 0; i <5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.get(String.valueOf(temp));
}).start();
}
}
}
//自定义缓存
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
public void set(String key,Object object){
System.out.println(Thread.currentThread().getName()+"开始写入key:"+key);
map.put(key,object);
System.out.println(Thread.currentThread().getName()+"写入key:"+key+"完成");
}
public void get(String key){
System.out.println(Thread.currentThread().getName()+"开始读取key:"+key);
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取key:"+key+"完成");
}
}
/*
读写混乱
Thread-9开始读取key:4
Thread-3开始写入key:3
Thread-7开始读取key:2
Thread-2开始写入key:2
Thread-4开始写入key:4
Thread-6开始读取key:1
*/
代码实现之添加读写锁
读写锁有助于提高效率相比于一般的锁,可以存在多个线程去读。也可以保障了线程能够写入完成
package com.concurrent.demo9;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 独占锁(写锁)只能被一个线程占有
* 共享锁(读锁)可以被多个线程共有
* 读-读 可以同时多个线程、
* 读-写 只能同时存在一个线程
* 写-写 只能同时一个线程存在
* @author lane
* @date 2021年05月26日 上午10:45
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//5个线程同时写入缓存
for (int i = 0; i <5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.set(String.valueOf(temp),Thread.currentThread().getName());
}).start();
}
//5个线程同时读取缓存
for (int i = 0; i <5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.get(String.valueOf(temp));
}).start();
}
}
}
//自定义缓存
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
//读写锁必须一致
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void set(String key,Object object){
try {
//加上写锁
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"开始写入key:"+key);
map.put(key,object);
System.out.println(Thread.currentThread().getName()+"写入key:"+key+"完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放写锁
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
try {
//添加读锁
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"开始读取key:"+key);
map.get(key);
System.out.println(Thread.currentThread().getName()+"读取key:"+key+"完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放读锁
readWriteLock.readLock().unlock();
}
}
}
//结果
/*
Thread-2开始写入key:2
Thread-2写入key:2完成
Thread-3开始写入key:3
Thread-3写入key:3完成
Thread-4开始写入key:4
Thread-4写入key:4完成
Thread-0开始写入key:0
Thread-0写入key:0完成
Thread-1开始写入key:1
Thread-1写入key:1完成
Thread-5开始读取key:0
Thread-8开始读取key:3
Thread-6开始读取key:1
Thread-5读取key:0完成
Thread-7开始读取key:2
Thread-7读取key:2完成
Thread-6读取key:1完成
Thread-8读取key:3完成
Thread-9开始读取key:4
Thread-9读取key:4完成
*/
11. 阻塞队列
什么情况下我们会使用 阻塞队列:多线程并发处理,线程池!
学会使用队列:添加、移除 的四组API
四组API
操作方式 | 抛出异常 | 不抛出异常 | 阻塞等待 | 阻塞超时 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(,) |
移除 | remove() | poll() | take() | pull(,) |
获取队首 | element() | peek() | - | - |
代码实现
1. 测试add、remove阻塞时候会抛出异常
/**
* 测试add、remove阻塞时候会抛出异常
* @author lane
* @date 2021/5/26 上午11:54
*/
public static void testAddRemove(){
//指定队列长度和类型
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("======================");
System.out.println("队首元素为:"+blockingQueue.element());
System.out.println("======================");
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
}
public static void main(String[] args) throws InterruptedException {
/* 测试第一组异常添加删除
true
true
true
Exception in thread "main" java.lang.IllegalStateException: Queue full
======================
队首元素为:a
======================
a
b
c
Exception in thread "main" java.util.NoSuchElementException*/
testAddRemove();
}
2. 测试offer和poll队列满或空的时候不会抛出异常
/**
* offer和poll队列满或空的时候不会抛出异常
* @author lane
* @date 2021/5/26 下午12:11
*/
public static void testOfferPoll(){
//指定队列长度和类型
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("d"));
System.out.println("======================");
System.out.println("队首元素为:"+blockingQueue.peek());
System.out.println("======================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
}
public static void main(String[] args) throws InterruptedException {
// 测试第二组不会抛异常,返回false
/* true
true
true
false
======================
队首元素为:a
======================
a
b
c
null*/
testOfferPoll();
}
3. 测试put和take满空的时候会阻塞
/**
* put和take满空的时候会阻塞
* @author lane
* @date 2021/5/26 下午12:12
*/
public static void testPutTake() throws InterruptedException {
//指定队列长度和类型
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue(3);
//无返回值
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
// blockingQueue.put("d");
System.out.println("======================");
System.out.println("队首元素为:"+blockingQueue.peek());
System.out.println("======================");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
}
public static void main(String[] args) throws InterruptedException {
/* 第三组测试put和take会阻塞
======================
队首元素为:a
======================
a
b
c
阻塞。。。*/
testPutTake();
}
4. 测试offert和poll满空的时候会阻塞指定阻塞时间
/**
* offert和poll满空的时候会阻塞指定阻塞时间
* @author lane
* @date 2021/5/26 下午12:12
*/
public static void testOfferPollTime() throws InterruptedException {
//指定队列长度和类型
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("d",2, TimeUnit.SECONDS));
System.out.println("阻塞2s");
System.out.println("======================");
System.out.println("队首元素为:"+blockingQueue.peek());
System.out.println("======================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));
System.out.println("阻塞2s");
}
public static void main(String[] args) throws InterruptedException {
/*第四组超时阻塞
true
true
true
false
阻塞2s
======================
队首元素为:a
======================
a
b
c
null
阻塞2s*/
testOfferPollTime();
}
12. 同步队列
SynchronousQueue 同步队列
Java 6的并发编程包中的SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。
不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开
特点:
- 不能在同步队列上进行 peek,因为仅在试图要取得元素时,该元素才存在;
- 除非另一个线程试图移除某个元素,否则也不能(使用任何方法)添加元素;也不能迭代队列,因为其中没有元素可用于迭代。队列的头是尝试添加到队列中的首个已排队线程元素; 如果没有已排队线程,则不添加元素并且头为 null。
- 对于其他 Collection 方法(例如 contains),SynchronousQueue 作为一个空集合。此队列不允许 null 元素。
- 它非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。
- 对于正在等待的生产者和使用者线程而言,此类支持可选的公平排序策略。默认情况下不保证这种排序。 但是,使用公平设置为 true 所构造的队列可保证线程以 FIFO 的顺序进行访问。 公平通常会降低吞吐量,但是可以减小可变性并避免得不到服务。
- SynchronousQueue的以下方法:
- iterator() 永远返回空,因为里面没东西。
- peek() 永远返回null。
- put() 往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。
- offer() 往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。
- offer(2000, TimeUnit.SECONDS) 往queue里放一个element但是等待指定的时间后才返回,返回的逻辑和offer()方法一样。
- take() 取出并且remove掉queue里的element(认为是在queue里的。。。),取不到东西他会一直等。
- poll() 取出并且remove掉queue里的element(认为是在queue里的。。。),只有到碰巧另外一个线程正在往queue里offer数据或者put数据的时候,该方法才会取到东西。否则立即返回null。
- poll(2000, TimeUnit.SECONDS) 等待指定的时间然后取出并且remove掉queue里的element,其实就是再等其他的thread来往里塞。
- isEmpty()永远是true。
- remainingCapacity() 永远是0。
- remove()和removeAll() 永远是false
总结
没有容量
进去一个元素,必须等待取出来之后,才能再往里面放一个元素
代码实现
package com.concurrent.demo10;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
/**
* 同步队列
* @author lane
* @date 2021年05月26日 下午2:01
*/
public class SynchronousQueueDemo {
public static void main(String[] args) {
//创建同步队列
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
//生产数据
new Thread(()->{
try {
synchronousQueue.put("a");
System.out.println(Thread.currentThread().getName()+"添加了数据a");
synchronousQueue.put("b");
System.out.println(Thread.currentThread().getName()+"添加了数据b");
synchronousQueue.put("c");
System.out.println(Thread.currentThread().getName()+"添加了数据c");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//消费数据
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"取出了数据:"+synchronousQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"取出了数据:"+synchronousQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"取出了数据:"+synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
/*
Thread-0添加了数据a
Thread-1取出了数据:a
Thread-0添加了数据b
Thread-1取出了数据:b
Thread-0添加了数据c
Thread-1取出了数据:c
*/
13. 线程池#重点
线程池:
三大方法、7大参数、4种拒绝策略
池化技术:
程序的运行,本质:占用系统的资源! 优化资源的使用!=>池化技术
线程池、连接池、内存池、对象池///… 创建、销毁。十分浪费资源
池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后返还。
线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理
4、线程复用
5、可以控制最大并发数
线程池工具类Executors不推荐
package com.concurrent.demo12Pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线程池工具类三大方法 Executors不推荐
* 线程池自定义创建
* @author lane
* @date 2021年05月26日 下午2:46
*/
public class ThreadPoolExecutorsDemo {
public static void main(String[] args) {
//创建单一线程
ExecutorService threadPoolSingle = Executors.newSingleThreadExecutor();
//创建固定线程个数
ExecutorService threadPoolFix = Executors.newFixedThreadPool(5);
//创建弹性线程个数最大21亿
ExecutorService threadPoolCache = Executors.newCachedThreadPool();
try {
for (int i = 0; i <10 ; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPoolSingle.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了");
});
}
/* for (int i = 0; i <10 ; i++) {
threadPoolFix.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了");
});
}
for (int i = 0; i <10 ; i++) {
threadPoolCache.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了");
});
}*/
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPoolSingle.shutdown();
threadPoolFix.shutdown();
threadPoolCache.shutdown();
}
}
}
实现效果
#单线程 只有一个
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
pool-1-thread-1执行了
#固定线程 定义5个只有5个
pool-2-thread-2执行了
pool-2-thread-1执行了
pool-2-thread-3执行了
pool-2-thread-5执行了
pool-2-thread-4执行了
pool-2-thread-5执行了
pool-2-thread-3执行了
pool-2-thread-2执行了
pool-2-thread-1执行了
pool-2-thread-4执行了
#弹性线程 取决于电脑 现在10个线程一起
pool-3-thread-4执行了
pool-3-thread-8执行了
pool-3-thread-1执行了
pool-3-thread-7执行了
pool-3-thread-5执行了
pool-3-thread-10执行了
pool-3-thread-2执行了
pool-3-thread-6执行了
pool-3-thread-3执行了
pool-3-thread-9执行了
源码分析
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// 本质ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时了没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般
不用动
RejectedExecutionHandler handle // 拒绝策略) {if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数的解读
核心线程corePoolSize: 正常执行的线程
**最大核心线程数maximumPoolSize:**等待队列满的时候会启用,必须大于等于核心线程
**超时时间keepAliveTime:**超过指定时间没人调用就会释放掉核心线程外的线程(max-core的线程)
**超时单位TimeUnit:**指定超时时间的单位
**阻塞队列BlockingQueue:**业务达到核心线程之后会放入阻塞队列中,阻塞队列满了之后会开启最大线程中的线程
**线程工厂ThreadFactory:**线程创建的工厂
**拒绝策略RejectedExecutionHandler:**阻塞队列满了线程也达到最大线程个数时候,再来的业务如何拒绝的策略
四种拒绝策略:
/**
* new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
*/
代码实现自定义线程池
package com.concurrent.demo12Pool;
import java.util.concurrent.*;
/**
* 线程池自定义创建
* @author lane
* @date 2021年05月26日 下午2:59
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
int cpuNum = Runtime.getRuntime().availableProcessors();
System.out.println("cpu可以并行个数:"+cpuNum);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3,
10,
3,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>(7),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
/**
* new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
*/
//默认的拒绝策略
// private static final RejectedExecutionHandler defaultHandler = new ThreadPoolExecutor.AbortPolicy();
try {
for (int i = 0; i <10 ; i++) {
//参数是runnable对象
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了");
}
);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
}
}
//核心线程数3 + 阻塞队列数7 < =i业务数10,只会在核心线程3个执行,大于业务数的时候才会另外开辟线程直到最大线程数
/*
cpu可以并行个数:8
pool-1-thread-2执行了
pool-1-thread-2执行了
pool-1-thread-3执行了
pool-1-thread-1执行了
pool-1-thread-3执行了
pool-1-thread-2执行了
pool-1-thread-3执行了
pool-1-thread-1执行了
pool-1-thread-3执行了
pool-1-thread-2执行了
*/
如何设置最大线程个数
cpu密集型:
一般最大线程数个和cpu最大并发数相同
int cpuNum = Runtime.getRuntime().availableProcessors();
System.out.println("cpu可以并行个数:"+cpuNum);
//cpu可以并行个数:8
IO密集型:
一般最大线程数是两倍于你耗费IO业务数
14. lambda表达式
对于1.8及之后新的四大核心特性,必须要掌握
- lambda 表达式
- 链式编程
- 函数式接口
- stream流式操作
lambda表达式的表现形式:
()->{};
lambda表达式的格式:
() 用来写参数的, 对应抽象方法中()的参数类型及声明直接copy过来
-> 用来进行参数传递
{} 重写方法的代码都写到大括号中,对应匿名内部类{}重写方法体直接copy过来
lambda表达式要求:
- 必须是一个接口
- 必须只有一个抽象方法(只有一个抽象方法的接口,称之为函数式接口)
- 必须有上下文来推断哪个接口哪个方法,否则编译都不会通过(一般都是表达式作为参数)
lambda表达式的省略:(不建议看)
- () 中的参数类型声明可以省略掉,可以推断出来
- () 中如果只有一个参数,小括号可以省略
- {} 中如果只有一行不是返回值的代码,大括号可以省略,分号也可以省略(必须同时省略)
- 当只有一行返回值的时候,在大括号和分号同时省略的基础上,return 也必须省略
代码演示
package com.concurrent.demo15Lambda;
/**
* lambda
* @author lane
* @date 2021年05月26日 下午11:21
*/
public class TestLambdaDemo {
public static void main(String[] args) {
//会出错,没有上下文,推断不出来
// ()->{ System.out.println("aa");};
//匿名内部类
TestLambda testLambda = new TestLambda() {
@Override
public void test(String aa) {
System.out.println("test方法执行了");
}
};
testLambda.test("hello");
//必须只有一个抽象方法才行,接口中如果有两个方法就会编译出错了
//lambda
TestLambda testLambda1 = ((aa) -> {
System.out.println("lambda的test方法执行了");
});
testLambda1.test("hello");
/* lambda表达式的省略(不建议看)
1. () 中的参数类型声明可以省略掉,可以推断出来
2. () 中如果只有一个参数,小括号可以省略
3. {} 号中如果只有一行不是返回值的代码,大括号可以省略,分号也可以省略(必须分号和大括号同时省略)
4. {} 当只有一行返回值的时候,在大括号和分号同时省略的基础上,return 也可以省略
*/
//0lambda的标准样式
TestLambdaOmit testLambdaOmit0 = (String aa, String bb) -> {
System.out.println("0lambda的标准样式");
return aa + bb;
};
//1lambda参数类型声省略掉
TestLambdaOmit testLambdaOmit1 = (aa, bb) -> {
System.out.println("1lambda参数类型声省略掉");
return aa + bb;
};
//2()中如果只有一个参数,小括号可以省略
TestLambda testLambda2 = (aa -> {
System.out.println("2()中如果只有一个参数,小括号可以省略");
});
//3{} 号中如果只有一行代码,大括号可以省略,分号也可以省略
TestLambda testLambda3 = (aa -> System.out.println("3{} 号中如果只有一行代码,大括号可以省略,分号也可以省略"));
//4{}当只有一行返回值的时候,在大括号和分号同时省略的基础上,return 也可以省略
TestLambdaOmit testLambdaOmit4 = (aa, bb) -> aa + bb;
// System.out.println("4{}当只有一行返回值的时候在大括号和分号同时省略的基础上,return 也可以省略");
//0
System.out.println("========下面是测试省略============");
System.out.println(testLambdaOmit0.testOmit("hello", "world"));
//1
System.out.println(testLambdaOmit1.testOmit("hello", "world"));
//2
testLambda2.test("hello");
//3
testLambda3.test("hello");
//4
System.out.println(testLambdaOmit4.testOmit("hello", "world"));
}
}
/*
test方法执行了
lambda的test方法执行了
========下面是测试省略============
0lambda的标准样式
helloworld
1lambda参数类型声省略掉
helloworld
2()中如果只有一个参数,小括号可以省略
3{} 号中如果只有一行代码,大括号可以省略,分号也可以省略
helloworld
*/
15. ForkJoin
什么是 ForkJoin
在JDK中,提供了这样一种功能:它能够将复杂的逻辑拆分成一个个简单的逻辑来并行执行,待每个并行执行的逻辑执行完成后,再将各个结果进行汇总,得出最终的结果数据。有点像Hadoop中的MapReduce。
ForkJoin是由JDK1.7之后提供的多线程并发处理框架。ForkJoin框架的基本思想是分而治之。什么是分而治之?分而治之就是将一个复杂的计算,按照设定的阈值分解成多个计算,然后将各个计算结果进行汇总。相应的,ForkJoin将复杂的计算当做一个任务,而分解的多个计算则是当做一个个子任务来并行执行。
Java并发编程的发展
对于Java语言来说,生来就支持多线程并发编程,在并发编程领域也是在不断发展的。Java在其发展过程中对并发编程的支持越来越完善也正好印证了这一点。
- Java 1 支持thread,synchronized。
- Java 5 引入了 thread pools, blocking queues, concurrent collections,locks, condition queues。
- Java 7 加入了fork-join库。
- Java 8 加入了 parallel streams。
分治法
把一个规模大的问题划分为规模较小的子问题,然后分而治之,最后合并子问题的解得到原问题的解。
步骤
①分割原问题;
②求解子问题;
③合并子问题的解为原问题的解。
我们可以使用如下伪代码来表示这个步骤。
if(任务很小){
直接计算得到结果
}else{
分拆成N个子任务
调用子任务的fork()进行计算
调用子任务的join()合并计算结果
}
ForkJoin框架的本质
是一个用于并行执行任务的框架, 能够把一个大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务的计算结果。在Java中,ForkJoin框架与ThreadPool共存,并不是要替换ThreadPool在Java 8中引入的并行流计算,内部就是采用的ForkJoinPool来实现的。
ForkJoinPool主要使用**分治法(Divide-and-Conquer Algorithm)**来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool能够使用相对较少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概200万+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。
当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法向任务队列中再添加一个任务并在等待该任务完成之后再继续执行。而使用ForkJoinPool就能够解决这个问题,它就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。
核心递归
ForkJoin 特点工作窃取算法
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
Fork/Join框架局限性:
对于Fork/Join框架而言,当一个任务正在等待它使用Join操作创建的子任务结束时,执行这个任务的工作线程查找其他未被执行的任务,并开始执行这些未被执行的任务,通过这种方式,线程充分利用它们的运行时间来提高应用程序的性能。为了实现这个目标,Fork/Join框架执行的任务有一些局限性。
(1)任务只能使用Fork和Join操作来进行同步机制,如果使用了其他同步机制,则在同步操作时,工作线程就不能执行其他任务了。比如,在Fork/Join框架中,使任务进行了睡眠,那么,在睡眠期间内,正在执行这个任务的工作线程将不会执行其他任务了。
(2)在Fork/Join框架中,所拆分的任务不应该去执行IO操作,比如:读写数据文件。
(3)任务不能抛出检查异常,必须通过必要的代码来出来这些异常。
ForkJoin框架的实现
forkJoin框架中一些重要的类如下所示
ForkJoinTask类
ForkJoinTask封装了数据及其相应的计算,并且支持细粒度的数据并行。ForkJoinTask比线程要轻量,ForkJoinPool中少量工作线程能够运行大量的ForkJoinTask。
ForkJoinTask类中主要包括两个方法fork()和join(),分别实现任务的分拆与合并。
fork()方法类似于Thread.start(),但是它并不立即执行任务,而是将任务放入工作队列中。跟Thread.join()方法不同,ForkJoinTask的join()方法并不简单的阻塞线程,而是利用工作线程运行其他任务,当一个工作线程中调用join(),它将处理其他任务,直到注意到目标子任务已经完成。
我们可以使用下图来表示这个过程。
ForkJoinTask有3个子类:
- RecursiveAction:无返回值的任务,实现Runnable。
- RecursiveTask:有返回值的任务,实现Callable。
- CountedCompleter:完成任务后将触发其他任务,执行一个自定义的钩子函数。
代码实现
继承实现如RecursiveTask这个类重写其中的方法
ForkJoin任务类
package com.concurrent.demo17ForkJoin;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
/**
* 任务需求:求和计算的任务!
* 3000 6000(ForkJoin) 9000(Stream并行流)
* // 如何使用 forkjoin
* // 1、forkjoinPool 通过它来执行
* // 2、计算任务 forkjoinPool.execute(ForkJoinTask task)
* // 3. 计算类要继承 ForkJoinTask
* @author lane
* @date 2021年05月27日 下午3:04
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
//起始值
private long start;
//结束值
private long end;
//临界值
private long temp = 10000l; //10000以下就不用拆分了
public ForkJoinDemo(long start, long end) {
this.start = start;
this.end = end;
}
//任务需求:求和1+2+..+20亿
//计算方法
@Override
protected Long compute() {
//剩余任务过小,就不拆分了
if ((end-start)<temp){
long sum = 0l;
for (long i = start; i <=end ; i++) {
sum +=i;
}
return sum;
}else{
//fork join 拆分执行,比线程更轻量效率更高
//中间值拆
long middle = (start+end)/2;
//递归拆分两个子任务
ForkJoinDemo task1 = new ForkJoinDemo(start,middle);
//拆分任务放入工作队列中
task1.fork();
ForkJoinDemo task2 = new ForkJoinDemo(middle+1,end);
task2.fork();
//运行其他任务
Long join1 = task1.join();
Long join2 = task2.join();
return join1+join2;
}
}
}
ForkJoin测试类
分别测试 求和 1+2 +3 + …20个小目标 花费的时间
- 传统main线程
- 2个线程 (是通过手动拆分的)
- 4个线程(是通过手动拆分的)
- forkJoin
- stream流式
package com.concurrent.demo17ForkJoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.FutureTask;
import java.util.function.LongBinaryOperator;
import java.util.stream.LongStream;
/**
* 测试下不同方式求和sum = 1+2+..20个小目标 的执行时间
* @author lane
* @date 2021年05月27日 下午3:25
*/
public class ForkJoinTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//基本单线程求和
test1(); //sum:2000000001000000000基本求和方式花费时间:797
//多线程求和
test2();//sum:2000000001000000000两个线程方式花费时间:461
test2_4(); //sum:2000000001000000000四个线程方式花费时间:358
//forkjoin递归求和
test3(); //sum:2000000001000000000forkjoin求和方式花费时间:640
//stream求和
test4();//sum:2000000001000000000stream流求和方式花费时间:462
}
//基本的求和方式
public static void test1(){
long currentTimeMillis1 = System.currentTimeMillis();
long sum =0l;
for (long i = 0l; i <=20_0000_0000l ; i++) {
sum+=i;
}
long currentTimeMillis2 = System.currentTimeMillis();
System.out.println("sum:"+sum +"基本求和方式花费时间:"+(currentTimeMillis2-currentTimeMillis1));
}
/**
* 有一种方式是放在数组里使用runnable
* 这是用的callable
* int fromInt = max * i / threads + 1; // 边界条件
* int toInt = max * (i + 1) / threads; // 边界条件
* 循环创建线程
* @author lane
* @date 2021/5/27 下午6:02
*/
//多线程2个线程的求和方式,为了省事就没正式写
public static void test2() throws ExecutionException, InterruptedException {
long currentTimeMillis1 = System.currentTimeMillis();
FutureTask<Long> runnable1 = new FutureTask<Long>(()->{
long sum11 =0l;
for (long j = 0; j <10_0000_0000l ; j++) {
sum11 += j;}
return sum11;
});
FutureTask<Long> runnable2 = new FutureTask<Long>(()->{
long sum11 =0l;
for (long j = 10_0000_0000l; j <=20_0000_0000l ; j++) {
sum11 += j;}
return sum11;
});
new Thread(runnable1).start();
new Thread(runnable2).start();
Long sum1 = runnable1.get();
Long sum2 = runnable2.get();
long sum = sum1+sum2;
long currentTimeMillis2 = System.currentTimeMillis();
System.out.println("sum:"+sum+"两个线程方式花费时间:"+(currentTimeMillis2-currentTimeMillis1));
}
//多线程4个线程的求和方式,总感觉不对
public static void test2_4() throws ExecutionException, InterruptedException {
long currentTimeMillis1 = System.currentTimeMillis();
FutureTask<Long> runnable1 = new FutureTask<Long>(()->{
long sum11 =0l;
for (long j = 0; j <5_0000_0000l ; j++) {
sum11 += j;}
return sum11;
});
FutureTask<Long> runnable2 = new FutureTask<Long>(()->{
long sum11 =0l;
for (long j = 5_0000_0000l; j <10_0000_0000l ; j++) {
sum11 += j;}
return sum11;
});
FutureTask<Long> runnable3 = new FutureTask<Long>(()->{
long sum11 =0l;
for (long j = 10_0000_0000l; j <15_0000_0000l ; j++) {
sum11 += j;}
return sum11;
});
FutureTask<Long> runnable4 = new FutureTask<Long>(()->{
long sum11 =0l;
for (long j = 15_0000_0000l; j <=20_0000_0000l ; j++) {
sum11 += j;}
return sum11;
});
new Thread(runnable1).start();
new Thread(runnable2).start();
new Thread(runnable3).start();
new Thread(runnable4).start();
Long sum1 = runnable1.get();
Long sum2 = runnable2.get();
Long sum3 = runnable3.get();
Long sum4 = runnable4.get();
long sum = sum1+sum2+sum3+sum4;
long currentTimeMillis2 = System.currentTimeMillis();
System.out.println("sum:"+sum+"四个线程方式花费时间:"+(currentTimeMillis2-currentTimeMillis1));
}
//forkjoin求和方式
public static void test3() throws ExecutionException, InterruptedException {
long currentTimeMillis1 = System.currentTimeMillis();
//通过forkjoinpool执行任务
ForkJoinPool forkJoinPool = new ForkJoinPool();
//创建任务
ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0l,20_0000_0000l);
//提交任务
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
//获取返回结果
Long sum = submit.get();
long currentTimeMillis2 = System.currentTimeMillis();
System.out.println("sum:"+sum +"forkjoin求和方式花费时间:"+(currentTimeMillis2-currentTimeMillis1));
}
//stream流求和方式
public static void test4(){
long currentTimeMillis1 = System.currentTimeMillis();
//Stream流式求和(]
long reduce = LongStream.rangeClosed(0l, 20_0000_0000l).parallel().reduce(0, Long::sum);
long currentTimeMillis2 = System.currentTimeMillis();
System.out.println("sum:"+reduce +"stream流求和方式花费时间:"+(currentTimeMillis2-currentTimeMillis1));
}
}
测试结果
sum:2000000001000000000基本求和方式花费时间:788
sum:2000000001000000000两个线程方式花费时间:556
sum:2000000001000000000四个线程方式花费时间:232
sum:2000000001000000000forkjoin求和方式花费时间:478
sum:2000000001000000000stream流求和方式花费时间:371
Process finished with exit code 0
16. 异步回调
Future 设计的初衷: 对将来的某个事件的结果进行建模
异步回调类似前端的ajax,页面加载完之后再调用ajax方法去动态的添加修改一些数据
juc中的异步回调
main线程执行的时候,开辟另外一个线程执行
main线程一直执行自己的代码,执行到最后可以获取前面异步执行的结果
有正常执行返回结果,也有异常执行返回结果,如果没有执行完成,则会阻塞
注意:get方法会阻塞main
简单代码实现
package com.concurrent.demo18future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* CompletableFuture
* @author lane
* @date 2021年05月28日 下午4:10
*/
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
System.out.println("===========test2=================");
test2();
}
//无返回值类型异步
public static void test1() throws ExecutionException, InterruptedException {
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"异步方法执行完成了");
});
System.out.println("main方法一直执行");
// 获取阻塞执行结果
Void aVoid = completableFuture.get();
System.out.println(aVoid);
}
//有返回值的执行
private static void test2() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"执行有返回值的");
int i =1/0;
return 1024;
});
Integer integer = completableFuture.whenComplete((t, u) -> {
System.out.println("t:" + t);// 正常的返回结果
System.out.println("u:" + u);//异常信息
// u:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
}).exceptionally((e) -> {
System.out.println(e.getMessage());
return 400;
}).get();
System.out.println("执行异步回调返回的结果是:"+integer);
}
}
代码测试结果
#正常执行时
main方法一直执行
ForkJoinPool.commonPool-worker-3异步方法执行完成了
null
===========test2=================
ForkJoinPool.commonPool-worker-3执行有返回值的
t:1024
u:null
执行异步回调返回的结果是:1024
Process finished with exit code 0
#异常执行时
main方法一直执行
ForkJoinPool.commonPool-worker-3异步方法执行完成了
null
===========test2=================
ForkJoinPool.commonPool-worker-3执行有返回值的
t:null
u:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
执行异步回调返回的结果是:400
17 Java内存模型JMM
JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。
JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
JMM 的规定:
- 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
Volatile 是 Java 虚拟机提供轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
JMM : Java内存模型,不存在的东西,概念!约定!
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
18. Volatile
Volatile 是 Java 虚拟机提供轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
看下这篇文章,了解下基础的知识
[volatile关键字解析](https://www.cnblogs.com/dolphin0520/p/3920373.html)
摘抄一部分
1.volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
先看一段代码,假如线程1先执行,线程2后执行:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
volatile 保证原子性吗?
从上面知道 volatile 关键字保证了操作的可见性,但是 volatile 能保证对变量的操作是原子性吗?
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
大家想一下这段程序的输出结果是多少?也许有些朋友认为是 10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于 10000 的数字。可能有的朋友就会有疑问,不对啊,上面是对变量 inc 进行自增操作,由于 volatile 保证了可见性,那么在每个线程中对 inc 自增完之后,在其他线程中都能看到修改后的值啊,所以有 10 个线程分别进行了 1000 次操作,那么最终 inc 的值应该是 1000*10=10000。
这里面就有一个误区了,volatile 关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是 volatile 没办法保证对变量的操作的原子性。
正确解读原子性
刚才的inc++操作来说,这个操作其实细分为三步,读inc的值给temp,将temp+1,赋值给inc。
1、当线程1将inc读入内存,然后被阻塞。
2、线程2也将inc读入内存中,然后执行过第二步,temp+1,然后被阻塞。
3、线程1被唤醒,此时并没有对inc执行写操作,所以线程1不需要重新从内存读,所以执行完+1操作被赋值后重新写入主存中。
4、线程2被唤醒,由于inc执行了写操作,导致线程2中的inc缓存失效,所以从内存中重新读进来此时的inc值,由于已经执行过第二步了,不需要再执行tem+1操作了,直接将原来temp赋值给inc,然后重新写入内存。由于第二次读取的时候没有对读取到最新的值进行赋值temp和temp+1 ,此时写入内存中的inc还是相当于少加了一次。
原子性 : 不可分割
线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败。
实际上在我学习C预言的时候了解过i++这类的底层的,知道其中的步骤挺多的,现在都忘了,还是看到别人的文章才想起来呢
代码演示
package com.concurrent.demo19Volatile;
import java.util.concurrent.TimeUnit;
/**
*
* @author lane
* @date 2021年05月28日 下午6:42
*/
public class VolatileDemo {
public static void main(String[] args) throws InterruptedException {
int num = 1;
boolean flag = false;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"子线程开始执行");
while (!false){
//一直等待
}
}).start();
//执行一系列操作
TimeUnit.SECONDS.sleep(2);
System.out.println("main执行修改flag的知");
//可以退出循环了
flag = false;
System.out.println("main执行结束");
}
}
没有添加volatile的时候的执行结果如下,main线程修改了主内存中的flag的值,而子线程并不知道,仍然在while循环体内执行
添加关键字volatile之后的执行结果,子线程并没有一直死循环下去
原子性代码演示
package com.concurrent.demo19Volatile;
import java.util.concurrent.TimeUnit;
/**
* volatile保证了可见性
* volatile不能保证原子性
* 可以保证原子性的有
* 1. synchronized
* 2. lock
* 3. atomic类的自增
* @author lane
* @date 2021年05月28日 下午6:42
*/
public class VolatileDemo {
private static volatile boolean flag = false;
private static int num = 0;
private static volatile int volatileNum = 0;
public static void main(String[] args) throws InterruptedException {
//测试原子性
testAtomic();
}
//测试原子性
public static void testAtomic(){
//10个线程,每个加1000 ,想要的是10000
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j <1000 ; j++) {
num++;
volatileNum++;
}
}).start();
}
//测试了几次发现结果都不对
//原来是忘了加这个了,必须保证子线程执行完之后再看结果才行
//默认有两个线程分别是main和gc
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println("num的值为:"+num);
System.out.println("volatileNum的值为:"+volatileNum);
//num的值为:7756
//volatileNum的值为:9550
}
}
测试结果不加volatile和添加volatile的值都不对都不是10000;
真正能实现原子性的方法
- 采用synchronized
- 采用Lock
- 采用AtomicInteger
采用synchronized代码实现
package com.concurrent.demo19Volatile;
import java.util.concurrent.TimeUnit;
/**
* volatile保证了可见性
* volatile不能保证原子性
* 可以保证原子性的有
* 1. synchronized
* 2. lock
* 3. atomic类的自增
* @author lane
* @date 2021年05月28日 下午6:42
*/
public class VolatileDemo {
private static volatile boolean flag = false;
private static int num = 0;
private static volatile int volatileNum = 0;
public static void main(String[] args) throws InterruptedException {
testAtomic();
}
//测试原子性
public static void testAtomic(){
//10个线程,每个加1000 ,想要的是10000
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j <1000 ; j++) {
synchronized (VolatileDemo.class){
num++;
volatileNum++;
}
}
}).start();
}
//测试了几次发现结果都不对
//原来是忘了加这个了,必须保证子线程执行完之后再看结果才行
//默认有两个线程分别是main和gc
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println("num的值为:"+num);
System.out.println("volatileNum的值为:"+volatileNum);
}
}
实现结果,保证了原子性,结果是10000
lock方式代码实现
package com.concurrent.demo19Volatile;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* volatile保证了可见性
* volatile不能保证原子性
* 可以保证原子性的有
* 1. synchronized
* 2. lock
* 3. atomic类的自增
* @author lane
* @date 2021年05月28日 下午6:42
*/
public class VolatileDemo {
private static volatile boolean flag = false;
private static int num = 0;
private static volatile int volatileNum = 0;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//测试原子性
testAtomic();
}
//测试原子性
public static void testAtomic(){
//10个线程,每个加1000 ,想要的是10000
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j <1000 ; j++) {
//2.lock方式实现
lock.lock();
try {
num++;
volatileNum++;
} finally {
lock.unlock();
}
}
}).start();
}
//测试了几次发现结果都不对
//原来是忘了加这个了,必须保证子线程执行完之后再看结果才行
//默认有两个线程分别是main和gc
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println("num的值为:"+num);
System.out.println("volatileNum的值为:"+volatileNum);
}
}
atomin类实现
package com.concurrent.demo19Volatile;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* volatile保证了可见性
* volatile不能保证原子性
* 可以保证原子性的有
* 1. synchronized
* 2. lock
* 3. atomic类的自增
* @author lane
* @date 2021年05月28日 下午6:42
*/
public class VolatileDemo {
private static volatile boolean flag = false;
private static int num = 0;
private static volatile int volatileNum = 0;
private static Lock lock = new ReentrantLock();
private static AtomicInteger atomicIntegerNum = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
//测试原子性
testAtomic();
}
//测试原子性
public static void testAtomic(){
//10个线程,每个加1000 ,想要的是10000
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j <1000 ; j++) {
//加1递增
num++;
volatileNum++;
atomicIntegerNum.getAndIncrement();
}
}).start();
}
//测试了几次发现结果都不对
//原来是忘了加这个了,必须保证子线程执行完之后再看结果才行
//默认有两个线程分别是main和gc
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println("num的值为:"+num);
System.out.println("volatileNum的值为:"+volatileNum);
System.out.println("atomicIntegerNum的值为:"+atomicIntegerNum);
}
}
指令重排
cpu会改变代码的执行顺序,不会影响最后的结果,因为有些代码不可以改变顺序就不改变,有些没有严格的顺序要求就可能改变了其执行顺序,是为了cpu的运行效率,单线程不会出线问题,但是在并发情况下就可能出现问题了。单线程可以保证代码必须有前后关系的顺序不变。多线程则无法保证了。如下面的代码
//线程1
init();//初始化
flag = true;//修改标记位
//判断是否初始化完成
while(!flag){
....
//未初始化等待...
}
//线程2
//初始话完成就可以执行一系列操作了
dosomething();
此时会出现未初始化就执行本应该在初始化之后的操作
源代码–>编译器优化的重排–> 指令并行也可能会重排–> 内存系统也会重排—> 执行
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。
如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
happens-before原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
这8条原则摘自《深入理解Java虚拟机》。
volatile可以避免指令重排:
内存屏障。CPU指令。作用:
1、保证特定的操作的执行顺序!
2、可以保证某些变量的内存可见性 (利用这些特性volatile实现了可见性)
Volatile 再次总结下
-
可以保持 可见性。
-
不能保证原子性
-
由于内存屏障,可以保证避免指令重排的现象产生!
19. CAS
CAS是什么
CAS是compareandswap的简称,从字面上理解就是比较并更新,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。
CAS 基本原理
CAS 主要包括两个操作:Compare和Swap,有人可能要问了:两个操作能保证是原子性吗?可以的。
CAS 是一种系统原语,原语属于操作系统用语,原语由若干指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子指令保证了原子性,由操作系统硬件来保证。
在 Intel 的 CPU 中,使用 cmpxchg 指令。
回到 Java 语言,JDK 是在 1.5 版本后才引入 CAS 操作,在sun.misc.Unsafe这个类中定义了 CAS 相关的方法。
方法被声明为native,如果对 C++ 比较熟悉可以自行下载 OpenJDK 的源码查看 unsafe.cpp
CAS在Java中的应用
目前 CAS 在 JDK 中主要应用在 J.U.C 包下的 Atomic 相关类中
package com.concurrent.demo22cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author lane
* @date 2021年05月30日 下午10:21
*/
public class CASDemo {
//设置初始值
public static AtomicInteger atomicInteger= new AtomicInteger(2021);
public static void main(String[] args) {
//参数为期望值,如果期望值和原先的值一样那么就执行set设置新值
boolean b = atomicInteger.compareAndSet(2021, 2022);
int i = atomicInteger.get();
System.out.println("设置之后的值是:"+i+" 设置是否成功"+b);
boolean b2 = atomicInteger.compareAndSet(2021, 2023);
int i2 = atomicInteger.get();
System.out.println("设置之后的值是:"+i2+" 设置是否成功"+b2);
}
}
/*
设置之后的值是:2022 设置是否成功true
设置之后的值是:2022 设置是否成功false
*/
代码所示,当执行第一次atomicInteger.compareAndSet(2021, 2022);
的时候,首先是获取到atomicInteger的最初的值发现是2021 ,和我们传入的第一个参数期望值2021是相等的,那么就把atomicInteger值修改为2022,成功了
第二次进行atomicInteger.compareAndSet(2021, 2022);
的时候比较的发现atomicInteger的值是2022,而我们传入的值是2021和2022不一致,就会设置新的值失败!
CAS缺点:
1、 循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
ABA问题
假设一种情况是,初始值是2021,在我们比较的时候,有另一个线程B修改了我们的值为2022此时,我们再修改自然是失败的,但是假如另一个线程B修改2022后再修改为2021,此时线程A发现和我们期望值一致,就执行设置方法。好像一切都没发生过,实际则是线程B已经操作过了,可能会出现问题
20. 原子引用
带版本号 的CAS原子操作,就在上面的基础上加了版本号,这样即使被其它线程B修改了之后再修改回之前的发现版本号不一致,也会更新失败
代码示例
package com.concurrent.demo22cas;
import com.concurrent.demo16.Human;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author lane
* @date 2021年05月30日 下午11:34
*/
public class AtomicReferenceDemo {
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
//初始化一个原子引用类,参数为初始期望值和版本号
//因为范型是Integer ,值不能超过128不然就不是从缓存中取会出错
public static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
//修改值为110
public static void main(String[] args) {
//线程A修改100->110
new Thread(()->{
//获取版本号
int stamp = atomicStampedReference.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//参数分别为期望值、更新值、版本号、新的版本号
boolean b = atomicStampedReference.compareAndSet(100, 110, stamp, atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName()+"修改"+b+"版本号为"+atomicStampedReference.getStamp());
},"A").start();
//线程B修改100->110->100
new Thread(()->{
//获取版本号
int stamp = atomicStampedReference.getStamp();
//参数分别为期望值、更新值、版本号、新的版本号
boolean b1 = atomicStampedReference.compareAndSet(100, 120, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName()+"修改"+b1+"版本号为"+atomicStampedReference.getStamp());
boolean b2 = atomicStampedReference.compareAndSet(120, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName()+"修改"+b2+"版本号为"+atomicStampedReference.getStamp());
},"B").start();
}
}
/*
B修改true版本号为2
B修改true版本号为3
A修改false版本号为3(版本号1与新得到的版本号3不一致,所以修改失败)
*/
线程A修改100->110
线程B修改100->110->100
打印结果:
B修改true版本号为2
B修改true版本号为3
A修改false版本号为3
因为线程A发现版本号应该是1与新得到的版本号3不一致,所以修改失败,避免了ABA问题
注意:
Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实
例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;
在里面如果使用Integer如果超过128会发现期望值明明是(2020==2020)实际却是false而修改失败,这是一个大坑
详细的可以看下JAVA CAS实现原理与使用
21. 锁的分类
序号 | 锁名称 | 应用 |
---|---|---|
1 | 乐观锁 | CAS |
2 | 悲观锁 | synchronized、vector、hashtable |
3 | 自旋锁 | CAS |
4 | 可重入锁 | synchronized、Reentrantlock、Lock |
5 | 读写锁 | ReentrantReadWriteLock,CopyOnWriteArrayList、CopyOnWriteArraySet |
6 | 公平锁 | Reentrantlock(true) |
7 | 非公平锁 | synchronized、reentrantlock(false) |
8 | 共享锁 | ReentrantReadWriteLock中读锁 |
9 | 独占锁 | synchronized、vector、hashtable、ReentrantReadWriteLock中写锁 |
10 | 重量级锁 | synchronized |
11 | 轻量级锁 | 锁优化技术 |
12 | 偏向锁 | 锁优化技术 |
13 | 分段锁 | concurrentHashMap |
14 | 互斥锁 | synchronized |
15 | 同步锁 | synchronized |
16 | 死锁 | 相互请求对方的资源 |
17 | 锁粗化 | 锁优化技术 |
18 | 锁消除 | 锁优化技术 |
1. 公平锁、非公平锁
从其它等待中的线程是否按顺序获取锁的角度划分–公平锁与非公平锁
公平锁: 非常公平, 不能够插队,必须先来后到!
非公平锁:非常不公平,可以插队 (默认都是非公平)
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2. 可重入锁
从一个线程能否递归获取自己的锁的角度划分–重入锁(递归锁)
- 可重入锁:可以再次进入方法A,就是说在释放锁前此线程可以再次进入方法A(方法A递归)。
- 不可重入锁(自旋锁):不可以再次进入方法A,也就是说获得锁进入方法A是此线程在释放锁钱唯一的一次进入方法A。
3. 悲观锁、乐观锁
如果将锁在宏观上进行大的分类,那么所只有两类,即悲观锁和乐观锁。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
乐观锁的实现思想–CAS(Compare and Swap)无锁
CAS并不是一种实际的锁,它仅仅是实现乐观锁的一种思想,java中的乐观锁(如自旋锁)基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
关于CAS的原理,有兴趣可以参看 详细的可以看下JAVA CAS实现原理与使用。另外,在Java中,java.util.concurrent.atomic包下的原子类也都是基于CAS实现的。
4. 自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
5. 死锁、活锁
1.什么是死锁
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
2.死锁形成的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生:
互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
package com.concurrent.demo23deadlock;
import java.util.concurrent.TimeUnit;
/**
* @author lane
* @date 2021年05月31日 上午12:38
*/
public class DeadLockDemo {
public static String lockA ="A" ;
public static String lockB= "B" ;
public static void main(String[] args) {
new Thread(()->{
synchronized (lockA){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"lockA" +lockA+"lockB"+lockB);
}
}
},"T1").start();
new Thread(()->{
synchronized (lockB) {
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + "lockA" + lockA + "lockB" + lockB);
}
}
},"T2").start();
}
}
死锁故障排查
- 使用 jps -l 定位进程号
- 使用jstack pid查看故障代码
活锁
活锁和死锁在表现上是一样的两个线程都没有任何进展,但是区别在于:死锁,两个线程都处于阻塞状态,说白了就是它不会再做任何动作,我们通过查看线程状态是可以分辨出来的。而活锁呢,并不会阻塞,而是一直尝试去获取需要的锁,不断的try,这种情况下线程并没有阻塞所以是活的状态,我们查看线程的状态也会发现线程是正常的,但重要的是整个程序却不能继续执行了,一直在做无用功。举个生动的例子的话,两个人都没有停下来等对方让路,而是都有很有礼貌的给对方让路,但是两个人都在不断朝路的同一个方向移动,这样只是在做无用功,还是不能让对方通过。
参考文章
22. AQS详解
AQS原理
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
**注意:AQS是自旋锁:**在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
AQS实现的具体方式如下:
如图示,AQS维护了一个volatile int state和一个FIFO线程等待队列,多线程争用资源被阻塞的时候就会进入这个队列。state就是共享资源,其访问方式有如下三种:
getState();setState();compareAndSetState();
AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
不同的自定义的同步器争用共享资源的方式也不同,参考AQS详解
23. 锁的本质
如果一份资源需要多个线程同时访问,需要给该资源加锁。加锁之后,可以保证同一时间只能有一个线程访问该资源。资源可以是一个变量、一个对象或一个文件等。
锁是一个“对象”,作用如下:
-
这个对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用。最简单的情况是这个state有0、1两个取值,0表示没有线程占用这个锁,1表示有某个线程占用了这个锁。
-
如果这个对象被某个线程占用,记录这个线程的thread ID。
-
这个对象维护一个thread id list,记录其他所有阻塞的、等待获取拿这个锁的线程。在当前线程释放锁之后从这个thread id list里面取一个线程唤醒。
要访问的共享资源本身也是一个对象,例如前面的对象myClass,这两个对象可以合成一个对象。代码就变成synchronized(this) {…},要访问的共享资源是对象a,锁加在对象a上
当然,也可以另外新建一个对象,代码变成synchronized(obj1) {…}。这个时候,访问的共享资源是对象a,而锁加在新建的
对象obj1上。
资源和锁合二为一,使得在Java里面,synchronized关键字可以加在任何对象的成员上面。这意味 着,这个对象既是共享资源,同时也具备“锁”的功能!
锁实现原理
在对象头里,有一块数据叫Mark Word。在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字段:锁标志位和占用该锁的thread ID。因为不同版本的JVM实现,对象头的数据结构会有各种差异。
总结
文章是看了狂神视频总结的,其中查阅了很多资料。狂神的并发视频,只是简单录的三天视频课程,其中涉及到的知识基本上是常用的知识,当然了,也是明显感觉到有些内容还是涉及到的面太少了,我查了好多资料,部分知识的深度还是只是简单的使用,听的挺嗨的,入门可以看。