Java SE 学习笔记(十五)—— 多线程(1)

1 实现多线程

1.1 多线程相关概念


并行与并发:

  • 并行:在同一时刻,有多个指令在多个CPU上同时执行。
  • 并发:在同一时刻,有多个指令在单个CPU上交替执行。

进程与线程:

  • 进程:是正在运行的程序
    • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
    • 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
    • 并发性:任何进程都可以同其他进程一起并发执行
  • 线程:是进程中的单个顺序控制流,是一条执行路径
    • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
    • 多线程:一个进程如果有多条执行路径,则称为多线程程序

多线程是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。

1.2 多线程的实现方式

1.2.1 继承Thread类


Java是通过java.lang.Thread类来代表线程的

示例代码

public class ThreadDemo {
    public static void main(String[] args) {
        // 3. 创建 MyThread 类的对象
        MyThread t = new MyThread();
        // 4. 启动线程
        t.start();
        
        // 注意:以下代码块不要写在启动子线程之前
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程" + i);
        }
    }
}

// 1. 定义一个类 MyThread 继承 Thread 类
class MyThread extends Thread {
    // 2. 重写run()方法,里面定义线程要干嘛
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程" + i);
        }
    }
}

疑问:

  • 可以把主线程任务放在子线程任务之前吗?
    • 不可以, 这样主线程一直是先跑完的,相当于一个单线程的效果
  • 为什么要重写run()方法?
    • 因为run()是用来封装被线程执行的代码
  • run()方法和strat()方法的区别?
    • run():封装线程执行的代码,直接调用,相当于普通方法的调用,表示的仅仅是创建对象,用对象去调用方法,并没有开启线程
    • start():启动一个新的线程,然后由JVM调用此线程的run()方法

继承Thread类方式创建线程的优缺点:

  • 优点:编码简单,可以直接使用Thread类中的方法
  • 缺点:存在单继承的局限性,线程类继承 Thread 后,不能继承其他类,不便于扩展。

1.2.2 实现Runnable接口


Thread 构造方法

在这里插入图片描述

示例代码

public class ThreadDemo {
    public static void main(String[] args) {
        // 3. 创建MyRunnable类的【任务对象】
        MyRunnable target = new MyRunnable();
        // 4. 把MyRunnable类任务对象交给【线程对象】Thread
        Thread t = new Thread(target);
        // 5. 启动线程
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程" + i);
        }
    }
}

// 1. 定义一个线程任务类 MyRunnable 实现 Runnable 接口
class MyRunnable implements Runnable {
    // 2. 重写run()方法,里面定义线程要干嘛
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程" + i);
        }
    }
}

实现 Runnable 接口方式创建线程的优缺点:

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
  • 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的(第一种方式也一样,run方法是无返回值的)。

匿名内部类形式

public class ThreadDemo {
    public static void main(String[] args) {
        // 1. 创建Runnable类的匿名内部类任务对象
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程" + i);
                }
            }
        };
        // 2. 把Runnable类任务对象交给线程对象Thread
        Thread t = new Thread(target);
        // 3. 启动线程
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程" + i);
        }
    }
}

1.2.3 实现Callable接口


前 2 种线程创建方式都存在一个问题:

  • 它们重写的 run 方法均不能直接返回结果。不适合需要返回线程执行结果的业务场景

JDK 5.0 提供了 Callable 和 FutureTask 来实现。这种方式的优点是:可以得到线程执行的结果

FutureTask 的API

在这里插入图片描述

示例代码

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3. 创建Callable类的任务对象
        Callable<String> call1 = new MyCallable(100);
        // 4. 把Callable类的任务对象交给FutureTask对象
        // FutureTask对象作用
        //     - 是Runnable的对象(实现了Runnable接口),可以交给Thread了
        //     - 可以在线程执行完毕后通过调用其get方法得到线程执行完成的结果
        FutureTask<String> f1 = new FutureTask<>(call1);
        // 5. 交给线程处理
        Thread t1 = new Thread(f1);
        t1.start();

        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();
		// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才执行结果
        String s1 = f1.get();
        System.out.println("第一个结果是:" + s1);
		// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才执行结果
        String s2 = f2.get();
        System.out.println("第二个结果是:" + s2);

    }
}

// 1. 定义一个类MyCallable实现Callable接口
// 应该声明线程任务执行完成后的结果的数据类型(泛型中应该声明线程任务执行完毕后返回结果的类型)
class MyCallable implements Callable<String> {
    private int n;

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

    // 2. 重写call()方法,封装要做的事情
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程执行的结果为:" + sum;
    }
}

实现Callable接口方式创建线程的优缺点

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

三种实现方式的对比:

在这里插入图片描述

2 Thread常用方法


  1. Thread 的构造器

在这里插入图片描述

  1. Thread 获取和设置线程名称

在这里插入图片描述

示例代码

