0 基础概念:
多线程 通过 同时运行多个任务 完成 提高效率、特殊需求(同时完成)。
一个程序即一个JVM进程,一个进程内有一个执行main()的主线程main线程,可有其他多个线程。
- 并发:同一时刻,有多个指令在单个CPU上交替执行
- 并行:同一时刻,有多个任务在多个CPU上同时执行
此处CPU实际指CPU“线程参数”
1 实现方式:
有三种:
- 创建一个新类继承Thread 类,重写run()方法。
- 创建一个新类实现Runnable接口,并实现其run方法。并将该类的实例对象传至Thread类的构造函数。
- 利用Callable接口和Futuer接口,同Runnable,但还可以返回值、抛出异常。需要借助FutureTask包装器(即Futuer接口的实现类)。
1.1 继承Thread类:
- 定义一个Thread的子类
- 重写 run()方法
- 创建对象,使用 对象.strat();
例:
PrintThread.java:
public class PrintThread extends Thread {
public void run(){
for (int i =1;i<=30;i++){
System.out.println(this.getName()+":\t"+i);
}
}
}
Main:
public static void main(String[] args) {
PrintThread t1=new PrintThread();
PrintThread t2=new PrintThread();
t1.setName("MyThread 1");
t2.setName("MyThread 2");
t2.run();//只会执行方法,不会创建新线程
System.out.println(t2.isAlive());//F
t1.start();
t2.start();
System.out.println(t2.isAlive());//T
}
结果看到两条线程交替输出。
1.2 实现Runnable接口:
- 定义一个类实现Runnable接口,重写run()
- 创建自定义类对象
- 创建Thread对象,构造函数参数为自定义类的对象
例:
public class MyRun implements Runnable{
@Override
public void run() {
//获取当前线程的对象
Thread t= Thread.currentThread();
for (int i =1;i<=30;i++){
System.out.println(t.getName()+": "+i);
}
}
}
1.3 利用Callable接口和Futuer接口
- 创建自定义类实现Callable接口<泛型:表示结果类型>
- 重写call()。他有返回值,表示多线程运行的结果。
- 创建自定义类对象(表示多线程要执行的任务)
- 创建FutureTask类的对象,构造函数参数为自定义类对象(作用:管理多线程运行的结果)
- 创建Thread对象,构造函数参数为FutureTask类对象,启动。
例:
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <=100 ; i++) {
sum+=i;
}
return sum;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc=new MyCallable();
FutureTask<Integer> futureTask =new FutureTask<>(mc);
Thread t1=new Thread(futureTask);
t1.start();
System.out.println(futureTask.get());
}
1.4 三者对比:
线程实现方式 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可以扩展性较差,不能再继承其他的类 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 |
实现Callable接口 | 同上,可以获取线程结果。 | 编程相对复杂,不能直接使用Thread类中的方法 |
3 常用成员方法:
方法名称 | 说明 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒,睡眠期间会被抢走执行权 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
public static void yield() | 出让线程/礼让线程 |
public static void join() | 插入线程/插队线程 |
3.1 线程的优先度
线程调度方法
- 抢占式调度(随机)【JAVA中使用】
- 非抢占式调度(轮流)
setPriority(int newPriority) | 设置线程的优先级(1~10,默认5) |
final int getPriority() | 获取线程的优先级 |
优先级越高,抢占到的概率高
3.2 守护线程
当其他的非守护线程执行完毕以后,守护线程会 陆续 结束。
JVM退出时,不必关心守护线程是否已结束。
final void setDaemon(boolean on) | 设置为守护线程 |
4 线程的生命周期/状态
注:Java实际上并没有定义“运行”这个状态,是因为运行时JVM会把线程交给操作系统。
5 线程安全问题:
线程安全问题通常涉及到共享资源的访问和修改,如果没有适当的同步机制,就可能导致数据不一致、数据损坏、死锁等问题。
5.1 同步代码块
把操作共享数据的代码锁起来,使其面对多个线程对象,轮流执行:
关键字:s
synchronized(锁对象){ ……. }
- 锁默认打开,有一个线程进去了,锁自动关闭
- 里面的代码全部执行完毕,线程出来,锁自动打开
- 锁对象随意创建但必须唯一,常用当前类的字节码对象(类名.class)
例:
TicketWindow.java:
public class TicketWindow extends Thread{
static int ticket=1;
static int total=0;
static Object obj=new Object();//锁对象
public static void setTotal(int n){
total=n;
}
@Override
public void run(){
while (true){//注意此处条件不能为“ticket<=total”,判断条件并没有被锁住,会出现超票
synchronized (obj) {//所不能包住while,否则一个线程全部卖完了
if (ticket>total)
break;
try {
Thread.sleep(150);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName()+"卖出了第 "+ticket+" 张票");
ticket++;
}
}
}
}
Test:
public class Test {
public static void main(String[] args) {
TicketWindow t1=new TicketWindow();
TicketWindow t2=new TicketWindow();
TicketWindow t3=new TicketWindow();
TicketWindow.setTotal(25);
t1.setName("第一售票口");
t2.setName("第二售票口");
t3.setName("第三售票口");
t1.start();
t2.start();
t3.start();
}
}
5.2 同步方法
把synchronized关键字加到方法上
- 同步方法锁住方法内所有代码
- 锁对象不能自己指定
- 非静态:this
- 静态:当前类的字节码文件对象
5.3 Lock锁
上文的锁是自动打开/关闭的,想要手动开关锁,要使用Lock接口的实现类ReentrantLock来创建锁对象。
- void lock() 上锁
- void unlock() 释放锁
6 死锁
哲学家进餐问题
避免锁的嵌套
7 生产者和消费者(等待唤醒机制)
生产者消费者模式是一个经典的多线程协作模式。
生产者线程负责生产数据并将其存储到共享缓冲区,消费者线程则从缓冲区中取出数据进行消费。
等待唤醒机制:是解决生产者和消费者问题的关键机制。当缓冲区已满时,生产者线程需要等待(进入阻塞状态),直到有消费者线程从缓冲区中取出数据,释放出空间后,生产者线程才能被唤醒继续生产;同理,当缓冲区为空时,消费者线程需要等待(进入阻塞状态),直到有生产者线程生产数据放入缓冲区后,消费者线程才能被唤醒继续消费。
常见方法: 用锁对象去调用而非线程对象
- void wait(); 当前线程等待,直到被其他线程唤醒
- void notify(); 随机唤醒单个线程
- void notifyAll() 唤醒所有线程(常用 )
例:
- Space.java
public class Space {
//缓冲区是否为空
public static int flag=0;
//消费者最大消耗次数
public static int Count=20;
//锁对象
public static Object lock=new Object();
}
- Producer.java:
public class Producer extends Thread{
private void produce(){
System.out.println("生产者生产了资源");
Space.flag=1;
}
@Override
public void run(){
while (true){
synchronized (Space.lock){
if (Space.Count==0){//共享数据消耗次数达到上限
break;
}else{
if(Space.flag!=0){//缓冲区不空,则wait
try {
Space.lock.wait();//隐含让当前线程与这个锁绑定
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{//缓冲区空,生产,并将缓冲区状态改为不空
produce();
Space.lock.notifyAll();
}
}
}
}
}
}
- Consumer.java:
public class Consumer extends Thread{
private void consume(){
Space.Count--;
System.out.println("消费者消耗了一次。还可消耗 "+Space.Count+" 次");
Space.flag=0;
}
@Override
public void run() {
while (true){
synchronized (Space.lock){
if(Space.Count==0){//共享数据消耗次数达到上限
break;
}else{
if(Space.flag==0){//缓冲区空,则wait
try {
Space.lock.wait();//隐含让当前线程与这个锁绑定
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{//缓冲区不空,消费,并将缓冲区状态改为空
consume();
Space.lock.notifyAll();
}
}
}
}
}
}
Test.java:
public static void main(String[] args) {
Producer p=new Producer();
Consumer c=new Consumer();
c.start();
p.start();
}
阻塞队列:
ArrayBlockingQueue
LinkedBlockingQueue
put(E e)
take()
8 线程池
频繁创建和销毁大量线程需要消耗大量时间,所以有了线程池:通过复用、管理一组线程来完成任务队列。
核心原理:
- 创建一个线程池,线程池中初始为空
- 提交任务时,线程池会复用已有的线程对象去完成任务;任务执行完毕,线程归还给池子。如果池子中没有空闲线程,可以创建新线程(不超过预订数量)去完成任务。
- 如果提交任务时,线程池中没有空闲线程且无法创建新线程,任务就会排队等待。
代码实现:
- 创建线程池 ExecutorService
- 提交任务
- 所有任务全部执行完毕,关闭线程池。
例:
public static void main(String[] args) {
//1.创建池子
ExecutorService pool1=Executors.newFixedThreadPool(3);
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.关闭池子(实际服务一般不会手动关闭)
pool1.shutdown();
}
线程池容量最大为三。提交了五个任务,三个线程完成。
8.1 自定义线程池:
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
该方法有七个参数:
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
参数 | 描述 |
---|---|
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 空闲线程存活时间 |
unit | 时间单位 |
workQueue | 任务队列:LinkBlockingQuene(可变)、 ArrayBlockingQuene(不可变) |
threadFactory | 线程工厂:指定线程如何生产 |
handler | 任务拒绝策略:线程池已达最大容量且线程都在工作、任务队列也达到最大容量(四种拒绝策略) |
四种任务拒绝策略:
任务拒绝策略 | 描述 |
---|---|
AbortPolicy | 默认的拒绝策略,抛出 RejectedExecutionException 异常 |
DiscardPolicy | 直接丢弃任务、且不抛出异常(不推荐) |
DiscardOldestPolicy | 丢弃处于任务队列头部的任务,添加被拒绝的任务 |
CallerRunsPolicy | 调用任务的 run(),绕过线程池直接运行 |
例:
public class MyPoolTest {
public static void main(String[] args) {
//自定义线程池对象
ThreadPoolExecutor pool=new ThreadPoolExecutor(
3,//核心线程数
10,//最大线程数
60,//空闲线程最大存活时间
TimeUnit.SECONDS,//单位
new ArrayBlockingQueue<>(3),//任务队列(限定长度为三)
Executors.defaultThreadFactory(),//默认线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略(注意是一个内部类)
);
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.shutdownNow();
}
}