java多线程
多线程作用
提高程序效率,cpu在多个线程间切换
并发与并行
并发:同一时刻,多个指令在单个cpu交替执行
并行:同一时刻,多个指令在多个cpu同时执行
实现方式
方式一:继承Thread类方式
-
自定义类声明为Thread子类
-
重写run方法
-
书写线程执行的代码
-
测试类中创建子类对象t1,并且启动线程
t1.start();
//setName方法可以设置线程名字
t1.setName("xc");
//在线程内部可以通过getName()方法获取
方式二:实现Runable类,重写run方法
-
自定义类,实现Runable接口
-
重写run方法
-
测试类中创建自定义类对象rn
-
测试类中新建一个Thread对象t1,构造函数传入自定义对象rn
-
测试类中启动t1
t1.start();
问题:Runable接口中的run方法,无法调用Thread类的方法,getName之类的方法无法调用?
//获取到当前线程的对象
Thread t = Thread.currentThread();
方式三:利用Callable接口和Future接口方式实现
目的:方式一二无返回值,无法获取多线程执行结果
特点:可以获取多线程执行结果
(记忆法:自己run,没人管,自己call,有回应)
-
自定义类实现Callable接口,泛型即为返回结果的类型
-
重写call方法
-
测试类中创建Callable对象mc(表示多线程要执行的任务)
-
测试类中创建(Future接口实现类)FutureTask的对象ft,构造函数传入mc (管理多线程运行结果)
-
测试类中创建Thread类对象t1,构造函数传入ft,启动线程
t1.start();
- 获取方法执行结果
ft.get();
三种方式对比
一二种方式无法获得返回值,第三种可以获取返回值。
第一种最简单,但是可扩展性最差:无法继承其他对象。
二三中扩展性强,不能直接调用Thread类中的方法。
Thread常见成员方法
String getName()
- 返回此线程名称
- 线程默认名字为 Thread-x(序号从0开始)
void setName(String name)
- 设置当前线程名字
- 也可以通过Thread的构造方法,传入名字。注意第一种实现直接继承Thread,构造方法不能继承,需要手动实现构造方法,调用父类构造。
static Thread currentThread()
- 返回当前线程对象
- main方法中直接调用currentThread,获得的线程getName结果为main
static void sleep
- 让线程休眠指定时间,单位为毫秒。休眠结束后会醒来,继续执行。
setPriority(int newPriority)
- 设置线程优先级(只能影响概率)
- java里线程优先级最小1,最大10,默认5
- java采取抢占式调度(随机性)
final int getPriority()
- 获取线程优先级
final void setDaemon(bookean on)
- 设置为守护线程,守护线程生命周期依赖非守护进程
- 当设置为true后,非守护线程结束了,守护线程会陆续结束(不是立刻)
- 如:性能监控工具的指标采集进程,随应用的停止而终止
public static void yield()
- 出让线程
- 出让当前cpu执行权,令代码执行相对均匀
public static void join()
- 插入线程
- 使用对象调用
//作用是把t线程插入到当前线程之前,只有t线程执行完毕,当前线程才继续
//当前线程指的是运行t.join()这段代码的线程
t.join()
生命周期
注意:进程执行sleep() 结束休眠后返回就绪状态,重新抢夺执行权,不会马上执行代码。睡眠也相当于出让执行权。
安全问题及解决
问题及原因
想让多个线程访问同一个变量,可以在类上定义静态属性,但是仍然会有同步问题。
原因:进程执行过程中随时可能被抢夺执行权。
解决方法
同步代码块
//锁对象是任意的,随便创建一个对象即可,但是要保证对每个执行该代码线程是【唯一的】 。最常见的实现是当前类的字节码对象,Xxx.class。一个可行的方法是:定义一个static类属性。
//锁并不保证cup执行权不被抢夺,而是保证就算被抢夺,其他线程也只能等待,因为未解锁。
synchronized(锁对象){
//被锁住的代码
}
同步方法
将synchronized关键字加在方法上面。
修饰符 synchronized 返回值类型 方法名(方法参数){
...
}
同步方法的锁对象是java规定的:
- 当前方法为非静态:this //即当前方法的调用者
- 当前方法为静态:当前类的字节码文件对象
若使用方法二runable实现多线程,因为runable实现类对象是作为Thread的构造参数传入,即使创建多个线程也只需要一个对象,所以多个线程访问同一个变量可以不加static,因为是同一个对象。
抽取策略:将同步代码块中所有代码抽取为一个方法。
lock锁
特点:支持手动上锁,释放锁
在类上定义一个Lock类型的成员属性
Lock是接口,不可以直接实例化,使用实现类ReentrantLock来实例化
lock.lock();
//代码逻辑
lock.unlock();
注意:若采用方式一,Lock类型成员属性需要定义为static!!!
注意:涉及跳转逻辑的,需要保证lock.unlock();可以成功执行。推荐的方法是使用try catch finally,将unlock逻辑放在finally中,保证一定会执行。
实际应用
Stringbuilder 是线程不安全的
Stringbuffer 是线程安全的,每个方法都是同步方法
多线程程序编写步骤
- 循环
- 同步代码块
- 判断共享数据是否到达末尾(到了末尾)
- 判断共享数据是否到达末尾(没到末尾)
生产者消费者(等待唤醒机制)
多线程执行具有随机性,等待唤醒机制使线程执行有序。
成员
生产者:负责生产数据
消费者:负责消费数据
第三者:控制执行顺序
- 需要有一个状态量,判断当前允许 生产者/消费者 活动
- 需要有一个锁变量,线程的sychronized,wait,notify均需要借助它调用
- 可能需要一个共享数据,用于控制生产者与消费者交替的轮数
常见方法
//当前进程等待,直到被其他线程唤醒,会释放当前持有的所有锁
void wait();
//随机唤醒单个线程
void notify();
//唤醒所有线程
void notifyAll();
阻塞队列
相当于成员中第三者的角色
put take方法,底层实现了同步机制。
实现类
ArrayBlockingQueue
底层是数组,有界,需要指定队列长度
LinkedBlockingQueue
底层是链表,无界
阻塞队列实现生产者消费者
生产者消费者必须共用阻塞队列
测试类中实现阻塞队列,构造传给生产者与消费者
生产者调用 queue.put("xxx");
消费者调用 queue.take();
线程六大状态
没有运行状态?
JVM层次结构位于操作系统与应用层之间,线程运行起来后交由操作系统管理,这样有助于解耦