public class ThreadDemo {
    // main方法是由主线程负责调度的
    public static void main(String[] args) {
        // 也可以通过构造器直接给线程命名,MyThread类中要有对应的有参构造方法
        MyThread t1 = new MyThread("1号");
//        t1.setName("1号");
        t1.start();
        System.out.println(t1.getName());

        MyThread t2 = new MyThread("2号");
//        t2.setName("2号");
        t2.start();
        System.out.println(t2.getName());

        // 哪个线程执行它,它就得到哪个线程对象(当前线程对象)
        Thread m = Thread.currentThread();
        System.out.println(m.getName());

        // 主线程任务
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:"+i);
        }
    }
}

class MyThread extends Thread{

    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    // 子线程任务
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
        }
    }

}

  1. Thread 类获得当前线程的对象(静态方法)

在这里插入图片描述

注意:

  • 此方法是 Thread 类的静态方法,可以直接使用 Thread 类调用。
  • 这个方法是在哪个线程执行中调用的,就会得到哪个线程对象
  1. Thread 类的线程休眠方法(静态方法)

在这里插入图片描述

示例代码

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:"+i);
            if(i==3){
            	// 主线程休眠3秒钟之后继续执行
                Thread.sleep(3000);
            }
        }
    }
}

3 线程安全


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

取钱业务模拟

  • 需求:

    • 小明和小红是一对夫妻,他们有一个共同的账户,余额是 10 万元,模拟 2 人同时去取钱 10 万。
  • 分析:

    • 需要提供一个账户类,创建一个账户对象代表 2 个人的共享账户。
    • 需要定义一个线程类,线程类可以处理账户对象。
    • 创建 2 个线程对象,传入同一个账户对象。
    • 启动 2 个线程,去同一个账户对象中取钱 10 万。

账户类

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 drawMoney(double money) {
        // 判断是谁来取钱
        String name = Thread.currentThread().getName();
        // 判断账户是否够钱
        if (this.money>=money){
            // 取钱
            System.out.println(name+"取走了"+money+"元");
            // 更新余额
            this.money-=money;
            System.out.println(name+"取走钱后剩余"+this.money+"元");
        }else {
            // 余额不足
            System.out.println("抱歉!"+name+",余额不足!");
        }
    }
}

取钱线程类

// 取钱的线程类
public class DrawThread extends Thread{
    // 接收处理的账户对象
    private Account account;

    public DrawThread(Account account, String name) {
        super(name); // *
        this.account=account;
    }

    @Override
    public void run() {
        // 取钱操作
        account.drawMoney(100000);

    }
}

测试类

public class ThreadDemo {
    public static void main(String[] args){
        // 1. 创建共享账户
        Account account = new Account("ABC123", 100000);
        // 2. 创建两个线程对象,代表小明和小红同时进来取钱了
        new DrawThread(account,"小明").start();
        new DrawThread(account,"小红").start();
    }
}

一种可能得输出:

小红取走了100000.0元
小明取走了100000.0元
小红取走钱后剩余0.0元
小明取走钱后剩余-100000.0元

线程安全问题出现的原因?

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

4 线程同步


线程同步正是为了解决线程安全问题,即加锁,让多个线程实现先后依次访问共享资源,这样就保证了线程安全。

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

在这里插入图片描述

加锁的方式常见的有同步代码块、同步方法。

4.1 同步代码块


同步代码块的作用是把出现的线程安全问题的核心代码上 ,原理是每次只能有一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

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

同步代码块格式:

synchronized(任意对象) { 
	操作共享资源的代码(核心代码)
}

注意:

  • synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
  • 默认情况下,锁是打开的,只要有一个线程进去了,锁就会关闭,当线程执行完出来了,锁才会自动打开

锁对象的规范要求:

  • 锁对象只要对于当前同时执行的线程来说是同一个对象即可,但建议使用共享资源作为锁对象。
  • 对于实例对象建议使用this作为锁对象。(this表示当前类的对象)
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象

在上述取钱例子中,使用同步代码块上锁,只需修改账户类

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;
    }

// 对于静态方法建议使用字节码(类名.class)对象作为锁对象
//    public static void test(){
//        synchronized (Account.class);
//    }

    public void drawMoney(double money) {
        // 判断是谁来取钱
        String name = Thread.currentThread().getName();
        // 同步代码块
        // 对于实例对象建议使用this作为锁对象
        synchronized (this) {
        //synchronized ("aaa") { // 锁对象用任意唯一的对象并不好,因为这会影响其他无关线程的执行(可能会出现一把锁锁住千家万户的情况)
            // 判断账户是否够钱
            if (this.money>=money){
                // 取钱
                System.out.println(name+"取走了"+money+"元");
                // 更新余额
                this.money-=money;
                System.out.println(name+"取走钱后剩余"+this.money+"元");
            }else {
                // 余额不足
                System.out.println("抱歉!"+name+",余额不足!");
            }
        }
    }
}

