多线程-3
8.线程池
提到池,大家应该能想到的就是水池。水池就是一个容器,在该容器中存储了很多的水。那么什么是线程池呢?线程池也是可以看做成一个池子,在该池子中存储很多个线程。
线程池存在的意义:
系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系
统资源的消耗,这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就
会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。
线程池的设计思路 :
- 准备一个任务容器
- 一次性启动多个(2个)消费者线程
- 刚开始任务容器是空的,所以线程都在wait
- 直到一个外部线程向这个任务容器中扔了一个"任务",就会有一个消费者线程被唤醒
- 这个消费者线程取出"任务",并且执行这个任务,执行完毕后,继续等待下一次任务的到来
线程池-Executors默认线程池
概述 : JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。
我们可以使用Executors中所提供的静态方法来创建线程池
static ExecutorService newCachedThreadPool() 创建一个默认的线程池
static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池

创建线程池的两个静态方法
1.newCachedThreadPool 创建一个默认的线程池
public class MyThreadPoolDemo1 {
public static void main(String[] args) throws InterruptedException {
//1.创建一个默认的线程池对象,池子中默认是空的,默认最多可容纳int类型的最大值
ExecutorService threadPool = Executors.newCachedThreadPool();
//Executors --- 可以帮助我们创建线程池对象
//ExecutorService --- 可以帮助我们控制线程池
threadPool.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
//Thread.sleep(2000);
threadPool.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
threadPool.shutdown();
}
}
执行结果
pool-1-thread-1在执行了
pool-1-thread-2在执行了
2.newFixedThreadPool 创建一个指定最多线程数量的线程池
public class MyThreadPoolDemo2 {
public static void main(String[] args) {
//参数不是初始值而是最大值
ExecutorService threadPool = Executors.newFixedThreadPool(10);
threadPool.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
threadPool.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
threadPool.shutdown();
}
}
执行结果
pool-1-thread-1在执行了
pool-1-thread-2在执行了
ThreadPoolExecutor
自己创建线程池对象,就要创建ThreadPoolExecutor线程池对象
public class MyThreadPoolDemo3 {
public static void main(String[] args) {
// 参数一:核心线程数量
// 参数二:最大线程数
// 参数三:空闲线程最大存活时间
// 参数四:时间单位
// 参数五:任务队列
// 参数六:创建线程工厂
// 参数七:任务的拒绝策略
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,10,2, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
threadPoolExecutor.submit(new MyRunable());
threadPoolExecutor.submit(new MyRunable());
threadPoolExecutor.shutdown();
}
}
执行结果
pool-1-thread-2在执行了
pool-1-thread-1在执行了
总结:
corePoolSize: 核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime: 空闲线程最大存活时间,不能小于0
unit: 时间单位
workQueue: 任务队列,不能为null
threadFactory: 创建线程工厂,不能为null
handler: 任务的拒绝策略,不能为null
参数详解
参数四:时间单位 要用TimeUnit里面的枚举
参数五:任务队列 让任务在队列中等着,等有线程空闲了,再从这个队列中获取任务并执行
参数六:创建线程工厂 按照默认的方式创建线程对象
参数七:任务的拒绝策略
-
什么时候拒绝任务
当提交的任务 > 池子中的最大线程数量+队列容量
-
如何拒绝
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
volatile
当A线程修改了共享数据时,B线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题
1,堆内存是唯一的,每一个线程都有自己的线程栈。
2 ,每一个线程在使用堆里面变量的时候,都会先拷贝一份到变量的副本中。
3 ,在线程中,每一次使用是从变量的副本中获取的。
Volatile关键字 : 强制线程每次在使用的时候,都会看一下共享区域最新的值
代码实现 : 使用volatile关键字解决
package com.itheima.myvolatile;
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.setName("小路同学");
t1.start();
MyThread2 t2 = new MyThread2();
t2.setName("小皮同学");
t2.start();
}
}
package com.itheima.myvolatile;
public class Money {
public static volatile int money = 100000;
}
package com.itheima.myvolatile;
public class MyThread1 extends Thread {
@Override
public void run() {
while(Money.money == 100000){
}
System.out.println("结婚基金已经不是十万了");
}
}
package com.itheima.myvolatile;
public class MyThread2 extends Thread {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Money.money = 90000;
}
}
synchronized解决
1 ,线程获得锁
2 ,清空变量副本
3 ,拷贝共享变量最新的值到变量副本中
4 ,执行代码
5 ,将修改后变量副本中的值赋值给共享数据
6 ,释放锁
原子性
概述 : 所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
count++ 不是一个原子性操作, 他在执行的过程中,有可能被其他线程打断
在进行count++的时候线程的执行权有可能被抢走
** volatile关键字不能保证原子性**
解决方案 : 我们可以给count++操作添加锁,那么count++操作就是临界区中的代码,临界区中的代码一次只能被一个线程去执行,所以count++就变成了原子操作。
package com.itheima.threadatom2;
public class AtomDemo {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();
for (int i = 0; i < 100; i++) {
new Thread(atom).start();
}
}
}
class MyAtomThread implements Runnable {
private volatile int count = 0; //送冰淇淋的数量
private Object lock = new Object();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//1,从共享数据中读取数据到本线程栈中.
//2,修改本线程栈中变量副本的值
//3,会把本线程栈中变量副本的值赋值给共享数据.
synchronized (lock) {
count++;
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}
}
原子性_AtomicInteger
概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。因为变
量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。本次我们只讲解
使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:
AtomicBoolean: 原子更新布尔类型
AtomicInteger: 原子更新整型
AtomicLong: 原子更新长整型
以上3个类提供的方法几乎一模一样,所以本节仅以AtomicInteger为例进行讲解,AtomicInteger的常用方法如下:
public AtomicInteger(): 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
int get(): 获取值
int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
代码实现 :
package com.itheima.threadatom3;
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomIntergerDemo1 {
// public AtomicInteger(): 初始化一个默认值为0的原子型Integer
// public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
public static void main(String[] args) {
AtomicInteger ac = new AtomicInteger();
System.out.println(ac);
AtomicInteger ac2 = new AtomicInteger(10);
System.out.println(ac2);
}
}
package com.itheima.threadatom3;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomIntergerDemo2 {
// int get(): 获取值
// int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。
// int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
// int addAndGet(int data): 以原子方式将参数与对象中的值相加,并返回结果。
// int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值。
public static void main(String[] args) {
// AtomicInteger ac1 = new AtomicInteger(10);
// System.out.println(ac1.get());
// AtomicInteger ac2 = new AtomicInteger(10);
// int andIncrement = ac2.getAndIncrement();
// System.out.println(andIncrement);
// System.out.println(ac2.get());
// AtomicInteger ac3 = new AtomicInteger(10);
// int i = ac3.incrementAndGet();
// System.out.println(i);//自增后的值
// System.out.println(ac3.get());
// AtomicInteger ac4 = new AtomicInteger(10);
// int i = ac4.addAndGet(20);
// System.out.println(i);
// System.out.println(ac4.get());
AtomicInteger ac5 = new AtomicInteger(100);
int andSet = ac5.getAndSet(20);
System.out.println(andSet);
System.out.println(ac5.get());
}
}
AtomicInteger-内存解析
AtomicInteger原理 : 自旋锁 + CAS 算法
CAS算法:
有3个操作数(内存值V, 旧的预期值A,要修改的值B)
当旧的预期值A == 内存值 此时修改成功,将V改为B
当旧的预期值A!=内存值 此时修改失败,不做任何操作
并重新获取现在的最新值(这个重新获取的动作就是自旋)
9.悲观锁和乐观锁
synchronized和CAS的区别 :
相同点:在多线程情况下,都可以保证共享数据的安全性。
不同点:synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作共享数据之前,都会上锁。(悲观锁)
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。
如果别人修改过,那么我再次获取现在最新的值。
如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)
10.并发工具类HashTable
HashMap是线程不安全的(多线程环境下可能会出现问题)
为了保证数据的安全性我们可以使用hashtable,但是Hashtable的效率极其低下
Hashtable
- Hashtable采取悲观锁synchronized的形式保证数据的安全性
- 只要有线程访问,就会将整张表全部锁起来,所以hashTable的效率低下
ConcurrentHashMap
ConcurrentHashMap出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。
基于以上两个原因我们可以使用JDK1.5以后所提供的ConcurrentHashMap。

