JUC
文章目录
什么是JUC?
JUC是JDK提供的为解决线程问题的工具类,即java.util.concurrent的缩写。
1.线程与进程
首先我们需要明白的是进程 > 线程 (虽然并不严谨,但是可以这样认为)
用一句专业术语来描述他们之间的区别:进程是CPU资源分配的最小单位,线程是CPU调度的最小单位
知乎上一个匿名用户很好的比喻:
- 进程
一个进程就是CPU执行 ,是程序在执行过程当中CPU资源分配的最小单位,并且进程都有自己的地址空间,包含了运行态、就绪态、阻塞态、创建态、终止态五个状态。
- 线程
线程是CPU调度的最小单位,它可以和属于同一个进程的其他线程共享这个进程的全部资源
- 两者之间的关系
一个进程包含多个线程,一个线程只能在一个进程之中。每一个进程最少包含一个线程。
- 两者之间的区别
其实最根本的区别在刚开始就说了:进程是CPU资源分配的最小单位,线程是CPU调度的最小单位
进程之间的切换开销比较大,但是线程之间的切换开销比较小。
CPU会把资源分配给进程,但是线程几乎不拥有任何的系统资源。因为线程之间是共享同一个进程的,所以线程之间的通信几乎不需要系统的干扰。
1.1线程状态
Thread.State
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,(新建)
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,(准备就绪)
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,(阻塞)
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,(不见不散)
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,(过时不候)
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;(终结)
}
1.2并发和并行
并发(concurrency)和并行(parallellism)是:
- 解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
- 解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
- 解释三:并行是在多台处理器上同时处理多个任务。如 hadoop 分布式集群,并发是在一台处理器上“同时”处理多个任务。
1.2.1并发
并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
就想前面提到的操作系统的时间片分时调度。打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。
1.2.2并行
并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
这里面有一个很重要的点,那就是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的『同时进行』。
1.3wait和sleep的区别
wait/sleep
功能都是当前线程暂停,有什么区别?
wait放开手去睡,放开手里的锁
sleep握紧手去睡,醒了手里还有锁
2.Lock接口
2.1Synchronized回顾
Synchronized就像汽车的自动档,我们使用时不需要手动关闭。
1、资源类就是纯的资源类,不去实现Rrunnable接口
2、方法内部的细节必须写在方法内部(高内聚) 创建的多线程只调用资源类的方法
3、创建一个资源类对象,多个线程去操作一个资源类
买票案例代码:
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
ticket.saleTicket();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
ticket.saleTicket();
}
}, "B").start();
}
}
// 志愿类
class Ticket {
private int number = 1000;
public synchronized void saleTicket() {
if (number > 0) {
try {
TimeUnit.MILLISECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "买到了第" + number-- + "张票还剩下" + number + "张票");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.2Lock
Lock就像汽车的手动挡,需要手动开启,手动关闭。
- Lock是一个
接口
- 定义了最常用的加锁
lock()
解锁unlock()
- 一般使用:
使用模板:
lock();//上来先锁住,下面是业务逻辑
try{
xxx业务逻辑
}finally{
unlock(); //解锁操作必须放在finally块里 保证不会死锁
}
使用Lock实现同步:
package com.wjj.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 首先需要写我们的资源类,已经资源类需要执行的方法
*/
class Ticket {
private int number = 30;
private 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();
}
}
}
/**
* 然后是我们的相应的方法,去调用资源类的方式
* 并且实现多线程操作
*/
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 1; i <= 30; i++) {
ticket.sale();
}
}, "BB").start();
new Thread(() ->{
for (int i = 1; i <= 30; i++) {
ticket.sale();
}
}, "AA").start();
new Thread(() ->{
for (int i = 1; i <= 30; i++) {
ticket.sale();
}
}, "CC").start();
}
}
3.线程间通信
3.1多线程编程模板:
- 判断
- 干活
- 通知
3.2生产者+消费者问题
1、生产者+消费者
2、通知等待唤醒机制
3.2.1synchronized实现
wait notify notifyAll(防止虚假唤醒)
- 设计到的三个方法:必须只能写在同步代码块中或者同步方法中 同步代码块的监视器必须和调用wait方法的对象是一个 否则报异常
- wait() 一旦执行此方法,当前线程就进入阻塞状态,并且释放已获得的锁意味着其他的线程可以进入同步代码块
- notify() 执行此方法,会随机唤醒一个wait()的线程,如果有多个线程被wait,就唤醒优先级高的
- notifyAll() 唤醒所有正在wait()的进行
- 这三个方法都是定义在java.lang.Object中的 且不能使用在lock锁的代码里面
- synchronized wait notify notifyAll 这四个必须在一起使用
代码
package com.wjj.demo;
/**
* @Description:
*现在两个线程,
* 可以操作初始值为零的一个变量,
* 实现一个线程对该变量加1,一个线程对该变量减1,
* 交替,来10轮。
*
* * 笔记:Java里面如何进行工程级别的多线程编写
* 1 多线程变成模板(套路)-----上
* 1.1 线程 操作 资源类
* 1.2 高内聚 低耦合
* 2 多线程变成模板(套路)-----下
* 2.1 判断
* 2.2 干活
* 2.3 通知
*/
//创建资源类
class ShareOne {
private int num = 0;
public synchronized void incr() throws InterruptedException {
//判断
if (num != 0) {
this.wait();
}
//干活
num++;
System.out.println(Thread.currentThread().getName() + "\t" + num);
//通知
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//判断
if (num != 1) {
this.wait();
}
//干活
num--;
System.out.println(Thread.currentThread().getName() + "\t" + num);
//通知
this.notifyAll();
}
}
public class NotifyWaitDemo {
public static void main(String[] args) {
ShareOne shareOne = new ShareOne();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
shareOne.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "AA").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
shareOne.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "BB").start();
}
}
但是以上代码例子有问题
如果换成四个线程呢?
并且程序无法停止
所以:
- 必须使用while进行判断条件,不能使用if判断
- 因为我们在判断条件的时候,假设现在A(加1)线程来了,判断这时的data是1,就wait()释放锁并且阻塞,,然后又有一个B(加一)线程来了,经过判断也wait()阻塞,注意这时这两个线程都已经进入了方法判断代码块内,然后一个(减一)线程将data的值减了一,后唤醒了所有wait()的线程,注意如果这时我们使用的是if的话,这两个线程就不会再次判断data的值了,直接进行后续的加1,所以导致多加(或类似的多减的操作),这跟if的判断有关,因为if只会判断一次,而while会进行多次循环判断,此时两个加一线程都被唤醒了,因为第一次不满足条件,那么就还会去判断data的值,最后只有一个线程才能完成加1,另一个则继续wait()
所以要改为:
public synchronized void incr() throws InterruptedException {
//判断
while (num != 0) {
this.wait();
}
//干活
num++;
System.out.println(Thread.currentThread().getName() + "\t" + num);
//通知
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
//判断
while (num != 1) {
this.wait();
}
//干活
num--;
System.out.println(Thread.currentThread().getName() + "\t" + num);
//通知
this.notifyAll();
}
}
3.2.2使用Lock实现
-
Lock锁要实现生产者消费者只能使用Condition接口中的
await()
signal()
方法 -
可以实现精准的唤醒 notify和notifyAll都不可以,因为notify是随机唤醒一个等待的线程,notifyAll是唤醒所有等待的线程,无法实现精准的通知唤醒
-
新版的写法步骤 判断->业务->修改标志位/唤醒
package com.wjj.demo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareDataOne {
private int num = 0;
private Lock lock = new ReentrantLock();
private Condition cd = lock.newCondition();
public void incr() {
lock.lock();
try {
while (num != 0) {
cd.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "\t" + num);
cd.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decr() {
lock.lock();
try {
while (num != 1) {
cd.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "\t" + num);
cd.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class LockTest {
public static void main(String[] args) {
ShareDataOne shareDataOne = new ShareDataOne();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
shareDataOne.incr();
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
shareDataOne.decr();
}
}
}, "B").start();
}
}
3.2.3线程间定制化通信
题目描述:
使用线程通信,交替打印ABC这三个字符。
三个线程ABC交替打印ABC字符串,要求线程执行顺序是A,B,C 这时就需要精准的唤醒A打印完通知B,B打印完通知C,C打印完通知A,都需要修改标志位
思路:定义一个标志位,当flag是"A"时,A打印,打印完修改flag为"B",并唤醒B,B打印完修改flag为"C",C打印完修改flag为"A",并唤醒A …
代码:
package com.wjj.demo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareData {
private char flag = 'A';
Lock lock = new ReentrantLock();
Condition cdA = lock.newCondition();
Condition cdB = lock.newCondition();
Condition cdC = lock.newCondition();
public void printA() {
lock.lock();
try {
while (flag != 'A') {
cdA.await();
}
System.out.println(Thread.currentThread().getName() + "\t" + flag);
flag = 'B';
cdB.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (flag != 'B') {
cdB.await();
}
System.out.println(Thread.currentThread().getName() + ":\t" + flag);
flag = 'C';
cdC.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (flag != 'C') {
cdC.await();
}
System.out.println(Thread.currentThread().getName() + "\t" + flag);
flag = 'A';
cdA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ThreadOrderAccess {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
shareData.printA();
}
}
},"打印A的线程").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
shareData.printB();
}
}
},"打印B的线程").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
shareData.printC();
}
}
},"打印C的线程").start();
}
}
3.3集合不安全
3.3.1List不安全
- ArrayList是线程不安全的,在多线程下的修改会报异
java.util.ConcurrentModificationException
解决方案:
- 1、改用Vector
- 2、使用Collections工具类中的synchronizedList()方法,将ArrayList转成一个线程安全的SynchronizedList类的对象,也是List接口的间接实现类
- 3、使用CopyOnWriteArrayList类代替ArrayList,写时复制
一般在高并发下我们只用第三种,使用CopyOnWriteArrayList类 读数据时不加锁,在写数据时(删除 添加 修改)时先复制原数组并加Lock锁
底层也是一个Object类型的数组
private transient volatile Object[] array;
add()方法的源码:
大致就是先去复制一个原来的数组,长度加1,然后对新数组进行操作,操作之后使用set方法替换原数组 使用的是Lock锁(JDK1.8)
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取原数组
Object[] elements = getArray();
//获取原长度
int len = elements.length;
//基于原数组复制一个新数组,长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将添加的元素放到新数组最后的位置
newElements[len] = e;
//新数组替换原数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
3.3.2Set不安全
- 使用Collectios工具类的synchronizedSet方法将HashSet转为线程安全的
- 使用
CopyOnWriteArraySet
类代替HashSet
3.3.3Map不安全
HashMap底层是Node类型
的数组,默认长度是16,默认负载因子是0.75 默认最大长度是2的30次方,扩容是2倍,
- 使用Collectios工具类的synchronizedMap方法将HashMap转为线程安全的
- 使用HashTable,线程安全,古老的集合(一般不用)
- 使用
ConcurrentHashMap
替换HashMap
1、ConcurrentHashMap不能存储null的key和value
2、使用了分段锁
技术 效率高
3.3.4解决方式: 写时复制
写时复制(Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。
其核心思想是,如果有多个调用者同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这个过程对其他的调用者是透明的(transparently)。
此作法的主要优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。
可以参考文章:
面试官:写时复制(Copy-On-Write)在Java中是如何被应用的吗? - 知乎 (zhihu.com)
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();
}
}
CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,
而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容器Object[] newElements里添加元素。
添加元素后,再将原容器的引用指向新的容器setArray(newElements)。
这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
4.多线程锁
4.1 8锁问题
例子:Lock_8
- 标准访问,先打印短信还是邮件
- 停4秒在短信方法内,先打印短信还是邮件
- 普通的hello方法,是先打短信还是hello现在有两部手机,先打印短信还是邮件
- 两个静态同步方法,1部手机,先打印短信还是邮件
- 两个静态同步方法,2部手机,先打印短信还是邮件
- 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
- 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
package com.wjj.demo;
import java.util.concurrent.TimeUnit;
class Phone {
public synchronized void sendSMS() throws Exception {
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
/**
* @author xialei
* <p>
* 1 标准访问,先打印短信还是邮件
* 2 停4秒在短信方法内,先打印短信还是邮件
* 3 新增普通的hello方法,是先打短信还是hello
* 4 现在有两部手机,先打印短信还是邮件
* 5 两个静态同步方法,1部手机,先打印短信还是邮件
* 6 两个静态同步方法,2部手机,先打印短信还是邮件
* 7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
* 8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
* ---------------------------------
* @Description: 8锁
*/
public class Lock_8 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "AA").start();
Thread.sleep(100);
new Thread(() -> {
try {
phone.sendEmail();
//phone.getHello();
//phone2.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, "BB").start();
}
}
运行答案:
1、短信
2、短信
3、Hello
4、邮件
5、短信
6、短信
7、邮件
8、邮件
A 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,
所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁——类对象本身,
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
而不管是同一个实例对象的静态同步方法之间,
还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
5.Collable接口
- JDK5的新特性,Callable接口(异步任务)+FutureTask实现多线程
- Callable接口源码: 使用Callable实现多线程异步任务可以获取返回结果并捕获异常
- 标记了@FunctionalInterface注解: 是一个函数式接口
//是一个函数式接口,带有一个泛型V,是方法的返回值类型,只有一个方法call(),抛出了异常,也即我们可以在外部获取异步任务的异常
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
//与Runnable接口对比,run方法没有返回值,不能在外部捕获异常
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
FutureTask与Callable接口、Runnable接口的关系及常用方法:
FutureTask与Runnable接口的关系,可以发现,FutureTask就是Runnable接口的实现类
源码:FutureTask与Callable接口的关系,Callable作为FutureTask的一个属性,通过构造器传入Callable的实现类对象,从而可以在run方法里调用我们Callable的方法
//FutureTask的一个属性
private Callable<V> callable;
//FutureTask的构造器
public FutureTask(Callable<V> callable) {
this.callable = callable;
}
//在FutureTask重写的run方法里调用了call方法
public void run() {
result = c.call();
}
重要方法get()
我们可以通过FutureTask对象的get()方法获取异步任务的返回值
并捕获异步任务出现的异常
,注意:此方法是阻塞方法,会阻塞当前线程一直等到异步任务结束拿到返回值
代码与原理
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
class MyThread implements Runnable{
@Override
public void run() {
}
}
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"come in callable");
return 200;
}
}
public class CallableDemo {
public static void main(String[] args) throws Exception {
//FutureTask<Integer> futureTask = new FutureTask(new MyThread2());
FutureTask<Integer> futureTask = new FutureTask(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 1024;
});
FutureTask<Integer> futureTask2 = new FutureTask(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 2048;
});
new Thread(futureTask,"zhang3").start();
new Thread(futureTask2,"li4").start();
//System.out.println(futureTask.get());
//System.out.println(futureTask2.get());
//1、一般放在程序后面,直接获取结果
//2、只会计算结果一次
while(!futureTask.isDone()){
System.out.println("***wait");
}
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName()+" come over");
}
}
/**
原理:
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,
当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,
就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,
然后会返回结果或者抛出异常。
只计算一次
get方法放到最后
*/
6.JUC辅助类
6.1 CountDownLatch:减少计数
减少计数
原理
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
代码
package com.atguigu.thread;
import java.util.concurrent.CountDownLatch;
/**
*
* @Description:
* *让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。
*
* CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
* 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
* 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
*
* 解释:6个同学陆续离开教室后值班同学才可以关门。
*
* main主线程必须要等前面6个线程完成全部工作后,自己才能开干
* @author xialei
*/
public class CountDownLatchDemo
{
public static void main(String[] args) throws InterruptedException
{
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6; i++) //6个上自习的同学,各自离开教室的时间不一致
{
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t 号同学离开教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t****** 班长关门走人,main线程是班长");
}
}
6.2 CyclicBarrier:循环栅栏
原理
CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过CyclicBarrier的await()方法。
代码
package com.atguigu.thread;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
*
* @Description: TODO(这里用一句话描述这个类的作用)
* @author xialei
*
* CyclicBarrier
* 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,
* 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
* 直到最后一个线程到达屏障时,屏障才会开门,所有
* 被屏障拦截的线程才会继续干活。
* 线程进入屏障通过CyclicBarrier的await()方法。
*
* 集齐7颗龙珠就可以召唤神龙
*/
public class CyclicBarrierDemo
{
private static final int NUMBER = 7;
public static void main(String[] args)
{
//CyclicBarrier(int parties, Runnable barrierAction)
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, ()->{System.out.println("*****集齐7颗龙珠就可以召唤神龙");}) ;
for (int i = 1; i <= 7; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+"\t 星龙珠被收集 ");
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
6.3 Semaphore信号灯
原理
在信号量上我们定义两种操作:
acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
代码
package com.atguigu.thread;
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
*
* @Description: TODO(这里用一句话描述这个类的作用)
* @author xialei
*
* 在信号量上我们定义两种操作:
* acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),
* 要么一直等下去,直到有线程释放信号量,或超时。
* release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
*
* 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
*/
public class SemaphoreDemo
{
public static void main(String[] args)
{
Semaphore semaphore = new Semaphore(3);//模拟3个停车位
for (int i = 1; i <=6; i++) //模拟6部汽车
{
new Thread(() -> {
try
{
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t 抢到了车位");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+"\t------- 离开");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
7.读写锁 ReentrantReadWriteLock
例子:ReadWriteLockDemo
代码
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+"\t 正在写"+key);
//暂停一会儿线程
try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace(); }
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t 写完了"+key);
}
public Object get(String key){
Object result = null;
System.out.println(Thread.currentThread().getName()+"\t 正在读"+key);
try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace(); }
result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t 读完了"+result);
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
8. BlockingQueue阻塞队列
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
阻塞队列的用处
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
种类分析
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
- LinkedTransferQueue:由链表组成的无界阻塞队列。
- LinkedBlockingDeque:由链表组成的双向阻塞队列。
BlockingQueue核心方法
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 阻塞队列
*/
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// List list = new ArrayList();
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.element());
//System.out.println(blockingQueue.add("x"));
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// 第二组
// System.out.println(blockingQueue.offer("a"));
// System.out.println(blockingQueue.offer("b"));
// System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("x"));
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// 第三组
// blockingQueue.put("a");
// blockingQueue.put("b");
// blockingQueue.put("c");
// //blockingQueue.put("x");
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// 第四组
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("a",3L, TimeUnit.SECONDS));
}
}
9.线程池
9.1引言
我们在真实业务中,使用的基本都是线程池,并且保证一个系统基本上只有一到两个线程池,系统将所有的异步任务都交给线程池去处理.可以控制系统的稳定性(稳定性永远是第一位,高并发写的在牛逼,运行几秒系统崩了也没卵用)。因为前三种方式频繁的创建销毁造成系统的资源不必要开销,并且一直无休止的开启线程可能会造成资源耗尽,直接导致系统崩溃。
为什么要使用线程池?
1、降低资源的消耗
2、提高响应速度
3、提高线程的可管理性
9.2线程池的创建及七大参数详解
线程池本质上就是ThreadPoolExecutor
这个类,看一下它的继承及实现关系
public class ThreadPoolExecutor extends AbstractExecutorService
public abstract class AbstractExecutorService implements ExecutorService
public interface ExecutorService extends Executor
//最顶级的接口 里面只定义了 void execute(Runnable command) 方法
public interface Executor
9.2.1七大参数及原生创建
- int corePoolSize
线程池中的核心线程数量,会一直保留在池中的(即使它们处于空闲状态),除非设置了allowCoreThreadTimeOut这个参数。
- int maximumPoolSize
线程池中最大可以开的线程数量
- long keepAliveTime
非核心|空闲线程的超时时间,超过这个时间空闲线程将被回收
- TimeUnit unit
时间单位 上面超时时间的时间单位
//传入TimeUnit枚举类中的对象
public enum TimeUnit {
NANOSECONDS {
},
MICROSECONDS {
},
MILLISECONDS {
},
SECONDS {
},
....
- BlockingQueue workQueue
阻塞队列,将多余的任务放入阻塞队列中,如果线程空闲了,就去队列中取出任务执行
==注意:==默认创建的队列的长度是Integer的最大值,我们必须显示的指定队列的长度
- ThreadFactory threadFactory
线程池中线程创建的工厂,有各个线程创建的细节,比如指定线程的名字等
- RejectedExecutionHandler handler
//四种拒绝策略
DiscardOldestPolicy --->丢弃队列中最老(最先入队)的任务
AbortPolicy --->直接丢弃新来的任务 抛出异常 (默认的)
CallerRunsPolicy --->直接调用run方法,相当于同步方法
DiscardPolicy --->直接丢弃新来的任务 不抛出异常
9.3使用Excutor工具类创建线程池
package com.wjj.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService threadPool1 = Executors.newFixedThreadPool(3);
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
ExecutorService threadPool3 = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 30; i++) {
threadPool3.execute(() ->{
System.out.println(Thread.currentThread().getName() + "-----执行");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool3.shutdown();
}
}
}
9.4线程池的工作流程
1、
线程池创建,准备好core数量的核心线程,准备接收任务
2、
当核心线程全部都在执行任务,将在来的任务直接放入阻塞队列中(注意,可不是直接在开线程),当某一个核心线程执行完任务了就去队列中拿任务去执行。
3、
当队列中的任务也满了,这时才开启新的线程执行任务,最大只能开到max最大数量的线程数
4、
当所有的线程全在执行任务,队列中的任务也满了,这时在来的任务就要走拒绝策略
5、
当所有的任务全部执行完毕,经过了keepAliveTime时间没有新的任务,释放max-core这些线程。 (当设置了allowCoreThreadTimeOut(true)时,核心的线程空闲时间达到keepAliveTime也会被回收)
9.5 线程池的几大重要的参数
参数名称 | 说明 |
---|---|
corePoolSize | 线程池中的常驻核心线程数 |
maximumPoolSize | 线程池中能够容纳同时 执行的最大线程数,此值必须大于等于1 |
keepAliveTime | 多余的空闲线程的存活时间 当前池中线程数量超过corePoolSize时,当空闲时间 达到keepAliveTime时,多余线程会被销毁直到 只剩下corePoolSize个线程为止 |
unit | keepAliveTime的单位 |
workQueue | 任务队列,被提交但尚未被执行的任务 |
threadFactory | 表示生成线程池中工作线程的线程工厂, 用于创建线程,一般默认的即可 |
handler | 拒绝策略,表示当队列满了,并且工作线程大于 等于线程池的最大线程数(maximumPoolSize)时如何来拒绝 请求执行的runnable的策略 |
9.6 线程池的拒接策略
等待队列已经排满了,再也塞不下新任务了
同时,线程池中的max线程也达到了,无法继续为新任务服务。
这个时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不
会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中
尝试再次提交当前任务。
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。
如果允许任务丢失,这是最好的一种策略。
注 :以上内置拒绝策略均实现了RejectedExecutionHandle接口
在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?(超级大坑)
答案是一个都不用,我们工作中只能使用自定义的
Executors中JDK已经给你提供了,为什么不用?
9.7 手动实现线程池
演示代码
package com.test.demo;
import java.util.concurrent.*;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
3L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
// new ThreadPoolExecutor.AbortPolicy() // 如果线程池中的线程都被使用,阻塞队列中也满了就直接抛出异常
// new ThreadPoolExecutor.CallerRunsPolicy()// 同上,但是不会直接抛出异常,而是
// new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardPolicy()
);
try {
for (int i = 0; i < 30; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ "号线程执行业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}