Java学习笔记@多线程

笔者:unirithe

日期:11/10/2021

参考资料:

JDK8在线帮助文档

Java教程视频p164-p173

线程

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

启动程序执行后,Main方法执行的是一条单独的执行路径

public static viod main(String[] args){
    // ...
}

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

多线程

  • 指软硬件上实现多条执行流程的技术

消息通信、购物系统都离不开多线程技术

多线程的创建

java.lang.Thread

public class Thread extends Object implements Runnable

当Java虚拟机启动时,通常有一个非守护进程线程(通常调用某些指定类的名为main的方法)。 Java虚拟机将继续执行线程,直到发生以下任一情况:

  • 已经调用了Runtime类的exit方法,并且安全管理器已经允许进行退出操作。
  • 所有不是守护进程线程的线程都已经死亡,无论是从调用返回到run方法还是抛出超出run方法的run

Thread类的构造器

构造器 描述
Thread(String name); 指定线程名称
Thread(Runnable target) 封装Runnable对象成为线程对象
Thread(Runnable target, String name) 综合上面的两个构造方法

方式一: 继承Thread类

实现步骤:

  • 定义子类PrimeThread继承java.lang.Thread ,重写run()方法
  • 创建PrimeThread 类的对象
  • 调用线程对象的start() 方法启动线程(启动后会执行run方法)
class PrimeThread extends Thread {
	long minPrime;
	PrimeThread(long minPrime) {
		this.minPrime = minPrime;
	}

	public void run() {
        // compute primes larger than minPrime
              . . .
    }
}

new PrimeThread().start();

优点:编程简单

缺点:存在单继承的局限性,线程类已经继承Thread,无法继承其他类,不利于扩展

Q : 为什么不直接调用 run 方法?

W: 若直接调用run方法会当成普通方法执行,此时相当于还是单线程

​ 只有调用start方法才是启动一个新的线程执行

尽量把start()方法的调用放在主线程的前面

方式二:实现Runnable接口

实现步骤:

  • 定义一个线程任务类 RunnableImpl 实现 Runnable接口 重写 run()方法
  • 创建RunnableImpl 任务对象
  • RunnableImpl 任务对象交给Thread处理
  • 调用线程对象的start() 方法启动线程
class RunnableImpl implements Runnable {
	long minPrime;
	RunnableImpl(long minPrime) {
		this.minPrime = minPrime;
	}

	public void run() {
		// compute primes larger than minPrime
		. . .
	}
}
new Thread(new RunnableImpl()).start();

优点:只实现了Runnable接口,可继续继承类和实现其他接口,扩展性强

缺点:编程多一层对象包装,如果有线程执行则结果不可以直接返回

范例:使用匿名内部类实现Runnable接口

new Thread(new Runnable(){
    @Override
    public void run(){
        //...
    }
}).start();

// Lambda表达式简化
new Thread() -> {
    @Override
    public void run(){
        //...
    }
}).start();

方式三:实现Callable接口(JDK5)

前两种方式都存在的问题:

  • 重写的run方法均不能直接返回结果

  • 不适合需要返回线程执行结果的业务场景

    可使用JDK5.0 提供的 java.util.concurrent.Callablejava.util.concurrent.FutureTask解决

FutureTask 作用:

  • 是Runnable的对象(实现了Runnable接口),可交给Thread

  • 可以在线程执行完毕后通过调用其get方法得到线程执行完成的结果

@FunctionalInterface
public interface Callable<V>
    
public class FutureTask<V> 
extends Object 
implements RunnableFuture<V>

public interface RunnableFuture<V> extends Runnable, Future<V> 

FutureTask的API

方法名称 描述
public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象
public V get() throws Exception 获取线程执行call方法返回的结果

