多线程
多线程
Rrocess 和Thread
- 程序:是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
- 而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位
- 通常一个进程中有多个线程,当然一个进程最少有一个线程,不然没意义。线程是CPU调度和执行的单位
注意:很多线程是模拟出来的,真正的多线程是指多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉
知识总结:
- 线程就是独立的执行路径
- 在程序运行是,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预
- 对同一份资源操作时,如cpu调度时间,,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致想
Thread
- 自定义线程类继承Thread类
- 重写run方法,编写线程执行体
- 创建线程对象,调用start方法启动线程
主线程和其他线程是并行执行
线程开启不一定立即执行,由CPU调度执行
小结
-
继承Thread类
子类继承Thread类具备多线程能力 启动线程:子类对象.start() 不建议使用:避免OOp单继承局限性
-
实现Runnable接口
实现接口Runnable具有多线程能力 启动线程:传入目标对象 + Thread对象.start() 推荐使用:避免OOP继承局限性,灵活方便,方便同一个对象被多个线程使用
Lamda表达式
函数式接口的定义:
- 任何借口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口
- 对于函数式接口,我们可以通过lamda表达式来创建该接口的对象
总结:
lambda不大表达式只有一行代码情况下才能简化为一行,如果是多行,那么就用代码块来包装
必须是函数式接口
多个参数也可以,但是参数之间必须要加括号
同步方法
由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块
同步方法: public synchronized void method(int args){}
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获取调用该方法的对象的锁,否则线程会阻塞,一旦方法执行,就独占该锁。直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized将会影响效率
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有”两个以上对象的锁“。就可能会发生“死锁”问题
产生死锁的四个条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用之前,不能进行强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大的线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API:
ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
<T>Future<T>submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池