线程与进程
-
进程
一个进程就是CPU执行的单个任务的过程,是程序在执行过程当中CPU资源分配的最小单位,并且进程都有自己的地址空间,包含了运行态、就绪态、阻塞态、创建态、终止态五个状态。 -
线程
线程是CPU调度的最小单位,它可以和属于同一个进程的其他线程共享这个进程的全部资源 -
两者之间的联系
一个进程包含多个线程,一个线程只能在一个进程之中。每一个进程最少包含一个线程。 -
两者之间的区别
进程是CPU资源分配的最小单位,线程是CPU调度的最小单位
进程之间的切换开销比较大,但是线程之间的切换开销比较小。
CPU会把资源分配给进程,但是线程几乎不拥有任何的系统资源。因为线程之间是共享同一个进程的,所以线程之间的通信几乎不需要系统的干扰。
核心概念:
- 线程是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程、gc线程;
- main()是主线程,系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关,先后顺序不能人为干预;
- 对同一份资源操作时会存在资源抢夺问题,需要加入并发控制;
线程会带来额外开销,如CPU调度时间,并发控制开销; - 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
------------------------------------------------我是分割线,今天是阴天-------------------------------------------------------
线程的创建
- 继承Thread类(重点),重写run()方法,调用start()方法启动
- 实现Runnable接口(常用),与方法一的区别是不用继承Thread类,而是实现Runnable。启动方式需要将实现Runnable接口的实例化后放入Thread类中。
- 实现Callable接口(了解),好处是可以定义返回值和抛出异常
线程池
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。
线程池的好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不用每次创建)
- 便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后终止
实现方法:通过接口 ExecutorService 和 工具类 Executors 的 newFixedThreadPool()方法。
方法 | 说明 |
---|---|
newFixedThreadPool(int nThreads) | 指定工作线程数量的线程池 |
newCachedThreadPool() | 处理大量短时间工作任务的线程池 1、试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程; 2、如果线程闲置的时机超过阈值,则会被终止并移除缓存; 3、系统长时间闲置的时候,不会消耗什么资源 |
newSingleThreadExecutor() | 创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它。 |
newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize) | 定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程。 |
newWorkStealingPool() | 内部会构建 ForkJoinPool,利用 working-stealing 算法,并行地处理任务,不保证处理顺序。JDK1.7 及以上版本 Java 提供了 Fork/Join 框架,Fork/Join 是把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架 。 |
------------------------------------------------我真的是分割线-------------------------------------------------------
线程状态控制、优先级、守护线程
线程状态
创建状态、就绪状态、阻塞状态、运行状态、死亡状态
方法 | 说明 |
---|---|
void setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程,并执行其他线程 |
void interrupt() | 中断线程(不推荐用这个方式中断) |
boolean isAlive() | 测试线程是否处于活动状态 |
线程休眠sleep()
- sleep( 时间 ) 指定当前线程阻塞的毫秒数
- sleep存在异常 InterruptedException
- sleep时间达到后线程进入就绪状态
- sleep可以模拟网络延时,倒计时等
- 每一个对象都有一个锁,sleep不会释放锁
线程礼让 yield()
- 礼让线程,即让当前运行状态的线程转为就绪状态
- 让CPU重新调度,但不一定成功。
线程强制执行 join()
通俗点就是让某个线程插队,直到该线程执行完,其它线程才能执行。
线程状态观察
线程的状态包括:
- NEW :尚未启动的线程
- RUNNABLE:在Java虚拟机执行的线程
- BLOCKED:被阻塞的等待监视器锁定的线程
- WAITING:正在等待另一个线程执行的线程
- TIMED_WAITING:等待另一给被指定执行等待时间的线程
- TERMINATED:已终止的线程
可以通过Thread的getState()方法获取到一个Thread.State类型。
线程优先级
Java线程的优先级用数字表示,从1~10,这三个是默认的:
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 1;
通过 getPriority() 、setPriority(int)来获取和设置优先级。
注意:优先级低只是意味着获得调度的概率低,可能CPU也会先调用优先级低的,这就是性能倒置。但大多数情况还是先调用优先级高的。同时,设置优先级要在 start() 前面
守护线程 daemon
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机无需确保守护线程执行完毕
- 守护线程例如:后台记录操作日志、监控内存、垃圾回收等
通过 setDaemon( boolean ) 来给一个线程设置为守护线程。
------------------------------------------------我是分割线啊-------------------------------------------------------
线程安全
同步方法和同步代码块(synchronized)
synchronized 方法控制对"对象"的访问,每个线程中执行到该方法时,会对拥有该方法的对象上锁直到该方法结束,而其他线程必须等待锁解除才能执行该方法。
Lock锁
前面使用 synchronized 的同步方法和同步代码块都是隐式上锁和解锁,JDK 5.0开始,Java 就提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。
ReentrantLock lock = new ReentrantLock();
lock.lock(); //上锁
…要上锁的代码…
lock.unlock(); //解锁
synchronized与Lock对比:
- Lock是显式锁(手动打开、关闭),synchronized是隐式锁,出了作用域自动关闭。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,且有更好的扩展性。
- 优先使用:Lock > 同步代码块 > 同步方法
死锁
即多个线程相互持有对方需要的资源,然后形成僵持。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而被阻塞,对已获得的资源保持不放
- 不可剥夺条件:进程已经获得的资源,在未使用完前不能被强行剥夺
- 循环等待条件:若干进程之间形成一种首尾相连的循环等待资源关系
解决方法:多数情况下,消除第四个条件成立
------------------------------------------------我是分割线。-------------------------------------------------------
线程之间的通信
Java 提供了几个方法来解决线程之间的通信问题:
方法 | 作用 |
---|---|
wait() | 表示线程一直等待,直到有其他线程通知(与sleep()不同,可以释放锁) |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级高的优先调度 |
注意:这些都是Object类的方法,只能在同步方法或同步代码块中使用,否则会抛出异常。
本文借鉴土豆的热爱