实现步骤:

  • 定义类实现Callable接口,重写call方法
  • FutureTask 把 Callable 对象封装成线程任务对象
  • 调用Thread的start方法启动线程,执行任务
  • 线程执行完毕后,通过FutureTask的get方法去获取任务执行的结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class Demo{
    public static void main(String[] args) throws Exception {
        Callable<String> call = new CallableImpl(100);
        FutureTask<String> f1 = new FutureTask<>(call);
        Thread t1 = new Thread(f1);
        t1.start();
        System.out.println(f1.get()); // get 方法会一直等待,直到该线程结束
        System.out.println("所有线程执行完毕.");
    }
}
class CallableImpl implements Callable<String>{
    private int n ;

    public CallableImpl(int n) {
        this.n = n;
    }

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return "子线程执行的结果是: " + sum;
    }
}

优点:

  • 扩展性强,线程任务类只是实现接口,可继续继承类和实现接口
  • 可在线程执行完毕后获取线程执行的结果

缺点:

  • 编程较复杂

不同创建方式对比

方式 复杂性 扩展性 继承性 可获取结果
继承Thread 简单 单继承
实现Runnable接口 较复杂 可继承
实现Callable接口 最复杂 科技城

常用方法

方法 描述
public static Thread currentThread() 获取当前线程
void setName(String name) 设置线程的名称
getName() 获取线程的名称
public static void sleep(long time) 当前线程休眠,单位:毫秒
public void run() 线程任务方法
public void start() 线程启动方法

范例:返回主线程的名称

class Demo{
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}

范例:设置线程名称

class myThread extends Thread{
    myThread(String name){
        super(name);
    }
    public void run(){
        //...
    }
}
class Demo{
    public static void main(String[] args) {
        new myThread("子线程").start();
    }
}

线程安全问题

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

出现原因:

  • 存在多线程并发
  • 同时访问共享资源
  • 存在修改共享资源

案例:两个人有一个共同账户,余额是1万元,他们同时取钱1万

分析:

  • 账户类,表示两个人的共享账户
  • 线程类,处理账户对象
  • 创建两个线程对象,传入同一个账户对象
  • 启动两个线程,去同一个账户对象中取1万

demo.java

public class Demo{
    public static void main(String[] args) {
        Account acc = new Account("银行卡账户", 10000);
        new WithdrawThread(acc, "甲").start();
        new WithdrawThread(acc, "乙").start();
    }
}

Account.java

public class Account{
    private String cardId;
    private double money;
    public Account(){}
    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
    public void withdrawMoney(double money){
        // 获取当前线程的名称,即取钱的用户
        String name = Thread.currentThread().getName();
        if (this.money >= money) {
            // 取钱
            System.out.println(name + "成功取出: " + money + " 元");
            // 更新数据
            this.money -= money;
            System.out.println("存款剩余: " + this.money);
        } else{
            System.out.println(name +"因余额不足取钱失败. ");
        }
    }
}

WithdrawThread.java

public class WithdrawThread  extends Thread{
    private Account acc;

    public WithdrawThread(Account acc, String name) {
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
        acc.withdrawMoney(10000);
    }
}

运行结果

乙成功取出: 10000.0 元
甲成功取出: 10000.0 元
存款剩余: 0.0
存款剩余: -10000.0

线程同步

  • 为了解决线程安全问题

取钱案例出现问题的原因

  • 多个线程同时执行,发现账户余额都足够的

如何保证线程安全? 让多个线程先后依次访问共享资源。

核心思想

加锁,把共享资源上锁,每次只允许单个线程访问,访问后才解锁

方式一:同步代码块

作用:把出现线程安全问题的核心代码上锁

原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行

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

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

基于上一个取钱案例,修改Account.java的代码如下:

    public void withdrawMoney(double money){
        // 获取当前线程的名称,即取钱的用户
        String name = Thread.currentThread().getName();
        // 同步代码块
        synchronized (this) {
            if (this.money >= money) {
                // 取钱
                System.out.println(name + "成功取出: " + money + " 元");
                // 更新数据
                this.money -= money;
                System.out.println("存款剩余: " + this.money);
            } else{
                System.out.println(name +"因余额不足取钱失败. ");
            }
        }
    }

