初识Java多线程

Java中如何创建新线程?

第一种方式:继承Thread类

  1. 写一个子类继承Thread
  2. 重写run方法
  3. 创建该类的对象,代表一个线程
  4. 调用start方法启动线程,该线程会执行run方法

这种方式的优点在于编码方式简单,但是该类已经继承了Thread类,不能继承其他类。

注意:

  1. 启动线程时一定调用start方法,而非run方法(直接调用run方法会被当成是普通方法执行,只有调用start方法才会启动一个新线程)
  2. 不能把主线程的任务放在启动子线程的语句之前(要先启动子线程,否则主线程会先跑完,相当于单线程执行)

第二种方式:实现Runnable接口

  1. 定义一个实现类实现Runnable
  2. 重写run方法
  3. 实例化该实现类任务对象Runnable target = new MyRunnable();
  4. 把任务对象交给一个线程对象处理并调用Thread对象的start方法new Thread(target).start();

这种方式的优点在于任务类只是实现接口,还是可以继续继承其他类、实现其他接口的,扩展性强。

可以使用匿名内部类的写法简化代码的写法。

// 写法一
Runnable target = new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 5; ++i) {
            System.out.println("子线程输出"+ i);
        }
    }
};
new Thread(target).start();

// 写法二(简化一)
new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 5; ++i) {
            System.out.println("子线程输出"+ i);
        }
    }
}).start();

// 写法三(简化二)
new Thread(() -> {
    for (int i = 0; i < 5; ++i) {
        System.out.println("子线程输出"+ i);
    }
}).start();

前述两种方式都存在问题:线程的任务执行完毕后无法直接返回数据,这存在局限性。jdk5.0提供了Callable接口和FutureTask类来解决这个问题。

第三种方式:使用Callable接口和FutureTask类

  1. 定义一个类实现Callable<>接口,重写call方法,定义要封装的任务以及要返回的数据
  2. 创建一个Callable对象,封装成FutureTask<>任务对象
  3. 把任务对象交给Thread线程对象,启动子线程
  4. 要获取结果时,调用任务对象的get方法

FutureTask对象是一个任务对象,实现了Runnable接口,它的作用在于,在线程执行完毕之后,调用实例的get方法可以获取任务执行结果。

这种方式的优点在于扩展性强,而且能够获得子线程执行的结果,不过编码比较复杂。

Java中的Thread类提供了针对线程对象的一些常用api,供使用者调用。

方法签名 描述
public String getName() 当前线程的名字,默认名字是Thread-索引
public void setName(String name) 为线程设置名字
public static Thread currentThread() 获取当前执行的线程对象
public static void sleep(long time) 让当前执行的进程休眠毫秒
public final void join() 当前线程阻塞直到调用join方法的线程执行完毕

线程安全问题

线程安全问题出现的原因是存在同时执行的多个线程同时访问(修改)同一个共享资源。线程同步可以用来解决线程安全问题。线程同步(加锁)的方式有三种。

第一种方式:同步代码块

把访问共享资源的代码上锁,每次只允许一个线程进入,执行完毕后自动解锁。

synchronized (锁) {
    // 访问共享资源的代码
}

建议用共享资源作为锁,比如:如果代码块在实例方法中,建议使用this作为锁;如果代码块在类的静态方法中,建议使用类名.class作为锁

第二种方式:同步方法

把访问共享资源的方法整个上锁,这种方式不需要显式指定锁(实例方法默认使用this,静态方法使用类名.class作为锁),锁的范围是整个方法代码

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

这种方式与第一种方式相比,同步代码块的方式加锁范围更小更灵活,性能比较好,但是同步方法的代码可读性更好

第三种方式:Lock锁

Lock锁是jdk5开始提供的操作,可以创建出锁对象进行加解锁,更加灵活方便。注意Lock是接口,可以使用实现类比如ReentrantLock来实例化Lock对象

// 类内声明
privat final Lock lck = new ReentrantLock();

// 使用try-catch-finally加解锁
try {
    lck.lock();
    // 业务代码,访问共享资源
} catch (Exception e) {
    // 处理异常
} finally {
    lck.unlock();
}

线程间通信和线程池

线程通信,是指多个线程共同操作共享资源时,不同线程之间通过某种方式互相通信,以相互协调

