02_基础加强_线程

概述

线程(thread)是一个程序内部的一条执行路径。

我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。

程序中如果只有一条执行路径,那么这个程序就是单线程的程序。

多线程是什么?就是指从软硬件上实现多条执行流程的技术

多线程创建

继承Thread类

Thread类:

  • Java是通过java.lang.Thread 类来代表线程的。
  • 按照面向对象的思想,Thread类提供了实现多线程的方式:
    • 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
    • 创建MyThread类的对象
    • 调用线程对象的start()方法启动线程(启动后还是执行run方法的)

优缺点:

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

Thread常用方法:

方法名称 说明
String getName() 获取当前线程的名称,默认线程名称是Thread-索引
void setName(String name) 将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称
public static Thread currentThread(): 返回对当前正在执行的线程对象的引用
public static void sleep(long time) 让当前线程休眠指定的时间后再继续执行,单位为毫秒。

实现Runnable接口

  • 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  • 创建MyRunnable任务对象
  • 把MyRunnable任务对象交给Thread处理。
  • 调用线程对象的start()方法启动线程

Thread构造器:

构造器 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 封装Runnable对象成为线程对象
public Thread(Runnable target ,String name ) 封装Runnable对象成为线程对象,并指定线程名称

优缺点:

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
  • 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。

实现Callable接口

JDK5开始支持。

  • 得到任务对象
    1. 定义类实现Callable接口,重写call方法,封装要做的事情。
    2. 用FutureTask把Callable对象封装成线程任务对象。
  • 把线程任务对象交给Thread处理。
  • 调用Thread的start方法启动线程,执行任务
  • 线程执行完毕后,通过FutureTask类的get方法去获取任务执行的结果。

FutureTask实现了Runnable接口,可直接抛给Thread调用。API:

方法名称 说明
public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象。
public V get() throws Exception 获取线程执行call方法返回的结果。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadDemo3 {
    public static void main(String[] args) {
        Callable<String> call = new MyCallable();// 3、创建Callable任务对象
        FutureTask<String> f1 = new FutureTask<>(call);// 4、把Callable任务对象 交给 FutureTask 对象
        // 5、交给线程处理
        Thread t1 = new Thread(f1);
        // 6、启动线程
        t1.start();

        try {
            // 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
            String rs1 = f1.get();
            System.out.println("结果:" + rs1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

优缺点:

  • 优点:
    • 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
    • 可以在线程执行完毕后去获取线程执行的结果。
  • 缺点:编码复杂一点。

线程安全

概述

多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题,称为线程安全问题。

如:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。

如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?

线程同步

加锁共享资源,每次只能一个线程进入访问完毕解锁后,其他线程才能进来。

同步代码块

把出现线程安全问题的核心代码给上锁。

synchronized(同步锁对象) {
	操作共享资源的代码(核心代码)
}

理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可

规范上:建议使用共享资源作为锁对象。

  • 对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象

同步方法

把出现线程安全问题的核心方法给上锁

修饰符 synchronized 返回值类型 方法名称(形参列表) {
	操作共享资源的代码
}

同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。

如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!

如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

Lock锁

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。

创建:

方法名称 说明
public ReentrantLock() 获得Lock锁的实现类对象

方法:

方法名称 说明
void lock() 获得锁
void unlock() 释放锁

线程通信

线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。

前提:线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全。

Object类的等待和唤醒方法:

方法名称 说明
void wait() 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法
void notify() 唤醒正在等待的单个线程
void notifyAll() 唤醒正在等待的所有线程

注意:上述方法应该使用当前同步锁对象进行调用。

线程池

概述

线程池就是一个可以复用线程的技术。

不使用线程池的问题:如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。

创建

JDK 5.0起提供了代表线程池的接口:ExecutorService。

ExecutorService

使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

参数一:指定线程池的线程数量(核心线程): corePoolSize,不能小于0

参数二:指定线程池可支持的最大线程数: maximumPoolSize,>= 核心线程数量

参数三:指定临时线程的最大存活时间: keepAliveTime,不能小于0

参数四:指定存活时间的单位(秒、分、时、天): unit

参数五:指定任务队列: workQueue,用于存储未分配到线程的任务,不能为null

参数六:指定用哪个线程工厂创建线程: threadFactory,不能为null

参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler,不能为null

临时线程什么时候创建呢?

新任务提交时发现核心线程都在忙任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

常用方法:

方法名称 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable 任务
Future submit(Callable task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务
void shutdown() 等任务执行完毕后关闭线程池
List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

示例:

import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 1、创建线程池对象
        ArrayBlockingQueue blockingQueue= new ArrayBlockingQueue<>(1);
        ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 2,
                6, TimeUnit.SECONDS,  blockingQueue, Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy() );

        // 2、给任务线程池处理。
        Runnable target = new MyRunnable(); // run方法中运行了sleep方法
        pool.execute(target);
        pool.execute(target);
        System.out.println(pool.getActiveCount()); // 1
        System.out.println(blockingQueue.size()); // 1
      
        pool.execute(target); // 创建临时线程
        System.out.println(pool.getActiveCount()); // 1
        System.out.println(blockingQueue.size()); // 2
 				// 不创建,拒绝策略被触发!!!
				pool.execute(target);

        // 关闭线程池(开发中一般不会使用)。
        // pool.shutdownNow(); // 立即关闭,即使任务没有完成,会丢失任务的!
        pool.shutdown(); // 会等待全部任务执行完毕之后再关闭(建议使用的)
    }
}

超出线程上限后的处理策略:

策略 详解
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常,不推荐
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 由主线程负责调用任务的run()方法从而绕过线程池直接执行

Executors

使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

方法名称 说明
public static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。
public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newSingleThreadExecutor () 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。

大型并发系统环境中存在的风险:

方法名称 存在问题
newFixedThreadPool
newSingleThreadExecutor
允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误(java.lang.OutOfMemoryError)
newCachedThreadPool
newScheduledThreadPool
创建的线程数量最大上限是Integer.MAX_VALUE, 线程数可能会随着任务1:1增长,也可能出现OOM错误

定时器

定时器是一种控制任务延时调用,或者周期调用的技术。

例如闹钟、定时邮件发送。

Timer

使用Timer类实现定时任务。

构造器 说明
public Timer() 创建Timer定时器对象

方法:

方法 说明
public void schedule(TimerTask task, long delay, long period) 开启一个定时器,按照计划处理TimerTask任务

缺点:

  • Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。
  • 可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。

ScheduledExecutorService

ScheduledExecutorService是 jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。

创建:

Executors的方法 说明
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 得到线程池对象

触发定时方法:

ScheduledExecutorService的方法 说明
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 周期调度方法

优点:基于线程池,某个任务的执行情况不会影响其他定时任务的执行。

并行与并发

正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的。

  • 并发:CPU同时处理线程的数量有限,会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
  • 并行:在同一个时刻上,同时有多个线程在被CPU处理并执行

生命周期

Java总共定义了6种状态,放在Thread类的内部枚举类中。

public class Thread{
     ...
     public enum State {
    	NEW,
    	RUNNABLE,
    	BLOCKED,
    	WAITING,
    	TIMED_WAITING,
    	TERMINATED;
    }
     ...
}
线程状态 描述
NEW(新建) 线程刚被创建,但是并未启动。
Runnable(可运行) 线程已经调用了start()等待CPU调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
posted @ 2023-07-06 17:11  水木夏  阅读(5)  评论(0编辑  收藏  举报