文章目录
并发编程
1、进程和线程
程序:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
进程:程序的一次执行过程,是系统运行程序的基本单位。
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。 说明:线程作为CPU调度和执行的单位,线程切换的开销小。一个进程在其执行的过程中可以产生多个线程。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
Java没有权限开启线程以及操作硬件,但是可以调用native方法来实现,其调用的是底层的c++代码
并发和并行
并发,指的是多个事情,在同一时间段内同时发生了。 多个任务之间是互相抢占资源的。
并行,指的是多个事情,在同一时间点上同时发生了。多个任务之间是不互相抢占资源的。
线程的几个状态
- NEW 新生
- RUNNABLE 运行
- BLOCKED 阻塞
- WAITING 等待
- TIMED_WAITING 超时等待
- TERMINATED 终止
wait/sleep的区别
1、来自不同的类
wait来自Object类
sleep来自Thread类
2、关于锁的释放
wait会释放锁,sleep不会释放锁
3、使用范围不同
wait必须在同步代码块中使用
sleep任何地方都可以
4、是否需要捕获异常
wait不需要捕获异常
sleep必须要捕获异常
2、Synchronized 与Lock 的区别
底层实现:
- Lock基于 AQS实现,通过state和一个CLH队列来维护锁的获取与释放
- synchronized需要通过 monitor,经历一个从用户态到内核态的转变过程,更加耗时
其他区别
1、Synchronized 内置的Java关键字,Lock是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock可以判断
3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁
4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。
5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;
6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;
总结
synchronized的锁可重入、不可中断、非公平。而Lock锁可重入、可判断、可公平(两者皆可)
3、虚假唤醒
当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功
实例: 生产者和消费者 我们想实现的是生产者造一个,消费者就消费一个。一共四个线程,两个生产 两个消费
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.incream();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decream();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.incream();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decream();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class Data{
private int num = 0;
public synchronized void incream() throws InterruptedException {
//注意这个if
if (num != 0){
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + " => " + num);
this.notifyAll();
}
public synchronized void decream() throws InterruptedException {
//注意这个if
if (num == 0){
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + " => " + num);
this.notifyAll();
}
}
运行结果如下:明显不是我们想要的,此时发生了虚假唤醒的现象
解决方式 ,if 改为while即可,防止虚假唤醒
结论:就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
这也就是为什么用while而不用if的原因了,因为线程被唤醒后,执行开始的地方是wait之后
上面是使用synchronized关键字实现线程同步的
还可以使用lock实现,并且可以实现唤醒指定线程,而不是全部唤醒。实例:
public class C {
public static void main(String[] args) {
Data3 data = new Data3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printA();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printB();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printC();
}
}, "C").start();
}
}
class Data3{
private int num = 1;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void printA() {
lock.lock();
try {
while (num != 1){
condition1.await();
}
num = 2;
System.out.println(Thread.currentThread().getName() + "=> AAAAA");
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (num != 2){
condition2.await();
}
num = 3;
System.out.println(Thread.currentThread().getName() + "=> BBBBB");
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (num != 3){
condition3.await();
}
num = 1;
System.out.println(Thread.currentThread().getName() + "=> CCCCC");
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
A线程执行完唤醒 B线程
B线程执行完唤醒 C线程
C线程执行完唤醒 A线程
4、集合不安全
4.1、List不安全
ArrayList 在并发情况下是不安全的,多线程情况下的add可能会出现 java.util.ConcurrentModificationException 并发修改异常。
要实现线程安全的List有三种方法:
List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略
核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;
其add源码如下:
写入时加Lock锁,然后复制一份原数组,创建一个比原数组长度多1的新数组,然后写入新元素,用新数组替换原来的旧数组。setArray其实就是一个新数组替换旧数组的过程。
private transient volatile Object[] array;
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;
}
4.2、Set不安全
和List一样,也是线程不安全的
线程安全的Set有两种:
- 使用Collections工具类的synchronized包装的Set类
- 使用CopyOnWriteArraySet 写入复制的JUC解决方案
CopyOnWriteArraySet 的add和CopyOnWriteArrayList的add差不多,其底层使用的是CopyOnWriteArrayList。add时会先使用indexOf
函数判断原数组中是否有相同元素存在,然后保留一份原数组的快照,防止多线程情况下,插入重复元素。下面是源码:
private final CopyOnWriteArrayList<E> al;
public boolean add(E e) {
return al.addIfAbsent(e);
}
//addIfAbsent方法是CopyOnWriteArrayList类的方法
//检查是否有重复元素,并保留一份添加前的快照
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
//加lock锁后,会再一次获取当前数组,
//若当前数组与之前保留的快照不同,则会检查数组中是否有和待添加元素e相同的元素,若没有相同元素,则继续添加元素e
//若当前数组与之前保留的快照相同,则添加待添加元素
//添加时新元素时会创建一个len+1的新数组,将原数组的组拷贝过来,并添加e,然后覆盖原数组。
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
4.3、Map不安全
同样的HashMap基础类也存在并发修改异常,
解决方法:
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
Map<String, String> map = new ConcurrentHashMap<>();
5、Callable
Callable与继承Thread、实现Runnable接口创建的线程区别:
1、可以有返回值;
2、可以抛出异常;
3、方法不同,run()/call()
下面是实现多线程的三种方法:继承Thread类、实现Runnable接口、实现Callable接口用Runnable接口的实现类FutureTask作为桥梁。
public class CallableTest {
public static void main(String[] args) throws Exception {
//继承Thread类
new A().start();
//实现Runnable接口 Lambda表达式
new Thread(() -> System.out.println("Runnable Run...")).start();
//Callable接口,Thread类的构造器只能接收Runnable接口,要想使用Callable接口开启一个线程
//需要一个Runnable的实现类来做桥梁.这个类就是FutureTask
//Callable接口与Runnable接口的区别: 1.可以有返回值; 2. 可以抛出异常 3.方法不同
FutureTask<String> futureTask = new FutureTask<String>(() -> {
System.out.println("Callable Run...");
return "Callable Test";
});
new Thread(futureTask).start();
//打印Callable的call方法的返回值
//get方法可能会产生阻塞
System.out.println(futureTask.get());
}
}
class A extends Thread {
@Override
public void run() {
System.out.println("Thread Run...");
}
}
6、JUC的常用辅助类
6.1、CountDownLatch(闭锁)
通过一个 state(相当于计数器)来实现的,计数器的初始值是 线程的数量或者任务的数量
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "==> Go Out");
countDownLatch.countDown(); // 每个线程都数量 -1
},String.valueOf(i)).start();
}
countDownLatch.await(); // 等待计数器归零 然后向下执行
System.out.println("close door");
}
}
主要方法:
- countDown 减一操作;
- await 等待计数器归零
- getCount 返回当前计数
await 等待计数器归零,就唤醒,再继续向下运行
6.2、CyclickBarrier(栅栏类)
- 栅栏类似于闭锁(CountDownLatch),它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于, 所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而 栅栏用于等待其他线程。
- CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。 当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。 如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。
7为设定的线程数,当线程数增加到7时,由进入的最后一个线程来执行这个行为。
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 主线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> {
System.out.println("召唤神龙");
});
for (int i = 1; i <= 7; i++) {
// 子线程
int finalI = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集了第" + finalI + "颗龙珠");
try {
cyclicBarrier.await(); // 加法计数 等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
线程调用 await() 表示自己已经到达栅栏
6.3、CyclicBarrier 与 CountDownLatch 区别
- CountDownLatch 是一次性的。 CyclicBarrier 是可循环利用的
- CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。 CyclicBarrier 参与的线程职责是一样的
- 对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。 而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须互相等待,然后继续一起执行。 CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
6.4、Semaphore
Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。
- Semaphore实现限流
车位只有三个,也就是说最多停三辆车。
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量,停车位,限流
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i <= 6; i++) {
new Thread(() -> {
// acquire() 得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位");
}catch (Exception e) {
e.printStackTrace();
}finally {
semaphore.release(); // release() 释放
}
}).start();
}
}
}
Thread-1抢到车位
Thread-0抢到车位
Thread-2抢到车位
Thread-0离开车位
Thread-2离开车位
Thread-1离开车位
Thread-5抢到车位
Thread-3抢到车位
Thread-4抢到车位
Thread-5离开车位
Thread-3离开车位
Thread-6抢到车位
Thread-4离开车位
Thread-6离开车位
原理:
-
semaphore.acquire()获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!
-
semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!
- Semaphore实现锁
public class SemaphoreLockDemo {
public static void main(String[] args) {
final SemaphoreLock lock = new SemaphoreLock();
for (int i = 0; i < 2; i++) {
new Thread(()->{
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " get the lock.");
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName() + " release the lock.");
lock.unlock();
}
}).start();
}
}
static class SemaphoreLock{
private final Semaphore semaphore = new Semaphore(1);
public void lock() throws InterruptedException {
semaphore.acquire();
}
public void unlock() {
semaphore.release();
}
}
}
Thread-0 get the lock.
Thread-0 release the lock.
Thread-1 get the lock.
Thread-1 release the lock.
6.5、Exchanger
- 用于两个工作线程之间交换数据的封装工具类
- 简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则 第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据
Exchanger 泛型类型,其中 V 表示可交换的数据类型
public class ExchangerDemo {
public static void main(String[] args) {
final Exchanger<String> exchanger = new Exchanger<>();
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " start . ");
try {
/**
* 如果这里睡200ms的话,应该是B线程先拿出数据,然后B线程等待A线程。因为是B先给的数据,
*/
TimeUnit.MILLISECONDS.sleep(2000);
String exchange = exchanger.exchange("I am come from T-A");
System.out.println(Thread.currentThread().getName() + " get value : " + exchange);
System.out.println(Thread.currentThread().getName() + " end . ");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " start . ");
try {
String exchange = exchanger.exchange("I am come from T-B");
System.out.println(Thread.currentThread().getName() + " get value : " + exchange);
System.out.println(Thread.currentThread().getName() + " end . ");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
}
}
- exchange方法
- 等待另一个线程到达此交换点,然后将给定对象传输给它,接收其对象作为回报。
- 可以被打断
- 如果已经有个线程正在等待了,则直接交换数据
7、读写锁
7.1、ReentrantReadWriteLock读写锁
如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。
我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。
但是这次我们采用更细粒度的锁:ReadWriteLock 和 ReadReadLock读写锁来保证。写锁时独占锁,写时不允许其他线程进行读写;读锁为共享锁,读时其他线程也可以读,但是不能写。
public class ReadWriteLockDemo {
public static void main(String[] args) {
// MyCache myCache = new MyCache();
MyCacheLock myCache = new MyCacheLock();
for (int i = 0; i < 6; i++) {
int temp = i;
new Thread(() -> {
myCache.set(String.valueOf(temp), String.valueOf(temp));
}, String.valueOf(i)).start();
}
for (int i = 0; i < 6; i++) {
int temp = i;
new Thread(() -> {
myCache.get(String.valueOf(temp));
}, String.valueOf(i)).start();
}
}
}
//不加锁,线程不安全 读写数据时会被插队
class MyCache{
private volatile Map<String, String> map = new HashMap<>();
public void set(String key, String value){
System.out.println(Thread.currentThread().getName() + " 写入 " + key);
map.put(key, value);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 写入ok...");
}
public void get(String key){
System.out.println(Thread.currentThread().getName() + " 读取 " + key);
map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取ok...");
}
}
//细粒度的读写锁,写时独占、读时共享
class MyCacheLock{
private volatile Map<String, String> map = new HashMap<>();
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void set(String key, String value){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 写入 " + key);
map.put(key, value);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 写入ok...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public void get(String key){
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 读取 " + key);
map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取ok...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
}
7.2、ReentrantReadWriteLock读写锁的问题
1、深入分析ReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写线程去抢锁,这是一种悲观的读锁,会出现写饥饿。
2、有100个线程访问某个资源,如果有99线程个需要读锁,1个线程需要写锁,此时,写的线程很难得到执行
7.3、StampedLock读写锁
3、StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入
。这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
4、乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
- StampedLock实现悲观的读锁
public class StampedLockDemo1 {
private static final StampedLock stampedLock = new StampedLock();
private static Integer DATA = 0;
public static void write() {
long stamp = -1;
try {
stamp = stampedLock.writeLock();// 获取写锁
DATA++;
System.out.println("写-->" + DATA);
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public static void read() {
long stamp = -1;
try {
stamp = stampedLock.readLock();// 获取悲观读锁
System.out.println("读-->" + DATA);
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
//写任务
Runnable writeTask = () -> {
write();
};
//读任务
Runnable readTask = () -> {
read();
};
try {
//一个线程写,9个线程读
for (int i = 0; i < 9; i++) {
executor.submit(readTask);
}
executor.submit(writeTask);//写线程要写最后
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
输出结果:
读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 读–>0 写–>1
- StampedLock乐观读锁
public class StampedLockDemo2 {
private static final StampedLock stampedLock = new StampedLock();
private static Integer DATA = 0;
public static void write() {
long stamp = -1;
try {
stamp = stampedLock.writeLock();// 获取写锁
DATA++;
System.out.println("写-->" + DATA);
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public static void read() {
long stamp = stampedLock.tryOptimisticRead();
/**
* 1、在这块可能会有写锁抢锁,修改数据,所以用validate检查乐观读锁后是否有其他写锁发生
* 判断执行读操作期间,是否存在写操作,如果存在则validate返回false
* 2、如果有写锁抢锁,修改了数据,那么就要获取悲观锁。因为写锁在修改数据的过程中,你不能直接
* 去读,只能老老实实拿到读锁再去读,才不会发生线程安全问题
*/
if (!stampedLock.validate(stamp)){
try {
stamp = stampedLock.readLock();
System.out.println("悲观读-->" + DATA);
return;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
System.out.println("乐观读-->" + DATA);
}
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(5), Executors.defaultThreadFactory());
// ExecutorService executor = Executors.newFixedThreadPool(10);
//写任务
Runnable writeTask = () -> {
for (int i = 0; i < 10; i++) {
write();
}
};
//读任务
Runnable readTask = () -> {
for (int i = 0; i < 10; i++) {
read();
}
};
try {
//一个线程写,9个线程读
for (int i = 0; i < 9; i++) {
executor.submit(readTask);
}
executor.submit(writeTask);
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
8、阻塞队列
8.1、BlockQueue
是Collection的一个子类
使用场景:多线程并发处理、线程池
BlockingQueue 有四组api
方式 | 抛出异常 | 不会抛出异常,有返回值 | 阻塞,等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer(timenum.timeUnit) |
移出 | remove | poll | take | poll(timenum,timeUnit) |
判断队首元素 | element | peek | - | - |
示例如下:
public class BlockingQueueDemo {
private ArrayBlockingQueue queue = new ArrayBlockingQueue(3);
//抛异常
@Test
public void test1() {
queue.add("a");
queue.add("b");
queue.add("c");
// queue.add("d"); //Queue full
System.out.println(queue.element());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
// System.out.println(queue.remove()); //NoSuchElementException
// System.out.println(queue.element()); //NoSuchElementException
}
//有返回值、不抛异常
@Test
public void test2() {
queue.offer("a");
queue.offer("b");
queue.offer("c");
System.out.println("offer: " + queue.offer("d")); //false
System.out.println("peek: " + queue.peek());
System.out.println("poll: " + queue.poll());
System.out.println("poll: " + queue.poll());
System.out.println("poll: " + queue.poll());
System.out.println("poll: " + queue.poll()); // null
}
//阻塞等待 一直等待
@Test
public void test3() throws InterruptedException {
queue.put("a");
queue.put("b");
queue.put("c");
// queue.put("d"); //一直等待
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take()); //一直等待
}
//超时等待 超过设置时间就不会再等待
@Test
public void test4() throws InterruptedException {
queue.offer("a", 2, TimeUnit.SECONDS);
queue.offer("b", 2, TimeUnit.SECONDS);
queue.offer("c", 2, TimeUnit.SECONDS);
System.out.println("超时插入...");
//2秒还是无法插入 返回false 然后进行下一步
System.out.println(queue.offer("d", 2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS));
System.out.println(queue.poll(2, TimeUnit.SECONDS)); //2秒获取不到就离开 返回null
}
}
8.2、同步队列
同步队列 没有容量,也可以视为容量为1的阻塞队列;
进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;
put方法 和 take方法;
Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;
put了一个元素,就必须从里面先take出来,否则不能再put进去值!
并且SynchronousQueue 的take是使用了lock锁保证线程安全的。
public class SynchronousQueueDemo {
public static void main(String[] args) {
SynchronousQueue<String> queue = new SynchronousQueue<>();
new Thread(() -> {
try {
queue.put("a");
System.out.println(Thread.currentThread().getName() + " put a");
TimeUnit.SECONDS.sleep(1);
queue.put("b");
System.out.println(Thread.currentThread().getName() + " put b");
TimeUnit.SECONDS.sleep(1);
queue.put("c");
System.out.println(Thread.currentThread().getName() + " put c");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T1").start();
new Thread(() -> {
try {
String s1 = queue.take();
System.out.println(Thread.currentThread().getName() + " take " + s1);
String s2 = queue.take();
System.out.println(Thread.currentThread().getName() + " take " + s2);
String s3 = queue.take();
System.out.println(Thread.currentThread().getName() + " take " + s3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T2").start();
}
}
T1 put a
T2 take a
T1 put b
T2 take b
T1 put c
T2 take c
9、线程池(重点)
线程池:三大方法、七大参数、四种拒绝策略
池化技术
程序的运行,本质:占用系统的资源!我们需要去优化资源的使用 ===> 池化技术
线程池、JDBC的连接池、内存池、对象池 等等。。。。
资源的创建、销毁十分消耗资源
池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。
9.1、线程池的好处:
1、降低资源的消耗;
2、提高响应的速度;
3、方便管理;
线程复用、可以控制最大并发数、管理线程;
9.2、线程池:三大方法
- ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
- ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
- ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
9.3、七大参数
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大的线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂 创建线程的 一般不用动
RejectedExecutionHandler handler //拒绝策略
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。
9.4、拒绝策略
1. AbortPolicy(): //该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常。超出最大承载,就会抛出异常:队列容量大小+maxPoolSize
2. CallerRunsPolicy(): //该拒绝策略为:哪来的去哪里 哪个线程调用的该线程池就让哪个线程去执行
3. new ThreadPoolExecutor.DiscardPolicy(): //该拒绝策略为:队列满了,丢掉异常,不会抛出异常。
4. new ThreadPoolExecutor.DiscardOldestPolicy(): //该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常
9.5、如何设置线程池的大小
1、CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小
2、I/O密集型:
在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。
10、四大函数式接口
函数式接口:只有一个方法的接口
Function<R, T>
函数型接口 有输入有输出。R apply(T t);
R和T为泛型Predicate<T>
断定型接口 有输入有输出boolean test(T t);
输入为自定义输入,输出为判断结果 布尔值Supplier<T>
供给型接口 只有输出T get();
Consumer<T>
消费型接口 只有输入void accept(T t);
11. ForkJoin
ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!
大数据中:MapReduce 核心思想->把大任务拆分为小任务!
11.1、ForkJoin 特点: 工作窃取!
实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!
11.2、如何使用ForkJoin?
-
1、通过ForkJoinPool来执行
-
2、计算任务 execute(ForkJoinTask<?> task)
-
3、计算类要去继承ForkJoinTask;
ForkJoin 的计算类
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
private Long temp = 10000L; //临界值
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if ((end - start) < temp){
Long sum = 0L;
for (Long i = start; i <= end; ++i) {
sum += i;
}
return sum;
} else {
Long middle = start + (end - start) / 2;
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork();
ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
task2.fork();
return task1.join() + task2.join();
}
}
}
使用
@Test
public void test02() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> forkJoinDemo = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum: " + sum + "耗时: " + (end - start));
}
并行流计算,速度很快
@Test
public void test03(){
long start = System.currentTimeMillis();
Long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum: " + sum + "耗时: " + (end - start));
}
12. 异步回调
Future 设计的初衷:对将来的某个事件结果进行建模!
类似于Ajax
但是我们平时都使用CompletableFuture
12.1、没有返回值的runAsync异步回调
public static void main(String[] args) throws ExecutionException, InterruptedException
{
// 发起 一个 请求
System.out.println(System.currentTimeMillis());
System.out.println("---------------------");
CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
//发起一个异步任务
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+".....");
});
System.out.println(System.currentTimeMillis());
System.out.println("------------------------------");
//输出执行结果
System.out.println(future.get()); //获取执行结果
}
12.2、有返回值的异步回调supplyAsync
//有返回值的异步回调
CompletableFuture<Integer> completableFuture=CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(2);
int i=1/0;
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1024;
});
//whenComplete里面传入一个消费者型的函数式接口 无论成功还是失败 这个方法都会执行
//其中第一个参数为执行成功的返回值,第二个为执行失败的错误信息
//exceptionally 相当于发生异常的回调函数,若发生异常就会执行里面的代码,有点类似于try catch里面的catch
//只不过exceptionally里面传入的为一个函数型函数式接口.当发生异常时,获取到的值就是这块的返回值
System.out.println(completableFuture.whenComplete((t, u) -> {
//success 回调
System.out.println("t=>" + t); //正常的返回结果
System.out.println("u=>" + u); //抛出异常的 错误信息
}).exceptionally((e) -> {
//error回调
System.out.println(e.getMessage());
return 404;
}).get());
whenComplete: 有两个参数,一个是t 一个是u
T:是代表的 正常返回的结果;
U:是代表的 抛出异常的错误信息;
如果发生了异常,get可以获取到exceptionally返回的值;