初识线程

Posted by t298 on June 24, 2021

基本概念

进程:进程是一个程序在其自身的地址空间中的一次执行活动,它是系统运行程序的基本单位。进程是资源申请,调度和独立运行的单位。

线程:线程是进程中一个单一的连续控制流程。一个进程可以有多个线程,但是线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间。

线程优先级

单核计算机只有一个CPU,各个线程轮流获得CPU的使用权,才能执行任务:

  • 优先级较高的线程有更多获得CPU的机会,反之亦然。
  • 优先级用整数表示,取值范围是1~10,一般情况下,线程的默认都是5.

生命周期

创建状态

1)是指使用new实例化一个线程对象,但该对象还未使用start()方法启动线程这个阶段,该阶段只在内存的堆中为该对象的实例分配了内存空间,但是线程还不能参与抢夺CPU的使用权。

2)创建线程对象完毕后,启用该线程对象的是start()方法,而不是run()方法。

就绪状态

1)是指线程对象使用run()方法后运行完run()方法的阶段,线程一但进入就绪阶段,jvm就会为该线程创建方法的调用栈和计数器等。

2)在某一个单位时间内(时间片内),CPU只能执行一个线程。CPU正在执行的这个线程,状态可以称为正在运行状态。

3)凡是处于就绪状态的线程都被视为活动的,可以使用isAlive()方法测试线程是否处于就绪状态,可以使用activeCount()来查看当前线程所在线程池的活动线程数。

阻塞状态

1)阻塞状态其实有4种(睡眠状态,阻塞状态,挂起状态,等待状态),一般来说,阻塞状态和就绪状态是可以相互切换的。

2)使用sleep()方法可以线程进入睡眠状态,让其他进程得到运行机会,但是用sleep方法必须捕获InterruptedExecption异常。

3)使用wait方法使线程进入等待状态,使用I/O中断让线程进入阻塞状态。

死亡状态

1)一旦线程运行完run方法,线程即进入死亡状态,Java虚拟机会销毁处于死亡状态的线程对象所占用的系统资源。

2)线程执行时遇到一个未捕获的异常,线程会被终止并进入死亡状态;调用stop方法也可以让线程进入死亡状态,但是容易造成死锁。

线程池

Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

普通线程池

  • newFixedThreadPool(int nThreads) 方法,创建一个固定长度的线程池。
    • 每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化。
    • 当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
  • newCachedThreadPool() 方法,创建一个可缓存的线程池。
    • 如果线程池的规模超过了处理需求,将自动回收空闲线程。
    • 当需求增加时,则可以自动添加新线程。线程池的规模不存在任何限制。
  • newSingleThreadExecutor() 方法,创建一个单线程的线程池。
    • 它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它。
    • 它的特点是,能确保依照任务在队列中的顺序来串行执行。

定时任务线程池

  • newScheduledThreadPool(int corePoolSize) 方法,创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。
  • newSingleThreadExecutor() 方法,创建了一个固定长度为 1 的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。

线程池的关闭方式

ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是:

  • shutdown() 方法,不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
  • shutdownNow() 方法,立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

线程安全

线程安全定义

当多个线程访问某个一类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的(即在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成)。

饿汉式单例就是线程安全的。

如何解决线程安全问题

可以通过加锁的方式:

  • 同步(synchronized)代码块:只需要将操作共享数据的代码放在synchronized
  • 同步(synchronized)方法:将操作共享数据的代码抽取出来放到一个synchronized方法里面就可以了
  • 加同步锁 lock() 以及释放同步锁unlock()

什么是死锁、活锁

死锁,是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

活锁,任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

产生死锁的必要条件

  • 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁的解决方法

  • 撤消陷于死锁的全部进程。
  • 逐个撤消陷于死锁的进程,直到死锁不存在。
  • 从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失。 从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。

什么是悲观锁、乐观锁

1)悲观锁

悲观锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

  • 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
  • Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

2)乐观锁

乐观锁,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

线程间通信

  • 共享内存

    在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。

  • 消息传递

    在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 wait() 和 notify() ,或者 BlockingQueue 。