创建一个新的类,该类继承 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("子线程输出");
}
}
优点:线程人物类只是实现接口,可以继续继承类和实现接口,扩展性强
缺点:编程多一层对象包装,如果线程执行结果不可以直接返回
/** 目标:多线程的创建方式二:实现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接口
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();
}
前两种方式存在的问题:
- 重写的run方法不能直接返回结果,不适合需要返回线程执行结果的业务场景
如何解决
- Callable和FutureTask,可以得到线程的执行结果
步骤
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。
/**
目标:理解线程创建方式三:Callable、FutureTask.
*/
public class ThreadDemo3 {
public static void main(String[] args) {
// 3、创建一个Callable的执行对象。
Callable call = new MyCallable(100);
// 4、把Callable对象交给未来任务对象
/**
未来任务对象的作用:
1、Runnable的实现类对象,可以交给Thread线程对象。
2、可以在未来线程执行完毕后,去获取线程返回的结果的。
*/
FutureTask f1 = new FutureTask<>(call);
// 5、交给Thread线程对象
Thread t1 = new Thread(f1);
// 6、启动线程
t1.start();
Callable call2 = new MyCallable(200);
FutureTask 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 {
private int n;
public MyCallable(int n){
this.n = n;
}
/**
2、重写call方法,定义执行的任务和返回的结果
*/
@Override
public String call() throws Exception {
return n;
}
}
三种方式对比
方式 | 优点 | 缺点 |
继承Thread类 |
编程比较简单,可以直接使用Thread类中的方法 |
扩展性较差,不能再继承其他的类,不能返回线程执行的结果 |
实现Runnable接口 |
扩展性强,实现该接口的同时还可以继承其他的类。 |
编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 |
扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 |
编程相对复杂 |
方法 | 说明 |
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对象交给线程对象,并指定线程名称 |
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
出现原因:
- 存在多线程并发
- 同时访问共享资源
- 存在修改共享资源
取钱问题
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();
}
}
加锁:让多个线程实现先后依次访问共享资源
把出现线程安全问题的核心代码上锁,每次只能一个线程占锁进入访问
缺点:会影响其它无关线程的执行
同步锁对象要求
- 实例方法使用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 +"来取钱,余额不足~~");
}
}
}
把出现线程安全问题的核心方法给上锁
每次只能一个线程进入,执行完毕后自动解锁,其它线程才可以进来执行
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认用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 +"来取钱,余额不足~~");
}
}
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(); // 解锁!
}
}
}
线性通信三个常见方法
void wait() |
当前线程等待,直到另一个线程调用notify() 或 notifyAll()唤醒自己 |
void notify() |
唤醒正在等待对象监视器(锁对象)的单个线程 |
void notifyAll() |
唤醒正在等待对象监视器(锁对象)的所有线程 |
线程池:复用线程的技术
得到线程对象:
方式一:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
方式二:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
public ThreadPoolExecutor(int corePoolSize,// 指定线程池的核心线程数量,不能小于0
int maximumPoolSize,// 指定线程池可支持的最大线程数,大于等于上一个参数
long keepAliveTime,// 指定临时线程的最大存活时间,不能小于0
TimeUnit unit,// 指定存活时间的单位(秒分时天)
BlockingQueue workQueue,//指定任务队列,不能为null
ThreadFactory threadFactory,//指定用哪个线程工厂创建线程,不能为null
RejectedExecutionHandler handler) //指定线程忙,任务满时,新任务该如何,不能为null
临时线程创建时机:
- 新任务提交时发现核心线程都在忙,任务队列也都满,还可以创建临时线程时,才会创建临时线程
拒绝任务的时机:
- 核心线程和临时线程都在忙,任务队列也都满时,新任务过来才会开始拒绝任务
使用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 |
执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务 |
void shutdown() |
等任务执行完毕后关闭线程池 |
List<Runnable> shutdownNow() |
立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
新任务拒绝策略
ThreadPoolExecutor.AbortPolicy |
丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
ThreadPoolExecutor.DiscardPolicy: |
丢弃任务,但是不抛出异常 这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy |
抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy |
由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
使用ExecutorService的submit(Callable
ExecutorService的常用方法
void execute(Runnable command) |
执行任务/命令,没有返回值,一般用来执行 Runnable 任务 |
Future |
执行Callable任务,返回未来任务对象获取线程结果 |
void shutdown() |
等任务执行完毕后关闭线程池 |
List<Runnable> shutdownNow() |
立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
Future f = pool.submit(new MyCallable(100));
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。
- 特点:单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
- 缺点:可能会因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。
public Timer() |
创建Timer定时器对象 |
public void schedule(TimerTask task, long delay, long period) |
开启一个定时器,按照计划处理TimerTask任务 |
jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。
优点:基于线程池,某个任务的执行情况不会影响其他定时任务的执行
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) |
得到线程池对象Executors的方法 |
public ScheduledFuture> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) |
周期调度方法 ScheduledExecutorService的方法 |
正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的。
- 并发:CPU分时轮询的执行线程
CPU同时处理线程时,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
- 并行:在同一个时刻,同时有多个线程被CPU处理并执行
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方法而死亡。 |