1. 线程
- 是一个程序内部的一条执行流程
- 程序中如果只有一条执行流程,那这个程序就是单线程的程序
2. 多线程
- 指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)
2.1. 如何创建多条线程
- Java通过java.lang.Thread类的对象来代表线程
2.1.1. 方式一:继承Thread类
// 1. 继承Thread类
public class MyThread extends Thread{
// 2. 重写run方法
@Override
public void run() {
System.out.println("子线程");
}
}
public static void main(String[] args) {
// 3. 创建子线程对象
Thread t = new MyThread();
// 4. 启动子线程
t.start();
System.out.println("主线程");
}
- 优点:编码简单
- 缺点:已经继承Thread,无法继承其他类,不利于功能的扩展
- 启动线程必须调用start方法,而不能调用run方法,否则会将线程对象当作一个普通的对象。
- 不要把主线程任务放在启动子线程之前。
2.1.2. 方式二:实现Runnable接口
// 1. 定义任务类, 实现Runnable接口
public class MyRunnable implements Runnable {
// 2. 重写run方法
@Override
public void run() {
System.out.println("Runnable子线程");
}
}
public static void main(String[] args) {
// 3. 创建一个任务对象
Runnable runnable = new MyRunnable();
// 4. 把任务对象交给一个线程对象, 启动线程
new Thread(runnable).start();
System.out.println("主线程");
}
- 任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强
2.1.3. 方式三:利用Callable接口、FutureTask类实现
- 以上两种方式的问题:如果线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。
- 使用Callable接口,可以返回线程执行完毕后的结果
- 未来任务对象的作用,是一个任务对象,实现了Runnable接口;可以在线程执行完毕之后,用未来任务对象调用get方法获取执行完的返回结果。
// 1. 实现Callable接口
public class MyCallable implements Callable<String> {
// 2. 重写call方法
@Override
public String call() throws Exception {
// 描述线程任务, 返回线程执行后的结果
int sum = 0;
for(int i = 1; i <= 100; i++){
sum += i;
}
return String.valueOf(sum);
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 3. 创建一个Callable对象
Callable<String> callable = new MyCallable();
// 4. 将Callable对象封装成FutureTask任务对象
FutureTask<String> futureTask = new FutureTask<>(callable);
// 5. 使用任务对象创建一个线程对象, 并启动
new Thread(futureTask).start();
// 6. 获取线程执行完毕返回的结果
String s = futureTask.get();
System.out.println(s);
}
2.2. Thread的常用方法
3. 线程安全
- 多个线程,同时访问同一共享资源,且存在修改该资源的时候,可能会出现业务安全问题
4. 线程同步
- 是用于解决线程安全问题的方案
- 让多个线程实现先后依次访问共享资源,这样就解决了安全问题
4.1. 线程同步的方案
- 加锁,每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程再加锁进来。
4.2. 加锁的方式
4.2.1. 方式一:同步代码块
- 作用:把访问共享资源的核心代码块给上锁,以此保证线程安全
- 写法
- 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
- 注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象)
- Ctrl + ALT + T选第九个,快速生成同步代码块
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象;静态方法建议使用字节码(类名.class)对象作为锁对象
4.2.2. 方式二:同步方法
- 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
- 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
4.2.3. 方式三:Lock锁
- 可以通过它创建锁对象,进行加锁和解锁
- Lock是接口,不能直接实例化,可以采用他的实现类ReentrantLock来创建锁对象。
5. 线程通信
- 当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,一相互协调,并避免无效的资源争夺
- 生产者消费者问题
上述方法应该使用当前同步锁对象进行调用
public class Test5 {
public static void main(String[] args) {
// 有三个生产者线程负责生产包子, 每次只能有一个包子放在桌子上
// 两个消费者线程负责吃包子, 每次只有一个线程能将包子吃掉
// 1. 创建一个桌子对象
Desk desk = new Desk();
// 2. 创建三个厨师线程
new Thread(() ->{
while (true) {
desk.put(); // 抢桌子
}
}, "厨师1").start();
new Thread(() ->{
while (true) {
desk.put(); // 抢桌子
}
}, "厨师2").start();
new Thread(() ->{
while (true) {
desk.put(); // 抢桌子
}
}, "厨师3").start();
// 3. 创建两个吃货线程
new Thread(() -> {
while (true){
desk.get();
}
},"吃货1").start();
new Thread(() -> {
while (true){
desk.get();
}
},"吃货2").start();
}
}
public class Desk {
private List<String> list = new ArrayList<>();
public synchronized void put() {
try {
String name = Thread.currentThread().getName();
if(list.size() == 0){
list.add(name + "包的包子");
System.out.println(name + "包了一个包子");
Thread.sleep(2000);
// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}else { // 有包子了
// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}
}catch (Exception e){
e.printStackTrace();
}
}
public synchronized void get() {
try {
if(list.size() == 1){ // 有包子
String name = Thread.currentThread().getName();
System.out.println(name + "吃了" + list.get(0));
list.clear(); // 清空
Thread.sleep(1000);
// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
} else {
// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
6. 线程池
- 线程池就是一个可以复用线程的技术
- 代表线程池的接口,ExecutorService
6.1. 创建线程池对象
6.1.1. 使用ExecutorService的实现类ThreadPoolExecutor创建线程池对象
// 通过ThreadPoolExecutor创建一个线程池对象
/*
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
*/
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
注意事项:
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
- 如果是计算密集型任务,核心线程数 = CPU核数 + 1
- 如果是IO密集型任务,核心线程数 = CPU核数 * 2
6.1.2. 使用Executors(线程池工具类)调用静态方法返回不同特点的线程池对象
- 这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象
- 大型并发系统环境中使用Executors如果不注意可能会出现系统风险
6.2. 线程池处理Runnable任务、Callable任务
7. 并发、并行、线程的生命周期
7.1. 进程
- 正在运行的程序(软件)就是一个独立的进程。
- 线程是属于进程的,一个进程中可以同时运行多个线程。
- 进程中的多个线程其实是并发和并行执行的。
7.2. 并发
进程中的线程是由CPU负责调度执行的,但CPU能同时处理的线程数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
7.3. 并行
在同一时刻上,同时有多个线程在被CPU调度执行。
7.4. 线程的生命周期
线程从创建到销毁的过程中,经历的各种状态及状态的转换