线程池,是一种可以复用线程的技术。为什么需要使用线程池以及复用线程?web应用中,用户每发起一个请求,后台就需要创建一个新线程来处理。创建新线程的开销很大,同时请求过多时会产生大量线程,会严重影响系统性能。

线程池分为两个部分:工作线程WorkThread(也叫核心线程)和任务队列WorkQueue。线程池可以指定创建一定数量的工作线程,用于处理任务队列中等待处理的任务,一个线程在处理完当前任务之后,可以接着处理队列中正在等待处理的任务。为了避免系统资源耗尽,可以限制核心线程的数量和任务队列的大小。注意,任务队列中的对象一定要实现Runnable和Callable接口,才能称为任务对象。

创建线程池第一种方式:使用ExecutorService接口的一个实现类ThreadPoolExecutor创建线程池对象

public ThreadPoolExecutor(int corePoolSize,
                         int maximumPoolSize,
                         long keepAliveTime,
                         TimeUnit unit,
                         BlockingQueue<Runnable> workQueue,
                         ThreadFactory threadFactory,
                         RejectedExecutionHandler handler)
参数 描述
corePoolSize 核心线程的数量
maximumPoolSize 最大线程数量,两个参数之差表示可以创建的临时线程的数量
keepAliceTime 临时线程的存活时间
unit 临时线程存活时间单位(秒分时天)
workQueue 线程池的任务队列
threadFactory 线程池的线程工厂,用来创建线程
handler 任务拒绝策略(线程均忙且任务队列已满的情况下如何处理新任务)
ExecutorService pool =
    new ThreadPoolExecutor(3, 5, 8,
                          TimeUnit.SECONDS,
                          new ArrayBlockingQueue<>(4),
                          Executors.defaultThreadFactory(),
                          new ThreadPoolExecutor.AbortPolicy());

临时线程创建时机:新任务提交时,核心线程都忙,任务队列已满,且此仍可以创建临时线程。拒绝新任务的时机:核心线程和临时线程都忙,且任务队列已满,新任务再提交时。

线程池处理Runnable对象/任务:

// 先定义类实现Runnable接口,重写run方法

Runnable target = new MyRunnable();
pool.execute(target);  // 线程池自动创建新线程,自动处理任务,自动执行
pool.execute(target);
pool.execute(target);  // 核心线程用满
pool.execute(target);  // 在任务队列中等待,复用核心线程
pool.execute(target);
pool.execute(target);
pool.execute(target);  // 任务队列满了
pool.execute(target);  // 开始使用临时线程
pool.execute(target);  // 临时线程用完
pool.execute(target);  // 开始拒绝新任务,这里由于AbortPolicy会抛出异常

pool.shutdown();  // 等待任务执行完毕后关闭线程池
// pool.shutdownNow();  // 直接关闭

线程池处理Callable对象/任务:

// 先定义类实现Callable<>接口,重写call方法

Future<String> f1 = pool.submit(new MyCallable(100));  // execute执行Runnable任务,submit执行Callable任务
Future<String> f2 = pool.submit(new MyCallable(200));

// 调用get方法获取执行结果
String res1 = f1.get();
String res2 = f2.get();

第二种方式:使用线程池的工具类Executors调用静态方法返回不同特点的线程池对象

方法 描述
newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为异常而结束,线程池会补充一个新线程替代
newSingleThreadExecutor() 创建只有一个线程的线程池,如果线程因异常而结束,线程池会补充一个新线程
newCachedThreadPool() 线程数量随任务增加而增加,如果线程任务执行完毕后空闲了60s会被回收
newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以在给定延迟后运行任务,或定期执行任务

这些方法实际上都是在调用ThreadPoolExecutor。

核心线程的数量应该设置为多少?无定论,建议计算密集型任务,设置为CPU核数+1;IO密集型任务,设置为CPU核数乘以二。

注意,阿里的编程规范要求,不能使用Executors的静态方法创建线程池,而需要使用ThreadPoolExecutor手动指定各项参数。这种方式明确线程池的规则和设定,避免系统资源的耗尽。

posted @ 2024-07-30 09:38  随机生成一个id  阅读(1)  评论(0编辑  收藏  举报