-
基本概念
- 程序、进程、线程的基本概念和关系
- 程序:静态的指令集;不占系统资源;也不被系统调用;不能作为独立运行的单位,以文件形式存储在磁盘上。
- 进程:程序的执行活动;使用系统资源; 是资源申请、调度、独立运行的单位;一个程序可以对应多个进程。例如著名的QQ多开。
- 线程:被称为轻量进程;大多数OS将其作为时序调度的基本单位;没有明确的协调情况下,线程相互同时或异步地执行。
- 与进程关系:线程共享所属进程的内存地址空间,因此同一进程中线程访问相同的变量,并从同一堆中分配对象,从而相对进程间通信机制实现了良好的数据分享。但是需要我们用明确的同步机制来管理共享数据,否则会造成数据的不可预知的更改。
- 为什么进行多线程编程
- 多线程优点:
- 更好的实现并发
- 开发维护性能:恰当使用线程,可以降低开发和维护开销,并提高复杂应用的性能。
- 切换:CPU切换线程比切换进程的开销小。
- 创建、撤销:线程创建和撤销的开销比进程的小。
- 多线程优点:
- Java在多线程应用的优势
- 语言级提供对多线程程序设计的支持
- 线程模型 1:1 N:1 M:N 自己去了解吧,这里不赘述
- Java线程的生命周期
- 创建(新线程):
- 条件:new创建线程实例后,仅作为对象实例存在;
- 状态:JVM没有为其分配 CPU时间片和其他线程所需运行资源
- 就绪:
- 条件:位于创建状态的线程调用start()方法后且只调一次。
- 状态:得到除CPU时间之外的其他系统资源,等待JVM的线程调度器按线程优先级调度,从而使线程拥有Cpu时间片
- 运行:
- 就绪态线程获取到cpu 就进入运行态
- 等待/阻塞
- 线程运行过程中被剥夺资源或等待某些事件就进入等待/阻塞状态;条件:
- sleep()方法被调用(线程主动放弃所占用的cpu资源,但不会释放线程锁),
- 线程使用wait()等待条件变量
- 调用suspend()方法将线程状态转换为挂起状态。//废弃
- 或处于I/O等待(直到该方法返回之前)
- 状态:线程将释放占有的所有资源,但是并不释放锁,所以容易引发死锁,直到应用程序调用resume()恢复线程运行。等待事件结束或得到足够的资源就进入就绪态。
- 线程运行过程中被剥夺资源或等待某些事件就进入等待/阻塞状态;条件:
- 死亡
- 线程结束(run()结束),线程抛出一个未捕获的Exception和Error,调用线程对象的stop()方法后
- JVM收回线程占用的资源
- 创建(新线程):
- 程序、进程、线程的基本概念和关系
-
线程创建与调度
-
Thread类
- java.lang.Thread类
- run方法
- 实现多线程最直接简单的方法:继承Thread类并重写run方法并构建其对象。
- Thread的API构造方法
-
Runnable接口
-
实现Runnable接口,那么该类可以继承其他父类。(java单继承)
-
可使用Lambda表达式
-
实现方式:线程类实现Runnable接口,并重写run方法。然后传入Thread类的参数。
//实现runnable接口 public class C implements Runnable{ public void run(){ } public static void main(String[] args){ C c=new C(); Thread thread=new Thread(c); } } //lambda表达式通过Runnable实现线程 public class C2{ public static void main(String[] args){ Runnable runnable=()->{方法体}; Thread thread=new Thread(runnable); } }
-
-
Callable接口
- 同上
- 区别:call方法会带回返回值
-
-
线程启动和停止
-
线程启动:start
- 主线程:执行main方法的线程
- 主线程特殊之处:
- 它是产生其他子线程的线程
- 通常最后结束,因为要执行子线程的关闭工作。
- 主线程中启动其他线程,不能直接调用线程对象的run(),而是start()
- run和start方法区别:start会创建新线程,run不会。
- 注意:start()方法调用后并不是立即执行多线程代码,而是将线程为就绪状态,什么时候运行是由OS调度决定。针对同一个线程对象,start方法只能调用一次。
- run和start方法区别:start会创建新线程,run不会。
-
线程停止:
- stop():用于停止一个已经启动的线程;已被废弃,不建议使用,因为不安全。
- 因为它会解锁线程所有已经锁定的监视程序,在这个过程中会出现不确定性,导致这些监视器程序监视的对象出现不一致的情况,或者引发死锁。
-
如何安全的停止一个正在运行的线程
-
线程对象run方法执行完后,线程会自然消亡,因此如果需要在运行过程中提前停止线程,可以通过改变共享变量值的方法让run方法结束
public class ThreadStopEX extends Thread{ private boolean flag=true; //修改共享变量,促使run方法循环结束,从而完成方法的调用,线程自然消亡。 //这里我们只要提供一个条件可以让run方法结束就是ok的 public void stopThread(){ falg=false; } public void run(){ //这里线程处于一个忙等待状态(等待共享变量更改为false,会很消耗资源哦! while(){ flag; } public static void main(String[] args) throws Exception{ ThreadStopEX tse=new ThreadStopEX(); tse.start(); tse.stopThread();//通过更改flag的值来让线程退出忙等待,然后自然死亡。 } } }
-
-
如何安全的停止一个由于等待某些事件发生而被阻塞的线程呢?
- 具体场景:线程因为等候资源、数据而调用Thread.sleep方法、Object类的wait方法造成线程阻塞,并使线程处于不可运行状态;此时即使主程序将线程的共享变量设置为false,线程也无法检查循环标志,也就无法中断。
- 具体解决建议:使用Thread类的interrupt方法:它会中断一个正在运行的线程,但它可使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出阻塞代码。
- 判断线程是否通过interrupt方法被终止
- 静态方法interrupted:判断当前线程是否被中断
- 非静态方法isInterrupted:判断其他线程是否被中断
- 注意:
- 需要区分捕获到的中断异常是否由我们主动调用interrupt方法引起。因为还有别的因素可能导致该异常,而某些时候即时产生了此异常也应该让线程继续执行,此时我们需要编写代码来进行区分
- interrupt()不能阻断I/O阻塞或线程同步引起的线程阻塞。例如一个线程等待键盘输入或等待网络连接等I/O资源。
public class stopSleepThread{ public static void main(String[] args){ SleepTread st =new SleepThread(); st.start(); st.interrupt();//使得被阻塞的线程抛出一个中断异常,从而使得线程提前结束阻塞状态。但不会中断一个正在运行的线程。 } } class SleepThread extends Thread{ boolean flag=true; public void run(){ while(flag){ try{ Thread.sleep(1000*60*60); }catch(InterruptedException e){ System.out.print("线程中断"); flag=false; } } } }
-
如何处理由I/O资源引起的线程阻塞时的线程中断问题
-
关闭底层I/O通道,人为引发异常等方式从而进行共享变量重新赋值而跳出run方法。
-
nio支持非阻塞式的事件驱动读取操作,在这种模式下,不需要关闭底层资源即可通过interrupt方法直接中断其等待操作。
-
-
线程不同状态之间的转换
- 见图
-
守护线程
- Java线程划分
- 守护线程
- 指在程序运行时在后台提供一种通用服务的线程,比如垃圾回收线程就是一个称职的守护者。
- 特点:守护线程不是程序中不可或缺的部分。非守护线程运行,程序就不会结束;所有非守护线程结束时,程序终止,同时杀死进程中的所有守护线程。
- 用户线程
- 与守护线程几乎没有区别,唯一的不同之处就在于虚拟机的退出:如果用户线程已经全部退出运行,只剩下守护线程存在,JVM也就退出了。
- 守护线程
- 线程转换为守护线程:
- 调用Thread对象的setDaemon(true)方法(默认守护线程属性为false,即默认创建线程对象为非守护的用户线程)
- 注意
- 不能把正在运行的常规线程设置为守护线程即thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出IllegalThreadStateException异常。
- Daemon线程中产生的新线程也是Daemon。
- 不要将所有的应用都分配给Daemon线程进行服务,可能出现一些问题;比如读写操作或者计算逻辑放入守护线程,可能这些操作并没有运行,JVM就已经退出了。
- 注意
- 调用Thread对象的setDaemon(true)方法(默认守护线程属性为false,即默认创建线程对象为非守护的用户线程)
- 判断线程是否为守护线程:isDaemon()方法
- Java线程划分
-
线程类重要方法的作用
-
熟悉Thread API
-
Thread 的名字
- 设置名字:setName() 或者构造时指定
- 构造方法提供
- setName(String name)方法给定
- 获取名字:getName()
- 默认名字:如果没有为线程显式提供名字,Java按照thread-0,thread-1—的默认方式为线程提供默认的名字(main方法启动的主线程名字为main)
- 设置名字:setName() 或者构造时指定
-
Thread优先级
-
优先级:高优先级的线程比低优先级有更高的几率得到执行(即优先级高不一定有优势)
-
java线程的优先级是一个整数,取值范围为1(Thread.MIN_PRIORITY)-5(Thread.NORMPRIORITY)-10(Thread.MAX_PRIORITY)
- 注意:
- 事实上Java线程在没有明确指定的情况下,其优先级并不一定为5,而是和父线程(创建本线程的线程)的优先级保持一致,main默认5
- 优先级不能超过1-10,否则抛出IllegalArgumentException,建议使用3个常量来确定。
- 线程优先级不能超过其线程组的优先级
- 注意:
-
设置优先级:setPriority(final修饰,不能被子类重写)
-
线程主动放弃占据的资源
-
Thread提供2个静态方法来让线程主动放弃占据的资源
-
sleep():
- 使当前线程进入阻塞状态:
- 执行sleep的线程在指定时间内不会执行,同时sleep方法不会释放锁资源(即如果正在运行的线程占有某个资源的同步锁,它不会释放掉这个同步锁,其他线程仍然不能访问该资源)
- 即使没有其他等待运行的线程,当前线程也会等待指定时间
- 对于其他等待执行的线程被CPU调度的机会是均等的
- java并不保证线程在阻塞给定的时间后能够马上执行。
- 使当前线程进入阻塞状态:
-
yield():
- 使当前线程转入暂停执行(即就绪可执行)的状态
- 所以执行yield的线程可能在进入就绪状态后马上又被执行。
- 如果没有其他等待运行的线程,当前线程会马上恢复执行
- 会将优先级相同或更高的线程运行
- 所以这里也有了解释
- 使当前线程转入暂停执行(即就绪可执行)的状态
- yield与sleep的区别:sleep提供阻塞时长,可让低优先级线程得到执行机会,而yield使线程进入就绪可运行状态,没有阻塞时长,只能让相同或更高优先级线程有执行机会,甚至有时JVM认为不符合最优资源调度时会忽略该方法调用(System.gc())
-
-
-
-
* 对方线程对象.join方法:等待其他线程结束;当前运行的线程调用另一线程的join方法,当前运行线程将转到阻塞状态,直到另一个线程执行结束,然后当前运行线程恢复运行。
* t.join:阻塞调用此方法的线程,直到t线程完成,此线程再继续。
* 通常用于main主线程,等待其他线程结束后再结束主线程。
* 解析:join实现通过wait。当main线程调用t.join时,main线程会获得线程对象t的锁,调用该方法的wait方法,直到该对象唤醒main线程。
-
线程组:Java提供的一个线程统一管理工具,构建线程组对象后,线程可以通过Thread类的构造方法将自己加入线程组。线程组API interrupt可以中断线程组中所有线程。
- 熟悉线程组API
- 线程组最大优先级限制该组线程的优先级:该线程组的线程优先级不能超过该组优先级。
- 注意:系统线程组的最大优先级默认为Thread.MAX_PRIORITY
- 创建线程组的时候最大优先级默认为父线程组的最大优先级(如果未指定父线程组,则其父线程组默认为当前线程所属线程组)
- setMaxPriority的问题:
- 更改本线程组及其子线程组(递归)的最大优先级
- 但不能影响已经创建的直接或间接属于该线程组的线程的优先级。只有试图改变子线程优先级或者创建新的子线程的时候,线程组的最大优先级才起作用。
- 关于线程优先组需要注意:
- Thread.setPriority可能根本不会做任何事情,与os和jvm版本有关
- 线程优先级对于不同线程调度器可能有不同含义
- 线程优先级通常是全局和局部的优先级设定的组合。Java的setPriority是局部优先级。通常是一种保护方式
- 不同系统有不同的线程优先级的取值范围。这样就有可能出现几个线程在同一OS中有不同的优先级,在另一os中却有相同的优先级。
- 关于线程优先组需要注意:
-
线程同步 [数据可见性、指令重排序、原子性的问题]
-
不同步会发生的问题
-
Java内存模型中的数据可见性和JVM重排序
-
Java为保证平台无关性,隔离了应用程序与操作系统内存模型,定义了自己的内存模型。
-
Java内存模型
-
内存分主内存和工作内存
-
主内存所有线程共享,工作内存每个线程分配一份,各线程工作内存彼此独立,互不可见。
-
线程启动,JVM为每个线程分配一块工作内存,不仅包含线程内部定义的局部变量,也包含线程所需使用的共享变量(非线程内构造的对象)的副本,为提高执行效率(副本中寻址并读取数据比直接读取主内存更快)
-
Java内存模型定义一系列工作内存和主内存之间交互的操作与操作之间顺序的规则。
例如共享普通变量,约定在变量在工作内存中发生变化之后,必须要写回主内存;但这个约定只规定结果而并没有规定时机,所以可能在实际写回主内存之前,有很多线程已经在主内存中读取已经无效的数据。
-
-
JVM指令重排序
-
重排序:编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。
-
通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数、充分复用寄存器的存储值。
-
举个例
指令1:计算一个值赋给变量A并存入寄存器 指令2:与A无关,但需要占用寄存器(假设占用A的寄存器) 指令3:要使用A的值,且与第二条指令无关 按照顺序一致性模型: 指令1执行后A放入寄存器, 指令2执行后A不再存在 指令3A重新被读入寄存器。 这个过程,A的值没有发生变化。 JVM指令重排序: 通常编译器会交换第二条和第三条指令的位置。 这样第一条指令结束时A存在于寄存器中,然后我们从寄存器直接读取A的值,降低重复读取的开销。
-
-
顺序一致性模型:即假设指令执行的顺序是被编写在代码中的顺序,与处理器或其他因素无关。这种模型效率很低。
-
千万不要随意假设指令执行的顺序
-
数据依赖:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,构成数据依赖。
-
数据依赖类型
同一变量: 写后读 a=1;b=a; 写一个变量之后,再读这个位置 写后写 a=1;a=2; 写一个变量后,再写这个变量 读后写 b=a;a=1; 读这个变量后,再写这个变量 这三种情况,只要重排序两个操作的执行顺序,程序执行结果将会被改变。 因此:编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。 就是说:单线程环境下,指令执行的最终效果应当与顺序执行下的效果一致,否则优化就失去意义。--as if serial semantics
-
-
重排序对多线程环境带来的影响隐患
public class HappensBeforeTest{ int a=0; boolean flag=false; public void write(){ a=1;//1 flag=true;//2 } public void read(){ if(flag){//3 int i=a*a;//4 } } } 问题:flag变量是个标记,用来标识变量a是否已被写入。假设有两个线程A和B,A首先执行write,随后B线程执行read()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入。 思考:不一定看到。 分析:操作1和操作2没有数据依赖关系(因为不是对同一变量操作。),编译器和处理器可以对这两个操作重排序;同样操作3和操作4没有数据依赖关系,所以也可以进行重排序。
-
-
-
synchronized关键字使用
-
volatile关键词 :用于声明时修饰变量
- 两条语义
- 保证变量在内存模型中的数据可见性
- volatile变量java要求工作内存中发生变化之后,必须马上写回主内存;线程读取volatile变量,必须马上到主内存中去取最新值而不是读取本地工作内存的副本。
- 换句话说:线程A对变量X进行修改后,在线程A后面执行的其他线程都能看到变量X的变动。
- 禁止指令重排序:对volatile变量的操作不能进行重排序。
- 保证变量在内存模型中的数据可见性
- 注意:volatile修饰变量不能保证对其操作的原子性。也就是说volatile缩短了普通变量在不同线程之间执行的时间差,但仍然不能保证原子性。
- 当前:随着虚拟机的不断优化,如今普通变量的可见性已经好多了所以volatile如今没有啥使用场景
- 两条语义
-
Java对32位内的数据的load和save操作都是原子的。
-
线程同步:需要将多条代码视作一个整体调度单位,希望这个调度单位在多线程环境的调度顺序不影响任何结果。换句话:保证数组可见性、防止重排序、代码段原子保护。
- 主要作用:实现线程安全的类。
- java使用synchronized关键字对操作进行同步处理。
-
JVM的几个规范:
- JVM中,每个对象和类在逻辑上都是和一个监视器相关联的
- 为了实现监视器的排他性监视能力(即保证资源只能同时被一个线程访问),JVM为每一个对象和类都关联了一个锁,锁住了一个对象,就是获得对象相关联的监视器。
- 锁和监视器的区别:锁为实现监视器提供必要的支持
- 监视器:
- 类似令牌,获取令牌的线程才能操作资源,操作完成释放令牌,下一个线程才有机会重新获取令牌。
- 进入->获得->持有->释放->退出
- 一个线程可以允许多次对同一对象上锁,对于每一个对象来说,JVM维护一个计数器,记录对象被加了多少次锁,没被锁的对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器减一,当计数器跳到0的时候,锁被完全释放了。
- java虚拟机中的一个线程在它到达监视区域开始处的时候请求一个锁,JAVA程序每一个监视区域都与一个对象引用相关联。
- 监视器:
-
Java中通过synchronized关键字获得对象锁,实现线程同步。
-
synchronized会降低程序的性能 所以我们要尽量缩短同步区域
-
实现同步有两种方法:
- 同步方法 :synchronized修饰方法
- 同步代码块(临界区同步快)
- 目的:希望防止多个线程同时访问方法内部的部分代码而不是整个方法
- synchronized:用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制
- 作用:可以适当降低同步整个方法带来的性能消耗
-
-
线程死锁
- 死锁的条件:同时满足4个就会发生死锁
- 互斥条件:线程使用的资源至少一个是不能共享的
- 资源持有:持有资源
- 不能抢占:资源持有后不能被抢夺
- 循环等待:我等你,你等他,他等我。
- 防止死锁:破坏4个条件中的一个就可以
- 最容易破坏的条件:条件四
- 死锁的条件:同时满足4个就会发生死锁
-
线程间通讯
-
线程间通信的意义
- 目的:使线程间能够互相发送信号。或使线程能够等待其他线程的信号。
-
wait、notify、notifyAll方法使用
- 利用标志性的共享变量实现线程通信
public class EggTest{ //保证数据可见性和禁止指令重排序。 volatile boolean hasEggs=false; Thread human=new Thread(()->{ //忙等待:循环轮询方式 //缺点:没有对运行等待线程的CPU进行有效利用;除非平均等待时间非常短。 //让等待线程进入睡眠或者非运行状态才是明智的,直到它接受到等待的信号。-----引入java内建的等待机制 while(true){ if(!hasEggs){ //没有鸡蛋,则只是等待。 System.out.println("等待"); }else{ System.out.println("收获"); hasEggs=false; } } }); Thread hen=new Thread(()->{ while(true){ try{ Thread.sleep(10000); }catch(InterruptedException e){ e.printStackTrace(); } //有了鸡蛋后设置共享变量的值。 hasEggs=true; } }); } 注意: 1.线程human和hen必须获得指向同一hasEggs共享实例的引用。 2.需要处理的数据可以存放在一个共享缓存区。
- Java内建的等待机制 —进行线程通信
- 允许线程等待信号的时候变为非运行状态。
- 实现:java.lang.Object类定义三个方法,wait()、notify()和notifyAll()来实现等待机制
- wait方法:线程一旦调用任意对象的wait方法,线程会变为非运行状态,且释放所持有监视器对象的锁。直到另一个线程调用同一个对象的notify/notifyAll方法。
- 注意:与Thread sleep方法的区别:sleep方法不会释放持有监视器对象的锁。
- 补充:wait具有时间参数,超过时间,即使没有其他线程调用notify,仍然会唤醒线程。
- notify/notifyAll方法:线程调用一个对象的notify方法,正在等待该对象的所有的线程中将有一个线程被唤醒并允许执行。(唤醒的线程是随机的,且不能指定)。也提供一个notifyAll唤醒正在等待一个给定对象的所有线程。
- 注意:
- 一个问题:如果一个线程先于被通知线程调用wait前调用notify,被通知线程可能错过这个信号。某些情况下,这可能使等待线程永远等待,不再醒来。
- 解释:notify和notify不会保存调用它们的方法,因为当这两个方法被调用时,可能没有线程处于等待状态。通知信号后便丢弃了。
- 调用wait或notify的先决条件:线程必须获取某个对象的锁。即线程必须在同步块里调用wait和notify。JVM实现,当调用wait,首先检查当前线程是否为锁的拥有者,不是抛出llegalMonitorStateException
- 注意:不要使用全局对象,字符串常量等,而是用对应唯一的对象。
- 解释:例如:空字符串(或其他常量字符串)作为锁的同步块
- JVM/编译器会将常量字符串转换为同一对象。这表示,即使有两个不同实例,但它们都引用相同的字符串实例。存在一个风险:第一个实例上调用wait方法的线程会被在第二个实例上调用notify的线程唤醒。
-
-
管道流通信
- 实现:Java的Pipe管道输入流与Pipe管道输出流实现类似管道的功能,用于不同线程间相互通信。
- 注意:不要再一个线程中同时使用管道输入流和管道输出流,可能会造成死锁。
-
-
线程高级调度 线程池、信号量、生产者消费者模式
-
线程池概念与作用
-
为什么需要线程池(池化资源技术产生原因)
-
如果每次请求就创建一个新线程,开销非常大,特别是实际使用中,服务器创建和销毁线程花费的时间和系统资源相当大。
-
活动的线程也需要消耗资源,如果JVM创建过多线程,可能致使系统过渡消耗内存或“切换过度”而导致资源不足。
-
所以为了限制任何时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象进行服务。
总结一句话:解决线程生命周期开销问题和资源不足问题。消除线程创建带来的延迟,使应用程序响应更快。
-
-
一个比较简单的线程池构成
- 线程池管理器
- 创建、销毁并管理线程池,将工作线程放入线程池
- 工作线程
- 一个可以循环执行任务的线程,没有任务时等待
- 任务队列
- 提供缓冲机制,将没有处理的任务放入任务队列中
- 任务接口
- 每个任务必须实现的接口,主要规定任务入口、任务执行完后的收尾工作、任务的执行状态等,工作线程通过该接口调度任务的执行。
- 线程池管理器
-
-
实现基础的线程池
- 这里参见代码
-
信号量的概念与作用
- 信号量 参见信号量.md或者博客
- 资源调度,同步互斥都可以实现。
-
实现基础的信号量
- Semaphore类的实现和理解
-
生产者-消费者模式
- 这个也请理解一下。
-
-
JDK1.5+的新工具
-
线程池调度器
-
Executor :JDK5的异步框架
-
作用:将任务提交和任务执行进行解耦;使得开发人员不再关注各类任务线程的实现,而将后台异步执行的内容抽象为单个任务。执行任务的人只需把Task描述清除后提交即可。
流程:提交Callable对象给ExecutorService(如常用的线程池ThreadPoolExecutor),得到一个Future对象,调用Future对象的get()等待执行结果。
-
Executor框架的重要核心接口和类
- Executor接口 :
- 一个可提交可执行任务的工具
- 解耦任务提交和任务执行
- 主要替代显示的创建和运行线程
- ExecutorService接口
- 提供异步的管理一个或多个线程终止、执行过程(Future)的方法
- 和其实现类提供简便的方式提交任务并获取任务执行结果,封装任务执行的全部过程。
- ScheduledExecutorService接口
- 主要工具类 Executors用来获取实现ExecutorService接口的子类
- 提供一系列工厂方法用于创建任务执行器,返回的任务执行器都实现ExecutorService接口(且绝大部分执行器都完成池化操作。)
- Executor接口 :
-
-
生成ExecutorService实现类 内置的常见工厂方法及生成的任务调度器特征 步骤1
// Executors类的静态方法 获取 ExecutorService实现类 ExecutorService newFixedThreadPool(int nThreads) //创建固定数目线程的线程池 ExecutorService newCachedThreadPool() //创建一个可缓存的线程池 ExecutorService newSingleThreadExecutor() //创建一个单线程化的Exrcutor 保证线程顺序执行 ScheduledExecutorService newScheduledThreadPool(int corePoolSize)//创建一个支持定时及周期性的人物执行的线程池,多数情况可用来替代Timer类。
- ExecutorService的方法 步骤2
- execute(Runnable)
- 异步方式执行,无法获取返回结果
- submit(Runnable)
- 返回一个Future对象,该对象可以判断Runnable任务是否结束执行。
- submit(Callable)
- Callable可以返回一个结果
- 返回值同样从该方法返回的Future对象中获取。
- invokeAny(…)
- 参数:接收一个包含Callable对象的集合
- 返回值:返回集合中某个Callable的结果,而且无法保证调用之后返回的结果是集合中的哪个Callable结果。看自写的代码。
- 如果一个任务运行完毕或抛出异常,方法会取消其他Callable的执行。也就是找到一个东西,那么不需要再找了。
- invokeAll(…)
- 参数:Callable对象集合
- 返回值:返回一个包含Future对象的集合,可通过该集合管理每一个Callable的执行结果。找出所有东西
- 注意:任务可能因为异常而导致运行结束,所以它可能不是真的成功运行。而这个我们无法通过Future对象来了解这个差异。
-
关闭ExecutorService 步骤3
- 使用完毕,我们应该关闭它,保证其中的线程不会继续保持运行状态
- 场景一:如果程序通过main主线程启动,主线程退出,但还有一个活动的ExecuterService存在于程序中;思考:会如何? 解答:程序将继续保持运行状态。存在于ExecutorService中的活动线程会阻止JVM关闭。
- shutdown方法:关闭ExecutorService中的线程;注意:它并不会马上关闭,而是不再接收新的任务,一旦所有线程结束执行当前任务,它才会真的关闭。而所有在shutdown方法前提交到ExecutorService的任务都会执行。
- shutdownNow方法:希望立即关闭ExecutorService。注意:尝试马上关闭所有正在执行的任务,并且跳过所有已经提交但是还没有运行的任务。对于正在执行的任务,是否能够成功关闭是无法保证的。
-
-
Runnable与Callable
- 区别:前者方法为run,不返回任何任务。后者方法为call,返回执行后的结果。
- 都符合函数式接口的标准
-
锁对象
- Lock接口:为了更清晰的表达如何加锁和释放锁。比如在哪儿加锁,在哪儿释放锁。
- 默认实现:ReentrantLock
- 可重入的独占锁:比synchronized关键字有更清晰的语义;构造器可接受一个可选参数,是否是公平锁,默认为非公平锁。
- 公平锁
- 先来一定排队,一定先获取锁
- 非公平锁
- 不保证上述条件。但吞吐量更高。
-
ThreadLocal
- 问题:需要在同一个线程中共享一个变量。(变量取值在不同线程中是不同的)
- 解决1:创建一个共享散列表,键为线程对象,线程共享的变量作为值;每次通过散列表.get(Thread.cuurentThread())获取共享变量的本线程版本
- 解决2:ThreadLocal:线程内变量共享工具 优化并性能更佳
- 这个过程同时解决了命名冲突的问题。例如 web开发中,各个层都是在一个线程中,可以将一个变量放到ThreadLocal,然后各个层都可以用,单有人跟我说,这个其实是有点安全问题的。
- ThreadLocal的作用:
- 解决多线程程序并发问题
- ThreadLocal:Thread的局部变量,当使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每个线程都独立的改变自己的副本,而不影响其他线程对应的副本。
- ThreadLocal类的方法:
- void set(T value)
- 将此线程局部变量的当前线程副本中的值设置为指定值。
- void remove()
- 移出此线程局部变量的当前线程的值
- protected T initialValue()
- 返回此线程局部变量的当前线程的"初始值"
- T get()
- 返回此线程局部变量的当前线程副本中的值
- void set(T value)
- 问题:需要在同一个线程中共享一个变量。(变量取值在不同线程中是不同的)
-
信号量对象
- JDK5提供信号量的默认实现
-
原子操作类 这个暂且略过
- 定义:java提供粒度更细,量级更轻,并且在多核处理器具有高性能的原子操作类。因为原子操作类将竞争的范围缩小到单个变量上。
- 包位置:Java.util.concurrent.atomic
- 相当于泛化的volatile变量,支持原子读取-修改-写操作
- 例AtomicInteger 表示一个int类型的数值,提供get和set,这些volatile类型的变量在读取与写入上有相同的内存语义。
- 原子更新类的类型
- 原子更新基本类型
- AtomicBoolean 原子更新boolean
- AtomicInteger 原子更新integer
- AtomicLong 原子更新Long
- 原子更新数组类型
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
- 原子更新引用
- AtomicReference
- AtomicReferenceFieldUpdater
- AtomicMarkableReference
- 原子更新属性
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicStampedReference
- 原子更新基本类型
-
-
任务调度
-
任务调度类型
- 定时任务
- 重复任务
- 项目运行过程中通过控制时间间隔完成反复执行的任务(例如自动化数据备份)
-
Timer
-
简介:Java最早提供的任务调度器,可以支持定时任务和重复任务的调度。
-
Timer的重要任务调度方法
schedule(TimerTask task,Date time) //指定时间执行指定的任务 schedule(TimerTask task,Date firstTime,long period) //安排指定的任务在指定的时间开始进行重复的固定延迟执行 schedule(TimerTask task,long delay) //指定延迟后执行指定的任务 scheduleAtFixedRate(TimerTask task,Date firstTime,long period) //安指定的任务在指定的时间开始进行重复的固定速率执行 scheduleAtFixedRate(TimerTask task,long delay,logn period) //安排指定任务在指定的延迟后开始进行重复的固定速率执行
-
-
TimerTask
-
抽象类,是Runnable的子类,所以具有run方法定义任务执行的内容。且扩展
cancel() //取消此计时器任务 scheduledExecutinonTime() //返回此任务最近实际执行的已安排执行时间
-
满足Timer对任务本身的抽象规定:用于声明需要调度的任务重需要执行的指令代码
-
注意:
- Timer不能保证任务在所指定的时间内执行
- 因此不能保证任务在所指定的时间内执行
- 导致任务延迟执行的情况
- 有大量线程在等待执行
- GC机制的影响导致延迟
-
JDK5 利用Executor任务调度框架中提供并行化的任务调度执行机制替代Timer和TimerTask组合
- 替代工具 java.util.concurrent.ScheduledThreadPoolExecutor
-
-
Quartz任务调度
-
简介:完全由java编写的开源任务调度框架。整合了许多额外功能。简单地创建一个实现org.quartz.Job接口的java类。
//Job接口包含唯一的方法 public void execute(JobExecutionContext context) throwsJobExecutionException;
-
Quartz采用基于多线程的架构。启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业。
-
继承Job接口并重写execute来实现我们的任务。
public class PrintQuartzTask implements Job{ public void execute(jobExecutionContext context) throws JobExecutionException{ //trigger 触发器 System.out.println("任务启动"+new Date()+"by"+context.getTrigger().getName()); } }
-
进行任务调度
public class QuartzScheduleTest{ public static void main(String[] args){ try{ //创建Scheduler工厂 SchedulerFactory schedulerFactory=new StdSchedulerFactory(); //创建Scheduler调度器 通过工厂的getScheduler Scheduler scheduler=schedulerFactory.getScheduler(); //配置任务 包括名字、组、任务class对象 JobDetail jobDetail=new JobDetail("打印任务",Scheduler.DEFAULT_GROUP,PrintQuartzTasd.class); //创建一个触发器 包括名字,组并配置 SimpleTrigger simpleTrigger=new SimpleTrigger("simpleTrigger",Scheduler.DEFAULT_GROUP); simpleTrigger.setStartTime(new Date(System.currentTimeMillis())); simpleTrigger.setRepeatInterval(100); simpleTrigger.setRepeatCount(10); //调度器 调度任务 传入任务细节 以及触发器 并启动 scheduler.scheduleJob(jobDetail,simpleTrigger); scheduler.start(); }catch(Exception ex){ ex.printStackTrace(); } } }
-
-
-
-
Java线程补充知识点:
- ThreadLocal、BlockingQueue、CountingSemaphore和ConcurrentHashMap
- 并发流、Fork/Join线程池、CompletableFuture的问题
-
现在有线程T1、T2、T3。如何保证T2线程在T1之后执行,并且T3在T2之后执行
Thread的join方法。 T3中调用T2线程对象.join,在T2中调用T1线程对象.join。
-
Java中新的Lock接口对于同步代码块有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,如何保证数据完整性。
Lock接口最大优势:为读和写提供两个单独的锁,可构建高性能数据结构。例如ConcurrentHashMap和条件阻塞。
-
Java中wait和sleep方法有什么区别?
主要区别就在:是否释放锁和监视器 sleep等待时不会释放任何锁或监视器。 wait多用于线程间通信
-
如何在Java中实现一个阻塞队列
wait和notify方法实现 可以用java5的并发类重新实现一次
-
在java中编写代码解决消费者生产者问题
原生java BlockingQueue解决
-
写一段死锁代码。在java中如何解决死锁?
-
什么是原子操作?Java中有哪些原子操作
-
Java中volatile关键字是什么?如何使用它?和java同步方法有什么区别?
需要掌握volatile变量在并发环境中如何确保可见性、有序性、一致性
-
什么事竞态条件?如何发现并解决竞态条件?
-
Java中如何转储线程(thread dump)?如何分析它?
在unix中,可以使用kill -3然后线程转储日志会打印在屏幕上,可以使用Ctrl+break查看。 然后是如何分析转储日志
-
start方法和run方法的区别?
调用start方法,会新建一个线程并执行run方法中代码块 而直接调用run方法,则不会新建一个新线程
-
java中如何唤醒阻塞线程?
Io阻塞,那么没有方式可以中断线程。关闭IO资源/或人为引发异常 如果是因为调用wait、sleep、join方法,可以中断异常,通过interrupt方法,抛出一个中断异常。
-
Java中CyclicBarriar和CountdownLatch有什么区别?
CyclicBarrier在屏障打开后(所有线程到达屏障点),可以重复使用 CountdownLatch不行
-
什么是不可变类,对于编写并发应用有什么帮助?
为什么Java中的String是不可变的
-
在多线程环境中遇到的最多的问题是什么?如何解决的?
内存干扰、竞态条件、死锁、活锁、线程饥饿等问题
补充:题
-
Java中绿色线程和本地线程的区别?
-
线程和进程的区别
-
多线程的上下文切换是什么?
-
死锁和活锁的区别?死锁和饥饿的区别?
-
Java中使用什么线程调度算法
-
Java中线程调度是什么?
-
线程中如何处理某个未处理异常?
-
什么事线程组?为什么java中不建议使用线程组?
-
为什么使用Executor框架比直接创建线程要好?
-
JavaExecutor和Executors的区别?
-
Windows和linux系统上分别如何找到占用CPU最多的线程?