Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

线程和进程的区别

线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存来存储本地数据。

创建线程有哪几种方式?分别有什么优缺点?

创建线程

  • 继承Thread类,重写run()方法;
  • 实现Runnable接口,实现run()方法,并将该实现类作为参数传入Thread构造器。
  • 实现Callable接口,重写call()方法,并包装成FutureTask对象,再作为参数传入Thread构造器。

继承Thread类优缺点

优点是编码简单,缺点是不能再继承其他的类,功能单一。

实现Runnable接口优缺点

优点是可以继承其他类,避免单继承的局限性;适合多个相同程序代码的线程共享一个资源,实现解耦操作,代码和线程独立。缺点是实现相对复杂。

实现Callable接口优缺点

优点是相比方式二可以获取返回值,缺点是实现复杂。

线程的状态

  • new(新建):线程刚刚被创建,但是并未启动,还没有调用start方法。

  • Runnable(可运行):线程可以在Java虚拟机中执行的状态,但是这个“执行”,不一定是真的运行,也可能是在等待CPU资源。所以在网上,有人把这个状态区分为READY和RUNNING两个,一个表示的start了,资源一到位随时可以执行,另一个表示真正的执行中。

  • Blocked(锁阻塞):当一个线程试图获取一个锁对象,而该对象被其他的线程持有,则该线程进入Blocked状态;比较经典的就是synchronized关键字,这个关键字修饰的代码块或者方法,均需要获取到对应的锁,在未获取之前,其线程的状态就一直为BLOCKED,当该线程持有锁时,该线程将变成Runnable状态。如果线程长时间处于这种状态下,我们就要当心,看看是否出现死锁的问题了。

  • Waiting(无限等待):一个线程在等待另一个线程执行动作是,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

  • Timed Waiting(计时等待):同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。如:

  • Object.wait()

  • Thread.join()

  • Thread.sleep

  • LockSupport.park()

这一状态将一直保持到超时期满或者收到唤醒通知。

  • THRMINATED(死亡状态):因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

volatile关键字

  • 保证被修饰的变量对所有线程可见,在一个线程修改一个变量的值后,新值对其他线程是可以立即获取的。
  • 禁止指令重排序,被修饰的变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile修饰的变量时总是会返回最新写入的值。
  • 不会执行加锁操作,不会导致线程阻塞,主要适用于一个变量被多个线程共享,多个线程均可对这个变量执行赋值或读取的操作,
  • volatile可以严格保证变量的单次读写操作的原子性,但不能保证像i++ 这种操作的原子性,因为i++ 在本质上是读、写两次操作。

synchronized关键字

  • 用于为Java对象、方法、代码块提供线程安全的操作,属于排它的悲观锁,也属于可重入锁。
  • synchronzied修饰的方法和代码块在同一时刻只能有一个线程访问,其他线程只能等待当前线程释放资源后才能访问。
  • Java中的每个对象都有一个monitor监视器对象,加锁就是在竞争monitor,对代码块加锁是通过在前后分别加上monitorentermonitorexit指令实现的,对方是否加锁是通过一个标记位来判断的。

线程池是什么?为什么需要线程池?

  • 在生产中为每一个任务创建一个线程存在一些缺陷,如果无限制地大量创建线程会消耗很多资源,影响系统稳定性和性能,产生内存溢出等问题。
  • 线程池是管理一组同构工作线程的资源池,线程池与工作队列密切相关,工作队列中保存了所有需要等待执行的任务。工作线程的任务很简单,从工作队列获取任务,执行任务,返回线程池并等待下一次任务。
  • 线程池通过重用现有的线程,可以在处理多个请求时分摊线程在创建和撤销过程中的开销,另一个好处是当请求到达时工作线程通常已经存在,不会出现等待线程而延迟的任务的执行,提高了响应性。通过调整线程池的大小,可以创建足够多的线程保持处理器处于忙碌状态,同时还可以防止线程过多导致内存资源耗尽。

创建线程池时,ThreadPoolExecutor构造器中都有哪些参数以及各自的含义

  • corePoolSize: 线程池核心大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。
  • maximumPoolSize: 线程池最大大小,表示可同时活动的线程数量的上限。
  • keepAliveTime:存活时间,如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过基本大小时,这个线程将被终止。
  • unit: 存活时间的单位,可选的参数为TimeUnit枚举中的几个静态变量: NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。
  • workQueue: 线程池所使用的阻塞队列。
  • thread factory:线程池使用的创建线程工厂方法,可省略,将使用默认工厂。
  • handler:所用的拒绝执行处理策略,可省略,将使用默认拒绝执行策略。

创建线程池的方法有哪些?

可以通过Executors的静态工厂方法创建线程池,内部通过重载ThreadExecutorPool不同的构造器创建线程池。

  • newFixedThreadPool,创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的异常而结束,那么线程池会补充一个新的线程)。将线程池的核心大小和最大大小都设置为参数中指定的值,创建的线程不会超时,使用LinkedBlockingQueue。
  • newCachedThreadPool,创建一个可缓存的线程池,如果线程池的当前规模超过了处理器需求,那么将回收空闲的线程,而当需求增加时,可以添加新的线程,线程池的规模不存在任何限制。将线程池的最大大小设置为Integer.MAX_VALUE,而将核心大小设置为0,并将超时设为1分钟,使用SynchronousQueue,这种方法创建出的线程池可被无限扩展,并当需求降低时自动收缩。
  • newSingleThreadExecutor,一个单线程的Executor,创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来代替。确保依照任务在队列中的顺序来串行执行。将核心线程和最大线程数都设置为1,使用LinkedBlockingQueue。
  • newScheduledThreadPool,创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer,使用DelayedWorkQueue。

线程池的工作原理

  • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。即使队列里面有任务,线程池也不会马上执行它们。
  • 通过 execute(Runnable command)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是Runnable类型对象的run()方法。
  • 如果workerCount 小于corePoolSize,那么创建并启动一个线程执行新提交的任务。如果workerCount大于等于corePoolSize,且线程池内的阻塞队列未满,那么将这个任务放入队列。如果workerCount大于等于corePoolSize,且阻塞队列已满,若满足workerCount小于maximumPoolSize,那么还是要创建并启动一个线程执行新提交的任务。若阻塞队列已满,并且workerCount大于等于maximumPoolSize,则根据 handler所指定的策略来处理此任务,默认的处理方式直接抛出异常。也就是处理任务的优先级为: 核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  • 当一个线程没有任务可执行,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize时,那么这个线程会被停用掉,所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

start和run方法的区别

  • start方法用于启动线程,真正实现了多线程,调用了start方法后,会在后台创建一个新的线程来执行,不需要等待run方法执行完毕就可以继续执行其他代码。调用start方法时,该线程处于就绪状态,并没有开始运行。
  • run方法也叫做线程体,包含了要执行的线程的逻辑代码,在调用run方法并没有创建新的线程,而是直接运行run方法中的代码。

References

https://blog.csdn.net/qq_41112238/article/details/105074636

https://baijiahao.baidu.com/s?id=1655501582187466001&wfr=spider&for=pc

https://www.jianshu.com/p/ec94ed32895f

⬆︎TOP