多线程
并发和并行
- 需求:边打游戏边听音乐
- 问题:只能先后关系,不能同时发生
- 多线程或多进程来解决
- 并行和并发
- 并行:多件事情在同一时刻发生
- 并发:多件事情在同一时间段发生,同一时间段多个程序同时运行
- 单个cpu:一个时间点只能执行一个程序,多个程序可以交替执行
- 线程调度:多个线程以某个顺序执行 具体运行那个线程,取决于cpu的调度 jvm采用抢占式的调度方式,没有采用分时概念,谁先抢到谁先执行
- java程序中最少有几个线程:
- 主线程
- GC垃圾回收线程
一、线程和进程
1.1 概念
- 进程:
- 程序由指令和数据组成,指令要运行,数据要读写,需要将指令加载到CPU中,数据加载到内存中。在指令运行过程中,需要访问磁盘文件 、网络设备等。进程就是用来加载指令、管理内存、管理IO操作。
- 进程是一个具有独立功能的程序,可以申请和使用系统资源,是一个动态的概念,当一个程序被运行,从磁盘加载这个程序代码至内存中,此时,可以说开启了一个进程。
- 进程可以视作程序的一个实例。有一些程序可以同时运行多个实例进程,比如:记事本、浏览器、画图,有的程序只能启动一个实例进程,比如:电脑管家、360安全卫士等。
- 线程:
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令按照一定的顺序(顺序执行、选择执行、循环执 行),交给CPU执行
- java中,线程是作为最小调度单位,进程作为资源分配的最小单位
- 在windows系统中,进程是不活动的,只是作为线程的容器。
- 区别:
- 进程是负责加载管理资源的,线程是负责执行指令的。
- 进程基本是相互独立的,线程存在于进程中,是进程一个子集。
- 进程拥有共享的资源,如内存空间等,供内部的线程共享。
- 进程间通信较为复杂
- 同一台计算机的进程通信IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并且遵循共同的协议
- 线程通信比较简单,因为线程具有共享进程内的内存。多个线程可以访问同一个共享变量,可以实现任务调度
- 线程更轻量级,线程上下文切换成本要比进程上下文切换成本低
1.2 线程的创建和运行
多线程使用的场景:耗时较高的任务(大文件的查找、视频格式转换),应该开启一个新的线程,避免主线程被阻塞
-
方法一
继承 Thread类重写run方法
调用时,调用线程的start()方法,自动调用run()
注意
启动线程调用start()方法
不要调用run()方法
public class MusicDemo extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("听音乐"+i); } } } public static void main(String[] args) { MusicDemo musicDemo = new MusicDemo(); musicDemo.start();//启动线程 }
如果使用lambda表达式可以简化操作
public class FileUtil { public static void readFile(){ int t=new Random().nextInt(2000); try { Thread.sleep(t); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("文件读取成功,耗费"+t+"ms"); } }
public class Test02 { public static void main(String[] args) { //同步运行 //FileUtil.readFile(); //异步运行 /*Thread t=new Thread(){ @Override public void run() { FileUtil.readFile(); } }; t.start();*/ /* ReadThread t=new ReadThread(); t.start();*/ new Thread(FileUtil::readFile).start(); System.out.println("运行主程序"); } } /*class ReadThread extends Thread{ @Override public void run() { FileUtil.readFile(); } }*/
-
方法二
实现Runnable接口,重写run()方法
public class PlayGameDemo implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("打游戏"+i); } } }
启动(先创建对象,再创建Thread对象,通过构造器获得线程对象)
PlayGameDemo playGameDemo = new PlayGameDemo(); Thread thread = new Thread(playGameDemo); thread.start();
lambda表达式
public class Test03 { public static void main(String[] args) { /* System.out.println("主线程开始"); MyRunnableImpl impl=new MyRunnableImpl(); Thread thread=new Thread(impl); thread.start(); System.out.println("主线程结束");*/ System.out.println("主线程开始"); new Thread(()-> System.out.println("hello"),"线程1"); /*new Thread(() -> { while (true) { System.out.println("hello"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } },"线程1").start();*/ System.out.println("主线程结束"); } } /*class MyRunnableImpl implements Runnable{ @Override public void run() { while (true){ System.out.println("hello"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }*/
更推荐使用第二种方法,方法一把线程和任务合并在一起,方法二把线程和任务分开。使用Runnable更容易和线程池高级API配合,并且让类脱离了Thread继承体系。
-
使用匿名内部类来创建线程(只适用于只能使用一次的方式)
new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("统计人口数"+i); } } }).start();
-
FutureTask配合 Thread
Future,是jdk1.5引入的一个interface,用于异步获取结果。表示一个可能还没有完成的异步任务的结果,针对这个结果,可以添加回调处理,以便在任务执行成功或失败后作出相应的操作。
适用场景:执行一个长时间运行的任务,使用Future去执行,我们可以暂时去处理其他任务。等待长任务执行的结果,再做计算处理。
- 计算密集场景
- 处理大数据量
- 远程方法调用
接口中使用到的方法
- boolean cancel(boolean):取消任务
- boolean isCancelled():判断任务在完成之前,是否取消boolean
- isDone():任务是否完成。正常终止、异常或取消,都认为是任务完成
- V get():获取计算结果
- V get(long timeout,Timeunit unit):获取计算结果,超时时间
FutureTask类图结构
Callable接口和Runnable接口区别
- callable的call方法具有返回值,runnalbe的run方法没有返回值
- Runnable方法是run方法作为线程运行任务的入口,Callable接口方法是call方法
- call方法抛出异常,run方法不抛出异常,实现callable接口,可以对线程运行任务的异常进行捕获,从而得知异常的原因
- Callable配合Future对象,获取异步计算的结果
public class TestFutureTask {
public static void main(String[] args) throws ExecutionException,
InterruptedException{
//Future<Integer> future;
FutureTask<Integer> task=new FutureTask<>(() -> {
int sum=0;
for (int i = 1; i <= 100; i++) {
sum+=i;
}
Thread.sleep(1000);
return sum;
});
new Thread(task).start();
/*Thread.sleep(2000);
task.cancel(true);
System.out.println(task.isCancelled());
System.out.println(task.isDone());*/
/* new Thread(()->{
for (int i=1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+":"+(int)
(Math.random()*10));
}
},"线程2").start();*/
Integer x = null;//task.get();
try {
x = task.get(10000, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
System.out.println("订单超时,退款");
e.printStackTrace();
return;
}
System.out.println(x);
}
}
1.3 查看进程和线程
windows系统
- tasklist查看进程
- taskkill杀死进程 : taskkill /F /PID pid号
java
- jps查看所有java进程
- jstack:生成jvm当前时刻所有线程快照。
- jconsole:以图形界面方式查看java进程中线程的运行状态
1.4 线程的状态
1.4.1 操作系统层面划分
五种状态
- 【初始状态】:在编程语言层面上创建了线程对象,还没有与操作系统线程关联
- 【可运行状态】:当前线程已经与操作系统线程关联,可以由CPU调度执行
- 【运行状态】:获取了CPU时间片,在运行中的状态 当cpu时间片用完后,会从运行状态切换回可运行状态,导致线程上下文切换
- 【阻塞状态】: 如果调用了阻塞的API应用,例如BIO读写文件。会导致线程上下文切换,进入阻塞状态 当BIO操作完成之后,由操作系统唤醒阻塞的线程,切换到可运行状态 对于阻塞线程来讲,只要不被唤醒,调度器就不会考虑调度它们执行
- 【终止状态】:表示线程已经执行结束,生命周期全部执行完成,不会再转换为其它状态
1.4.2 编程语言层面划分
六种状态
根据javaAPI中Thread类下的State枚举类型,分为以下六种状态
NEW: 表示线程刚刚创建对象,还没有调用start()方法
RUNNABLE:调用了start()方法,java的RUNNABLE状态,包括了操作系统层面上可运行状态、运行状态 和阻塞状态(BIO导致的线程阻塞,在java程序中无法区分,仍然认为是可运行状态)
BLOCKED、WAITING、TIMED_WAITING:都是在Java层面,对阻塞状态的细分情况
TERMINATED:当前线程执行结束
1.4.3 代码演示系统状态
public class TestThreadState {
public static void main(String[] args) throws InterruptedException {
//NEW 线程创建,并没有调用start方法运行
//Thread t1=new Thread(()-> System.out.println("线程1"));
//System.out.println(t1.getState());
//RUNNABLE 有可能得到CPU时间片,也可能没有得到,也有可能是阻塞状态
/* Thread t2=new Thread(()->{while(true){}});
t2.start();
System.out.println(t2.getState());*/
/* Thread t2_1=new Thread(()->{
FileUtil.readFile();
FileUtil.readFile();
FileUtil.readFile();
},"t2_1");
t2_1.start();
System.out.println("主线程执行结束");*/
//System.out.println(t2_1.getState());
//TERMINATED
/* Thread t3=new Thread(()-> System.out.println("线程3"));
t3.start();
Thread.sleep(50);
System.out.println(t3.getState());*/
//TIMED_WAITING
Thread t4=new Thread(()->{
synchronized (TestThreadState.class) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t4.start();
Thread.sleep(50);
System.out.println(t4.getState());
//WAITING
/*Thread t5=new Thread(()->{
try {
t4.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t5.start();
Thread.sleep(50);
System.out.println(t5.getState());*/
//BLOCKED 阻塞状态 线程锁
Thread t6=new Thread(()->{
synchronized (TestThreadState.class){
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t6.start();
Thread.sleep(50);
System.out.println(t6.getState());
//join用法
/*System.out.println("主线程开始");
Thread t1=new Thread(()-> System.out.println("线程1"));
Thread t2=new Thread(()-> System.out.println("线程2"));
Thread t3=new Thread(()-> System.out.println("线程3"));
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("主线程结束");*/
}
}
1.5 线程中常用方法API
1.5.1 线程优先级方法
会提示任务调度器优先调度该线程
最大值:10 Thread.MAX_PRIORITY
最小值:1 Thread.MIN_PRIORITY
正常值:5 Thread.NORM_PRIORITY
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
设置优先级 setPriority
获取优先级 getPriority
测试代码
public class TestPriority {
public static void main(String[] args) {
Thread t1=new Thread(()->{
int x=0;
while (true){
System.out.println(Thread.currentThread().getName()+":"+x++);
}
},"线程1");
Thread t2=new Thread(()->{
int x=0;
while (true){
System.out.println("\t\t\t"+Thread.currentThread().getName()+":"+x++);
}
},"线程2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.NORM_PRIORITY);
t1.start();
t2.start();
}
}
1.5.2 sleep和yield方法
sleep方法
- 调用sleep让线程休眠,会将当前线程从运行状态进入到TIMED_WAITING状态
- 其他线程可以打断正在睡眠的线程(interrupt),在休眠的线程,被打断时,会抛出 InterruptedException
- 当线程休眠被打断,线程不一定会立即执行,需要获取CPU时间片
- 建议使用TimeUnit代替Thread.sleep方法,可读性更强
public class TestInterrupt {
public static void main(String[] args) throws InterruptedException {
//testSleepApp(); 使用sleep方法,降低CPU使用频率
Thread t1 = new Thread(() -> {
System.out.println("子线程开始");
try {
TimeUnit.SECONDS.sleep(1);
//Thread.sleep(1000);//子线程休眠
} catch (InterruptedException e) {
System.out.println("子线程休眠被唤醒");
e.printStackTrace();
}
System.out.println("子线程结束");
});
t1.start();
Thread.sleep(500);
t1.interrupt();//打断t1线程
}
public static void testSleepApp() {
new Thread(() -> {
while (true) {
try {
TimeUnit.MILLISECONDS.sleep(100);
//Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
yield方法
调用yield会让当前线程从运行状态,进入就绪状态,让出CPU,调度器执行其它线程。具体的实现依赖于操作系统的任务调度器。
public class TestYield {
public static void main(String[] args) {
Thread t1=new Thread(()->{
int x=0;
while (true){
System.out.println("线程1:"+x++);
}
});
Thread t2=new Thread(()->{
int x=0;
while (true){
Thread.yield();
System.out.println("\t\t\t线程2:"+x++);
}
});
t1.start();
t2.start();
}
}
区别:
- 都能使当前处于运行状态的线程暂时放弃cpu把机会给其他线程
- sleep方法给其他线程运行机会,但是不考虑其他线程的优先级,yield只会给相同优先级,或者更高优先级的线程运行机会
- sleep方法使用当前线程进入休眠状态,执行sleep的线程在指定的时间内,肯定不会被执行
- yield方法,让当前线程从运行状态重新回到可执行状态,有可能在进入到可执行状态后,马上又被执行
- sleep方法,使当前线程休眠一段时间,进入不可运行状态,这段时间长短是由程序决定的。
- yield方法,让出CPU占有权,时间是不可设定的。
- 调用sleep方法之后,线程进去计时等待状态,yield进入就绪状态
1.6 join方法
join方法等待当前加入的线程调用结束,才能继续向下执行
public class TestJoin {
static int x=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
x=100;
});
t1.start();
//t1.join();
t1.join(500);//要不然就是t1线程执行完成死亡了,要不然就是时间到了
System.out.println(x);
}
}
1.7 interrupt方法
打断sleep、wait、join方法,处理阻塞状态的线程,会清空打断的状态(false)
Thread t1 = new Thread(() -> {
System.out.println("子线程休眠");
sleep(3);
});
t1.start();
sleep(1);
t1.interrupt();
System.out.println(t1.isInterrupted());//false
打断正常运行的线程,不会清空打断状态,打断状态标记为true
Thread t2=new Thread(()->{
while (true){
System.out.println("子线程执行循环");
boolean b = Thread.currentThread().isInterrupted();//true
if(b){
System.out.println("线程被打断");
break;
}
}
});
t2.start();
sleep(0.3);
t2.interrupt();
1.8 守护线程
- 目的:为其他线程服务的—GC—
- 特点:主线程结束,守护线程结束
- 创建线程:默认是前台线程
- 获取守护线程thread.setDaemon(true),必须在start()之前
java程序中,有两种线程,一种是用户线程User Thread,一种是守护线程 Daemon Thread 指是是程序运行时,在后台提供的一种通用服务线程。当所有的用户线程结束时,守护线程也就终止 了,同时会杀死进程中的所有守护线程。
setDaemon(boolean b)方法:如果b为true,则将当前线程设置为守护线程,否则是用户线程
应用场景:java垃圾回收线程
public class TestDaemon {
public static void main(String[] args) {
Thread t1=new Thread(()->{
while (true){
System.out.println("守护线程执行......");
sleep(1);
}
},"t1");
/*Thread t2=new Thread(()->{
while (true){
System.out.println("用户线程执行");
//sleep(0.3);
boolean b=Thread.currentThread().isInterrupted();
if (b){
System.out.println("用户线程结束");
break;
}
}
},"t2");*/
t1.setDaemon(true);
t1.start();
//t2.start();
sleep(3);
//t2.interrupt();
System.out.println("主线程结束");
}
}
1.9 获取当前线程及其名称
- Thread t = Thread.currentThread();
- t.getName();
二、数据共享的问题(线程同步)
2.1 问题分析
代码分析,如下代码执行,有可能是0,有可能是正数,也有可能是负数
public class Test {
static int x=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 10000; i++) {
x++;
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 10000; i++) {
x--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(x);
}
}
在执行x++指令时,会生成的字节码如下:
getstatic #2 //获取静态变量x的值
iconst_1 //准备常量1
iadd //自增
putstatic #2 // 将修改后的值存入静态变量中
在执行x–指令时,会生成的字节码如下:
getstatic #2 // Field i:I
iconst_1
isub //自减
putstatic #2 // Field i:I
临界区:一段代码块中间,如果存在对共享资源的多线程读写操作,这段代码块称为临界区
当一个对象或者一个不同步的共享状态,被两个或两个以上线程同时修改时,对访问顺序必须严格执行,则会产生竞态条件。
2.2 问题解决 synchronized
2.2.1 简介
对象锁的概念,采用互斥的方式,让同一时间点最多只能有一个线程持有锁,其他线程想获取这个锁的时候,就会发生阻塞,就可以保护当前拥有锁的线程可以安全的执行临界区中的代码,不需要处理上下文切换,导致的问题。
2.2.2 语法
2.2.2.1 锁对象
synchronized(同步锁){
//临界区代码
}
同步锁是谁?
- 非静态的来说,同步锁就是this
- 静态的来说,类的字节对象(类名.class)
public class Test {
static int x = 0;
final static Object obj1 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (obj1) {
for (int i = 0; i < 10000; i++) {
x++; //4行字节码
}
}
});
Thread t2 = new Thread(() -> {
synchronized (obj1) {
for (int i = 0; i < 10000; i++) {
x--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(x);
}
}
2.2.2.2 锁方法
锁实例方法
public synchronized void decrement(){
x--;
}
//相当于
public void increment(){
synchronized (this){
x++;
}
}
锁类方法
public synchronized static void test(){
}
//相当于
public static void test1(){
synchronized (NumberObj.class){//在static方法中,不能使用this
}
}
-
-
-
为了保证每个线程都能正常执行,原子操作,一般的,把当前并发访问的共同资源作为同步监听对象
-
任何时候,只能允许一个线程拥有同步锁,谁拿到谁执行,其他的等待,
-
不要使用同步锁去修饰run方法,修饰后,某一个线程执行完了其他的线程才可以执行
-
-
同步方法
synchronized public void eat(){ //业务逻辑 }
-
synchronized字节码分析
通过javap -c Class文件查看字节码
Code:
0: getstatic #2 // Field obj:Ljava/lang/Object;
3: dup //将上面对象复制
4: astore_1 //将obj对象引用存储到临时变量中 slot 1中
5: monitorenter //将obj对象Markword设置为Monitor指令
6: getstatic #3 // Field x:I
9: iconst_1
10: iadd
11: putstatic #3 // Field x:I
14: aload_1 //从slot 1中 加载obj对象,找到对应的monitor对象
15: monitorexit //将obj对象Markword重置还原,唤醒EntryList
16: goto 24 //如果正常情况,代码执行到24标识,结束
19: astore_2 //异常情况 将异常对象e 存入slot 2
20: aload_1 //同14
21: monitorexit //同15
22: aload_2 //将异常对象从slot2加载出来
23: athrow //抛出异常
24: return
Exception table:
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #2 // Field obj:Ljava/lang/Object;
10: iconst_0
11: putstatic #3 // Field x:I
14: return
}
2.3 锁机制
-
锁机制(Lock)
Lock lock = new ReentranLock(); @Override public void run() { for (int i = 0; i < 30; i++) { eat(); } } public void eat() { //上锁 lock.lock(); if (num > 0) { System.out.println(Thread.currentThread().getName() + "吃了编号为" + (num--) + "的香蕉"); } //解锁 lock.unlock(); }
-
同步代码块,同步方法,同步锁如何选择?
-
尽量减少锁的作用域(Synchronized)
-
建议使用锁机制(很好的去控制锁的作用范围,性能更高)
-
Lock 和 Condition 接口:
- wait 和 notify 只能被同步监听锁对象调用,否则报错,但是Lock 机制根本没有同步锁,也没有获取释放锁的逻辑。
- 使用Condition 接口对象的 await singnal signalAll 方法取代 Object 类中的 wait notify notifyAll 方法
public class ShareResource {
private String name;
private String size;
private boolean isEmpty = true;//判断资源对象是否为空
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
/**
* 给生产者推送数据的
*
* @param name
* @param size
*/
public void push(String name, String size) {
lock.lock();//获取锁
try {
//判断资源对象是否为空,空的时候等待
while (!isEmpty){
condition.await();
}
this.name = name;
Thread.sleep(1000L);
this.size = size;
isEmpty = false;
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
public void pop() {
lock.lock();//上锁
try {
//如果资源为空,消费者等待
while (isEmpty){
condition.await();
}
Thread.sleep(1000L);
System.out.println("服装名称:" + this.name + " 尺码: " + this.size);
isEmpty = true;
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
}
2.4 .Monito
Moninter可理解为监视器或叫作管程
每个java对象都可以关联一个Monitor对象,如果 使用synchronized给对象加锁,对象头的mark word 中,被设置为指向Monitor对象的指针。
程序开始时Monitor中Owner为null
-
当Thread-2执行synchronized(obj)就会将Monitor的所有者设置为Thread-2,Monitor只能有一个 Owner,此时,markword中的状态从01转为10(重量级锁),markword中存储的是monitor的指 针,表示加锁
-
在Thread-2加锁的过程中,如果线程Thread-1和Thread-3也加锁synchronized(obj),则会进入 EntryList,状态为BLOCKED
-
Thread-2执行完同步代码块中的内容,然后唤醒EntryList中等待的线程,采用非公平方式竞争锁, 获取锁的线程,就是Monitor中的Owner
-
WaitSet中的Thread-0、Thread-1是之前获得过锁,但条件不满足,从而进入WAITING状态的线程
2.5 线程通信wait/notify
针对上面的图
- Owner线程发现条件不满足,调用wait方法,进入waitSet,变成WAITING状态
- BLOCKED和WAITING的状态下的线程,不占用CPU时间片
- BLOCKED线程在Owner线程释放锁时被唤醒
- WAITING线程在Owner线程调用notify或者notifyAll方法时,被唤醒。唤醒后不代表可以立刻获得 锁,需要进入EntryList重新竞争
方法的使用
- wait()方法,让进入obj监视器的线程到waitset中等待
- notify()方法,在obj监视器上正在waitset等待的线程中选择一个激活
- notifyAll()方法,在obj监视器上,所有waitset等待的线程全部激活
- 都属于Object类的方法,必须要求获得对象的锁,才能使用。
代码演示
public class TestWaitNotify {
static final Object LOCK=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
synchronized (LOCK){
System.out.println("线程1执行");
try {
LOCK.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1继续执行");
}
});
t1.start();
Thread t2=new Thread(()->{
synchronized (LOCK){
System.out.println("线程2执行");
try {
LOCK.wait(2000);//调用这个方法,会使当前线程等待,直到另一个线程
调用此对象的notify()或者是notifyAll()唤醒, 或者到了指定时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2继续执行");
}
});
t2.start();
//主线程休眠
TimeUnit.SECONDS.sleep(1);
System.out.println("主线程唤醒其他线程");
synchronized (LOCK) {
LOCK.notify();//
//LOCK.notifyAll();
}
}
}
wait和sleep区别
- sleep是Thread类的静态方法,wait是Object类的方法
- sleep是不需要和synchronized配合使得,wait是需要和synchronized配合使用
- sleep在执行方法时,进入休眠状态,不会释放对象锁,wait方法进入等待,会释放对象锁
- sleep和wait状态都是TIMED_WAITING
public class TestWaitSleep {
static final Object LOCK=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
synchronized (LOCK){
System.out.println("线程1执行");
try {
// LOCK.wait(2000);
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1继续执行");
}
});
t1.start();
TimeUnit.SECONDS.sleep(1);
synchronized (LOCK){
System.out.println("主程序执行");
}
}
}
2.6 wait和notify的使用方式
-
public class Test_wait_notify_sleep { static final Object LOCK = new Object(); static boolean b = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (LOCK) { System.out.println(getName() + "是否拿到工资:" + b); if (!b) { System.out.println(getName() + "没有工资,不工作,睡觉"); try { //TimeUnit.SECONDS.sleep(2); LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(getName() + "是否拿到工资:" + b); if (b) { System.out.println(getName() + "拿到工资,开始工作"); } } }, "员工李白").start(); //创建4个其他员工 for (int i = 0; i < 4; i++) { new Thread(() -> { synchronized (LOCK) { System.out.println(getName() + "开始工作"); } }, "其他人" + i).start(); } TimeUnit.SECONDS.sleep(1); new Thread(() -> { synchronized (LOCK) { b = true; System.out.println("老板给送工资了"); LOCK.notify(); } }).start(); } public static String getName() { return Thread.currentThread().getName(); } }
当有多个线程在等待,需要唤醒时
-
public class Test_wait_notify_sleep_2 { static final Object LOCK = new Object(); static boolean b = false;//李白工作条件 static boolean a = false;//杜甫工作条件 public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (LOCK) { System.out.println(getName() + "是否拿到工资:" + b); while (!b) { System.out.println(getName() + "没有工资,不工作,睡觉"); try { //TimeUnit.SECONDS.sleep(2); LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(getName() + "是否拿到工资:" + b); if (b) { System.out.println(getName() + "拿到工资,开始工作"); } } }, "员工李白").start(); new Thread(() -> { synchronized (LOCK) { System.out.println(getName() + "是否拿到工资:" + a); while (!a) { System.out.println(getName() + "没有工资,不工作,睡觉"); try { //TimeUnit.SECONDS.sleep(2); LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(getName() + "是否拿到工资:" + a); if (a) { System.out.println(getName() + "拿到工资,开始工作"); } } }, "员工杜甫").start(); //创建4个其他员工 /*for (int i = 0; i < 4; i++) { new Thread(()->{ synchronized (LOCK){ System.out.println(getName()+"开始工作"); } },"其他人"+i).start(); }*/ TimeUnit.SECONDS.sleep(1); new Thread(() -> { synchronized (LOCK) { a = true;//杜甫获取工资 System.out.println("老板给送工资了"); //LOCK.notify();//唤醒李白 虚假唤醒 LOCK.notifyAll();//唤醒所有的等待线程,还是有一个是虚假唤醒 } }).start(); TimeUnit.SECONDS.sleep(3); new Thread(() -> { synchronized (LOCK) { b = true;//李白获取工资 System.out.println("老板给送工资了"); //LOCK.notify();//唤醒李白 LOCK.notifyAll();//唤醒所有的等待线程,还是有一个是虚假唤醒 } }).start(); } public static String getName() { return Thread.currentThread().getName(); } }
用法
synchronized(LOCK){ while (判断条件){ LOCK.wait(); } //其他执行代码 } //另一个线程 synchronized(LOCK){ LOCK.notifyAll(); }
2.7 死锁
一个线程如果需要同时获取多把锁,容易发生死锁问题。
t1线程先获取笔,然后获取纸。t2线程先获取纸,然后获取笔。
这样就有可能发生死锁,代码:
public class TestDeadLock { public static void main(String[] args) { Object pen=new Object(); Object paper=new Object(); new Thread(()->{ synchronized (pen){ System.out.println("t1线程拿到了笔"); synchronized (paper){ System.out.println("t1线程拿到了纸"); System.out.println("t1可以写字了"); } } }).start(); new Thread(()->{ synchronized (paper){ System.out.println("t2线程拿到了纸"); synchronized (pen){ System.out.println("t2线程拿到了笔"); System.out.println("t2可以写字了"); } } }).start(); } }
出现死锁的定位:
jps指令,查看当前死锁线程的pid,再使用jstack pid查看状态
查找到一个java级别的死锁
通过jconsole检测死锁
死锁的四个条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:当前线程已获得的资源,在没有使用完之前,不能强行剥夺
- 循环等待条件:若干线程之间形成一种头尾相连的循环等待资源关系
死锁的处理
预防死锁,破坏四个条件中的一个,不能破坏互斥条件,其他三个可以破坏。
避免死锁,在资源动态分配过程中,使用某种方式阻止系统进入不安全状态。
检测死锁,允许系统在运行过程中发生死锁,可以设置检测操作对死锁的发生进行检测,并采用相关措 施去清除
解除死锁,采用资源剥夺法,撤销进程法,进程回退法等,将进程从死锁状态解脱出来。
2.7 volatile使用
问题重现,无法退出死循环
public class TestVolatile {
static boolean b=true;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while (b){
// System.out.println("t1线程正在执行");
}
});
t1.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("主线程将b的值改为false,停止t1线程");
b=false;
}
}
JVM 内存可见性
t1线程从主内存读取boolean类型的值,到工作内存
为了加快t1线程执行效率,将b存到t1线程中的工作内存中,做为高速缓存
使用volatile修饰变量
volatile是一个关键字,用来修饰变量,表示变量不可以被某些编译器因为一些未知因素改变。使用该关键字,可使编译器不再对这个变量进行优化,保证访问的稳定性。变量必须到主内存中进行读取,不再工作内存中进行缓存。
2.8 ThreadLocal
称作线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对于其他线程来讲是互相隔离的,这个变量是当前线程独有的变量。ThreadLocal变量在每个线程中都创建一个副本,这样每个线程可以访问自己内部的副本变量。减少同一个线程内多个函数或组件之间一些公共变量来回传递的复杂度。
注意点:
- 每个Tread内有自己的实例副本,这个副本只能由当前Thread使用
- 不存在多线程间共享数据的问题
- 通常被private static修饰,当一个线程结束时,它所使用的所有ThreadLocal的副本都可以被回收
应用场景:
- 线程并发场景:不适用于单线程,适用于多线程并发
- 传递数据场景:可以通过ThreadLocal在同一线程,多个不同组件中传递公共变量
- 线程隔离场景:每个线程的变量都是独立的,互不影响
测试代码
public class TestTheadLocal {
public static void main(String[] args) {
//testUseThreadLocal();
testSynchronized();
}
public static void testUseThreadLocal() {
DataDemo1 demo1 = new DataDemo1();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
demo1.setData(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + ":获 取" + demo1.getData());
}).start();
}
}
public static void testSynchronized() {
DataDemo demo = new DataDemo();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
synchronized (demo) {
demo.setData(Thread.currentThread().getName() + "的数 据");
System.out.println(Thread.currentThread().getName() + ": 获取" + demo.getData());
}
}).start();
}
}
public static void testNoThreadLocal() {
DataDemo demo = new DataDemo();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
demo.setData(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + ":获取" + demo.getData());
}).start();
}
}
}
class DataDemo {
private String data;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
class DataDemo1 {
ThreadLocal<String> local = new ThreadLocal<>();
public String getData() {
return local.get();
}
public void setData(String data) {
local.set(data);
}
}
}
ThreadLocal和Synchronized的区别
- Synchronized用于线程间的数据共享,而ThreadLocal用于线程间的数据隔离
- Synchronized是利用锁的机制,使用变量或代码块在某一时间点只能被一个线程访问。ThreadLocal 为每一个线程都提供了变量的副本,使得每个线程在某一时间点访问到的并不是同一个对象,就实现了 隔离多个线程对数的共享。
应用优点:
- 传递数据,保证每个线程绑定的数据,在需要的地方直接获取,避免参数直接传递带来的代码耦问题
- 线程隔离,数据之间相互隔离,同时又可以并发,避免使用锁的机制带来的性能损失
2.9 那些类是线程安全的
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable juc:java.util.concurrent包下的类
2.10 变量的线程安全
- 成员变量和静态变量
- 如果没有发生共享,是线程安全的
- 如果发生了共享,根据是否读写,分为两种情况
- 如果只做了读的操作,则是线程安全的
- 如果有读写操作,则代码是临界区,非线程安全的
- 局部变量
- 局部变量是线程安全
- 局部变量引用的对象不一定线程安全
2.11 线程的生命周期
-
生命周期:一个物品从出生—死亡
-
线程的生命周期(Thread.State):
-
NEW:使用new 创建线程对象的时候,只分配了空间,调用 start() 方法的时候,线程启动
-
RUNNABLE:(可运行状态)使用了 start 之后,变成了两种状态
- ready:准备就绪,等待 cpu 的调度
- running :获取到了 cpu 的调度
-
BLOCKED:(阻塞的状态)运行中的线程,因为某些原因,放弃了 cpu 暂停运行,此时 jvm 不会给线程分配 cpu
- 当 P线程处于运行状态时,但是,没有获取到同步锁, JVM 就会把 P线程放到同步锁对象的锁池中去,P阻塞
- 线程运行中发出(IO请求),此时阻塞
-
WAITING:(等待的状态)等待状态只能被其它线程唤醒,
- 运行中的线程调用了 wait() 方法,此时 jvm 把当前的线程存放在对象等待池中
-
TIMED_WAITING:(计时等待)wait(long),sleep(long),就变成计时等待
- 线程运行过程中,调用上面方法,此时 JVM把当前线程存在对象等待池中
-
TERMINATED:(停止)表示线程结束了
- 正常的执行完线程
- 出现异常
-
2.12 线程组
-
可以对线程集中管理,ThreadGroup
-
用户创建线程对象时,可以通过构造器指定所属的线程组
ThreadGroup groups = new ThreadGroup("商品上架线程组"); ThreadGroup groups1 = new ThreadGroup("fihfiwehuif"); GoodsThread a = new GoodsThread(groups, "A"); a.start(); new GoodsThread(groups,"B").start(); new GoodsThread(groups1,"C").start(); new GoodsThread("efwefewfewfe").start(); public GoodsThread(ThreadGroup group, String name) { super(group,name); }
-
A线程中创建B线程,B线程默认加入A线程线程组
-
默认创建的线程,都属于main 线程
三、ReentrantLock
3.1 简介
是可重入的互斥锁,会比使用synchronized更加灵活,并且具有更多的方法。
底层基于AbstractQueuedSynchronizer(AQS)实现。AQS是一个抽象同步队列器,作用是提供一个框架,简化程序员对锁同步控制工具的开发。程序员只需要继承AQS类,实现"尝试获取锁"方法,实现"尝 试释放锁"方法,就可以轻松实现可重入互斥锁。
AQS会把所有的请求线程构成一个同步队列,当一个线程执行完毕,会激活自己的后继节点。正在执行的线程并不在队列中,而等待执行的线程全部处于阻塞状态。
ReentrantLock实现了Lock接口,Lock接口是Java中对锁操作行为的统一规范。
3.2 特点
支持可重入
可中断
可以设置超时时间
可以设置公平锁和非公平锁
支持多个条件变量,相当于有多个waitset
3.3 可重入性
一个线程不用释放,可以重复的获取一个锁n次。在释放的时候,也需要相应的释放n次。
假设t1线程在上下文中获得了一个锁,当t1线程想要再次获取这个锁时,不会因为锁已经被自己占用, 而需要先等到锁的释放。
synchronized:无需释放锁,synchronized会自动释放锁
ReentrantLock:需要手动释放锁,加了几次锁,就需要释放几次
基本语法:
reentrantlock.lock();//加锁
try{
//代码
}finally{
reentrantlock.unlock();//解锁
}
3.4 测试可重入代码
public class Test {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock=new ReentrantLock();
Thread t1 = new Thread(() -> {
int index = 1;
try {
reentrantLock.lock();
System.out.println("最开始加锁");
while (true) {
try {
reentrantLock.lock();
System.out.println("第" + (++index) + "次加锁");
if (index == 10)
break;
} finally {
reentrantLock.unlock();
System.out.println("第" + index + "次解锁");
}
}
} finally {
//reentrantLock.unlock();
System.out.println("最后解锁");
}
});
t1.start();
new Thread(()->{
try {
reentrantLock.lock();
for (int i = 0; i < 3; i++) {
System.out.println("线程任务:"+Thread.currentThread().getName());
}
}finally {
reentrantLock.unlock();
}
}).start();
/*t1.join();
System.out.println(reentrantLock.isLocked());*/
}
}
3.5测试可中断
中断锁指的是锁在执行是,可以通过interrupt的通知,从而中断锁执行。
如果锁不可中断,可能会出现问题:当一个线程持有锁,出现异常时,只能一直阻塞等待。
public class Test01 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock=new ReentrantLock();
Thread t1 = new Thread(()->{
try{
lock.lock();
System.out.println("线程1获取到锁");
}finally {
lock.unlock();
}
});
t1.start();
Thread t2=new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
try{
System.out.println("线程2尝试获取锁");
//lock.lock();
lock.lockInterruptibly();//可被打断
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println("线程2被中断");
return ;
}
try{
System.out.println("线程2获取到锁");
}
finally {
lock.unlock();
}
});
t2.start();
TimeUnit.MILLISECONDS.sleep(1000);
if (t2.isAlive()){
System.out.println("执行线程中断操作");
t2.interrupt();
}else{
System.out.println("线程2执行完成");
}
System.out.println(lock.isLocked());
}
}
3.6 设置超时时间测试
public class Test02 {
public static void main(String[] args) {
ReentrantLock lock=new ReentrantLock();
Thread t1=new Thread(()->{
System.out.println("尝试获取锁");
//如果获取到锁,返回true,否则返回false
//boolean b = lock.tryLock();//false
boolean b = false;
try {
//最长等待3秒钟,如果还没有拿到锁,则认为获取锁失败
//如果3秒内拿到锁,则立即近回true
b = lock.tryLock(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!b){
System.out.println("获取锁失败");
return;
}
try{
System.out.println("已经拿到了锁");
System.out.println("执行正常操作代码");
}finally {
lock.unlock();
}
});
//主线程1秒后锁放锁
lock.lock();
try {
System.out.println("主线程获取锁");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
t1.start();
}
}
3.7 公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列第一位可以获得锁。
优点:所有的线程都能得到资源,不会出现线程饥饿的问题。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他线程都会阻塞,cpu唤醒阻塞线程的开锁很大。
非公平锁:多个线程去获取锁的时候,直接去尝试获取,获取不到再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开锁,整体吞吐效率会提高,可以减少CPU唤醒线程的数量
缺点:导致队列中有线程一直获取不到锁或者长时间获取不到锁,导致线程饥饿
public class Test03 {
public static void main(String[] args) {
ReentrantLock lock=new ReentrantLock(true);
Fair fair=new Fair(lock);
new Thread(fair,"t1").start();
new Thread(fair,"t2").start();
}
}
class Fair implements Runnable{
private Integer num=0;
private ReentrantLock lock;
public Fair(ReentrantLock lock) {
this.lock = lock;
}
@Override
public void run() {
while (num<=100){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"执行任务:"+num++);
}finally {
lock.unlock();
}
}
}
}
3.8 Condition
Condition的作用和wait\notify相同,wait和notify是和同步锁(synchronized)一起使用,Condition是和重入锁(ReetrantLock)一起使用的。
通过Lock接口的newConditon创建可以一个与当前重入锁绑定的Condition的实例
主要调用的方法:
await:会使线程进入等待,同时释放锁,当其他线程使用singal方法的时候,线程会重新获得锁并继续执行
singal: 会唤醒一个正在等待的线程 singalAll:会唤醒所有等待的线程
测试代码
public class Test04 {
static final ReentrantLock LOCK = new ReentrantLock();
static boolean b = false;//李白工作条件
static boolean a = false;//杜甫工作条件
static Condition bCondition = LOCK.newCondition();//李白等待的房间
static Condition aCondition = LOCK.newCondition();//杜甫等待的房间
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
LOCK.lock();
System.out.println(getName() + "是否拿到工资:" + b);
while (!b) {
System.out.println(getName() + "没有工资,不工作,睡觉");
try {
//TimeUnit.SECONDS.sleep(2);
//LOCK.wait();
bCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(getName() + "是否拿到工资:" + b);
if (b) {
System.out.println(getName() + "拿到工资,开始工作");
}
} finally {
LOCK.unlock();
}
}, "员工李白").start();
new Thread(() -> {
try {
LOCK.lock();
System.out.println(getName() + "是否拿到工资:" + a);
while (!a) {
System.out.println(getName() + "没有工资,不工作,睡觉");
try {
//TimeUnit.SECONDS.sleep(2);
//LOCK.wait();
aCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(getName() + "是否拿到工资:" + a);
if (a) {
System.out.println(getName() + "拿到工资,开始工作");
}
} finally {
LOCK.unlock();
}
}, "员工杜甫").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
try {
LOCK.lock();
a = true;//杜甫获取工资
System.out.println("老板给杜甫送工资了");
//LOCK.notify();//唤醒李白 虚假唤醒
//LOCK.notifyAll();//唤醒所有的等待线程,还是有一个是虚假唤醒
aCondition.signal();
} finally {
LOCK.unlock();
}
}).start();
TimeUnit.SECONDS.sleep(3);
new Thread(() -> {
try {
LOCK.lock();
b = true;//李白获取工资
System.out.println("老板给李白送工资了");
//LOCK.notify();//唤醒李白
//LOCK.notifyAll();//唤醒所有的等待线程,还是有一个是虚假唤醒
bCondition.signal();
} finally {
LOCK.unlock();
}
}).start();
}
public static String getName() {
return Thread.currentThread().getName();
}
}
四、线程池
4.1 简介
线程过多会带来调度开销,会影响整体性能。线程池就是存放线程的池子,在其中存放了很多可以复用的线程。可以使用线程池来维护多个线程,进行统一管理。 创建线程和销毁线程的开销是较大的(手动去new Thread类)。
4.2 优势
- 提高效率,创建好一定数量的线程放在线程池中,需要时,直接使用从池中取出,比需要时,创建一个线程对象要快。
- 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可以执行多个任务。
- 提升系统响应速度。假设创建线程时耗费时间为t1,执行业务逻辑耗费时间t2,销毁线程耗费时间t3,使用线程池,就节省t1和t3的时间
- 实现对线程管理,每个java线程池都会保持一些基本的线程统计信息,对线程进行有效管理
4.3 线程池架构
4.4 线程池状态
4.5 ThreadPoolExecutor
ThreadPoolExecutor executor=new ThreadPoolExecutor(int corePoolSize,int maximumPoolSize);
int corePoolSize:核心线程数(最多保留的线程数)
int maximumPoolSize:线程池所能容纳的最大线程数,超过这个数的线程,就会被阻塞,当任务队列 没有设置大小时,这个值无效。
long keepAliveTime:非核心线程的闲置超时时间,超过这个时间,就会回收
TimeUnit unit:时间单位,指的是keepAliveTime的时间单位
BlockingQueue< Runnable> workQueue:阻塞队列
ThreadFactory threadFactory:线程工厂,可以为线程创建时起名
RejectedExecutionHandler handler:拒绝策略
4.6 工作流程
- 线程池中最开始没有线程,当任务提交给线程池,线程池会创建一个新的线程来执行任务
- 当线程数达到core核心线程数,并且没有线程空闲,再加入的任务,会被加入到queue队列中排队,直到有空闲线程去执行这个任务
- 如果队列是有界队列,那任务超过队列大小,会创建(maximumPoolSize-corePoolSize),来创建救急线程
- 如果线程maximumPoolSize占满,仍然有新的任务进来,此时执行拒绝策略。
- 默认拒绝策略:AbortPolicy,抛出异常
- 放弃本次任务策略: DiscardPolicy,直接放弃任务的执行
- 由调用者执行:CallerRunsPolicy,线程池不去执行本次任务,由线程池的调用者线程去执行
- 取代最早任务执行: DiscardOldestPolicy,放弃队列中最早的任务,由本次任务取代执行
代码演示
public class TestPool {
public static void main(String[] args) {
test1();
//test2();
//test3();
//test4();
}
//核心线程数2个,最多3个,有1个救急线程
public static void test1() {
Factory factory = new Factory();
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3,
60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2), factory);
factory.setThreadName("线程1");
executor.execute(() -> System.out.println("任务1:" + Thread.currentThread().getName()));
factory.setThreadName("线程2");
executor.execute(() -> System.out.println("任务2:" + Thread.currentThread().getName()));
executor.shutdown();
}
//核心线程数2个,最多3个,有1个救急线程
//测试阻塞队列
public static void test2() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2));
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(2);//休眠2
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务1:" + Thread.currentThread().getName());
});
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务2:" + Thread.currentThread().getName());
});
executor.execute(() -> System.out.println("任务3:" + Thread.currentThread().getName()));
executor.execute(() -> System.out.println("任务4:" + Thread.currentThread().getName()));
executor.shutdown();
}
//核心线程数2个,最多3个,有1个救急线程
//测试救急线程
public static void test3() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3,
60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2));
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(2);//休眠2
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务1:" + Thread.currentThread().getName());
});
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务2:" + Thread.currentThread().getName());
});
executor.execute(() -> System.out.println("任务
3:"+Thread.currentThread().getName()));
executor.execute(() -> System.out.println("任务
4:"+Thread.currentThread().getName()));
executor.execute(() -> System.out.println("任务
5:"+Thread.currentThread().getName()));
executor.shutdown();
}
//测试拒绝策略
public static void test4() {
//拒绝策略,默认情况,抛出异常
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 3, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2), new ThreadPoolExecutor.AbortPolicy());
//放弃本次任务
executor = new ThreadPoolExecutor(2, 3, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2), new ThreadPoolExecutor.DiscardPolicy());
//由调用者来执行任务
executor = new ThreadPoolExecutor(2, 3,
60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2),
new ThreadPoolExecutor.CallerRunsPolicy());
//放弃最早任务,执行本次任务
executor = new ThreadPoolExecutor(2, 3, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2), new ThreadPoolExecutor.DiscardOldestPolicy());
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(2);//休眠2
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务1:" + Thread.currentThread().getName());
});
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务2:" + Thread.currentThread().getName());
});
executor.execute(() -> System.out.println("任务3:" + Thread.currentThread().getName()));
executor.execute(() -> System.out.println("任务4:" + Thread.currentThread().getName()));
executor.execute(() -> System.out.println("任务5:" + Thread.currentThread().getName()));
executor.execute(() -> System.out.println("任务6:" + Thread.currentThread().getName()));
executor.shutdown();
}
}
class Factory implements ThreadFactory {
private String threadName;
public String getThreadName() {
return threadName;
}
public void setThreadName(String threadName) {
this.threadName = threadName;
}
@Override
public Thread newThread(Runnable r) {
return new Thread(r, threadName);
}
}
4.7 通过工厂方式创建线程池
Executor:提供了execute()方法来执行已提交的Runnable目标实例
ExecutorService:继承于Executor,java异步目标任务的"执行者服务接口",对外提供异步任务接收服务
Executors:静态工厂类,通过静态工厂方法返回ExecutorService、ScheduledExecutorService等线程池对象
4.7.1 固定线程池 newFixedThreadPool
//源码
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
固定线程数:核心线程数和最大线程数相同,没有救急线程,也不需要处理超时时间
阻塞队列是没有界限的,可以放任意数量的任务
如果线程没有达到固定数量,每次提交一个任务,就会创建一个新的线程,直到线程数达到线程池固定数量
线程池大小一旦达到固定数量,就会保持不变,如果某个线程因为执行异常而结束,线程池会补充一个新线程
在接收异步任务的执行目标实例时,如果池中所有线程都在繁忙状态,新任务就会进入阻塞队列中
适用场景:
适用于任务量已知,相对耗时的任务
CPU密集型任务
缺点:
内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i <20; i++) {
executorService.execute(()->{
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":执行任务");
});
}
executorService.shutdown();
}
}
4.7.2 单线程化线程池newSingleThreadExecutor
new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())
单线程化线程池中的任务是按照提交的次序依次执行
只有一个线程,并且这个线程的存活时间是无限的
当线程池中这个唯一线程繁忙时,新提交的任务实例就会进入到阻塞队列中,并且阻塞队列是无界的
适用场景:
任务按照提交次序,一个一个的逐个执行
单线程化线程池和用户自定义创建的单线程区别:
用户创建一个单线程串行执行任务,如果任务执行失败而终止,则没有任何补救措施,而线程池的方式还会创建一个线程,保证池的正常工作。
单线程化线程池和固定线程线程池为1的有什么区别:
单线程化线程个数始终为1,不能修改,使用了装饰器模式,只对外暴露了ExecutorService接口,不能再调用ThreadPoolExecutor中的方法
固定线程池初始时为1,可以修改,对外暴露了ThreadPoolExecutor对象,经过强制转换,可以调用到其中的方法
参考代码
public static void fixed(){
ExecutorService executorService=Executors.newFixedThreadPool(1);
ThreadPoolExecutor executor=(ThreadPoolExecutor)executorService;
executor.setMaximumPoolSize(4);
executor.setCorePoolSize(3);
for(int i=0;i<20;i++){
executor.execute(()->{
try{
TimeUnit.MILLISECONDS.sleep(300);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":执行任务");
});
}
executor.shutdown();
}
4.7.3 可缓存线程池newCachedThreadPool
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
核心线程数是0
最大线程数是Integer.MAX_VALUE,所有的线程都是救急线程,没有核心线程,救急线程可以按Integer最大值创建
救急线程空闲生存时间是60秒,超过了60秒,就会回收空闲的线程
阻塞队列使用了SynchronousQueue,内部没有容器,一个线程,生产了产品,如果当前没有其他线程去消费这个产品,生产线程处于阻塞状态,等待另一个线程去做消费处理。
测试SynchronousQueue
public class TestSynchronousQueue {
public static void main(String[] args) {
SynchronousQueue<Integer> queue = new SynchronousQueue<>();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
System.out.println("开始执行生产:" + i);
try {
queue.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
System.out.println("消费一个产品:" + i);
try {
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
适用场景:
需要快速处理突发性强,耗时较短的任务场景
缺点:
线程池没有最大线程数量限制,如果大量的异步任务执行目标同时提交,可能会造成因创建线程过多而导致资源耗尽
public static void cached(){
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
executorService.execute(()->
System.out.println(Thread.currentThread().getName()+":执行任务"));
}
executorService.shutdown();
}
4.7.4 任务调度线程池newScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
基于线程池的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行。任务是并发执行,多个任务之间,互相不影响。
Timer单线程串行执行的,任务会有先后顺序
public static void testTimer(){
//测试一下单线程Timer串行执行
//必须按顺序,先执行任务1,后执行任务2,如果任务1中出现错误,整个全部停掉
Timer timer=new Timer();
TimerTask task1=new TimerTask(){
@Override
public void run(){
System.out.println("任务1");
int x=5/0;//算术异常
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
}
};
TimerTask task2=new TimerTask(){
@Override
public void run(){
System.out.println("任务2");
}
};
timer.schedule(task1,1000);
timer.schedule(task2,1000);
}
public static void testScheduleThread(){
//使用线程池执行两个不同任务,互不影响
ScheduledExecutorService executorService= Executors.newScheduledThreadPool(2);
executorService.schedule(()->{
try{
int x=5/0;
System.out.println(x);
}catch(ArithmeticException e){
e.printStackTrace();
}
System.out.println("任务1");
try{
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
},1,TimeUnit.SECONDS);
executorService.schedule(()->{
System.out.println("任务2");
},1,TimeUnit.SECONDS);
executorService.shutdown();
}
public static void testSchedule(){
ScheduledExecutorService executorService =Executors.newScheduledThreadPool(2);
//反复执行
/*executorService.scheduleAtFixedRate(()->{
System.out.println("hello schedule");
},1,1,TimeUnit.SECONDS);*/
//延迟执行
executorService.schedule(()->{
System.out.println("hello schedule");
},1,TimeUnit.SECONDS);
executorService.shutdown();
}
4.7.8 线程池其他API
void execute(Runnable command):执行Runnable任务
< T> Future < T> submit (Callable< T> task):提交任务,返回Future,获取任务执行结果
< T> List> invokeAll(Collection> tasks):提交集合中的所有任务
< T> T invokeAny(Collection> tasks)
throws InterruptedException, ExecutionException:提交集合中的所有任务,只要有一个任务执行完,将结果返回,其他任务不再执行
public class TestPoolApi {
public static void main(String[] args) throws InterruptedException,
ExecutionException, TimeoutException {
ExecutorService service = Executors.newFixedThreadPool(3);
//testSubmit(service);
//testInvokeAll(service);
testInvokeAny(service);
service.shutdown();
}
public static void testInvokeAny(ExecutorService service) throws
ExecutionException, InterruptedException, TimeoutException {
Integer x = service.invokeAny(
Arrays.asList(
() -> {
System.out.println("任务1");
TimeUnit.MILLISECONDS.sleep(800);
return 10;
},
() -> {
System.out.println("任务2");
TimeUnit.MILLISECONDS.sleep(1200);
return 20;
},
() -> {
System.out.println("任务3");
TimeUnit.MILLISECONDS.sleep(200);
return 30;
}));
System.out.println(x);
service.shutdown();
}
public static void testInvokeAnyTimeout(ExecutorService service) throws
ExecutionException, InterruptedException, TimeoutException {
Integer x = service.invokeAny(
Arrays.asList(
() -> {
System.out.println("任务1");
TimeUnit.MILLISECONDS.sleep(800);
return 10;
},
() -> {
System.out.println("任务2");
TimeUnit.MILLISECONDS.sleep(1200);
return 20;
},
() -> {
System.out.println("任务3");
TimeUnit.MILLISECONDS.sleep(200);
return 30;
}), 100, TimeUnit.MILLISECONDS);
System.out.println(x);
service.shutdown();
}
public static void testInvokeAll(ExecutorService service) throws
InterruptedException {
List<Callable<String>> list = Arrays.asList(
() -> {
System.out.println("任务1");
TimeUnit.MILLISECONDS.sleep(300);
return "aa";
},
() -> {
System.out.println("任务2");
TimeUnit.MILLISECONDS.sleep(800);
return "bb";
},
() -> {
System.out.println("任务3");
TimeUnit.MILLISECONDS.sleep(1000);
return "cc";
});
List<Future<String>> futures = service.invokeAll(list);
futures.forEach(future -> {
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
}
public static void testSubmit(ExecutorService service) {
/*Future<Integer> f=service.submit(() -> {
int sum=0;
for (int i=1;i<=100;i++)
sum+=i;
return sum;
});
try {
System.out.println(f.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}*/
service.submit(() -> {
System.out.println("执行任务");
});
}
}
4.7.9 线程池关闭API
shutdown():将线程池状态设置为SHUTDOWN,调用该方法,线程池不会立即停止运行。停止接收外部的提交任务,内部正在执行的任务和队列里等待的任务,会执行完成,才会真正停止。
shutdownow():将线程池状态设置为STOP,先停止接收一切外部提交的任务。忽略队列里等待的任务, 尝试将正在执行的线程interrupt中断,返回未执行的任务列表。
awaitTermination():当前线程阻塞,直到等待所有已提交的任务(包括正在执行的和队列中的)执行完, 或者等超时时间到,或者线程被中断,抛出InterruptedException。这个方法,会返回boolean类型的值,如果超时,会返回false。如果shutdown请求后所有任务执行完毕,会返回true
public static void testShutdown(ExecutorService service) throws
InterruptedException {
service.execute(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务1执行");
});
service.execute(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务2执行");
});
service.execute(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务3执行");
});
// service.shutdown();
// service.shutdownNow();
service.shutdown();
boolean b = service.awaitTermination(3, TimeUnit.SECONDS);
System.out.println("全部执行完成");
System.out.println(b);
}
}
五、LockSupport中park和unpark
可以完成线程按顺序执行的功能 ,类似于wait和notify wait
notify 和notifyAll必须配置Object Monitor一起使用,加锁
park和unpark不需要,以线程为单位来实现和阻塞和唤醒。
notify只能随机唤醒一个等待线程,notifyAll唤醒所有等待线程,不够精准。
park和unpark可以精准唤醒线程,并且可以先执行unpark
public class TestLockSupport {
//完成wait notify await singal
public static void main(String[] args) {
Thread t1=new Thread(()->{
LockSupport.park();
System.out.println("攻击防御塔");
});
Thread t2=new Thread(()->{
System.out.println("生产超级兵");
LockSupport.unpark(t1);
});
t1.start();
t2.start();
}
}