快捷方式:选中要上锁的代码块,ctrl+alt+t选择synchronized

4.2 同步方法


同步方法作用是把出现线程安全问题的核心方法给上锁。其原理是每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

就是把 synchronized 关键字加到方法上

同步方法的格式:

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

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  • 如果方法是实例方法:同步方法默认用 this 作为的锁对象。但是代码要高度面向对象!
  • 如果方法是静态方法:同步方法默认用类名 .class 作为的锁对象

是同步代码块好还是同步方法好一点?

  • 同步代码块锁的范围更小,同步方法锁的范围更大,所以同步代码块更好一点,但由于同步方法更方便,用的也更多。

同步的好处和弊端

  • 好处:解决了多线程的数据安全问题
  • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

4.3 Lock锁


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

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

在这里插入图片描述
Lock 实现提供比使用 synchronized 方法和语句可以获得更广泛的锁定操作。

在这里插入图片描述

在上述取钱例子中,使用 Lock锁上锁,只需修改账户类

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
    private String cardId;
    private double money;
    // 唯一不可替换
    private final Lock lock = new ReentrantLock();

    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 drawMoney(double money) {
        // 判断是谁来取钱
        String name = Thread.currentThread().getName();
        lock.lock();
            // 判断账户是否够钱
        try {
            if (this.money>=money){
                // 取钱
                System.out.println(name+"取走了"+money+"元");
                // 更新余额
                this.money-=money;
                System.out.println(name+"取走钱后剩余"+this.money+"元");
            }else {
                // 余额不足
                System.out.println("抱歉!"+name+",余额不足!");
            }

        } finally {
            lock.unlock(); // 避免前面报错,导致无法开锁,其他线程无法执行
        }
    }
}

5 线程池(重点)

5.1 线程池概述


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

不使用线程池的问题

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

5.2 实现线程池


如何得到线程池对象呢?

方式一:JDK 5.0 起提供了代表线程池的接口: ExecutorService,使用 ExecutorService的实现类ThreadPoolExcutor创建线程池对象。

ThreadPoolExecutor 构造器的参数说明

在这里插入图片描述

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

常见面试题

  • 临时线程什么时候创建?
    • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  • 什么时候会开始拒绝任务?
    • 核心线程临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。

5.3 线程池处理 Runnable 任务


ExecutorService 的常用方法

在这里插入图片描述

新任务拒绝策略

在这里插入图片描述

示例代码

Runnable 任务类

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"输出了:"+i);
        }
        try {
            Thread.sleep(1000000);
            System.out.println(Thread.currentThread().getName()+"休眠了");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

线程池处理任务

import java.util.concurrent.*;

public class ThreadPool {
    public static void main(String[] args) {
        // 创建线程池对象
/*        ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler)*/
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        // 给任务让线程池处理
        Runnable target = new MyRunnable();
        // 3个核心线程执行
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 任务队列里5个任务等待
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 创建2个临时线程
        pool.execute(target);
        pool.execute(target);
        // 3个核心线程和2个临时线程都在忙,且占据了线程池最大容量,且任务队列已满,拒绝新任务
        pool.execute(target);

        // 关闭线程(开发一般不会使用)
//        pool.shutdownNow(); // 立即关闭,会丢失任务
//        pool.shutdown(); // 会等待任务全部完成后关闭
    }
}

5.4 线程池处理 Callable 任务


示例代码

Callable 任务类

/**
    1、定义一个任务类 实现Callable接口  应该申明线程任务执行完毕后的结果的数据类型
 */
public class MyCallable implements Callable<String>{
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    /**
       2、重写call方法(任务方法)
     */
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n ; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()
                + "执行 1-" + n+ "的和,结果是:" + sum;
    }
}

线程池处理任务

/**
    目标:自定义一个线程池对象,并测试其特性。
 */
public class ThreadPool2 {
    public static void main(String[] args) throws Exception {
        // 1、创建线程池对象
        /**
         public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                                 RejectedExecutionHandler handler)
         */
        ExecutorService pool = new ThreadPoolExecutor(3, 5 ,
                6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5) , Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy() );

        // 2、给任务线程池处理。
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));
        Future<String> f5 = pool.submit(new MyCallable(500));

//        String rs = f1.get();
//        System.out.println(rs);

        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());
    }
}

5.5 Executors工具类实现线程池


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

Executors 得到线程池对象的常用方法

在这里插入图片描述

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

示例代码

/**
    目标:使用Executors的工具方法直接得到一个线程池对象。
 */
public class ThreadPoolDemo3 {
    public static void main(String[] args) throws Exception {
        // 1、创建固定线程数据的线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);

        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable()); // 已经没有多余线程了
    }
}

Executors使用可能存在的陷阱:大型并发系统环境中使用Executors如果不注意可能会出现系统风险。

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值