Java编程
文章目录
三、ReentrantReadWriteLock类(重入锁、读写锁、独占锁\互斥锁)
1.JUC线程同步类 CountDownLatch(门栓类)
一、Lock锁
1.Lock的子类
2.Lock的介绍
Lock锁可以实现synchronized的效果,即实现原子性、有序性和可见性。但它属于轻量级锁,可以手动获取锁和释放锁、可中断的获取锁、超时获取锁。
Lock只能给代码块上锁,synchronized可以给方法和代码块上锁。
Lock类实际上是一个接口,我们创建Lock锁,其实是实例化Lock的实现子类,用lock.lock来加锁,用lock.unlock来释放锁。在两者中间放置需要同步处理的代码。
3.Lock的基本使用
public class LockDemo01 {
private Lock lock = new ReentrantLock(); //创建Lock锁
public void testMethod(){
lock.lock();//上锁
try{
for (int i = 0 ;i < 100;i++){
System.out.println("ThreadName = " + Thread.currentThread().getName() + " " + (i + 1));
}
}finally {
//经典写法:在finally中解锁,防止前面代码异常,解不了锁。
lock.unlock();//解锁
}
}
public static void main(String[] args) {
LockDemo01 service = new LockDemo01();
new Thread(service::testMethod).start();
new Thread(service::testMethod).start();
new Thread(service::testMethod).start();
new Thread(service::testMethod).start();
new Thread(service::testMethod).start();
}
}
4.Lock的API
lock() | 获取锁 |
tryLock | 只尝试获取一次锁 |
tryLock(long time, TimUnit unit) | 在一定时间内尝试获取锁 |
lockInterruptibly() | 一直尝试获取,直到得到中断命令interrupt()。 |
unlock() | 解除锁 |
二、ReentrantLock类
1.ReentrantLock类
ReentrantLock类,可重入锁,单线程可以重复进入,默认是非公平锁。
Lock接口和ReentrantLock类的底层都是继承AbstractQueuedSynchronizer类。AQS是除了synchronized关键字之外的另一套锁机制。
获取锁
1)CAS操作抢占锁,抢占成功则修改锁的状态为1,将线程信息记录到锁当中,返回state=1
2)抢占不成功,tryAcquire获取锁资源,获取成功直接返回,获取不成功,新建一个检点插入到
当前AQS队列的尾部,acquireQueued(node)表示唤醒AQS队列中的节点再次去获取锁
释放锁
1)获取锁的状态值,释放锁将状态值-1
2)判断当前释放锁的线程和锁中保存的线程信息是否一致,不一致会抛出异常
3)状态只-1直到为0,锁状态值为0表示不再占用,为空闲状态
ReentrantLock的使用:
Lock lock = new ReentrantLock();
try{
lock.lock();
}finally{
lock.unlock();
}
2.公平锁和非公平锁
公平锁:按顺序排队一个一个获取锁。
非公平锁:各个线程相互竞争获取锁。默认是非公平锁。
创建公平锁:private Lock lock = new ReentrantLock(true);
3.可重入锁与不可重入锁
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
4.AQS
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,
即将暂时获取不到锁的线程加入到等待队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
三、ReentrantReadWriteLock类(重入锁、读写锁、独占锁\互斥锁)
Lock的读写规则:读读共享,读写互斥,写写互足
/**
* 读写锁的基本使用
* 读读共享,读写互斥,写写互足
*/
public class ReadWriterLockDemo {
private ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
private Lock readLock = rrwl.readLock(); //创建读锁
private Lock writerLock = rrwl.writeLock(); //创建写锁
//模拟读操作
public void read(){
String name = Thread.currentThread().getName();
//上锁
readLock.lock();
System.out.println(name+" ---- 正在读取数据 -----");
//模拟IO阻塞
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+" ----- 读取结束 -----");
//解锁
readLock.unlock();
}
//模拟写操作
public void writer(){
String name = Thread.currentThread().getName();
//上锁
writerLock.lock();
System.out.println(name+" ---- 正在写入数据 -----");
//模拟IO阻塞
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+" ----- 写入结束 -----");
writerLock.unlock();
}
//主程序
public static void main(String[] args) {
ReadWriterLockDemo readWriterLockDemo = new ReadWriterLockDemo();
//创建5个读线程并启动
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
readWriterLockDemo.read();
}
}).start();
}
//创建5个写线程并启动
for (int i = 0; i < 5; i++) {
//lamda表达式
new Thread(
()->{
readWriterLockDemo.writer();
}
).start();
}
}
}
四、同步锁的底层优化:锁升级
级别由高到低依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
普通对象在内存中的结构分为多部分,第一部分称为markwork,共64位。在对应锁对象的markword字段的低位字段标记锁的类型。
无锁状态:001
偏向锁状态:101
轻量级锁状态:00
重量级锁状态:10
五、volatile关键字
volatile:易变的,不稳定的意思。使用volatile修饰的变量,可以保证在多个线程之间的可见性,并且避免指令重排。但是无法保证操作的原子性。
1.线程安全三要素:可见性、原子性、有序性
可见性:
可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。
原理:CPU修改变量,是在cache中修改的,每个CPU都有cache,所以别的CPU是看不见的,使用volatile,是将变量写在主存上,这样每个CPU都能看见。
原子性︰
原子是世界上的最小单位,具有不可分割性。非原子操作都会存在线程安全问题,需我们使用同步技术( sychronized )来让它变成一个原子操作。一条线程开始执行同步代码块的内容,就一定要全部执行完,否则不允许切换线程(不允许其他线程进来)。
有序性:
Java语言提供了volatile和synchronized 两个关键字来保证线程之间操作的有序性,volatile是因为其本身包含“禁止指令重排序”的语义,synchronized是由“一个变量在同一个时刻只允许一条线程对其进行lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
synchronized可以保证可见性、原子性、有序性
volatile只能保证可见性和有序性,CAS可以保证原子性,volatile+CAS就是一种线程安全的方案,Lock底层使用的就是volatile+CAS。
2.理解CAS和ABA问题
CAS(Compare And Swap/Set):比较并交换,比较并修改。它的作用是,对指定内存地址的数据,校验它的值是否为期望值,如果是,就修改为新值,返回值表示是否修改成功。
ABA问题:当线程1读取数据A,想将其修改成B,但线程2已经读取并修成了C,线程3又读取修改成了A,这时候的A已经不是原来的A了,但线程1还是检验成了A,并将其修改成了B覆盖上去了。
解决方案:存储A的时候,在存储一个时间戳或者版本号,这样验证的时候既可以验证A,也可以验证A的时间戳或版本号。
使用原子类包装操作原子性
Java提供了一个非公开的类,sun.misc.UnSafe,来专门做操作底层的操作,它提供的方法都是native本地方法,它封装了一系列的原子化操作。
3.Volatile实现原子性
public class VolatileDemo02 {
//创建原子类的实例化
volatile static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) {
//执行10个线程,每个线程对1加10000下
for (int j = 0; j < 10; j++) {
new Thread(
()->{
for (int k = 0; k < 10000; k++) {
//volatile修饰这个i,无法保证i的原子性;i++底层也会转换为多条机器指令。所以加上CAS保证原子性。
i.incrementAndGet(); //效果等于 i++
}
}
).start();
}
//休眠2秒,输出结果
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
六、线程通信-消费者和生产者问题
线程通信的细节
细节1:进行线程通信的多个线程、要使用同一个同步监视器(product),还必须要调用该同步监视器的wait()、notify0. notifyAllO;
细节2:sleep()和wait()方法都会让出CPU,最大的区别是sleep()方法不会释放锁,wait()方法会释放锁。区别二,sleep()可以在任何地方用,wait()只能在同步代码块和同步方法中使用。
1.同步代码块的线程通信
线程通信的前提条件:生产者和消费者线程都要加锁,而且要是同一把锁。
消费者和生产者线程通信的解释:生产者先判断仓库是否是空的,如果仓库是满的,就进入等待状态,并释放锁。如果仓库是空的,就生产商品,并将仓库状态设置为满,并唤醒消费者进行消费。
消费会先判断仓库是否是满的,如果仓库是空的,就进入等待状态,并释放锁。如果仓库是满的,就消费商品,并将仓库状态设置为空,并唤醒生产者进行生产。
为什么没有设置唤醒的线程,就会唤醒消费者:
对象锁设置的是商品,当线程启动时会创建一个等待队列,生产者和消费者会在队列中等待锁。
当生产者进行唤醒时,只有消费者在等待。当消费者进行唤醒时,只有生产者在等待。
当唤醒消费者后,生产者判断仓库是满的后,进入等待,让出CPU,消费者就进行消费,消费完后,唤醒生产者,消费者会判断仓库为空后进入等待状态。
1)商品类
public class Product {
private String name; //馒头 玉米饼
private String color; //白色 黄色
private boolean flag; // 仓库状态
public Product() {}
public Product(String name, String color) {
this.name = name;
this.color = color;
flag= false;
}
public String getName() {
return name;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
2)生产者线程
public class ProduceRunnable implements Runnable {
private Product product;
public ProduceRunnable(Product product) {
this.product = product;
}
@Override
public void run() {
int num = 0;
while (true) {
synchronized (product) {
//如果仓库已满,就等待
if (product.isFlag()){
try {
product.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产商品,并输出
if (num % 2 == 0) {
product.setName("馒头");
//模拟生产中断
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
product.setColor("白色");
} else {
product.setName("玉米饼");
//模拟生产中断
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
product.setColor("黄色");
}
System.out.println("生产者线程生产商品:" + product.getColor() + product.getName());
//修改仓库的状态:已满
product.setFlag(true);
//通知消费者消费
product.notify();
}
num++;
}
}
}
3)消费者线程
public class ConsumeRunnable implements Runnable{
private Product product;
public ConsumeRunnable(Product product) {
this.product = product;
}
@Override
public void run() {
while (true){
synchronized (product){
//如果仓库为空,就等待
while(!product.isFlag()){
try{
product.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//消费商品
System.out.println("消费者线程消费商品:"+product.getColor()+product.getName());
//改变仓库状态:已空
product.setFlag(false);
//通知生产者生产
product.notify();
}
}
}
}
4)测试类
/**
* 测试生产者线程和消费者线程:
* 问题1:消费者消费的不是生产者的商品
* 原因:生产者和消费者线程,商品数据没有共享。
* 解决:不要在生产者类和消费者类中创建商品类,要通过构造方法或者Setter方法传递进去。
*
* 问题2:当生产中断时,生产的商品就会混乱,(黄色馒头,白色玉米饼)
* 原因︰没有进行线程同步﹔商品生产没有生产完,切换到消费者线程开始消费
* 解决:
* 1.给生产者加锁,必须完整的生产一个商品吏释放锁
* 2.也必须给消费者加锁,必须和生产者是同一把锁,生产者没有生产完毕,就没有释放锁,消费者及时获取了cPU了,也无法得到锁,从而无法消费
*
* 问题3:消费者和生产者没有交费,出现了供不应求或者供过于求
* 原因:生产者和消费者之间没有进行通信
* 仓库满,生产者应该等待;消费者消费完,应该通知生产者。
* 仓库空,消费者应该等待;生产者生产完后,应该通知消费者。
* 解决:进行线程通信
*
*/
public class Test {
public static void main(String[] args) {
//创建商品类
Product product = new Product();
//创建一个生产者线程和消费者线程
Runnable produceRunnable = new ProduceRunnable(product);
Thread t1 = new Thread(produceRunnable);
Runnable consumeRunnable = new ConsumeRunnable(product);
Thread t2 = new Thread(consumeRunnable);
//启动生产者和消费者线程
t1.start();
t2.start();
}
}
2.同步方法的线程通信
1)商品类
public class Product {
private String name; //馒头 玉米饼
private String color; //白色 黄色
private boolean flag; // 仓库状态
public Product() {
flag = false;
}
/**
* 生产一个商品
*/
public synchronized void produce(String name, String color) {
//如果仓库已满,就等待
if (isFlag()) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产商品,并输出
setName(name);
//模拟生产中断
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
setColor(color);
System.out.println("生产者线程生产商品:" + getColor() + getName());
//修改仓库的状态:已满
this.setFlag(true);
//通知消费者消费
this.notify();
}
/**
* 消费一个商品
*/
public synchronized void consume() {
//如果仓库为空,就等待
while (!isFlag()) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费商品
System.out.println("消费者线程消费商品:" + getColor() + getName());
//改变仓库状态:已空
setFlag(false);
//通知生产者生产
this.notify();
}
public String getName() {
return name;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
2)生产者线程
public class ProduceRunnable implements Runnable {
private Product product;
public ProduceRunnable(Product product) {
this.product = product;
}
@Override
public void run() {
int num = 0;
while (true) {
if (num % 2 == 0) {
product.produce("馒头","白色");
}else{
product.produce("玉米饼","黄色");
}
num++;
}
}
}
3)消费者线程
public class ConsumeRunnable implements Runnable{
private Product product;
public ConsumeRunnable(Product product) {
this.product = product;
}
@Override
public void run() {
while (true){
product.consume();
}
}
}
4)测试类
public class Test {
public static void main(String[] args) {
//创建商品类
Product product = new Product();
//创建一个生产者线程和消费者线程
Runnable produceRunnable = new ProduceRunnable(product);
Thread t1 = new Thread(produceRunnable);
Runnable consumeRunnable = new ConsumeRunnable(product);
Thread t2 = new Thread(consumeRunnable);
//启动生产者和消费者线程
t1.start();
t2.start();
}
}
3.Lock锁的线程通信
之前实现线程通信时,是生产者和消费者在一个等待队列中,会存在本来打算唤醒消费者,却唤醒一个生产者的问题。Lock锁中的Condition可以创建不同的等待队列,让生产者和消费者线程在不同的队列中等待,实现精准唤醒操作。
1)商品类
public class Product {
private String name; //馒头 玉米饼
private String color; //白色 黄色
private boolean flag; // 仓库状态
private Lock lock = new ReentrantLock(); //创建Lock锁
private Condition produceCondition = lock.newCondition(); //控制线程的等待与通知
private Condition consumeCondition = lock.newCondition(); //控制线程的等待与通知
public Product() {
flag = false;
}
/**
* 生产一个商品
*/
public void produce(String name, String color) {
lock.lock();
try{
//如果仓库已满,就等待
if (isFlag()) {
try {
//让生产者进入生产者等待队列
produceCondition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//生产商品,并输出
setName(name);
//模拟生产中断
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
setColor(color);
System.out.println("生产者线程生产商品:" + getColor() + getName());
//修改仓库的状态:已满
setFlag(true);
// produceCondition.signal(); 通知一个消费者消费
consumeCondition.signalAll(); //通知所有消费者消费
}finally{
lock.unlock();
}
}
/**
* 消费一个商品
*/
public void consume() {
lock.lock();
try{
//如果仓库为空,就等待
while (!isFlag()) {
try {
//让消费者进入消费者等待队列
consumeCondition.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//消费商品
System.out.println("消费者线程消费商品:" + getColor() + getName());
//改变仓库状态:已空
setFlag(false);
//consumeCondition.signal(); 通知一个生产者生产
produceCondition.signalAll(); //通知所有生产者生产
}finally {
lock.unlock();
}
}
public String getName() {
return name;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
2)生产者线程
public class ProduceRunnable implements Runnable {
private Product product;
public ProduceRunnable(Product product) {
this.product = product;
}
@Override
public void run() {
int num = 0;
while (true) {
if (num % 2 == 0) {
product.produce("馒头","白色");
}else{
product.produce("玉米饼","黄色");
}
num++;
}
}
}
3)消费者线程
public class ConsumeRunnable implements Runnable{
private Product product;
public ConsumeRunnable(Product product) {
this.product = product;
}
@Override
public void run() {
while (true){
product.consume();
}
}
}
4)测试类
public class Test {
public static void main(String[] args) {
//创建商品类
Product product = new Product();
//创建一个生产者线程和消费者线程
Runnable produceRunnable = new ProduceRunnable(product);
Thread t1 = new Thread(produceRunnable);
Runnable consumeRunnable = new ConsumeRunnable(product);
Thread t2 = new Thread(consumeRunnable);
//启动生产者和消费者线程
t1.start();
t2.start();
}
}
4.Condition类
Condition类的作用:能够更加精细的控制多线程的休眠与唤醒。 对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。 一个 Condition包含一个等待队列。一个Lock可以产生多个Condition,所以Lock拥有一个同步队列和多个等待队列。
Condition类的方法:
await() | 将线程添加到等待队列中 |
signal() | 唤醒一个等待队列中的线程 |
signalAll() | 唤醒所有等待队列中的线程 |
七、线程池
1.线程池:
创建好多个线程,放入线程池中,使用时直接获取引用,不使用时放回池中。可以避免频繁创建销毁、实现重复利用
2.线程池的好处
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 提高线程的可管理性避免线程无限制创建、从而销耗系统资源,降低系统稳定性,甚至内存溢出或者CPU耗尽
3.线程池的应用场合
- 需要大量线程,并且完成任务的时间短
- 对性能要求苛刻
- 接受突发性的大量请求
4.续用线程池执行大量无返回值的Runnable命令
public class RunnableTask {
public static void main(String[] args) {
//创建线程池
//线程池只有一个线程∶可以节省创建线程的时间,保证一定会有一个
ExecutorService pool = Executors.newSingleThreadExecutor();
//线程池中有固定数量的线程
//ExecutorService pool = Executors.newFixedThreadPool(10);
//线程池中有可变数量的线程
//ExecutorService pool = Executors.newCachedThreadPool();
//Scheduled:线程池用来执行间隔执行(每个30分钟执行一次)或者延迟执行(半小时之后执行,12:00执行)的任务
//ExecutorService pool = Executors.newScheduledThreadPool(10);
//使用线程
//使用线程池执行大量的Runnable命令
for (int i = 0; i < 200; i++) {
//将任务交给线程池执行
pool.execute(new MyRunnable(i));
}
//关闭线程
//pool.isShutdown(); //判断线程池有没有关闭
//pool.shutdownNow(); //立刻关闭线程池,返回还没有执行完的线程
pool.shutdown(); //关闭线程池
}
}
//模拟线程任务
class MyRunnable implements Runnable{
private int i;
public MyRunnable(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println("任务开始:"+i);
System.out.println("任务结束:"+i);
}
}
5.使用线程池执行大量的Callable任务(有返回值)
import java.util.concurrent.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class CallableTask {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程池
//线程池中有固定数量的线程
ExecutorService pool = Executors.newFixedThreadPool(10);
//使用线程
//使用线程池执行大量的Callable命令(有返回值)
List<Future> futureList = new ArrayList<>(); //储存返回的结果
for (int i = 0; i < 200; i++) {
//创建Callable任务
Callable<Integer> callable = new MyCallable();
//使用线程池执行有返回值任务
Future<Integer> submit = pool.submit(callable);
futureList.add(submit);
}
//输出结果
for (int i = 0; i < 200; i++) {
Future<Integer> task = futureList.get(i);
int result = task.get();
System.out.println(result);
}
//关闭线程
pool.shutdown();
}
}
//模拟Callable任务
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return new Random().nextInt(10);
}
}
6.线程池API
Executor:线程池顶级接口,只有一个方法
ExecutorService:真正的线程池接口
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Futuresubmit(Callable task):执行任务,有返回值,一般又来执行CallableVoid
- shutdown:关闭线程池
AbstractExecutorService :基本实现了ExecutorService的所有方法
ThreadPoolExecutor:默认的线程池实现类!
ScheduledThreadPoolExecutor:实现周期性任务调度的线程池
Executors :工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n);创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor) :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
7.ThreadPoolExecutor
ThreadPoolExecutor类的构造参数
public ThreadPoolExecutor(
//线程池的核心线程数量
int corePoolSize,
//线程池的最大线程量
int maximumPoolSize,
//线程的存活时间
long keepAliveTime,
//存活时间的单位:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS、MINUTES、HOURS、DAYS
TimeUnit unit,
//等待队列的类型
BlockingQueue<Runnable> workQueue,
//线程工厂
ThreadFactory threadFactory,
//设置拒绝任务的策略CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy、AbortPolicy
RejectedExecutionHandler handler
)
RejectedExecutionHandler handler参数:
1、CallerRunsPolicy :如果发现线程池还在运行,就直接运行这个线程
2、DiscardOldestPolicy:在线程池的等待队列中,将头取出一个抛弃,然后将当前线程放进去。
3、DiscardPolicy :什么也不做
4、AbortPolicy : java默认,抛出一个异常
八、ForkJoin框架
1.ForkJoin框架原理
Fork/Join框架是JAVA7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终汇总每个小任务结果得到大任务结果的框架。
ForkJoin框架的底层使用的是ForkJoinPool线程池,ForkJoinPool线程池也是继承AbstractExecutorService,和ThreadPoolExecutor线程池是兄弟线程池。
2.工作窃取算法( work-stealing )
一个大任务拆分成多个小任务,为了减少线程间的竞争,把这些子任务分别放到不同的队列中,并且每个队列都有单独的线程来执行队列里的任务,线程和队列一一对应。但是会出现这样一种情况:A线程处理完了自己队列的任务,B线程的队列里还有很多任务要处理。
A是一个很热情的线程,想过去帮忙,但是如果两个线程访问同一个队列,会产生竞争,所以A想了一个办法,从双端队列的尾部拿任务执行。而B线程永远是从双端队列的
头部拿任务执行。
注意:线程池中的每个线程都有自己的工作队列(PS,这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,
所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。
3.工作窃取算法的优点:
利用了线程进行并行计算,减少了线程间的竞争。
4.工作窃取算法的缺点︰
1、如果双端队列中只有一个任务时,线程间会存在竞争。
2、窃取算法消耗了更多的系统资源,如会创建多个线程和多个双端队列。
5.ForkJoin框架实例
public class SumTask extends RecursiveTask<Long> {
private long start;
private long end;
private long step = 10000;
public SumTask(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
if (end - start <= step) {
for (long i = start; i <= end; i++) {
sum += i;
}
} else {
//分解成两个任务
long mid = (start + end) / 2;
SumTask leftTask = new SumTask(start, mid);
SumTask rightTask = new SumTask(mid + 1, end);
//fork
leftTask.fork();
rightTask.fork();
//join
long leftSum = leftTask.join();
long rightSum = rightTask.join();
sum = leftSum + rightSum;
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//方式1: 没有重复的利用多核的优势,只有一个核在拼命的工作,其他核在一旁吃西瓜
long n = 1000000000L;
long sum = 0;
long start = System.currentTimeMillis();
for (int i = 0; i <= n; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("使用循环计算结果∶" + sum + " "+(end-start));
//方式2: 使用forkJoin框架
// 1.创建一个线程池
ForkJoinPool pool = new ForkJoinPool ( );
// 2.创建一个任务
SumTask sumTask = new SumTask (1,n) ;
// 3.将任务交给线程池
start = System.currentTimeMillis();
ForkJoinTask<Long> task = pool.submit(sumTask);
// 4.得到结果并输出
long result = task.get();
end = System.currentTimeMillis();
System.out.println("使用forkJoin框架计算:"+result+" "+(end-start));
// 5.关闭线程
pool.shutdown();
}
}
九、其他线程同步类
1.JUC线程同步类 CountDownLatch(门栓类)
在开发中经常遇到在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。之前是使用join()来实现的,但是不够灵活,某些场合和还无法实现,所以开发了CountDownLatch这个类,底层基于AQS。
CountDown是计数递减的意思,Latch是门栓的意思。内部维持一个递减的计数器。可以理解为初始有n个Latch,等 Latch 数量递减到0的时候,就结束阻塞执行后续操作。
CountDown( ):减少Latch的计数,如果计数达到零,释放所有等待的线程。
await():导致当前线程等待,直到到Latch计数到零,或者被interrupt。
CountDownLatch实例
上门栓,等执行完了一个子线程就去除一个门栓,等全部门栓去除了,就释放锁。
public class CountDownLatchDemo {
//创建门栓,两道
private static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println("----- 子线程1 -----");
countDownLatch.countDown(); //解开一道门栓
}).start();
new Thread(()->{
System.out.println("----- 子线程2 -----");
countDownLatch.countDown(); //解开一道门栓
}).start();
countDownLatch.await();
//当解开两道门栓,主线程开始执行
System.out.println("----- 主线程 -------");
}
}
2.JUC线程同步类 CyclicBarrier(回环屏障)
CountDownLatch优化了join()在解决多个线程同步时的能力,但CountDownLatch的计数器是一次性的。计数递减为О之后,再调用countDown()、await(将不起作用。为了满足计数器可以重置的目的,JDK推出了CyclicBarrier类。底层基于AQS。
Barrier:屏障,会等待线程数目满足指定数量后,冲破屏障,同时执行,
Cyclic:回环,冲破屏障后数量重置,开始下一轮线程的等待和冲破屏障。
实例
创建一道屏障,设置5个回环线程数量,当5个线程全部执行完,会推倒屏障(释放锁),等后面5个线程再次执行完,又一次推到屏障,再继续下一轮。
public class CyclicBarrierDemo {
//创建回环屏障,将回环线程设置成2
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
public static void main(String[] args) {
new Thread(()->{
System.out.println("task1 step1");
//等待有两个线程执行完,才能执行该线程
try {
cyclicBarrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("task1 step2");
//等待有两个线程执行完,才能执行该线程
try {
cyclicBarrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("task1 step3");
}).start();
new Thread(()->{
System.out.println("task2 step1");
//等待有两个线程执行完,才能执行该线程
try {
cyclicBarrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("task2 step2");
//等待有两个线程执行完,才能执行该线程
try {
cyclicBarrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("task2 step3");
}).start();
}
}
3.JUC线程同步类 Semaphore(计数信号量)
CountDownLatch和CyclicBarrier的计数器递减的,而Semaphore的计数器是递增的,并可指定计数器的初始值,并且不需要事先确定同步线程的个数,等到需要同步的地方指定个数即可。且Semaphore 也具有回环重置的功能,这一点和CyclicBarrier很像。底层也是基于AQS。
常用方法如下:
- release():释放许可证,将其返回到信号量,可用许可证的数量增加一个
- acquire(int n):从该信号量获取给定数量的许可证,数量不足就阻塞等待
实例
每执行完线程一次,就释放许可证,当许可证达标就能执行下一个回环的线程。
public class SemaphoreDemo {
//创建计数信号线程,并初始许可证为2
private static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println("----- 子线程1 -----");
semaphore.release(); //释放许可证,将其返回到信号量,可用许可证的数量增加一个
}).start();
new Thread(()->{
System.out.println("----- 子线程2 -----");
semaphore.release(); //释放许可证,将其返回到信号量,可用许可证的数量增加一个
}).start();
//从该信号量获取给定数量的许可证,数量不足就阻塞等待
semaphore.acquire(4);
System.out.println("----- 主线程 -------");
}
}
________________________________________________________________________
###
我见青山多妩媚,料青山见我应如是。