多线程
线程
- 进程有多个子任务,每一个子任务就是一个线程
进程
- 简单理解,正在运行的程序或软件
线程与进程之间的关系
- 线程依赖于进程而存在
- 一个进程里面至少有1个线程
- 用迅雷下载多个电影的例子
- 线程共享进程资源
串行
- 一个任务接一个任务按顺序执行
并行
- 多个任务,在同一时刻(时间点)同时执行
并发
- 多个任务,在同一时间段内同时执行
同步与异步
指的是被调用者
A调用B
同步: A的本次调用可以得到结果 (你走我不走)
异步: A的本次调用不会得到结果,等有了结果之后再通知A (你走你的我走我的)
Jvm是多线程的,至少有两个线程,main方法和GC回收垃圾
java采用的是抢占式的线程调度方式,优先级是1-10
多线程的实现方式一:继承Thread类
步骤
- 定义一个类继承Thread类
- 重写run方法
- 创建子类对象
- 通过start方法启动
注意事项
对象才代表一个线程,和类无关
一个线程不能多次启动
如果调用run方法就相当于普通的方法调用,并不是真正的启动线程
获取和设置线程名称
获取线程名称
String | getName() 返回该线程的名称 |
获取主线程名称
static Thread | currentThread() 返回对当前正在执行的线程对象的引用 |
设置线程名称
void | setName(String name) 改变线程名称,使之与参数 name 相同。 |
线程控制API
线程休眠sleep
static void | sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
线程加入(合并) join
void | join() 等待该线程终止。 |
哪个线程调用join,其他线程就必须等待该线程执行完才能执行
守护线程daemon
线程分类
- 用户线程
- 守护线程(为用户线程服务的) GC垃圾回收线程就是一个守护线程
void | setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。 |
当所有的线程中只剩下守护线程的时候,程序终止
多线程的实现方式二:实现Runnable接口
步骤
- 实现Runnable接口
- 重写run方法
- 创建子类对象
- 创建Thread对象,并且把实现了Runnable接口的子类对象作为参数传递
- start方法启动
注意
为什么Runnable中的run方法会运行在子线程当中
因为在Thread类中有一个target来判断我们传过来的Runnable接口的子类有没有重写run方法,如果有的话就执行
建议使用Runnable来实现多线程
解决多线程数据安全问题
产生的原因
- 多线程的运行环境(需求不能改)
- 多线程共享数据(需求不能改)
- 存在非原子操作
- 什么是原子操作? 一个操作要么1下完成,要么不完成
只能通过原子操作来解决多线程的安全问题
synchronized
同步代码块
锁对象 可以是任意的java对象(但是一定要保证是同一个)
同步方法
锁对象是this
静态方法
锁对象是类的字节码文件对象
细节
- 同步代码块中的锁对象可以是任意java对象,任意java对象都可以充当锁对象的这个角色,仅限于同步代码块当中
- 任意java对象内部,都存在这一个标志位,标志位用来表示加锁和释放锁
我们的代码是运行在某条执行路径下(某个线程),当某个线程要执行同步代码块
- 访问之前会尝试对锁对象加锁,如果没有加锁,可以访问执行
- 如果别的线程想要访问这个代码块,不能执行, 会处于阻塞状态
- 当这个线程访问完了同步代码块,退出之前,会释放锁,修改标志位
lock
不建议使用
使用方法:
在要上锁的代码前使用:lock.lock()
执行完毕后使用解锁:lock.unlock()
但一般要配合finally使用,因为不能自动关闭,所以通常是使用synchronized来实现同步
死锁
2个或以上线程争抢资源而造成的互相等待的现象就被称之为死锁
死锁产生的场景
一般出现在嵌套同步代码块中,嵌套的顺序不一致就会存在死锁
// 同步代码块嵌套 synchronized(objA){ synchronized(objB){ // 代码 } }
死锁的解决方法
1.更改加锁顺序
2.在外面加一把大锁,变成原子操作
线程间通信
wait
- 阻塞功能:
当在某线程中,对象上.wait(), 在哪个线程中调用wait(), 导致哪个线程处于阻塞状态 - 唤醒条件
在其他线程中,在同一个对象(即对象A)上调用其notify()或notifyAll() - 运行条件
当前线程必须拥有此对象监视器。
监视器:指synchronized代码块中的锁对象
即我们只能在当前线程所持有的synchronized代码块中的锁对象上调用wait方法,才能正常执行
如果我不在同步代码块中调用就会有这样一个异常
IllegalMonitorStateException - 执行特征
执行wait的时候会释放监视器,即释放锁
注意:Thread的sleep方法,执行的时候:
该线程不丢失任何监视器的所属权
notify
- 唤醒在此对象监视器上等待的单个线程。
- 如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。
- 选择是任意性的
notifyAll
唤醒所有等待的线程
完整的线程的状态转换
多线程工具
线程池
使用线程池的原因:run方法执行完了之后线程就死了,需要重新创建,因此需要引入线程池
//JDK5提供了一Executors来产生线程池,有如下方法: ExecutorService newCachedThreadPool() // 特点: // 1.会根据需要创建新线程,也可以自动删除,60s处于空闲状态的线程 // 2.线程数量可变,立马执行提交的异步任务(异步任务:在子线程中执行的任务) // 适用场景: 执行很多短期异步的小程序或负载较轻的服务器 ExecutorService newFixedThreadPool(int nThreads) // 特点: // 1.线程数量固定 // 2.维护一个无界队列(暂存已提交的来不及执行的任务) // 3.按照任务的提交顺序,将任务执行完毕 // 适用场景: 执行长期的任务 ExecutorService newSingleThreadExecutor() // 特点: // 1.单个线程 // 2.维护了一个无界队列(暂存已提交的来不及执行的任务) // 3.按照任务的提交顺序,将任务执行完毕 // 适用场景: 一个任务接一个任务执行
线程池的使用:ExecutorService(接口)
Future submit(Callable task)
Future<?> submit(Runnable task)
停止线程池:
shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
shutdownNow() 停止目前活动的所有任务
线程池的使用步骤
1.创建线程池
2.submit任务
多线程的实现方式三:实现Callable接口
会得到一个返回值,基本上用于在需要返回值的场景
Future
表示异步计算的结果
不使用线程池,可以用Future来执行Callable
使用future.get()可以获得异步计算的结果
Timer
schedule(TimerTask task, Date time) schedule(TimerTask task, long delay, long period)//常用 schedule(TimerTask task, Date firstTime, long period) scheduleAtFixedRate(TimerTask task, long delay, long period) 2跟4的区别 : 追赶特性
TimerTask是一个抽象类,需要子类继承并重写方法才能使用
使用future.get()可以获得异步计算的结果
Timer
schedule(TimerTask task, Date time) schedule(TimerTask task, long delay, long period)//常用 schedule(TimerTask task, Date firstTime, long period) scheduleAtFixedRate(TimerTask task, long delay, long period) 2跟4的区别 : 追赶特性
TimerTask是一个抽象类,需要子类继承并重写方法才能使用