基本概念
进程:进程是一个程序在其自身的地址空间中的一次执行活动,它是系统运行程序的基本单位。进程是资源申请,调度和独立运行的单位。
线程:线程是进程中一个单一的连续控制流程。一个进程可以有多个线程,但是线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间。
线程优先级
单核计算机只有一个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 。