运行结果

甲成功取出: 10000.0 元
存款剩余: 0.0
乙因余额不足取钱失败.

锁对象用任意唯一的对象好坏?

任意对象即 synchronized 里的参数为任意对象

  • 坏,会影响其他无关线程的执行

锁对象的规范要求

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

  • 对于实例方法建议使用 this 作为锁对象, 即 synchronized(this)

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

    public static void run(){
        synchronized(Account.class){
            //...
        }
    }
    

总结

  • 同步代码块如何实现线程安全?

    • 对核心代码使用synchronized进行加锁
    • 每次只能一个线程占锁进入访问
  • 同步代码块的同步锁对象有什么要求?

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

方式二: 同步方法

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

原理:每次只允许单个线程进入,执行完毕后自动解锁

格式:

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

范例:public synchronized void withdrawMoney(double money)

底层原理:

  • 同步方法底层有隐式锁对象,锁的范围是整个方法代码
  • 实例方法默认使用this作为锁对象
  • 静态方法默认用类名.class作为锁对象

同步代码块锁范围 比 同步方法锁的范围更大

方式三:Lock 锁

public interface Lock

实现类:

ReentrantLock的官方使用范例:

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

作用:

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

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

  • Lock是接口不能直接实例化,则使用其实现类ReentrantLock来构建Lock锁对象

    public ReentrantLock() 获得Lock锁的实现类对象

Lock的API

方法 描述
void lock() 获得锁
void unlock() 释放锁

范例:修改取钱案例中Account的代码

public class Account{
    private final Lock lock = new ReentrantLock();
    public void withdrawMoney(double money){
        // 获取当前线程的名称,即取钱的用户
        // 上锁
        lock.lock();
        try {
            if (this.money >= money) 
                // 取钱成功
            else
                // 取钱失败
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

*线程池

线程池指一个可复用线程的技术

使用线程池以前,存在的问题:若用户没发起请求,后台就创建一个新线程来处理,等待新任务又要创建新线程,增大了开销,影响系统性能

JDK5起提供了代表线程池的接口ExecutorService

public interface ExecutorService
extends Executor

实现类:

创建线程池对象

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

构造器:

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

参数说明:

参数 说明
int corePoolSize 线程池的线程数量(核心线程) >= 0
int maximumPoolSize 线程池可支持的最大线程数,>= corePoolSize
long keepAliveTime 临时线程的最大存活时间 >= 0
TimeUnit unit 存活时间的单位(秒、分、时、天)时间单位
BlockingQueue workQueue 时间任务队列,不能为null
ThreadFactory threadFactory 创建线程的线程工厂,不能为null
RejectedExecutionHandler handler 线程忙、任务满时候,处理的策略,不能为null

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

常见问题

  • 临时线程什么时候创建

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

  • 什么时候开始拒绝任务

    核心线程和临时线程都在忙,任务队列已满,新的任务过来的时候才开始任务拒绝

线程池处理Runnable任务

实现步骤:

  • 使用 ExecutorService的方法
  • void execute(Runnable runnable)

ExecutorService 常用方法

方法 描述
void execute(Runnable command) 一般用于执行Runnable任务
Future<T> submit(Callable<T> task) 执行任务,返回未来任务对象获取线程结果,一般用于执行Callable任务
void shutdown() 等任务执行完毕后关闭线程池
List<Runnable> shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务
import java.util.concurrent.*;

class RunnableImpl implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "输出了: HelloWorld ==> "  + i);
        }
    }
}

public class ThreadPoolDemo1 {
    public static void main(String[] args) {
        ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                6,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        RunnableImpl runnable = new RunnableImpl();
        pool.execute(runnable);
        pool.execute(runnable);
        pool.execute(runnable);
    }
}

新任务拒绝策略

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

线程池处理Callable任务