总结 :
1 ,HashMap是线程不安全的。多线程环境下会有数据安全问题
2 ,Hashtable是线程安全的,但是会将整张表锁起来,效率低下
3,ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样。
11.并发工具类-ConcurrentHashMap
并发工具类-ConcurrentHashMap1.7原理

并发工具类-ConcurrentHashMap1.8原理

总结 :
1,如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表
2,计算当前元素应存入的索引。
3,如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
4,如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。
5,当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性
注意:
Hashtable锁的是小表,ConcurrentHashMap锁的是大表
ConcurrentHashMap在默认情况下,最多允许16个线程同时访问
12.并发工具类-CountDownLatch

使用场景: 让某一条线程等待其他线程执行完毕之后再执行
代码实现 :
package com.itheima.mycountdownlatch;
import java.util.concurrent.CountDownLatch;
public class ChileThread1 extends Thread {
private CountDownLatch countDownLatch;
public ChileThread1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//1.吃饺子
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
//2.吃完说一声
//每一次countDown方法的时候,就让计数器-1
countDownLatch.countDown();
}
}
package com.itheima.mycountdownlatch;
import java.util.concurrent.CountDownLatch;
public class ChileThread2 extends Thread {
private CountDownLatch countDownLatch;
public ChileThread2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//1.吃饺子
for (int i = 1; i <= 15; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
//2.吃完说一声
//每一次countDown方法的时候,就让计数器-1
countDownLatch.countDown();
}
}
package com.itheima.mycountdownlatch;
import java.util.concurrent.CountDownLatch;
public class ChileThread3 extends Thread {
private CountDownLatch countDownLatch;
public ChileThread3(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//1.吃饺子
for (int i = 1; i <= 20; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
//2.吃完说一声
//每一次countDown方法的时候,就让计数器-1
countDownLatch.countDown();
}
}
package com.itheima.mycountdownlatch;
import java.util.concurrent.CountDownLatch;
public class MotherThread extends Thread {
private CountDownLatch countDownLatch;
public MotherThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//1.等待
try {
//当计数器变成0的时候,会自动唤醒这里等待的线程。
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.收拾碗筷
System.out.println("妈妈在收拾碗筷");
}
}
package com.itheima.mycountdownlatch;
import java.util.concurrent.CountDownLatch;
public class MyCountDownLatchDemo {
public static void main(String[] args) {
//1.创建CountDownLatch的对象,需要传递给四个线程。
//在底层就定义了一个计数器,此时计数器的值就是3
CountDownLatch countDownLatch = new CountDownLatch(3);
//2.创建四个线程对象并开启他们。
MotherThread motherThread = new MotherThread(countDownLatch);
motherThread.start();
ChileThread1 t1 = new ChileThread1(countDownLatch);
t1.setName("小明");
ChileThread2 t2 = new ChileThread2(countDownLatch);
t2.setName("小红");
ChileThread3 t3 = new ChileThread3(countDownLatch);
t3.setName("小刚");
t1.start();
t2.start();
t3.start();
}
}
总结 :
1. CountDownLatch(int count):参数写等待线程的数量。并定义了一个计数器。
2. await():让线程等待,当计数器为0时,会唤醒等待的线程
3. countDown(): 线程执行完毕时调用,会将计数器-1。
13.并发工具类-Semaphore
package com.cloudcore.SemaphoreTest;
import java.util.concurrent.Semaphore;
/**
* @Author: hepeng
* @Date: 2021/10/11 20:46
*/
public class MyRunnable implements Runnable{
//1.获得管理员对象,只能有一个
//参数的意思是最多允许多少条线程执行
private Semaphore semaphore = new Semaphore(2);
@Override
public void run() {
//2.获得通行证
try {
semaphore.acquire();
//3.开始行使
System.out.println("获得了通行证开始行使");
Thread.sleep(2000);
System.out.println("开始归还通行证");
//4.归还通行证
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.cloudcore.SemaphoreTest;
/**
* @Author: hepeng
* @Date: 2021/10/11 20:45
*/
public class MySemaphoreDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
for (int i = 0; i < 100; i++) {
new Thread(mr).start();
}
}
}
总结
Semaphore这个类可以看成一个管理员对象
里面的参数是最多允许多少条线程执行

浙公网安备 33010602011771号