Java多线程--线程与线程池

操作系统中线程的实现在这里插入图片描述现代操作系统的线程主要有三种实现:内核线程实现,用户线程实现,混合实现

内核线程(KLT):线程表由内核维护,由内核完成线程的切换,内核通过调度器对线程进行调度,并将线程的任务映射到处理器上,每个内核线程可以视为内核的一个分身。程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(LWP)(广义上来说,轻量级进程也是在用户空间的进程中的,所以也是一种用户线程)。LWP和KLT是一一对应的,是1:1的关系,因此也叫作一对一线程模型(1:1)。内核线程最大的特点就是,如果有轻量级进程发生了阻塞,不会影响整个进程的工作,内核会运行其他可运行的线程。缺点也是明显的:各种线程操作都需要系统调用,需要在用户态和内核态进行来回切换,代价高昂,而且因为占用内核空间,所以内核能支持的数量是有限的。

用户级线程(UT):狭义上的用户线程是指,完全建立在用户空间的线程库上,由所在进程实现管理的线程。最大的亮点在于可以在不支持多线程的操作系统之上实现多线程,如DOS,同时因为不需要切换到内核态,所以快速且低消耗,也能支持更大规模的线程数量。这种模型中,一个轻量级进程对应多个线程,因此叫做一对多线程模型(1:N),用户进程的优势在于不需要内核的支援,而因为没有内核的支援,所有的线程操作都需要用户自己处理,导致复杂性是其劣势。线程的创建,切换,调度都是需要考虑的问题,现在使用用户线程的程序已经越来越少了。java线程在JDK1.2之前,使用用户线程。

用户线程+轻量级进程的混合实现:使用轻量级进程作为用户线程和内核线程的桥梁的一种实现,用户线程和轻量级进程的比例不定,因此也叫多对多线程模型(N:M),UNIX家族中的Solaris提供了N:M的线程模型实现。

2.java虚拟机中线程的实现

jdk1.2 之前,java使用的是称为“绿色线程”的用户线程,而在1.2中,线程模型替换为基于操作系统原生线程模型来实现

操作系统支持什么样的线程模型,很大程度上影响java的线程模型,windows和linux系统提供的线程模型是1:1的,所有这两个平台上的JDK使用的是1:1的线程模型,一个java线程映射到一个轻量级进程中,Solaris系统同时支持1:1和N:M,该平台中的JDK可以指定参数选择线程模型。

操作系统的线程特性会对线程的并发规模和操作成本产生影响,但是对java程序的编写和运行来说是透明的。
在这里插入图片描述
从JMM层面 ,JVM线程其实是使用了内核线程的一个高级接口即所谓轻量级进程【是有内核实现的】的概念与系统内核线程(每个内核线程视为内核的一个分身)一比一的关系来执行任务逻辑,从用户态到内核态的过程后, 内核通过操纵调度器对线程进行分配资源,负责将任务给各个处理器上处理执行;因此创建线程的过程会消耗一定的系统资源,因此一个系统支持的轻量级进程数量是有限的。

线程的生命周期
在这里插入图片描述

  • 新建状态:
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  • 就绪状态:
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态:
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态:
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    ✓等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    ✓同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    ✓其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:
    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

Java中实现多线程有几种方法
继承Thread类;
实现Runnable接口;
实现Callable接口通过FutureTask包装器来创建Thread线程;
使用ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了ExecutorService来管理前面的三种方式)

如何停止一个正在运行的线程
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的
方法。
3、使用interrupt方法中断线程。

线程池(三/四大方法、7大参数、4种拒绝策略)

线程是稀缺资源,它的创建与销毁是一个相对偏重且耗资源的操作,而Java线程依赖与内核线程,创建线程需要进行操作系统状态切换(用户态-内核态),为避免资源过度消耗应重用线程执行多个任务。
线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控。

池化技术
程序的运行,本质:占用系统的资源! 优化资源的使用!=>池化技术
线程池、连接池、内存池、对象池///… 创建、销毁。十分浪费资源
池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。

线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理。
线程复用、可以控制最大并发数、管理线程