实现步骤:

  • 使用 ExecutorService方法
  • Futrue<T> submit(Callable<T> command)
import java.util.concurrent.*;

class CallableImpl implements Callable<String>{
    private int n;

    public CallableImpl(int n) {
        this.n = n;
    }

    @Override
    public String call() throws Exception {
        int sum  = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + " 执行 1-" + n + " 的结果为: " + sum;
    }
}
class Demo{
    public static void main(String[] args) throws Exception {
        ExecutorService pool = new ThreadPoolExecutor(3,
                5,
                6,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        Future<String> f1 = pool.submit(new CallableImpl(100));
        Future<String> f2 = pool.submit(new CallableImpl(200));
        Future<String> f3 = pool.submit(new CallableImpl(300));
        Future<String> f4 = pool.submit(new CallableImpl(400));
        Future<String> f5 = pool.submit(new CallableImpl(500));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
        System.out.println(f5.get());
    }
}

运行结果

pool-1-thread-1 执行 1-100 的结果为: 4950
pool-1-thread-2 执行 1-200 的结果为: 19900
pool-1-thread-3 执行 1-300 的结果为: 44850
pool-1-thread-2 执行 1-400 的结果为: 79800
pool-1-thread-2 执行 1-500 的结果为: 124750

Executors 工具类实现线程池

java.util.concurrent.Executors:线程池的工具类通过调用方法返回不同类型的线程池对象

常用方法

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

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

缺陷

大型并发系统环境中使用Executors若不注意可能会出现系统风险

  • newFixedThreadPool(int nThreads)newSingleThreadExecutor() 存在的问题:

允许请求的任务队列长度为Integer.MAX_VALUE ,可能出现java.lang.OutOfMemoryError异常

  • new CachedThreadPool()newScheduledThreadPool(int corePoolSize) 存在的问题:

创建的线程数量最大上限是Integer.MAX_VALUE ,线程数可能会随着任务 1 : 1 增长,也可能出现上面的异常

阿里云Java开发手册

【强制】线程池不允许使用Executors创建,而通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

说明:Executors返回的线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadPool

    允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM异常

  • CachedThreadPool 和 ScheduledThreadPool

    允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM异常

定时器

一种控制任务延时调用,或周期调用的技术

作用:闹钟、定时邮件发送

实现方式:

  • Timer
  • ScheduledExecutorService

Timer 定时器

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

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

Timer定时器的特点和存在的问题

  • Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
  • 可能因为其中的某个任务的异常使Timer线程挂掉,从而影响后续任务执行
Timer timer = new Timer();
timer.schedule(new TimerTask() {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() +"执行了一次");
	}
}, 1000, 2000);

ScheduledExecutorService定时器

public interface ScheduledExecutorService
extends ExecutorService
  • ScheduledExecutoreService是JDK5引入的并发包,为了弥补Timer的缺陷,其内部为线程池

public static ScheduledExecutorService new ScheduledThreadPool(int corePoolSize) 得到线程池对象

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

ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);

 // 开启定时任务
pool.scheduleAtFixedRate(new TimerTask() {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "执行输出: AAA");
	}
}, 0, 2, TimeUnit.SECONDS);

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

并发、并行

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

并发的理解:

  • CPU同时处理线程的数量有限
  • CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,所以感觉是在同时执行,这就是并发

并行的理解:

  • 在同一个时刻上,同时有多个线程在被CPU处理并执行

并发: CPU分时轮询的执行线程

并行:同一个时刻同时在执行

线程的生命周期

线程的状态:线程从生到死的过程,以及中间经历的各种状态及状态转换

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

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

线程的六种状态互相转换

image

线程状态 描述
NEW(新建) 线程刚被创建,但是未被启动
Runnable(可运行) 线程已经调用了start()等待cpu调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或notifyAll方法才能够唤醒
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleepObject.wati
Timinated(被终止) 因为 run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡
posted @ 2021-11-10 18:14  Unirithe  阅读(26)  评论(0)    收藏  举报