Java多线程学习笔记
Java多线程学习笔记
基本概念
进程与线程
进程:程序的基本执行实体
线程:操作系统中能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位
并行与并发
并行:在同一时刻,有多个指令在多个CPU上同时进行
并发:在同一时刻,有多个指令在单CPU上交替进行
多线程实现方式
1. 继承Thread类
// 1.1 创建一个继承Thread类的子类
// 1.2 重写run()方法
// 1.3 创建子类对象,并启动线程
class MyThread extends Thread {@Overridepublic void run() {// 线程执行的代码}
}
// 使用
MyThread t1 = new MyThread();
t1.setName("线程1");
t1.start();
特点:编程简单,可以直接使用Thread类中的方法,但扩展性较差,不能再继承其他的类
2. 实现Runnable接口
// 2.1 创建一个实现Runnable接口的实现类
// 2.2 实现run()方法
// 2.3 创建Runnable接口的实现类对象,创建Thread对象时作为一个参数来传递启动,并启动线程
class MyRun implements Runnable {@Overridepublic void run() {// 线程执行的代码}
}
// 使用
MyRun r1 = new MyRun();
Thread t1 = new Thread(r1);
t1.setName("线程1");
t1.start();
特点:扩展性强,实现该接口的同时还可以继承其他类,编程相对复杂,不能直接使用Thread类中的方法
3. 利用Callable接口和Future接口
// 3.1 创建一个类MyCallable实现Callable接口
// 3.2 重写call(),有返回值,表示多线程运行的结果
// 3.3 创建MyCallable对象(表示多线程要执行的任务)
// 3.4 创建Future对象(作用管理多线程运行的结果)
// 3.5 创建Thread类的对象,并启动(表示线程)
class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {// 线程执行的代码return 100; // 返回结果}
}
// 使用
MyCallable c1 = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(c1);
Thread t1 = new Thread(ft);
t1.setName("线程1");
t1.start();
Integer result = ft.get(); // 获取结果
特点:可以获取多线程运行的结果,扩展性强,实现该接口的同时还可以继承其他类
线程的成员方法
方法 | 描述 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置此线程的名称 |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
细节:
如果没有给线程设置名字,线程有默认的名字:
Thread-0
,Thread-1
, ...给线程设置名字可以使用set方法或构造方法
当JVM启动时,会自动启动多条线程,其中有一条是main线程
sleep
方法会让当前线程休眠指定时间,时间到了自动醒来
线程调度和优先级
调度方式
抢占式调度:CPU会根据线程的优先级来调度线程,优先级高的线程会优先被CPU调度
非抢占式调度:时间片轮转
线程优先级
public static final int MIN_PRIORITY = 1; // 最小优先级
public static final int NORM_PRIORITY = 5; // 默认优先级
public static final int MAX_PRIORITY = 10; // 最高优先级
// 设置和获取优先级
thread.setPriority(int newPriority);
int priority = thread.getPriority();
注意:优先级不是绝对的,只是影响调度的概率
其他方法
final void setDaemon(boolean on)
:设置线程为守护线程static void yield()
:出让线程/礼让线程final void join()
:插入线程/插队线程
细节:
守护线程:当其他的非守护线程执行完毕之后,守护线程会陆续结束
yield()
:让出当前CPU使用权,让当前线程从运行状态进入到就绪状态join()
:把指定线程插入到当前线程之前执行
线程的生命周期
状态 | 描述 |
---|---|
新建状态 | 创建线程对象,但还没有启动线程 |
就绪状态 | 调用start()方法,线程有执行资格,没有执行权 |
运行状态 | 线程获取CPU执行权,执行run()方法 |
死亡状态 | 线程执行完毕,不能再启动 |
阻塞状态 | 线程遇到阻塞式方法,没有执行资格和执行权 |
线程安全与同步
同步代码块
synchronized(锁对象) {// 操作共享数据的代码 }
特点:
锁默认打开,有一个线程进去了,锁自动关闭
里面的代码全部执行完毕,线程出来,锁自动打开
同步方法
// 非静态方法
public synchronized void method() {// 操作共享数据的代码
}
// 静态方法
public static synchronized void method() {// 操作共享数据的代码
}
特点:
同步方法是锁住方法里面所有的代码
锁对象:非静态方法使用
this
,静态方法使用当前类的字节码文件对象(类名.class
)
字符串类线程安全
StringBuilder
:线程不安全StringBuffer
:线程安全(每个成员方法都有synchronized
修饰)
Lock锁
// 创建锁
Lock lock = new ReentrantLock();
// 使用锁
lock.lock();
try {// 操作共享数据的代码
} finally {lock.unlock();
}
特点:比synchronized
更灵活,需要手动上锁和释放锁
死锁
死锁定义
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
产生死锁的原因
竞争资源
可剥夺资源:CPU和主存
不可剥夺资源:打印机、磁带机等
临时资源:硬件中断、信号、消息等
进程间推进顺序非法
死锁产生的4个必要条件
互斥条件:进程要求对所分配的资源进行排它性控制
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺
环路等待条件:在发生死锁时,必然存在一个进程-资源的环形链
解决死锁的基本方法
预防死锁
资源一次性分配:破坏请求条件
只要有一个资源得不到分配,也不给这个进程分配其他的资源:破坏请保持条件
可剥夺资源:破坏不可剥夺条件
资源有序分配法:破坏环路等待条件
具体技术
以确定的顺序获得锁
超时放弃:使用
tryLock(long time, TimeUnit unit)
方法
避免死锁
银行家算法:系统在进行资源分配之前预先计算资源分配的安全性
检测死锁
Jstack命令:生成java虚拟机当前时刻的线程快照
JConsole工具:JDK自带的监控工具
解除死锁
剥夺资源:从其它进程剥夺足够数量的资源给死锁进程
撤消进程:直接撤消死锁进程或撤消代价最小的进程
等待唤醒机制
方法
void wait()
:当前线程等待,直到被其他线程唤醒void notify()
:随机唤醒单个线程void notifyAll()
:唤醒所有线程
阻塞队列
接口层次
Iterable<E> → Collection<E> → Queue<E> → BlockingQueue<E>
实现类
ArrayBlockingQueue<E>
:底层是数组,有界LinkedBlockingQueue<E>
:底层是链表,无界(但不是真正的无界,最大为int的最大值)
JVM定义的线程状态
JVM中定义了6种线程状态:
状态 | 描述 |
---|---|
NEW | 至今尚未启动的线程 |
RUNNABLE | 正在Java虚拟机中执行的线程 |
BLOCKED | 受阻塞并等待某个监视器锁的线程 |
WAITING | 无限期地等待另一个线程来执行某一特定操作的线程 |
TIMED_WAITING | 等待另一个线程来执行取决于等待时间的操作的线程 |
TERMINATED | 已经退出的线程 |
注意:JVM中没有运行态,因为线程抢夺到CPU的执行权时,JVM将线程交给操作系统管理。
线程数据共享方式
构造方法传递
静态变量
线程栈内存结构
每个线程都有自己独立的栈内存
当线程调用方法时,会形成栈帧并压入线程栈中
栈帧包含:局部变量表、操作数栈、动态链接、方法出口等信息
栈帧随着方法调用而创建,随着方法结束而销毁
线程池
核心原理
创建一个线程池,线程池中是空的
提交任务时,线程池会创建新的线程对象,任务执行完毕,线程归还给线程池
再次提交任务时,不需要创建新的线程,直接复用已有的线程
如果提交任务时,线程池中没有空闲线程,也无法创建新线程,任务就会排队等待
三个临界点
当核心线程满时,再提交任务就会排队
当核心线程满,阻塞队列满时,会创建临时线程
当核心线程满,阻塞队列满,临时线程满时,会触发任务拒绝策略
Executor框架
Executor框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等。
框架结构
任务:
Runnable
接口或Callable
接口任务的执行:
Executor
接口和ExecutorService
接口异步计算的结果:
Future
接口和FutureTask
类
使用步骤
创建实现
Runnable
或Callable
接口的任务对象把任务对象交给
ExecutorService
执行如果执行
submit()
方法,将返回一个Future
对象主线程可以执行
Future.get()
等待任务完成,或Future.cancel()
取消任务
Executors工具类
Executors.newCachedThreadPool()
:创建一个没有上限的线程池Executors.newFixedThreadPool(int nThreads)
:创建一个固定大小的线程池
ThreadPoolExecutor类
线程池实现类ThreadPoolExecutor
是Executor框架最核心的类。
构造方法
public ThreadPoolExecutor(int corePoolSize, // 线程池的核心线程数量int maximumPoolSize, // 线程池的最大线程数long keepAliveTime, // 空闲线程存活时间TimeUnit unit, // 时间单位BlockingQueue<Runnable> workQueue, // 任务队列ThreadFactory threadFactory, // 线程工厂RejectedExecutionHandler handler) // 拒绝策略
参数说明
参数 | 描述 |
---|---|
corePoolSize | 线程池的核心线程数量 |
maximumPoolSize | 线程池的最大线程数 |
keepAliveTime | 当线程数大于核心线程数时,多余的空闲线程存活的最长时间 |
unit | 时间单位 |
workQueue | 任务队列,用来储存等待执行的任务 |
threadFactory | 线程工厂,用来创建线程 |
handler | 拒绝策略,当提交的任务过多而不能及时处理时,定制策略来处理任务 |
线程池大小设置
需要根据任务类型和系统资源合理设置线程池大小:
CPU密集型任务:线程数 ≈ CPU核心数
I/O密集型任务:线程数 ≈ CPU核心数 * (1 + 平均等待时间/平均工作时间)
代码示例
// 继承Thread类
class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + ": " + i);}}
}
// 实现Runnable接口
class MyRun implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + ": " + i);}}
}
// 实现Callable接口
class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i < 100; i++) {sum += i;}return sum;}
}
// 售票示例
class Tickets extends Thread {private static int ticket = 100;
@Overridepublic void run() {while (true) {if (ticket <= 0) {break;} else {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(getName() + "卖出第" + ticket + "张票");ticket--;}}}
}
// 实现Runnable接口的售票示例
class TicketsRunnable implements Runnable {private int ticket = 100;
@Overridepublic void run() {while (true) {synchronized (this) {if (ticket <= 0) {break;} else {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "卖出第" + ticket + "张票");ticket--;}}}}
}
// 线程池任务
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "执行任务");}
}