什么时候使用线程池?

  • 单个任务处理时间比较短
  • 需要处理任务数量很大

线程池的五种状态
在这里插入图片描述

  • Running :能接受新任务以及处理已添加的任务
  • Shutdown:不接受新任务,可以处理已经添加的任务
  • Stop:不接受新任务,不处理已添加的任务,并且中断正处理的任务
  • Tidying:所有任务已终止,ctl记录的“任务数量”为0,ctl负责记录线程池的运行状态与活动线程数量
  • Terminated:线程池彻底终止,则线程池转变为Terminated状态
线程池:三/四大方法

newSingleThreadExecutor();
单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务

newFixedThreadPool(5);
固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面迚入等待队列直到前面的任务完成才继续执行

newCachedThreadPool();
可伸缩的,遇强则强,遇弱则弱
可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是 60 秒无执行)的线程,当有任务来时,又智能的添加新线程来执行

但还有一种创建线程池的方法

newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程

线程池:7大参数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

参数介绍:

  • corePoolSize:线程池中核心线程数的最大值
  • maximumPoolSize:线程池中能拥有最多线程数
  • keepAliveTime:表示空闲线程的存活时间。
  • unit:表示keepAliveTime的单位。
  • workQueue:用于缓存任务的阻塞队列,对于不同的应用场景我们可能会采取不同的排队策略,这就需要不同类型的阻塞队列,在线程池中常用的阻塞队列有以下2种:

✓SynchronousQueue:此队列中不缓存任何一个任务。向线程池提交任务时,如果没有空闲线程来运行任务,则入列操作会阻塞。当有线程来获取任务时,出列操作会唤醒执行入列操作的线程。从这个特性来看,SynchronousQueue是一个无界队列,因此当使用SynchronousQueue作为线程池的阻塞队列时,参数maximumPoolSizes没有任何作用。

✓LinkedBlockingQueue:顾名思义是用链表实现的队列,可以是有界的,也可以是无界的,但在Executors中默认使用无界的。

  • threadFactory:指定创建线程的工厂
  • handler:表示当workQueue已满,且池中的线程数达到maximumPoolSize时,线程池拒绝添加新任务时采取的策略。一般可以采取以下四种取值。
线程池:4种拒绝策略

✓ThreadPoolExecutor.AbortPolicy() 抛出RejectedExecutionException异常 ->不处理,抛异常
✓ThreadPoolExecutor.CallerRunsPolicy() 由向线程池提交任务的线程来执行该任务 ->哪来的去哪里
✓ThreadPoolExecutor.DiscardOldestPolicy() 抛弃最旧的任务(最先提交而没有得到执行的任务) ->尝试去和最早的竞争,不会抛出异常!
✓ThreadPoolExecutor.DiscardPolicy() 抛弃当前的任务 ->丢掉任务,不会抛出异常!

池的最大的大小如何去设置!
了解:IO密集型,CPU密集型:(调优)
最大线程到底该如何定义
1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
2、IO 密集型 > 判断你程序中十分耗IO的线程, 程序 15个大型任务 io十分占用资源!
阿里巴巴开发手册:
4. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

2【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread() {
super.setName(“TimerTaskThread”);

}

3【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资 源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者 “过度切换”的问题。

线程相关问题
实现Runnable接口和Callable接口的区别
如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于Runnable 接口不会返回结果但是 Callable 接口可以返回结果。
什么是线程安全?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运
行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安全的
Thread类中的yield方法有什么作用?
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
Java线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方
法。
有三个线程T1,T2,T3,如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成
sleep()和wait() 有什么区别?
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

为什么wait, notify 和 notifyAll这些方法不在thread类里面?
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
为什么wait和notify方法要在同步块中调用?

  1. 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。
  2. 如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
  3. 还有一个原因是为了避免wait和notify之间产生竞态条件。

wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。
在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用notify()或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:“特殊状态已经被设置”。这个状态作为线程间通信的通道,它必须是一个可变的共享状态或变量
Java中interrupted 和 isInterruptedd方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线
程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值