目录
method1:创建默认的线程池---Executors.newCachedThreadPool()
method2:创建指定上限的线程池---Executors.newFixedThreadPool()
3.自己创建线程池---new ThreadPoolExecutor();
③关于shutdown()和shutdownNow()的区别
九、线程状态
线程从创建到消亡的流程:
在虚拟机中线程有六种状态:(查看API中 Thread.State)
分别为:新建状态、就绪状态、阻塞状态、等待状态、计时状态、结束状态
十、线程池
1.创建线程池的目的
我们之前的线程创建的弊端:①用到的时候都要创建,②使用完之后,线程就消失了。故此我们提供一个解决方案,创建一个容器(线程池),当我们需要执行第一个任务,那我们就创建一个线程Thread1:
此时当我们的任务1执行完,Thread1会退回到线程池,而且不会消亡。当有任务2,接着拿出来用,当有多个任务同时存在,那么线程池会自动创建新的线程
2.线程池的基本代码实现
method1:创建默认的线程池---Executors.newCachedThreadPool()
分为三个步骤:
步骤1:创建一个线程池,此时池子中为空---> 创建Executors中的静态方法
步骤2:有任务需要执行时,创建线程对象--->submit()
步骤3:所有任务全部执行完毕,关闭池子--->shutdown()
/*
体验线程池
*/
public static void main(String[] args) throws InterruptedException {
//1.创建一个默认的线程池对象,默认为空,最大容量为int类型的最大值
ExecutorService exp = Executors.newCachedThreadPool();
//其中Executors可以帮我们创建线程池对象
//而ExecutorService可以帮我们控制线程池
//2.添加线程任务
exp.submit(()->{
System.out.println(Thread.currentThread().getName()+"执行了代码1");
});
//特殊情况1:当两个任务间有时间间隔-->那么最后俩执行代码,都会有线程一去执行
//因为线程反应时间足够
Thread.sleep(1);
//特殊情况2:没有时间间隔-->由于来不及反应,则会新创建一个线程执行新的代码
exp.submit(()->{
System.out.println(Thread.currentThread().getName()+"执行了代码2");
});
//3.关闭线程池
exp.shutdown();
}
注意:当两个任务中间存在sleep()时间间隔,那么最后输出
当两个任务中间不存在时间间隔,那么最后输出
method2:创建指定上限的线程池---Executors.newFixedThreadPool()
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPoolDemo3 {
/*
创建指定上限的线程池
*/
public static void main(String[] args) {
//1.这里面的参数表示手动指定的线程池上限,然而池子创建默认的上限为0
ExecutorService exp2 = Executors.newFixedThreadPool(2);
//如何去查看为添加任务(未自动创建线程)的池子上限
//先将池子强转为ThreadPoolExecutor
ThreadPoolExecutor pool = (ThreadPoolExecutor) exp2;
//2.最后调用getPoolSize(),可以查看线程池上限
System.out.println(pool.getPoolSize());//0
//因为还没添加任务,池子未自动生成线程
//3.添加任务,查看池子现有的线程数
exp2.submit(()->{
System.out.println(Thread.currentThread().getName()+"线程,执行了任务1");
});
exp2.submit(()->{
System.out.println(Thread.currentThread().getName()+"线程,执行了任务2");
});
//添加任务的特殊情况:当任务数>池子中线程数
exp2.submit(()->{
System.out.println(Thread.currentThread().getName()+"线程,执行了任务3");
});
exp2.submit(()->{
System.out.println(Thread.currentThread().getName()+"线程,执行了任务4");
});
//4.输出现在线程池的线程数
System.out.println(pool.getPoolSize());//2
/*
因为这个时候,由于出现了任务,线程池自动创建了线程,这里=2是因为,我们设置
的线程池上限为2,当出现任务过多,他们则会交替进行执行
*/
//5.关闭线程池
exp2.shutdown();
}
}
注意:这里输出语句无规则,是因为最后输出线程池线程数的语句在main线程中,它也要和别的线程抢夺cpu执行权。所以造成了无序
3.自己创建线程池---new ThreadPoolExecutor();
在我们自己创建线程池之前,我们跟一下默认线程池Executors.newCachedThreadPool(),和创建指定上限的线程池Executors.newFixedThreadPool(),源码如下图:
我们发现,两个指向了同一个源码,而且里面return new ThreadPoolExecutor();然后我们去API文档中查询该类,发现ThreadPoolExecutor该类中,全为带参构造。我们可以根据参数类型去手动创建一个自定义线程池
但是前提我要明白线程池中需要的哪些部分,
import java.util.concurrent.*;
public class ThreadPoolDemo2 {
/*
体验手动new 一个线程池
需要的参数有:
1.核心线程数 ! < 0
2.最大线程数 ! <= 0,最大线程数 = 核心线程数 + 临时线程数
3.空闲线程最大存活时间 ! < 0
4.时间单位----在TimeUnit类中找
5.任务队列 ! = null
6.创建线程工厂 ! = null
7.任务的拒绝策略 ! = null
*/
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, //线程池中的核心线程数量
5, //线程池中最大线程数量(最大线程数=核心线程数+临时线程数)
2, //临时线程如果没有了任务,最大存货时间
TimeUnit.SECONDS, //存活的时间单位
new ArrayBlockingQueue<>(10), //阻塞队列:当线程不够用时,用来存储任务
Executors.defaultThreadFactory(), //线程工厂,使用Executors类中的defaultThreadFactory()也就是默认方式
new ThreadPoolExecutor.AbortPolicy()); //任务被线程池拒绝之后的策略(这里用内部类去写,表明超过数量的任务的处理策略)
//创建执行任务
pool.submit(()->{
System.out.println(Thread.currentThread().getName()+"检测是否执行任务1");
});
pool.submit(()->{
System.out.println(Thread.currentThread().getName()+"检测是否执行任务2");
});
//最后关闭线程池
pool.shutdown();
}
}
4.自己创建线程池的参数详解
①任务的拒绝策略流程:
首先我们要明白任务是怎么传入的
当线程池设定上限 + 阻塞队列设定容量 <= 任务数n,那么就不会有拒绝策略,但是此时如果存在任务n+1,那么该任务会落入拒绝策略中。
②任务拒绝策略的几种方式
//拒绝策略中的四种方式
ThreadPoolExecutor.AbortPolicy; 丢弃任务并抛出RejectedExecutionException异常,也是默认的策略
ThreadPoolExecutor.DiscardPolicy; 丢弃任务,但是不抛出异常,这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy; 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy; 调用任务中的run()方法绕过线程池直接运行
下面以这定义的这个线程池为例:
//拒绝策略方式一:AbortPolicy()
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1,
2,
2,
TimeUnit.HOURS,
new ArrayBlockingQueue<>(1),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());//在此处进行策略更改
//测试拒绝策略
for (int i = 1; i <= 5; i++) {
int y = i;
pool.submit(()->{
System.out.println(Thread.currentThread().getName()+"执行了"+y);
});
}
pool.shutdown();
-----------------------------------------------------------------------------------
//拒绝策略方式二:DiscardPolicy()
//拒绝策略方式三:DiscardOldestPolicy()
//拒绝策略方式四:CallerRunsPolicy()
方式一输出:
方式二输出:
方式三输出:
方式四输出:
③关于shutdown()和shutdownNow()的区别
shutdown():当线程池中的任务和阻塞队列中的任务执行完,之后才会关闭线程池
shutdownNow():当线程池中的任务完成之后,不管阻塞队列中是否存在未执行任务,都会立刻将线程池关闭!
十一、volatile问题
1.线程之间数据副本不更新问题
问题原因:两个线程最初开始获得的是共享区域中的数据副本,如果一个线程更改了共享区域数据,但是另一个线程却获取不到最新的。
public class VolatieDemo1 {
public static void main(String[] args) {
/*
1.我们发现输出框没有东西输出,并且不会停止
虽然男孩更改了money的值,但是女孩线程没有得到最新的值
---更改方法:在共享区域中,加入volatile修饰
这样最后输出的:钱变少了
*/
Girl girl = new Girl();
girl.setName("女孩线程:");
girl.start();
Boy boy = new Boy();
boy.setName("男孩线程:");
boy.start();
}
}
//定义一个女孩
class Girl extends Thread{
@Override
public void run() {
while(Money.money == 10_0000){
}
System.out.println("钱变少了");
}
}
//定义一个男孩
class Boy extends Thread{
@Override
public void run() {
try {
Thread.sleep(10);
Money.money = 9_0000;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//定义一个共享数据类,钱类
class Money {
//2.将这里加volatile修饰
public static volatile int money = 10_0000;
}
上述问题也可以用synchronized()同步代码块解决。就是在共享区域创建一个锁对象
class Money {
//2.将这里加volatile修饰
public static volatile int money = 10_0000;
//方法二。创建锁对象
public static Object lock = new Object();
}
------------------------------------------------
//定义一个女孩
class Girl extends Thread{
@Override
public void run() {
while (true) {
synchronized (Money.lock){
if(Money.money != 10_0000){
System.out.println("钱变少了");
break;
}
}
}
}
}
//定义一个男孩
class Boy extends Thread{
@Override
public void run() {
synchronized (Money.lock) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Money.money = 9_0000;
}
}
}
2.原子性---送花案例
原子性:多个操作是一个不可分割的整体要么同时成功,要么同时失败
有个结论:count++不是一个原子性的操作,其在执行的过程中,有可能被其它线程打断!!!
解决方案就是加入synchronized()锁;
public class MyAtomThreadDemo1 {
/*
测试原子性:目标,每次输出都能保证排序正确,也就是获取最新的值
结论:
1.当线程中没有volatile修饰变量count的时候,会造成输出结果有相同数量的玫瑰花
2.当我们在线程变量中加volatile修饰,输出不是每次最后都是10000,
volatile只能保证线程每次在使用共享数据的时候都是最新的值,但是不能保证
原子性!!!!
3.当我们在创建的线程中加入synchronized()锁,就会发现最后输出结果是按照
我们最初猜想的1----10000排序输出
*/
public static void main(String[] args) {
RunnableImpl1 runnableImpl1 = new RunnableImpl1();
for (int i = 0; i < 100; i++) {
new Thread(runnableImpl1).start();
}
}
}
//创建一个线程
class RunnableImpl1 implements Runnable{
private volatile int count = 0;
private Object lock = new Object();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
count++;
System.out.println(
Thread.currentThread().getName()+
"线程,送出了"+count+"玫瑰花");
}
}
}
}
当我们使用synchronized()锁解决了上述问题之后,代码还是有弊端,那就是运行效率不高,所以在JDK1.5之后,出现了一种用法简单、性能高效、线程还非常安全的类---AtomicInteger
十二、AtomicInteger类 ---原子性
1.构造方法
public AtomicInteger();-----初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue);-----初始化一个指定值的原子型Integer
2.成员方法
/*
原子性的常用方法
int get();-------------获取原子对象的值
int getAndIncrement();-以原子方式将当前值加1,并且返回自增前的值!
int incrementAndGet();-以原子方式将当前值加1,返回自增后的值!
int addAndGet();-------以原子方式将输入的数值与实例中的值相加,并返回
int getAndSet();-------以原子方式设置为newValue的值,并返回
*/
public static void main(String[] args) {
AtomicInteger a1 = new AtomicInteger(10);
int i = a1.get();//获取
System.out.println(i);//10
AtomicInteger a2 = new AtomicInteger(11);
int andIncrement = a2.getAndIncrement();//+1
System.out.println(andIncrement);//11
System.out.println(a2);//12
AtomicInteger a3 = new AtomicInteger(12);
int i1 = a3.incrementAndGet();//+1
System.out.println(i1);//13
AtomicInteger a4 = new AtomicInteger(13);
int i2 = a4.addAndGet(2);
System.out.println(i2);//15
AtomicInteger a5 = new AtomicInteger(14);
int andSet = a5.getAndSet(3);
System.out.println(andSet);//14
System.out.println(a5);//最后原子对象a5的值为3
3.送花案例优化---底层CAS算法
import java.util.concurrent.atomic.AtomicInteger;
public class AtomDemo1 {
/*
用原子类去优化刚刚synchronized锁,
这样就优化了我们刚刚送玫瑰花的问题
*/
public static void main(String[] args) {
Test1 test1 = new Test1();
for (int i = 0; i < 100; i++) {
new Thread(test1).start();
}
}
}
class Test1 implements Runnable{
//创建一个原子对象
AtomicInteger atm = new AtomicInteger();//初始值为0
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//调用原子对象中的自增1并且返回的方法incrementAndGet()
int i1 = atm.incrementAndGet();
System.out.println(Thread.currentThread().getName()+"送了"+i1+"玫瑰花");
}
}
}
小结:这个里面牵扯到了CAS算法,中的自旋操作
4.AtomicInteger原理---自旋锁+CAS算法
CAS算法:有3个操作数(内存值V,旧的预留值A,要修改的值B)
当旧的预留值A == 内存值,此时修改成功,将V改成B
当旧的预留值A != 内存值,此时修改失败,不做任何操作
并重新获得现在的最新值(这个重新获取的动作就是自旋)
5.悲观锁&乐观锁
synchronized和CAS的区别:
相同点:在多线程的情况下,都可以保证共享数据的安全性
不同点:①synchronized总是从最坏的角度出发,认为每一次获取数据时,都有可能被修改,所以在每次操作共享数据前,都会上锁(悲观锁)
②CAS时从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁,只不过在修改共享数据的时候,会检查一下,被人有没有修改过这个数据(乐观锁)
十三、并发工具类
当我们在多线程的情况下去操作集合,由于线程抢夺cpu的随机性,集合也可能产生错误
1.HashMap<> & Hashtable<>
HashMap<>是线程不安全的(多线程环境下可能会出现问题)
Hashtable<>是安全的,但是效率低下(因为Hashtable底层采用的synchronized悲观锁来保证数据安全)
2.CountDownLatch---三个孩子吃饺子案例
使用场景:让某一条线程等待其它线程执行完毕后再执行
import java.util.concurrent.CountDownLatch;
public class CountDownlatchDemo {
/*
这里是关于CountDownLatch应用
三个孩子吃饺子案例:妈妈要等待三个孩子都吃完饺子了才开始收拾碗筷
里面用到CountDownLatch类的两个方法,
方法一:await();等待
方法二:countDown();通知
原理:就是让CountDownLatch按照括号中的int参数进行自减,
当自减为0后,然后唤醒某个线程
*/
public static void main(String[] args) {
//先创建CountDownLatch对象,里面的为等待的执行的线程数
CountDownLatch countDownLatch = new CountDownLatch(3);
//这里的3代表为三个孩子
//创建四个线程并开启,将上面的对象传入
Monther monther = new Monther(countDownLatch);
monther.start();
Child1 child1 = new Child1(countDownLatch);
child1.setName("小明");
child1.start();
Child2 child2 = new Child2(countDownLatch);
child2.setName("小洪");
child2.start();
Child3 child3 = new Child3(countDownLatch);
child3.setName("大胖");
child3.start();
}
}
//创建三个 孩子线程
class Child1 extends Thread{
//传入要记得写构造方法
private CountDownLatch countDownLatch;
public Child1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//开始吃饺子
for (int i = 1; i <= 10; i++) {
System.out.println(getName()+"正在吃第"+i+"个饺子");
}
//吃完了说一声
countDownLatch.countDown();
}
}
class Child2 extends Thread{
private CountDownLatch countDownLatch;
public Child2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//开始吃饺子
for (int i = 1; i <= 15; i++) {
System.out.println(getName()+"正在吃第"+i+"个饺子");
}
//吃完了说一声
countDownLatch.countDown();
}
}
class Child3 extends Thread{
private CountDownLatch countDownLatch;
public Child3(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//开始吃饺子
for (int i = 1; i <= 20; i++) {
System.out.println(getName()+"正在吃第"+i+"个饺子");
}
//吃完了说一声
countDownLatch.countDown();
}
}
//创建一个妈妈类的线程
class Monther extends Thread{
private CountDownLatch countDownLatch;
public Monther(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
//1.等待三个孩子吃饺子
try {
//当计数器=0,就唤醒在这里等待的线程
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.三个孩子吃完之后开始收拾碗筷
System.out.println("妈妈正在收拾碗筷");
}
}
3.Semaphore---访问特定资源线程数
使用场景:比如高速路关口发放指定数量通行证,每次只允许一定数量的线程(车)进入
import java.util.concurrent.Semaphore;
public class MySemaphoreDemo {
public static void main(String[] args) {
RunnableImpl_ rai = new RunnableImpl_();
//创建100条线程(100辆车)
for (int i = 0; i < 100; i++) {
new Thread(rai).start();
}
}
}
//创建一个高速路关卡类
class RunnableImpl_ implements Runnable{
//1.获得管理员对象,参数表示每次只允许 2 辆车进入高速路
private Semaphore smp = new Semaphore(2);
@Override
public void run() {
try {
//2.获得通行证
smp.acquire();
//3.开始行驶
System.out.println(Thread.currentThread().getName()+"开始行驶了");
//设定形式时间
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"下高速");
//4.归还通行证
smp.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面的输出结果:最多只有两条线程一起开始一起结束!