1 继承Thread类
创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。
优点:编码简单
缺点:存在单继承的局限性,线程类继承Thread后,不能继承其它类,不便于扩展
/** 线程的创建方式一:继承Thread实现。*/
public class ThreadDemo1 {
public static void main(String[] args) {
// main方法本身是一个单线程在执行的,这个线程称为主线程。
// 3、创建线程对象
Thread t = new MyThread();
// 4、调用线程对象的start方法启动线程(最终还是调用线程的run方法)
t.start(); // 不能直接调用run方法,会当成普通方法执行
// new MyThread().start();
}
}
// 1、定义线程类继承了Thread类
class MyThread extends Thread{
// 2、重写run方法
@Override
public void run() {
System.out.println("子线程输出");
}
}
2 实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。
- 调用线程对象的start()方法启动线程
优点:线程人物类只是实现接口,可以继续继承类和实现接口,扩展性强
缺点:编程多一层对象包装,如果线程执行结果不可以直接返回
/** 目标:多线程的创建方式二:实现Runnable接口 */
public class ThreadDemo2 {
public static void main(String[] args) {
// 3、创建一个线程任务类的对象(此对象不是线程对象)
Runnable target = new MyRunnable();
// 4、把线程任务对象交给Thread线程对象。
// public Thread(Runnable target)
Thread t = new Thread(target);
// 5、启动线程
t.start();
// new Thresd(new MyRunnable()).start;
}
}
// 1、定义一个线程任务类实现Runnable接口
class MyRunnable implements Runnable{
//2、重写run方法
@Override
public void run() {
System.out.println("子线程任务输出");
}
}
匿名内部类实现Runnable接口
- 创建Runnable的匿名内部类
- 交给Thread处理
- 调用线程对象start()启动线程
public static void main(String[] args) {
// 匿名内部类的方式得到一个线程对象
// Runnable target1 = new Runnable() {
// @Override
// public void run() {
// System.out.println("子线程输出" );
// }
// };
// Thread t2 = new Thread(target1);
// t2.start();
//-------------上面注释代码简化
// new Thread(new Runnable() {
// @Override
// public void run() {
// System.out.println("子线程2输出");
// }
// }).start();
//-------------上面注释代码再简化
new Thread(() -> {
System.out.println("子线程输出" );
}).start();
}
3 实现Callable接口
前两种方式存在的问题:
- 重写的run方法不能直接返回结果,不适合需要返回线程执行结果的业务场景
如何解决
- Callable和FutureTask,可以得到线程的执行结果
步骤
- 得到任务对象( 一: 定义类实现Callable接口,重写call方法,封装要做的事情。二 用FutureTask把Callable对象封装成线程任务对象)
- 把线程任务对象交给Thread处理。
- 调用Thread的start方法启动线程,执行任务线
- 程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
/**
目标:理解线程创建方式三:Callable、FutureTask.
*/
public class ThreadDemo3 {
public static void main(String[] args) {
// 3、创建一个Callable的执行对象。
Callable<String> call = new MyCallable(100);
// 4、把Callable对象交给未来任务对象
/**
未来任务对象的作用:
1、Runnable的实现类对象,可以交给Thread线程对象。
2、可以在未来线程执行完毕后,去获取线程返回的结果的。
*/
FutureTask<String> f1 = new FutureTask<>(call);
// 5、交给Thread线程对象
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();
// 7、得到线程执行完毕后的结果。
try {
// 主线程执行到这儿,如果上面第一个线程没有执行完毕,这里会让出CPU,等待第一个线程执行完毕之后才能执行这个代码取结果
String rs = f1.get();
System.out.println(rs);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 主线程执行到这儿,如果上面第2个线程没有执行完毕,这里会让出CPU,等待第2个线程执行完毕之后才能执行这个代码取结果
String rs = f2.get();
System.out.println(rs);
} 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 {
return n;
}
}
三种方式对比
方式 | 优点 | 缺点 |
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。 | 编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 | 编程相对复杂 |
4 Thread常用方法
方法 | 说明 |
String getName() | 获取当前线程的名称,默认线程名称是Thread-索引 |
void setName(String name) | 设置线程名称 |
public static Thread currentThread(): | 返回对当前正在执行的线程对象的引用 |
public static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒。 |
public void run() | 线程任务方法 |
public void start() | 线程启动方法 |
构造器 | 说明 |
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 把Runnable对象交给线程对象 |
public Thread(Runnable target ,String name ) | 把Runnable对象交给线程对象,并指定线程名称 |
5 线程安全
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
出现原因:
- 存在多线程并发
- 同时访问共享资源
- 存在修改共享资源
取钱问题
public void drawMoney(double money) {
// 1、得到谁来取钱
String name = Thread.currentThread().getName();
// 2、判断当前账户对象的余额是否足够取钱10万
if(this.money >= money) {
System.out.println(name +"来取钱,吐出" + money);
// 3、更新余额
this.money -= money;
System.out.println(name +"取钱后,余额剩余:" + this.money);
}else {
System.out.println(name +"来取钱,余额不足~~");
}
}
/**
线程类(取钱)
*/
public class DrawThread extends Thread{
private Account acc;
public DrawThread(Account acc, String name){
super(name);
this.acc = acc;
}
@Override
public void run() {
// 小明 ,小红 : 取钱
acc.drawMoney(100000);
}
}
/**
目标:模拟线程安全问题(取钱模型,整取)
定义账户类。
定义线程类,处理账户。
*/
public class ThreadDemo1 {
public static void main(String[] args) {
// 1、创建一个共享的账户对象
Account acc = new Account("ICBC-110",100000);
// 2、创建2个线程对象分别代表小明和小红
new DrawThread(acc, "小明").start();
new DrawThread(acc, "小红").start();
}
}
6 线程同步
加锁:让多个线程实现先后依次访问共享资源
6.1 同步代码块
把出现线程安全问题的核心代码上锁,每次只能一个线程占锁进入访问
缺点:会影响其它无关线程的执行
同步锁对象要求
- 实例方法使用this作为锁对象
- 静态方法建议使用字节码(类名.class)对象作为锁对象
synchronized(同步锁对象) {
操作共享资源的代码(核心代码)
}
public void drawMoney(double money) {
// 1、得到谁来取钱
String name = Thread.currentThread().getName();
synchronized(this){
// synchronized(Account.class){
// 2、判断当前账户对象的余额是否足够取钱10万
if(this.money >= money) {
System.out.println(name +"来取钱,吐出" + money);
// 3、更新余额
this.money -= money;
System.out.println(name +"取钱后,余额剩余:" + this.money);
}else {
System.out.println(name +"来取钱,余额不足~~");
}
}
}
6.2 同步方法
把出现线程安全问题的核心方法给上锁
每次只能一个线程进入,执行完毕后自动解锁,其它线程才可以进来执行
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
- 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
修饰符 synchronized(锁对象) 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
public synchronized(this) void drawMoney(double money) {
// 1、得到谁来取钱
String name = Thread.currentThread().getName();
// 2、判断当前账户对象的余额是否足够取钱10万
if(this.money >= money) {
System.out.println(name +"来取钱,吐出" + money);
// 3、更新余额
this.money -= money;
System.out.println(name +"取钱后,余额剩余:" + this.money);
}else {
System.out.println(name +"来取钱,余额不足~~");
}
}
6.3 Lock锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
public ReentrantLock() | 获得Lock锁的实现类对象 |
void lock() | 获得锁 |
void unlock() | 释放锁 |
public class Account {
// 锁对象属于账户对象,而且唯一不可更改!
private final ReentrantLock lock = new ReentrantLock();
private String cardId;// 卡号
private double money; // 余额
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public void drawMoney(double money) {
// 1、得到谁来取钱
String name = Thread.currentThread().getName();
// 2、判断当前账户对象的余额是否足够取钱10万
lock.lock(); // 上锁
try {
if(this.money >= money) {
System.out.println(name +"来取钱,吐出" + money);
// 3、更新余额
this.money -= money;
System.out.println(name +"取钱后,余额剩余:" + this.money);
}else {
System.out.println(name +"来取钱,余额不足~~");
}
} finally {
lock.unlock(); // 解锁!
}
}
}
7 线程通信
线性通信三个常见方法
void wait() | 当前线程等待,直到另一个线程调用notify() 或 notifyAll()唤醒自己 |
void notify() | 唤醒正在等待对象监视器(锁对象)的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器(锁对象)的所有线程 |
8 线程池*
线程池:复用线程的技术
得到线程对象:
方式一:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
方式二:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
public ThreadPoolExecutor(int corePoolSize,// 指定线程池的核心线程数量,不能小于0
int maximumPoolSize,// 指定线程池可支持的最大线程数,大于等于上一个参数
long keepAliveTime,// 指定临时线程的最大存活时间,不能小于0
TimeUnit unit,// 指定存活时间的单位(秒分时天)
BlockingQueue<Runnable> workQueue,//指定任务队列,不能为null
ThreadFactory threadFactory,//指定用哪个线程工厂创建线程,不能为null
RejectedExecutionHandler handler) //指定线程忙,任务满时,新任务该如何,不能为null
临时线程创建时机:
- 新任务提交时发现核心线程都在忙,任务队列也都满,还可以创建临时线程时,才会创建临时线程
拒绝任务的时机:
- 核心线程和临时线程都在忙,任务队列也都满时,新任务过来才会开始拒绝任务
8.1 线程池处理Runnable任务
使用ExecutorService的execute(Runnable target)方法
// ThreadPoolExecutor创建线程池实例
ExecutorService pools = new ThreadPoolExecutor(3,
5,
8,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(6),
Executors.defaultThreadFactory() ,
new ThreadPoolExecutor.AbortPolicy());
pool.execute(new MyRunnable());
ExecutorService的常用方法
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行 Runnable 任务 |
Future<T> submit(Callable<T> task) | 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务 |
void shutdown() | 等任务执行完毕后关闭线程池 |
List<Runnable> shutdownNow() | 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
新任务拒绝策略
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
ThreadPoolExecutor.DiscardPolicy: | 丢弃任务,但是不抛出异常 这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
8.2 线程池处理Callable任务
使用ExecutorService的submit(Callable<T> command)方法
ExecutorService的常用方法
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行 Runnable 任务 |
Future<T> submit(Callable<T> task) | 执行Callable任务,返回未来任务对象获取线程结果 |
void shutdown() | 等任务执行完毕后关闭线程池 |
List<Runnable> shutdownNow() | 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
Future<String> f = pool.submit(new MyCallable(100));
8.3 Executors工具类实现线程池
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
- Executors工具类底层是基于线程池ExecutorService的实现类ThreadPoolExecutor的方式实现线程池对象
- Executors不适合做大型互联网场景的线程池方案,建议这种场景使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,避免资源耗尽的风险
Executors得到线程池对象的常用方法
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
public static ExecutorService newSingleThreadExecutor () | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
Executors使用存在的缺陷
public static ExecutorService newFixedThreadPool(int nThreads) | 允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误( java.lang.OutOfMemoryError ) |
public static ExecutorService newSingleThreadExecutor() | |
public static ExecutorService newCachedThreadPool() | 创建的线程数量最大上限是Integer.MAX_VALUE, 线程数可能会随着任务1:1增长,也可能出现OOM错误( java.lang.OutOfMemoryError ) |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) |
线程池不允许使用Executors 去创建,而是通过ThreadPoolExecutor的方式,这样
的处理方式是为了让人更加明确线程池的运行规则,规避资源耗尽的风险。Executors返回的线程池对象的弊端如下:
- FixedThreadPool和 SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- CachedThreadPool和 ScheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
9 定时器
Timer定时器
- 特点:单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
- 缺点:可能会因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。
public Timer() | 创建Timer定时器对象 |
public void schedule(TimerTask task, long delay, long period) | 开启一个定时器,按照计划处理TimerTask任务 |
ScheduledExecutorService
jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。
优点:基于线程池,某个任务的执行情况不会影响其他定时任务的执行
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 得到线程池对象Executors的方法 |
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) | 周期调度方法 ScheduledExecutorService的方法 |
10 并发、并行
正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的。
- 并发:CPU分时轮询的执行线程
CPU同时处理线程时,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
- 并行:在同一个时刻,同时有多个线程被CPU处理并执行
11 线程的生命周期
NEW:新建状态,创建线程对象
RUNNABLE:就绪状态,star方法
BLOCKED:阻塞状态,无法获得锁对象
WAITING:等待状态,wait方法
TIMED_WAITING:计时等待,sleep方法
TERMINATED:结束状态,全部代码运行完毕
NEW(新建) | 线程刚被创建,但是并未启动。 |
Runnable(可运行) | 线程已经调用了start()等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |