目录
多线程的三种实现方法:
父类为Thread类
Java是通过java.lang.Thread 类来代表线程的。
按照面向对象的思想,Thread类应该提供了实现多线程的方式。
方法一 直接继承Thread类
①定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
②创建MyThread类的对象
③调用线程对象的start()方法启动线程(启动后还是执行run方法的)
案例:调start(),而不是run()。告诉CPU,马上要启动一条新线程,如果直接调run()则是普通类的方法的调用,不是多线程。
小结
1、为什么不直接调用run方法,而是调用start启动线程。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
- 只有调用start方法才是启动一个新的线程执行。
2、把主线程任务放在子线程之前了。
- 这样主线程一直是先跑完的,相当于是一个单线程的效果了。
1.方式一是如何实现多线程的?
- 继承Thread类
- 重写run方法
- 创建线程对象
- 调用start()方法启动。
2.优缺点是什么?
- 优点:编码简单
- 缺点:存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展。
方法二 实现Runnable接口
创建一个类,实现Runnable接口
方式二优缺点:
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
小结
匿名内部类写法:也是不可以返回结果
前2种线程创建方式都存在一个问题:
他们重写的run方法均不能直接返回结果。
不适合需要返回线程执行结果的业务场景。
Android里可以用handler& message方式传递更新数据。
方式三 实现Callable接口
①得到任务对象
1.定义类实现Callable接口,重写call方法,封装要做的事情。
2.用FutureTask把Callable对象封装成线程任务对象。
②把线程任务对象交给Thread处理。
③调用Thread的start方法启动线程,执行任务
④线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
public class ThreadDemo3 {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable<String> call = new MyCallable(100);
// 4、把Callable任务对象 交给 FutureTask 对象
// FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
// FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
FutureTask<String> f1 = new FutureTask<>(call);
// 5、交给线程处理
Thread t1 = new Thread(f1);
// 6、启动线程
t1.start();
Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
String rs1 = f1.get();
System.out.println("第一个结果:" + rs1);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
String rs2 = f2.get();
System.out.println("第二个结果:" + rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
1、定义一个任务类 实现Callable接口 应该申明线程任务执行完毕后的结果的数据类型
*/
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
/**
2、重写call方法(任务方法)
*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n ; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
方式三优缺点:
优点:
- 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
- 可以在线程执行完毕后去获取线程执行的结果。
缺点:编程稍麻烦,还需要强制主线程等待
三种方式小结:
线程常用方法
区分线程:设置线程名称(有默认名称Thread-i,从0开始) setName(),获取线程名称getName()
拿到当前线程对象:Thread t = Thread.currentThread();
- 此方法是Thread类的静态方法,可以直接使用Thread类调用。
- 这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。
还可以通过构造器取名字
写一个子类,设置有参构造器
线程休眠方法
public static void sleep(long time)
让当前线程休眠指定的时间后再继续执行,单位为毫秒。
用法:
故意让项目卡,用户交钱再优化:注释掉(狗头)
小结
Thread类的一些构造器
实际开发几乎不会为线程专门取名字
线程安全
1.线程安全问题发生的原因是什么?
多个线程同时访问同一个共享资源且存在修改该资源的操作。
代码规范:设置了有参构造器,一般也要设置个无参构造器。
想在创建线程的时候顺便给线程取个名字,要在子类构造器里调用父类的构造器,super(name);
解决方法:线程同步
加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
这个锁对象可以是一个常量字符串,自带static final属性。
注意:
锁对象用任意唯一的对象好不好呢?
不好,会影响其他无关线程的执行
锁对象的规范要求:
- 规范上:建议使用共享资源作为锁对象。
- 对于实例方法建议使用this作为锁对象。
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
面向对象思维要在实战中强化,逐步深入了解并使用。
同步方法
二者比较:
从性能上分析,同步代码块范围小一点,同步方法锁的范围更大,所以同步代码块的性能更好一点。
但是实际中同步方法使用更方便,可读性更好。
小结
Lock锁
功能更强大,有抢占锁,等待时间自动释放锁,,,,,
在创建账户对象时,同时创建锁实例对象。
转存失败重新上传取消
使用非常方便:
使用中, 一般放在try,catch后面的finally里,保证成功解锁