3.9 Java之多线程
程序,进程与线程
- 程序运行到内存中(去加载),程序转化为进程
- 静态的为程序,动态为进程
- 进程细分,不同执行路径为线程
- OS多进程
- 垃圾回收为守护线程(不在程序中显示显现,后台运行,一直存在)
创建线程
法一
- JVM允许应用程序在执行过程中并发地创建多个线程
- 以往程序只有主线程(主方法里执行的线程,仅看前台,不考虑守护线程)
- 单核CPU同一时间只能执行一个命令(执行一个线程),高主频切换任务,似并行执行
- run方法与start方法不等效,run方法还是主线程中的对象调方法,没有启动线程
而start方法既启动线程,又调用run方法 - 线程start后不能再start,原因见下图
- 线程的状态(不为0,表示被启动)
法二
实现接口,重写run方法,有必要的加上类属性
- 传入实现接口的类,多态的应用
- 创建一个实现接口类对象,创建多个Thread类对象(共享一个实现了接口的类对象)
- 启动线程的说明:
- target是Thread类的属性,Thread类的run方法实质执行target的run方法
- target在构造器中被实例化,即执行传入形参的run方法
两种方法比较
- 实现更好,避免了只能单继承,因为接口可多实现
- 此外,对于操作同一份资源,实现方式更好
- 两者联系:runnable接口是和线程相关的接口,实现方式直面接口,而继承方式间接面对接口(通过Thread类)
线程常用方法
- target是个runnable(接口),Thread的run方法无实质执行内容
- yield方法显式地强制释放当前CPU的执行权,重新争抢,有可能继续原本线程或其他线程抢到CPU
- join方法使得只有子线程执行完,主线程才接着执行,要异常处理
- alive判断线程是否存活
- 处理异常只能用try/catch方式,不能用throws,因为Thread声明时没抛出异常,作为其子类,根据重写规则,也不能throws
线程的调度
- 分为时间片(先来后到),抢占式(优先级)两种
- 优先级高只能确保抢到CPU概率大,不等于先后
- 抢占式设置优先级,优先级从1到10,默认为5
多线程练习
- 不同任务即对应不同线程类子类的run方法
- 关注匿名类的简写
匿名类对象(继承Thread类)
- 创建继承于Thread类的匿名类对象,括号内重写run方法
窗口售票问题
继承方式
多窗口–》多线程–》多对象
- 涉及到对象的成员变量(属性)
- static声明变量,称为类变量或类属性,使得所有对象共用该属性
- 类变量缺点在于生命周期长,回收类时才会从内存中回收类变量
- 通过实现方式创建线程,不需要声明static变量
- CPU分配资源,执行run方法
- 顺序非依次输出的解释:先抢到值,后输出
实现接口方式
- 创建一个实现接口类的对象(w),创建多个Thread类对象(共享一个实现了接口的类对象w)
- 只需创建一个票数对应的对象(w),多个线程共享票数
- 实现接口的方式适合存在共享数据的问题
多线程优点
- 省去切换时间使得单线程效率更高
- 多线程开启,CPU切换运行
- JVM的后台垃圾回收线程为守护线程
- 当不存在前台用户线程,守护线程也退出,不执行相应任务,则JVM退出
多线程的生命周期
- start表明可以执行,等待分配到资源后,才会真正执行
- 阻塞完毕后,先到达就绪状态,去争抢CPU资源,抢占后才能执行
- suspend挂起当前线程
线程的同步机制(安全机制)
线程安全问题实例
- 出现-1票,因为ticket为1,大于0,进入了if语句,然后sleep方法使得阻塞,没有立刻打印,其他线程趁机执行,也同上过程,最后打印时出问题了(大家都通过了if语句,但ticket不够减了)
- 总之,未连贯完成对共享数据操作的锅
同步代码块
实现接口方式改写
- 抽象的方法没有抛出异常,所以重写的方法也不能抛出异常,所以用try/catch处理
- 同步方法有同步监视器,理解为两种状态(上锁,不上锁)
- 同步监视器选取的对象可任意(但要求不同线程对应同一对象)
- 同步方法把操作共享数据的方法封装,多个线程要想执行该代码块内容,需要取得同步监视器(锁),线程进行争抢锁,一线程抢到后,待到封装的语句块全部执行完毕,锁恢复争抢状态
- 三个线程共用一个锁
- 同步代码块的内容不要多,也不要少,比如不能把while循环包括在内
- 使用this:当前对象,即W(启动线程,执行target的run方法,而target为实现runnable的类对象),W只有一个,所以可以使用
继承方式改写
- 不能用this,因为不代表同一对象,同理Object对象也要static
子线程执行run方法,功能封装到run方法中,子线程调用对应run方法执行
同步方法
- 同步方法同一时间只能有一个线程执行
- 同步方法是当前对象调用,所以其锁为当前对象,不同于同步代码块,同步方法未显式地指明锁
实现接口方式改写
- 将操作共享数据的代码封装为show方法,修饰为同步方法,从而保证线程安全
继承方式改写(同步方法不可用)
- 使用同步方法,不能保证线程安全,由于同步方法的锁this指当前对象,而当前对象具有多个,没有共用同一把锁
- 只能用同步代码块方式
互斥锁
懒汉式线程安全问题
- 提供公用静态方法访问,返回实例
- 使用==判断是否为同一对象
- 若不加入线程安全处理,则线程A创建对象,sleep,最后返回首地址,a线程B趁着sleep时,再次创建对象,之前的对象不存在了,因为instance重新赋值,创建了新对象了,应该只创建一次,同样返回首地址值,比较,此时不相等
- instance作为共享数据
- 静态方法里无this,改用XXX.class,返回Class类的对象(反射机制)
- 因为同步代码块内相当于执行单线程,降低效率
- 改进方法:添加if语句,当有人进门了,直接宣布有人,不用等都处理完了,再说明有人
锁的机制
释放锁操作
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用suspend()和resume()来控制线程
- 重点:wait方法会释放锁,sleep方法不会释放锁
线程同步练习
实现接口方式
继承方式
- 多用户:多线程,而用户具有account属性,多个用户共用一个账户,进一步地指账户的余额(balance)
- sleep是为了让线程安全问题更明显,可省略
- 共享数据为account,进一步地为balance(抓住本质,操作共享数据的代码块为deposit方法)
- 保证账户公用,指向一个堆空间实体
- this在该deposit方法中是account对象,而account公用,所以可以用同步方法(默认this),可知继承方式也可考虑用同步方法(this使用得当)