多线程
- 进程
- 线程
- 概念
- 目前的程序是单线程
- 线程的组成部分
- 代码实现多线程的方式
- 第一种方式
- 第二种方式
- 第三种方式 -- 线程池
- 第四种方式:Callable
- 线程状态
- 线程同步
- 临界资源
- 原子操作
- 线程同步
- 线程同步第一种方式:同步代码块
- 线程同步第二种方式:同步方法
- 线程同步第三种方式:Lock
- 死锁
进程
- 进程的概念
操作系统(OS)中正在执行的应用程序,目前操作系统允许多个进程同时工作,被称为多进程 - 多进程并发执行原理
宏观上并行(一起执行);微观上串行(一个一个的执行),哪一个进程获取CPU时间片该进程获取执行权
线程
概念
在进程中,可以同时执行多个任务,每一个任务可以说是为一个线程,线程是进程的工作单位
线程也被称为轻量级的进程
目前的程序是单线程
以main函数的开始为开始,以main 函数的结束为结束,这个线程被称为主线程(Java中只有能代码实现多线程)
线程的组成部分
(1) cup
获取cpu时间片
(2) 数据
栈空间独立,堆空间共享每一个线程都有自己的独有栈空间;所有线程共享同一堆空间
(3) 程序代码
利用Java代码实现多线程
代码实现多线程的方式
第一种方式
- 类继承java.lang.Thread类,同时覆盖run()方法
注意:线程任务定义在run方法中 - 创建线程对象
MyThread t = new MyThread();
- 开启线程
利用 start开启线程,jvm执行线程时会自动的 调用run方法t.start();
第二种方式
- 类实现 java.lang.Runnable 接口,实现接口中 run() 方法
注意:实现run时,访问修饰符必须是 public ,任务代码定义在run方法中,当前类代表是任务类 - 创建目标对象
MyTarget tg = new MyTarget();
- 创建线程对象,同时将目标对象作为参数进行传递
Thread t = new Thread(tg);
- 开启线程:调用 start方法
t.start();
第三种方式 – 线程池
- 线程池
线程容器,将预先创建的线程对象存入到线程池中,只要将任务提交给线程池,会分配对象线程对象完成提交任务,线程池中的线程对象可以被重复使用 - 好处
避免频繁的创建线程和销毁,从而提高空间利用和执行效率 - 线程池常用的接口和类,位于 java.util.concurrent包中
3.1 Executor:线程池的顶级接口
3.2 ExecutorService:是 Executor 的子接口,线程池的核心接口
- submit(Runnable task):将线程任务提交给线程池
submit(Callable<\T> task):将线程任务提交给线程池 - shutdown(): 关闭线程池,将线程池的线程对象全部销毁
3.3 Executors:获取线程池对象的工具类,其中方法基本都为静态方法
- static ExecutorService newFixedThreadPool(int n):获取一 个固定数量线程的线程池,参数指定线程池中线程对象的数量
- static ExecutorService newCachedThreadPool() :获取动 态数量线程对象的线程池,根据提交的任务需求,不够用时, 则自动完成线程创建
第四种方式:Callable
- Callable接口
位于 java.util.concurrent 包中,类似于Runnable接口的应用,对应的对象代表线程任务
注意: Callable是泛型接口,泛型约束了接口中方法的返回值的数据类型 - 接口中的方法
V call():带有返回值的方法,同时可以抛出 异常
Future< T> f= pool.submit(c1); - Future是存储submit提交任务执行之后的结果
利用 Future中的 get方法获取执行的结果
线程状态
- 初始状态
线程已被创建但尚未执行(start() 尚未被调用) - 可执行状态
线程可以执行,虽然不一定正在执行。CPU 时间随时可能被分配给该线程,从而使得它执行 - 死亡状态
正常情况下 run() 返回使得线程死亡。调用 stop()或 destroy() 亦有同样效果,但是不被推荐,前者会产生异常,后者是强制终止,不会释放锁 - 阻塞状态
线程不会被分配 CPU 时间,无法执行
static void sleep(long ms) :如果在某一个线程中调用 Thread.sleep方法,则此线程进入有限期等待状态(计时状态/休眠,单位ms), 同时释放cpu;但是不释放该线程拥有得到锁标记
public final join():允许其他线程加入到当前线程中(调用join方法语句所在的线程),让调用join方法的线程先执行,等待其他线程任务执行结束之后,再执行自身的任务
线程同步
临界资源
被多个线程共享的同一个对象,此对象被称为临 界资源
原子操作
不可分割的多步操作,被视为一个整体,其执行顺序和步骤不能被打破
线程同步
多线程并发时,为了保证临界资源的正确性,而不能破坏程序中的原子操作(线程同步目的就是为保证原子操作的整体性,从而达到临界资源最终数据的正确性)
线程同步第一种方式:同步代码块
- 同步代码块对临界资源对象加锁
synchronized( 临界资源对象){
// 原子操作
}
- 定义位置
同步代码块定义在方法内部 - 执行的原理
线程执行过程中,遇到同步代码块,只有线 程获取临界资源对象锁标记时,才能执行同步代码块{}中的 内容,并且只有{}中内容全部执行完成,才能释放该线程拥 有的锁标记;如果临界资源对象的锁标记被其他线程占用, 该线程进入阻塞状态(Blocked状态-等着需要的锁标记释放, 去获取),直到拿到所需要的锁标记才从阻塞状态结束,同时 再获取到cpu资源,才能继续执行同步代码块{}中内容
注意:线程同步时,不同的线程必须是抢占同一个对象锁 标记,才能达到线程同步的目的(原子操作不被破坏,临界资 源的正确性)
线程同步第二种方式:同步方法
修饰符 synchronized 返回值 方法名(形参列表){
// 原子操作
}
- 被 synchronized 修饰方法被称为同步方法
0 同步方法等价于同步代码块
修饰符 返回值类型 方法名(形参列表){
synchronized(this){
// 方法中语句(包含原子操作)
}
}
线程同步第三种方式:Lock
- Lock
接口,位于 java.util.concurrent.locks 包中,代表锁 - Lock中常用方法
- void lock():获取锁,如果被占用,则需要等待
- void unlock():释放锁
- 实现类
ReentrantLock
死锁
- 死锁现象:两个线程或是多个线程相互占用对方所需要的的 资源,而都不释放,导致彼此之间相互等待对方释放资源, 从而产生无限制的等待现象。
- 结果:出现死锁后,不会抛出异常,也没有任何提示,只是所有的线程都处于阻塞状态,如果没有外力介入,程序将无 法继续
- 解决方案
- 尽可能的调整加锁的方案
- 尽量避免同步的嵌套嵌套
- 可以采用线程间的通信,等待-通知
在Object类中有 wait(等待)-notify/notifyAll(通知)方法
- wait():等待
- 让当前进入等待状态,释放cpu的同时,释放拥有的锁标记
- wait方法调用必须使用在调用它的对象所在的同步代码块中
synchronized(o){
o.wait();
}
- notify:通知
- notify():通知一个线程从等待状态结束
notifyAll():通知所有的线程从等待状态结束 - notify/notifyAll方法的调用必须在调用它的对象所在同步代码块中
synchronized(o){
o.notify();
}
- notify和notifyAll只是起到通知的作用,不会释放锁标记
注意:调用 wait方法的对象和调用notify的对象是同一对象 (临界资源)