多线程相关概念 【了解】
多线程的三种实现方式 【重点】
Thread类的常用方法 【重点】
多线程安全问题 【重点】
多线程相关概念 【了解】
什么是多线程
是一种可以让程序同时执行多个任务的技术,可以提供程序的执行效率。
线程和进程的关系
进程:正在执行的程序(软件),占用内存和CPU,进程中至少包含一条线程,如果一条都没有则进程也会随之结束
线程:是进程中的一条执行路径(执行单元)
单线程:单条执行路径
多线程:多条执行路径
并发和并行
并行:有多个任务同时(同一时刻)执行,多核CPU支持
并发:有多个任务交替(同一时间段)执行,严格说并非真正的同时执行
Java中创建的多线程程序是并发执行的
多线程的三种实现方式 【重点】
方式一:继承Thread类
步骤:
1、自定义一个类继承Thread类
2、重写run方法,run方法就是线程任务所在的方法
public void run(){
}
3、在测试类中,创建自定义类的对象
4、启动线程
重写的原则:一大两同两小
* 重写的时候run方法的声明完成不能改
* run方法不能抛异常,如果run方法中有异常,只能try...catch处理
方式一代码解析:
1、start方法的作用
启动线程,并调用run方法
将run方法中设置的线程任务放到新开启的线程中进行执行
2、run方法的作用
用于设置线程任务,线程启动会执行其中的代码
由于不同的线程执行的任务不同,我们肯定需要对
父类Thread中的run方法进行重写,对于第一种实现方式来说
父类Thread中的run方法相当于没有做任务的事情
3、启动线程调用的是start还是run方法
调用start方法
start:启动线程并调用run方法
run:不会启动线程,只会普通方式在原来的线程中调用run方法
方式二:实现Runnable接口
步骤:
1、自定义一个类实现Runnable接口
2、重写run方法
3、在测试类中创建自定义类的对象
4、创建Thread类的对象,然后将自定义类的对象当做构造方法的参数传递进去
5、通过Thread类的对象调用start方法启动线程
方式二代码解析:
1、为什么多了一个步骤,将实现类对象传递到Thread类对象中?
自定义类的对象实现的是Runnable接口,没有start方法
不能直接调用并启动线程,所以要借助Thread类的对象来启动线程,
而我们的实现类就只做一件事情:设置线程任务
方式二让线程对象和线程任务分离了
2、 将线程任务设置到线程对象中,最终是如何执行到线程任务的
Thread th = new Thread(mt) -> this.target = target;
th.start() ->th.run() ->if (target != null) { target.run(); }
创建和启动线程的简化写法
1、匿名对象方法
方式一:
new MyThread().start();
方式二:
new Thread(new MyThread()).start();
2、匿名内部类
方式一:
new Thread(){
@Override
public void run() {
// 设置线程任务
}
}.start();
方式二:
new Thread(new Runnable() {
@Override
public void run() {
// 设置线程任务
}
}).start();
3、Lambda表达式
方式二:
new Thread(()->{
// 设置线程任务
}).start();
方式三:实现Callable接口
步骤:
1、自定义一个类实现Callable接口
2、重写call方法,作用类似于run方法也是用于设置线程任务的
call方法和run方法的两点不同:
1、call方法有返回值,而run方法没有返回值
2、call方法可以抛异常,而run方法不可以
3、在测试类中创建自定义类的对象
4、创建FutureTask的对象,将自定义类对象当做构造方法参数传递进去
5、创建Thread类的对象,将FutureTask的对象当做构造方法参数传递进去
6、通过Thread类的对象调用start方法启动线程
实现线程的三种方式的区别
1、方式一(继承方式)和另外两种(实现方式)的区别
方式一:
好处:继承了之后就是Thread类的子类,可以直接使用Thread类中的方法,编程相对简单
弊端:扩展性差,Java是单继承的,如果一个类已经继承了其他类,就不能使用这种方式了,
另外如果继承Thread类,下次要抽取父类也做不到了
另外两种:
好处:扩展性好,Java类在继承的同时可以实现接口,所以如果一个类已经继承了其他类,依然可以
使用这种方式,同样的即使实现了接口,也可以抽取父类
弊端:在这些实现类不能直接使用Thread类中的方法,编程相对复杂
2、方式三(实现Callable)和另外两种(继承Thread类和实现Runnable接口)的区别
方式三:
任务方法是call
可以返回线程任务执行之后的结果
支持在处理线程任务的时候抛出异常(throws)
另外两种:
任务方法是run
无法得到线程任务执行之后的结果
在处理线程任务的时候不能抛出异常
推荐的实现方式
方式二
方式三
方式一
Thread类的常用方法 【重点】
构造方法
Thread(Runnable r)
Thread(String name)
Thread()
Thread(Runnable r, String name)
成员方法
void start()
void run()
设置和获取线程名称
获取
String getName():获取线程名称
设置
线程如果没有设置名称,会有默认名称:
普通线程:Thread-x,x从0开始依次递增
主线程:主方法所在的线程,默认名称是main
* void setName(String name) :设置线程名称
* 构造方法
Thread(String name)
Thread(Runnable r, String name)
static Thread currentThread() :获取当前正在执行的线程对象
static void sleep(long millis) :线程休眠,参数传递的是毫秒值
设置和获取优先级
void setPriority(int newPriority)
int getPriority()
设置守护线程(用户线程/后台线程)
void setDaemon(boolean on):on传递true,就能设置为守护线程 【了解】
当所有的非守护线程执行完毕,守护线程也会随之结束。
多线程安全问题 【重点】
线程安全问题的演示
模拟三个窗口卖100张票,使用多线程实现
三个窗口:三个线程,线程任务一样的
演示看到的现象:卖出了重复票和不存在的票
线程安全问题的原因解析
重复票:tickets--之前出现的线程CUP被抢夺的情况
不存在的票:判断之后,卖票之前出现线程CUP被抢夺的情况
产生问题的通用条件:
有多线程,操作共享数据,而且操作的共享数据有多条语句。
如果是单线程不会有线程安全问题
如果是多线程但是没有操作共享数据,而是操作各自的数据也不会有线程安全问题
线程安全问题的解决
方案一:同步代码块
格式:
synchronized(锁对象){
}
synchronized:关键字,同步
锁对象:首先是一个对象,而且可以是任意对象,但是多线程看到的该对象必须是同一个
同步代码块的特点:
同一时刻只能有一个线程进入到同步代码块中执行。
线程进入到同步代码块之前必须先判断锁是否被其他线程获取,如果被获取了,只能在同步代码块之外等待
如果没有被获取,则当前线程获取锁,接下来才能进入到同步代码块中执行,执行完之后,才会归还锁,
其他线程才可以继续抢夺锁
方案二:同步方法
同步方法:使用synchronized关键字修饰的方法
非静态同步方法
修饰符 synchronized 返回值类型 方法名(参数列表){
方法体;
}
静态同步方法
修饰符 static synchronized 返回值类型 方法名(参数列表){
方法体;
}
方法大括号中的方法体内容都是被同步(被锁)的代码
面试题:
非静态同步方法和静态同步方法的锁分别是什么?
非静态同步方法:this
静态同步方法:当前类的字节码对象 类名.class
方案三:Lock锁
三个步骤:
1、创建锁
2、锁定
3、解锁
代码块
Java中只有四种代码块(局部代码块、构造代码块、静态代码块和同步代码块)
代码块:使用一对大括号包裹起来的代码区域
局部代码块
格式:
{
}
位置:
在方法中
作用:控制变量的作用域,或者节省一点点内存
特点:变量只能在局部代码块中使用,出了局部代码块则无法使用
构造代码块
格式:
{
}
位置:
类中方法外
作用:用于抽取所有构造方法前面的重复代码
特点:每执行一次构造方法都会执行一次构造代码块
而且先于构造方法执行
静态代码块
格式:
static{
}
位置:
类中方法外
作用:
一切一次性的操作,并且要非常早的操作(准备操作)都可以放到静态代码块中执行
比如:
jdbc注册驱动
加载配置文件
特点:
1、执行时机非常早,比当前类的主方法还早,随着类的加载而执行
2、一个类只会加载一次,所以静态代码块只会执行一次
面试题:分析以下代码的执行结果?



