一、什么是多线程
多线程是指从软硬件上实现多条执行流程的技术。
二、线程的创建方式
2.1 方法一:继承Thread类
1. 定义子类MyThread继承Thread类,重写run()方法
package com.hkd.thread;
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出"+ i);
}
}
}
2. main中创建MyThread类的对象,调用线程对象的start方法开启线程(启动后还是执行run方法)
package com.hkd.thread;
public class ThreadDemo1 {
public static void main(String[] args) {
// new一个新线程对象
Thread t = new MyThread();
// 调用start方法启动线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出"+ i);
}
}
}
疑问:
- 为什么不调用run(),而是调用start()开启线程?
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。只有调用start方法才是启动一个新的线程执行。 - 为什么子线程要放在主线程之前写?
如果主线程放在子线程之前了,主线程一直是先跑完的,相当于单线程了。
方法一的优缺点:
- 优点:编码简单
- 缺点:线程类已经继承Thread类,无法继承其他类,不利于扩展
2.2 方法二:实现Runnable接口
1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
package com.hkd.thread;
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出"+ i);
}
}
}
2. 创建MyRunnable任务对象,把MyRunnable任务对象交给Thread处理;调用线程对象的start方法开启线程。
package com.hkd.thread;
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建一个任务对象
Runnable target = new MyRunnable();
// 把任务对象交给Thread处理
Thread t = new Thread(target);
// 开启线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出"+ i);
}
}
}
在创建线程的时候,不会立即执行run(),而是等到start()执行的时候,才会开始执行run()。
方式二的优缺点:
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
- 缺点:编程多一层对象包装(任务对象),如果线程有执行结果是不可以直接返回的。
2.3 方法二的另一种写法(匿名内部类)
package com.hkd.thread;
public class ThreadDemo1 {
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出"+ i);
}
}).start();
// 把任务对象交给Thread处理
Thread t = new Thread(target);
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出"+ i);
}
}
}
2.4 方法三:实现Callable接口
1. 定义MyCallable类实现Callable接口,重写call方法,封装要做的事情
package com.hkd.thread;
import java.util.concurrent.Callable;
// 需声明线程任务执行完毕后的结果数据类型
public class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n){
this.n = n;
}
// 重写call方法
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "子线程的执行结果:"+ sum;
}
}
2. 用FutureTask把Callable对象封装成线程任务对象,把这个对象交给Thread处理,调用start方法开启线程,执行任务;线程执行完毕后,通过FutureTask的get方法获取任务执行的结果
package com.hkd.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建Callable对象
Callable<String> call1 = new MyCallable(100);
// 把Callable任务对象封装成FutureTask,它是Runnable对象(实现Runnable接口),可以交给Thread
// 可以在线程执行完毕之后通过get方法得到线程执行的结果
FutureTask<String> f1 = new FutureTask<>(call1);
// 交给线程处理
Thread t1 = new Thread(f1);
t1.start();
Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
// get方法获取任务执行的结果,如果上面代码没有执行完是不执行这里的
try {
String rs1 = f1.get();
System.out.println("线程1结果:"+ rs1);
} catch (Exception e) {
e.printStackTrace();
}
try {
String rs2 = f2.get();
System.out.println("线程2结果:"+ rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
方法三的优缺点:
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。可以在线程执行完毕后去获取线程执行的结果。
- 缺点:编码复杂一点。
三、线程提供的API
package com.hkd.thread.com.hkd.thread2;
public class ThreadMain {
public static void main(String[] args) throws Exception {
Thread t1 = new MyThread();
// 给当前线程命名
t1.setName("一号");
t1.start();
System.out.println(t1.getName());
Thread t2 = new MyThread();
t1.setName("二号");
t2.start();
System.out.println(t2.getName());
// 当前正在运行的线程对象
Thread m = Thread.currentThread();
System.out.println(m.getName());
for (int i = 0; i < 5; i++) {
System.out.println("main线程输出:" + i);
if(i==3)
// 让当前线程休眠
Thread.sleep(3000);
}
}
}
package com.hkd.thread.com.hkd.thread2;
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出:" +i);
}
}
}
四、线程安全问题
4.1 线程安全问题的原因
- 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题
- 出现安全问题的原因:多线程并发,同时访问共享资源,存在修改共享资源
4.2 解决方法——线程同步
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
- 线程同步的核心思想:
加锁。把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
4.2.1 解决方法一——同步代码块
- 作用:把出现线程安全问题的核心代码给上锁。
- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行
- 用
synchronized
给核心代码上锁 - 规范上:建议使用共享资源作为锁对象
- 对于实例方法:建议使用
this
作为锁对象 - 对于静态方法:建议使用字节码(类名.class)对象作为锁对象
public void drawmoney(double money) {
// 1. 获取谁来取钱
String name = Thread.currentThread().getName();
// 同步代码块
synchronized (this) {
// 2. 判断账户余额够不够
if(this.money >= money){
System.out.println(name + "来取钱成功,吐出" + money);
this.money -= money;
System.out.println(name + "取钱后剩余" + this.money);
} else {
System.out.println(name + "取钱,余额不足");
}
}
}
4.2.2 同步方法
- 作用:把出现线程安全问题的核心方法给上锁。
- 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
- 对于实例方法默认使用this作为锁对象。
- 对于静态方法默认使用类名.class对象作为锁对象。
//同步方法(锁起来)
public synchronized void drawmoney(double money) {
// 1. 获取谁来取钱
String name = Thread.currentThread().getName();
// 2. 判断账户余额够不够
if (this.money >= money) {
System.out.println(name + "来取钱成功,吐出" + money);
this.money -= money;
System.out.println(name + "取钱后剩余" + this.money);
} else {
System.out.println(name + "取钱,余额不足");
}
}
疑问:
同步代码块和同步方法哪个更好一些?
答:同步代码块锁的范围更小,同步方法锁的范围更大。
4.2.3 Lock锁
可以灵活地上锁和解锁。
public void drawmoney(double money) {
// 1. 获取谁来取钱
String name = Thread.currentThread().getName();
// 上锁
lock.lock();
try {
// 2. 判断账户余额够不够
if (this.money >= money) {
System.out.println(name + "来取钱成功,吐出" + money);
this.money -= money;
System.out.println(name + "取钱后剩余" + this.money);
} else {
System.out.println(name + "取钱,余额不足");
}
} finally {
//解锁
lock.unlock();
}
}
4.3 线程通信
- 什么是线程通信、如何实现?
- 所谓线程通信就是线程间相互发送数据。
- 线程通信常见形式
- 通过共享一个数据的方式实现。
- 根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。
- 线程通信实际应用模型
- 生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
- 一般要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。
4.4 线程通信的三个方法
上述方法应该用当前同步锁对象调用
五、线程池
5.1 什么是线程池
线程池就是一个可以复用线程的技术。
-
不使用线程池的问题
如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。
5.2 谁创建线程池
JDK 5.0起提供了代表线程池的接口:ExecutorService
5.3 如何得到线程对象
使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象,
ThreadPoolExecutor构造器的参数说明
5.4 线程池面试题
1. 临时线程是什么时候创建的?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
2. 什么时候开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
5.5 线程池处理Runnable任务
使用ExecutorService的方法:void execute
(Runnable target)
package com.hkd.threadpool;
import java.util.concurrent.*;
/**
* 自定义线程池对象
*/
public class threadpooldemo {
public static void main(String[] args) {
/**
* public ThreadPoolExecutor(int corePoolSize,
* int maximumPoolSize,
* long keepAliveTime,
* TimeUnit unit,
* BlockingQueue<Runnable> workQueue,
* ThreadFactory threadFactory)
*/
ExecutorService pool = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// 给任务线程池,Runnable任务
Runnable target = new MyRunnable();
pool.execute(target);
pool.execute(target);
pool.execute(target);
}
}
5.6 线程池处理Callable任务
使用ExecutorService的方法:Future<T> submit
(Callable<T> command)
package com.hkd.threadpool;
import com.hkd.thread.MyCallable;
import java.util.concurrent.*;
/**
* 自定义线程池对象
*/
public class threadpooldemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
/**
* 处理Callable任务
*/
// 提交Callable任务,返回Future任务对象
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}
在线程池执行submit()方法的时候,假设第一个线程执行的时候,第二个线程是否会执行是看线程调度器决定的,如果线程池中有足够的空闲线程,那么第二个线程任务可以立马执行,但如果线程池中的所有线程都在执行其他任务,则第二个线程将会等待,直到有空闲线程可以使用。