二、多线程02
2.1 线程调度
线程调度是指系统为线程分配CPU执行时间片的策略方式,主要调度方式有两种:协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。
协同式线程调度:
该调度策略模式下线程的执行时间由线程本身来控制,某一线程执行完了之后,会主动通知系统切换到另外一个线程上执行(可以想象下类似排队一样的场景)。协同式多线程的最大好处是实现简单,而且由于线程获取执行时间和切换由自己控制,切换操作对线程自己是可知的,所以没有什么线程同步的问题。但是它缺点很明显:如果一个线程编写有问题,那么线程运行了一部分之后就一直堵塞,一直不告诉系统进行线程切换,进程一直不让CPU执行时间严重时可能导致整个系统崩溃。
抢占式线程调度
该调度策略模式下每个线程的执行时间以及线程的切换都将有系统分配和控制;在这种实现线程调度的方式下,线程的执行时间是系统可控的,可能一个线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片,这种调度策略下如果某个线程阻塞了也不会导致整个进程阻塞的问题,Java使用的线程调度方式就是抢占式调度。
问题:
Java中既然是抢占式线程调度模式,那么我们哪些操作方法可以让线程获取CPU的执行时间片呢?又有哪些操作可以阻塞线程呢?下面我们就来看看线程常见的一些操作方法。
2.1.1 线程休眠(sleep)
static void | sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 |
static void | sleep(long millis, int nanos) 导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。 |
/**
* 1、Thread.sleep方法
* 调用sleep方法,当前线程会让出CPU的执行时间而进入阻塞等待状态,等待的时间一到就会再次进入就绪状态抢夺CPU的执行时间片。
* sleep方法只能睡眠当前的线程。
* 2、TimeUnit.XXX.sleep方法
* 相比第一种睡眠方法更直观,更简单,因为他可以直接规定时分秒,而不需要用毫秒进行转换
* DAYS:时间单位为二十四小时
* HOURS:时间单位为六十分钟
* MICROSECONDS:时间单位为千分之一毫秒
* MILLISECONDS:时间单位为千分之一秒
* MINUTES:时间单位为六十秒
* NANOSECONDS:时间单位为十亿分之一秒(纳秒)
* SECONDS:时间单位为一秒
*/
public class SleepTest {
public static void main(String[] args) {
Threadthread=newThread(newRunnable() {
@Override
publicvoidrun() {
//线程休眠
try {
//休眠3秒
//Thread.sleep(3000);
//休眠三天
TimeUnit.DAYS.sleep(3);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis());
}
});
thread.start();
}
}
2.1.2 线程礼让(yield)
容易被误用,尽量不使用该方法。
static void | yield() 对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。 |
/**
* 线程的礼让指的是当前线程暂时放弃CPU的执行时间礼让一下给其他线程,而礼让的同时自身进入就绪状态。
* 因为礼让之后自身也进入就绪状态,所以yeild礼让之后自身还是有几率抢夺到CPU的执行时间,
* 只不过一旦礼让之后优先级越高的线程抢夺到的几率越高。
*
* 输出0-19的数字,当数字是5的时候t1进行线程礼让
*/
public class YieldTest {
public static void main(String[] args) {
Threadthread1=newThread(newRunnable() {
@Override
publicvoidrun() {
for (inti=0; i<20; i++) {
System.out.println("当前的线程是:"+Thread.currentThread().getName()+",对应的值是"+i);
if (i==5){
//线程礼让
Thread.yield();
System.out.println(Thread.currentThread().getName()+"礼让");
}
}
}
},"t1");
Threadthread2=newThread(newRunnable() {
@Override
publicvoidrun() {
for (inti=0; i<20; i++) {
System.out.println("当前的线程是:"+Thread.currentThread().getName()+",对应的值是"+i);
}
}
},"t2");
thread1.start();
thread2.start();
}
}
2.1.3 线程优先级(Priority)
/**
* 1、线程同样也有优先级设定,线程的优先级从1到10;数字越大则优先级越高优先级就是默认的5
* 2、有默认的几个优先级:
* Thread.MIN_PRIORITY:值为1,最小优先级
* Thread.NORM_PRIORITY:值为5,中等优先级
* Thread.MAX_PRIORITY:值为10,最大优先级
* 其他的优先级可以用1-10的数字进行代替,如果优先级设置大于10当成10来看,同样小于1当成1来看
* 3、并不是优先级高的线程就一定会比优先级低的线程先执行,线程的优先级越高表示的是获取到的CPU执行时间片越多,跟优先级低的线程比抢夺到CPU执行时间的几率就越高。
*/
public class PriorityTest implements Runnable{
privateintticketNum=5;
@Override
public void run() {
while (true){
if (ticketNum>0){
//获取当前正在执行的线程的名字:Thread.currentThread().getName()
//默认名字的格式:Thread-0,1,2
System.out.println(Thread.currentThread().getName()+"==售出一张票,剩余的票数是"+ticketNum--);
}
}
}
public static void main(String[] args) {
PriorityTestpriorityTest=newPriorityTest();
Threadaaa=newThread(priorityTest, "aaa");
Threadbbb=newThread(priorityTest, "bbb");
aaa.setPriority(Thread.MIN_PRIORITY);
bbb.setPriority(Thread.MAX_PRIORITY);
aaa.start();
bbb.start();
}
}
2.1.4 线程插队(join)
“Java中如何让多个线程按照自己指定的顺序执行?”没错,这个问题最简单的实现就是使用Thread的join()方法来实现了。
Thread.join()的意思表示当前线程会从运行状态变为阻塞状态,需要等待插入的线程执行完终止之后,才会从thread.join的阻塞状态变为可运行状态。
/**
* 结果永远是Thread1先执行完,因为即便是Thread2抢占到进程,依旧会被Thread1插队
*/
public class JoinTest {
public static void main(String[] args) {
Threadthread1=newThread(newRunnable() {
@Override
publicvoidrun() {
for (inti=0; i<20; i++) {
System.out.println("当前的线程是:"+Thread.currentThread().getName()+",对应的值是"+i);
}
}
},"t1");
Threadthread2=newThread(newRunnable() {
@Override
publicvoidrun() {
try {
thread1.join();
System.out.println("插入队伍");
} catch (InterruptedExceptione) {
e.printStackTrace();
}
for (inti=0; i<20; i++) {
System.out.println("当前的线程是:"+Thread.currentThread().getName()+",对应的值是"+i);
}
}
},"t2");
thread1.start();
thread2.start();
}
}
2.1.5 守护线程(deamon)
守护线程又叫“服务线程”,它是后台线程,守护线程最显著的特点就是JVM中如果没有用户创建的前台线程的时候就会自动退出,所以守护线程一般都是给程序中其他对象和线程提供一些公共服务或者进行一些后台任务执行。
Java中通过setDaemon(true)来设置线程为“守护线程”。
public class DemonThread {
public static void main(String[] args) {
System.out.println("主线程开始。。。。");
Threadt=newThread(newRunnable() {
@Override
publicvoidrun() {
System.out.println(Thread.currentThread().getName()+"开始。。。。。。");
for (inti=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+"中i 的值是 = "+i);
}
System.out.println(Thread.currentThread().getName()+"结束。。。。。。");
}
},"守护线程");
t.setDaemon(true);
t.start();
//如果“守护线程”被设置为守护线程,那这里JVM会关闭
System.out.println("主线程结束。。。。");
}
/**
* “守护线程”未被设置为守护线程的运行结果
* 主线程开始。。。。
* 主线程结束。。。。
* 守护线程开始。。。。。。
* 守护线程中i 的值是 = 0
* 守护线程中i 的值是 = 1
* 守护线程中i 的值是 = 2
* 守护线程中i 的值是 = 3
* 守护线程中i 的值是 = 4
* 守护线程结束。。。。。。
*
* “守护线程”被设置为守护线程的运行结果
* 主线程开始。。。。
* 主线程结束。。。。
* 守护线程开始。。。。。。
* 守护线程中i 的值是 = 0
*/
}
结果分析:
从运行结果可以发现,“守护线程”被设置为守护线程的运行结果中没有输出守护线程结束。。。。
之所以结果是这样,是因为在启动前将名为"守护线程"的这个线程设置为了守护线程,当程序中只有守护线程存在时,JVM是可以退出的。也就是说,当JVM中只有守护线程运行时JVM会自动关闭。
因此当main方法调用结束后,main线程退出,此时守护线程没有运行结束,但是由于此时只有守护线程在运行,JVM将会关闭。因此不会输出守护线程结束。。。。
守护线程的优先级都很低,典型的守护线程就是GC(垃圾回收器),都知道GC主要负责我们JVM堆和方法区中内存的回收,如果JVM中除了GC线程外其他的都运行完没有了,那么GC也就不需要回收垃圾所以作为守护线程就自动退出了。
2.2 线程池
线程池:线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;
为什么使用线程池
根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处;
线程池的优势
1:线程和任务分离,提升线程重用性;
2:控制线程并发数量,降低服务器压力,统一管理所有线程;
3:提升系统响应速度,
假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;
2.2.1 线程池的构造参数解读
在Java中创建线程池常用的类是ThreadPoolExecutor,该类的全参构造函数如下:
public ThreadPoolExecutor(int corePoolSize, // 2 核心线程数量
int maximumPoolSize,// 8 最大线程数
long keepAliveTime, // 默认时间600 最大空闲时间
TimeUnit unit, // 最大空闲时间的单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 饱和处理机制
)
7个参数介绍:
1)corePoolSize:线程池中核心线程数的最大值
2)maximumPoolSize:线程池中能拥有最多线程数
3)workQueue:用于缓存任务的阻塞队列,对于不同的应用场景我们可能会采取不同的排队策略,这就需要不同类型的阻塞队列,在线程池中常用的阻塞队列有以下2种:
①SynchronousQueue<Runnable>:([ˈsɪŋkrənəs kjuː])此队列中不缓存任何一个任务。向线程池提交任务时,如果没有空闲线程来运行任务,则入列操作会阻塞。当有线程来获取任务时,出列操作会唤醒执行入列操作的线程。从这个特性来看,SynchronousQueue是一个无界队列,因此当使用SynchronousQueue作为线程池的阻塞队列时,参数maximumPoolSizes没有任何作用。
②LinkedBlockingQueue<Runnable>:顾名思义是用链表实现的队列,可以是有界的,也可以是无界的,但在Executors中默认使用无界的。
以上三个参数之间(参数1、2、3)的关系如下:
如果没有空闲的线程执行该任务且当前运行的线程数少于corePoolSize,则添加新的线程执行该任务。
如果没有空闲的线程执行该任务且当前的线程数等于corePoolSize同时阻塞队列未满,则将任务入队列,而不添加新的线程。
如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务。
如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于maximumPoolSize,则根据构造函数中的handler指定的策略来拒绝新的任务。
4)keepAliveTime:表示空闲线程的存活时间。
5)unit:表示keepAliveTime的单位。
6)handler:表示当workQueue已满,且池中的线程数达到maximumPoolSize时,线程池拒绝添加新任务时采取的策略。一般可以采取以下四种取值。
方法(策略) | 描述 |
ThreadPoolExecutor.AbortPolicy() | 抛出RejectedExecutionException异常 |
ThreadPoolExecutor.CallerRunsPolicy() | 由向线程池提交任务的线程来执行该任务 |
ThreadPoolExecutor.DiscardOldestPolicy() | 抛弃最旧的任务(最先提交而没有得到执行的任务) |
ThreadPoolExecutor.DiscardPolicy() | 抛弃当前的任务 |
7)threadFactory:指定创建线程的工厂
2.2.2 线程池工作原理(面试)
线程池工作原理图解:
![](https://i-blog.csdnimg.cn/blog_migrate/772b570c556b1d907579efb7fa5d99c1.png)
线程池工作原理解读:
①当线程池中线程数量小于 corePoolSize 则创建线程,并处理请求。
②当线程池中线程数量大于等于 corePoolSize 时,则把请求放入 workQueue 中,随着线程池中的核心线程们不断执行任务,只要线程池中有空闲的核心线程,线程池就从 workQueue 中取任务并处理。
③当 workQueue 已存满,放不下新任务时则新建非核心线程入池,并处理请求直到线程数目达到 maximumPoolSize(最大线程数量设置值)。
④如果线程池中线程数大于 maximumPoolSize 则使用 RejectedExecutionHandler 来进行任务拒绝处理。
2.2.3 常用的线程池
ThreadPoolExecutor构造函数的参数很多,使用起来很麻烦,为了方便的创建线程池,JavaSE中又定义了Executors类,Eexcutors类提供了四个创建线程池的方法,分别如下
①newCachedThreadPool( 可缓存线程池 )
②newFixedThreadPool( 指定工作线程数量的线程池 )
③newSingleThreadExecutor( 单线程化的Executor )
④newScheduleThreadPool( 可定时/周期的线程池 )
2.2.4 newCachedThreadPool
/** newCachedThreadPool
* 1、该方法创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
* 2、此类线程池的特点:
* 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Integer. MAX_VALUE)
* 空闲的工作线程会自动销毁,有新任务会重新创建
* 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
*/
public class TestCachedThreadPool {
public static void main(String[] args) {
ExecutorServiceexecutorService=Executors.newCachedThreadPool();
for (inti=0; i<10; i++) {
//需要将变量设置为一个局部变量,进行调用
intfinalI=i;
executorService.execute(newRunnable() {
@Override
publicvoidrun() {
System.out.println("i的值是"+finalI);
}
});
}
}
}
2.2.5 newFixedThreadPool
/** newFixedThreadPool
* 1、该方法创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
* 2、优点:具有线程池提高程序效率和节省创建线程时所耗的开销。
* 3、缺点:在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
*/
public class TestFixedThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
//需要将变量设置为一个局部变量,进行调用
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("i的值是"+ finalI);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
2.2.6 newSingleThreadExecutor
/**
* 1、该方法创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,
* 它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO,优先级)执行。
* 如果这个线程异常结束,会有另一个取代它,保证顺序执行。
* 2、单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
*/
public class TestSingleThreadPool {
public static void main(String[] args) {
ExecutorServiceexecutorService=Executors.newSingleThreadExecutor();
for (inti=0; i<10; i++) {
//需要将变量设置为一个局部变量,进行调用
intfinalI=i;
executorService.execute(newRunnable() {
@Override
public void run() {
System.out.println("i的值是"+finalI);
try {
Thread.sleep(1000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
}
});
}
}
}
2.2.7 newScheduleThreadPool
/**
* 1、该方法创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
* 2、newScheduledThreadPool中的方法
* schedule:在指定延迟时间后,创建和执行一个一次性的task
* scheduleAtFixedRate:创建一个周期性的任务,第一次执行是延迟initialDelay,后面的是initialDelay+n*period
* scheduleWithFixedDelay:创建一个周期性的任务,第一次执行是延迟initialDelay,后面的是延迟时间是前一次任务执行完的时间+delay
* 3、scheduleAtFixedRate和scheduleWithFixedDelay区别
* scheduleAtFixedRate ,中文意思为 以固定比率执行,参数有 Runnable command, long initialDelay,long period,TimeUnit unit
* 第1次执行的时间是initialDelay(unit),第2次执行的时间是initialDelay+period(unit),第3次执行的时间是initialDelay+period*2(unit),
* 依次类推。。。也就是,在任务开始后,period时间后,便会执行下一次任务。如果period时间到了,但上一次还没执行完毕,则等待,
* 直到上一次的任务执行完毕,然后马上执行本次任务。
* scheduleWithFixedDelay ,中文意思为 以固定的延迟来执行,参数有Runnable command, long initialDelay,long delay,TimeUnit unit ,
* 该方法第1次执行也是在initialDelay(unit)后,但第2次执行是在第1次执行完毕后算起的delay时间后再执行。
*
* 因此,这两个方法的不同点是,scheduleAtFixedRate 是从任务开始时算起,scheduleWithFixedDelay 是从任务结束时算起
*
*/
public class TestScheduledThreadPool {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
//需要将变量设置为一个局部变量,进行调用
int finalI = i;
//延期一秒进行执行
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println("schedule任务得到i的值是"+finalI);
}
},1, TimeUnit.SECONDS);
}
for (int i = 0; i < 5; i++) {
int finalI = i;
//创建一个周期性的任务,第一次执行是延迟initialDelay,后面的是延迟时间是前一次任务执行完的时间+delay
//在这个任务中,第一次执行是任务开始延迟3秒之后,第二次执行在第一次执行完5秒之后,后面每一次执行都是在上一次任务执行完延迟固定时间执行
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println("scheduleWithFixedDelay任务得到i的值是"+finalI);
}
},3, 5,TimeUnit.SECONDS);
}
for (int i = 0; i < 5; i++) {
int finalI = i;
//创建一个周期性的任务,第一次执行是延迟initialDelay,后面的是initialDelay+n*period
//在这个任务中,第一次执行是在任务开始延迟1秒之后,第二次执行是在1+1秒开始,第三次执行是在1+2*1秒开始
// 第一次之后在每次任务开始时间加上n*周期的时间执行;如果上次任务还没执行完,就等待完成后立即开始这次执行
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("scheduleAtFixedRate任务得到i的值是"+finalI);
}
},1, 1,TimeUnit.SECONDS);
}
}
}