Java - 多线程

多线程

概念

线程概念

  • 线程(Thread)是一个程序内部的一条执行流程
public static void main(String[] args){
	//代码块
	...
}
  • 程序中如果只有一条执行流程,那这个程序就是单线程的程序

多线程概念

  • 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)

多线程应用场景:12306、百度网盘(上传、下载)、消息通信、淘宝等都离不开多线程技术

创建多线程

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

多线程的创建方式一:继承Thread类

  1. 定义一个MyThread继承线程类java.lang.Thread,重写Thread的run()方法(将希望这个线程执行的任务写入)
  2. 创建MyThread类的对象
  3. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
/**
 * 1.让子类继承Thread线程类
 */
public class Mythread extends Thread{
    //2.1必须重写Thread类的run方法
    @Override
    public void run() {
        //描述该子线程的执行任务
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程Mythread输出:" + i);
        }
    }
}
/**
 * 目标:掌握线程的创建方式一:继承Thread类
 */
public class ThreadTest1 {
    //main方法是由一条默认的主线程负责执行
    public static void main(String[] args) {
        //3.创建Mythread线程类的对象代表一个子线程
        Thread t = new Mythread();
        //4.启动线程(自动执行run方法)
        t.start();//main主线程 t子线程

        //两条线程并发执行,每次运行程序顺序不一定跟之前的顺序相同
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

方法一优缺点:

  • 优点:编码简单
  • 缺点:线程类已经继承Thread类,而Java是单继承,无法继承其他类,不利于功能的扩展

多线程的创建方式二:实现Runnable接口

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法

  2. 创建MyRunnable任务对象

  3. 将MyRunnable任务对象交给Thread处理

    public Thread(Runnable target);
    封装Runnable对象成为线程对象
    
  4. 调用线程对象的start()方法启动线程

方式二的优缺点

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
  • 缺点:需要多一个Runnable对象(??这也算缺点嘛
/**
 * 1.创建一个任务类继承Runnable接口
 */
public class MyRunnable implements Runnable{
    //2.重写Runneable接口的run方法
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程输出:" + i);
        }
    }
}
public class ThreadTest2 {
    public static void main(String[] args) {
        //3.创建一个任务类对象
        Runnable target = new MyRunnable();
        //4.将任务类对象作为参数new一个线程类对象
        //  public Thread(Runnable target)
        new Thread(target).start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

线程创建方式二的匿名内部类写法

  1. 可以创建Runnable的匿名内部类对象(不用再创建MyRunnable类实现Runnable)
  2. 再交给Thread线程对象
  3. 再调用线程对象的start()启动线程
public class ThreadTest2_2 {
    public static void main(String[] args) {
        //1.直接创建Runnable接口的匿名内部类形式(任务对象)
        //使用匿名内部类创建一个Runnable任务对象
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程1输出:" + i);
                }
            }
        };
        new Thread(target).start();

        //简化形式1
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();

        //简化形式2 Lambda表达式
        new Thread(() -> {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程3输出:" + i);
                }
        }).start();


        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

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

  • 假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果
  • 解决:JDK5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)
    • 这种方法最大的优点:可以返回线程执行完毕后的结果

多线程的第三种创建方式:利用Callable接口、FutureTask类来实现

  1. 创建任务对象

    a. 定义一个类实现Callable接口(泛型接口),重写call方法,封装要做的事情,和要返回的数据

    b. 把Callable类型的对象封装成FutureTask(线程任务对象,FutureTask类实现了Runnable接口)

  2. 把线程任务交给Thread对象

  3. 调用Thread对象的start方法启动线程

  4. 线程执行完毕后,通过FutureTask对象的get方法去获取线程任务执行的结果

在这里插入图片描述

import java.util.concurrent.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 {
        //计算从1-n的和
        int sum = 0;
        for (int i = 1; i <= n ; i++) {
            sum += i;
        }
        return "从1-" + n + "的和为:" + sum;
    }
}
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * 目标:掌握吸纳成都创建方式三:实现Callable接口
 */
public class ThreadTest3 {
    public static void main(String[] args) throws Exception {
        //3.new一个Callable接口实现类的对象
        Callable<String> call1 = new MyCallable(100);

        //4.将Callable接口实现类的对象封装成FutureTask对象(FutureTask继承Runnable,所以本质是任务对象)
        // TODO 未来任务对象的作用?
        //  a.是一个任务对象,实现了Runnable接口,可封装为一个Thread线程对象
        //  b.可以在线程执行完毕之后,用未来任务对象调用get方法获取执行完毕后的结果
        FutureTask<String> f1 = new FutureTask<>(call1);

        //5.将FutureTask任务对象封装成一个Thread线程对象,调用start方法开始执行线程
        new Thread(f1).start();

        //再来一组实验
        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2 = new FutureTask<>(call2);
        new Thread(f2).start();

        //6.获取线程执行完毕后的返回结果
        // 注意:如果执行到这,加入上面的线程还没有执行完毕
        // 这里的代码会先暂停,等待上面线程执行完毕后才会获取结果
        String rs = f1.get();
        System.out.println(rs);

        String rs2 = f2.get();
        System.out.println(rs2);

    }
}

线程创建方式三的优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
  • 缺点:编码较复杂

多线程的注意事项

  1. 启动线程必须是调用start方法,不是调用run方法
    • 直接调用run方法会当成Java对象调用方法执行,此时相当于还是单线程执行
    • 只有调用start方法才是启动一个新的线程执行
  2. 不要把主线程任务放在启动子线程之前
    • 这样主线程一直是先跑完的,相当于是一个单线程的效果了

Thread的常用方法

image-20230926160120229

public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t1 = new MyThread("1号线程");
//        t1.setName("1号线程");
        t1.start();
        System.out.println(t1.getName());

        Thread 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 = 1; i <= 3; i++) {
            System.out.println(m.getName() + "主线程输出:" + i);
        }
        
        
        for (int i = 1; i <= 5; i++) {
            System.out.println(i);
            if (i == 3){
                //会让当前执行的线程暂停5秒,再继续执行
                Thread.sleep(5000);
            }
        }

        //join方法作用:让当前调用这个方法的线程先执行完
        Thread t1 = new MyThread("1号线程");
        t1.start();
        t1.join();


        Thread t2 = new MyThread("2号线程");
        t2.start();
        t2.join();

        Thread t3 = new MyThread("3号线程");
        t3.start();
        t3.join();
    }
}

Thread类还提供了诸如: yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会后续需要用到的时候提及

线程安全

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

取钱的线程安全问题

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        //1.创建一个账户对象,代表两个人的共享账户
        Account account = new Account("12345678","87654321",100000);

        //2.创建两个线程,分别代表 小明、小红,再去同一个账户取钱100000
        new d3_thread_safe.MyThread(account,"小明").start();

        new d3_thread_safe.MyThread(account,"小红").start();

//        System.out.println(account.getMoney());//运行的比两个线程要快,把这个放到线程的run方法体现

        //再来一组
        Account account1 = new Account("123456789","987654321",100000);

        new MyThread(account1,"小黑").start();
        new MyThread(account1,"小白").start();
    }
}

----------------------------------------------------
public class MyThread extends Thread{

    private Account account;
    public MyThread(Account account,String name){
        super(name);//给当前线程对象命名
        this.account = account;
    }
    @Override
    public void run() {
           account.drawMoney(100000);
    }
}

----------------------------------------------------
    public class Account {
    private String cardCode;
    private String password;
    private double money;

    public Account() {
    }

    public Account(String cardCode, String password, double money) {
        this.cardCode = cardCode;
        this.password = password;
        this.money = money;
    }

    public String getCardCode() {
        return cardCode;
    }

    public void setCardCode(String cardCode) {
        this.cardCode = cardCode;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    //小明、小红同时过来
    public void drawMoney(double money) {
        //1.取到当前线程对象的名字
        Thread t = Thread.currentThread();
        String name = t.getName();
            //2.判断余额够不够
            if (this.money >= money){
                System.out.println(name + "取了100000元成功~");
                this.money -= money;
                System.out.println(name + "取钱后账户余额剩余" + this.money);
            } else {
                System.out.println(name + "取钱失败...");
            }
    }
}

线程同步

  • 用来解决线程安全问题的方案

线程同步的思想

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题

线程同步的常见方案

  • 加锁: 每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能加锁进来

同步代码块

  • 作用:把访问共享资源的核心代码给上锁,以此保证线程安全
synchronized(同步锁){
	访问共享资源的核心代码
}
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

针对取钱问题的同步代码块的部分代码:

public void drawMoney(double money) {
    //1.取到当前线程对象的名字
    Thread t = Thread.currentThread();
    String name = t.getName();
    //实例方法使用共享资源上锁!此时this正好为共享资源
    synchronized (this) {
        //2.判断余额够不够
        if (this.money >= money){
            System.out.println(name + "取了100000元成功~");
            this.money -= money;
            System.out.println(name + "取钱后账户余额剩余" + this.money);
        } else {
            System.out.println(name + "取钱失败...");
        }
    }
}

同步锁的注意事项

  • 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug

锁对象随便选择一个唯一的对象好不好呢?

  • 不好,会影响其他无关线程的执行。

锁对象的使用规范

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

同步方法

  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
修饰符 synchronized 返回值类型 方法名称(形参列表){
	操作共享资源的代码
}
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行

同步方法底层原理

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

针对取钱问题的同步方法的部分代码:

public synchronized void drawMoney(double money) {
    //1.取到当前线程对象的名字
    Thread t = Thread.currentThread();
    String name = t.getName();
        //2.判断余额够不够
        if (this.money >= money){
            System.out.println(name + "取了100000元成功~");
            this.money -= money;
            System.out.println(name + "取钱后账户余额剩余" + this.money);
        } else {
            System.out.println(name + "取钱失败...");
        }
}

Lock锁

  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
public ReentrantLock()  
获取Lock锁的实现类对象

Lock的常用方法

void lock();//加锁
void unlock();//释放锁

针对取钱问题的Lock锁的部分代码

public class Account{
//创建了一个锁对象
private Lock lk = new ReentrantLock();

public class Account{
    //创建一个Lock锁对象
    private Lock l = new ReentrantLock();
    
    //小明、小红同时过来
    public void drawMoney(double money) {
        //1.取到当前线程对象的名字
        Thread t = Thread.currentThread();
        String name = t.getName();

        //2.判断余额够不够
        try {
            lk.lock();//加锁
            if (this.money >= money){
                System.out.println(name + "取了100000元成功~");
                this.money -= money;
                System.out.println(name + "取钱后账户余额剩余" + this.money);
            } else {
                System.out.println(name + "取钱失败...");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lk.unlock();//解锁
        }
    }
}

线程通信

  • 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者与消费者模型)

  • 生产者线程负责生产数据
  • 消费者线程负责消费生产者生产的数据。
  • 注意:生产者生产完数据应该等待自己,通知消费者消费(其实应该先唤醒别人,再等待自己);消费者消费完数据也应该等待自己,再通知生产者生产!

**ps:**线程通信的前提是一定要保证线程安全!!!

image-20230926162753533

需求:3个生产者线程(负责生产包子),每个线程每次只能生产一个放在桌子上;2个消费者线程(负责吃包子),每个线程每次只能吃桌子上的一个包子

public class ThreadTest {
    public static void main(String[] args) {
        //需求:3个生产者线程(负责生产包子),每个线程每次只能生产一个放在桌子上
        //     2个消费者线程(负责吃包子),每个线程每次只能吃桌子上的一个包子

        //创建一个桌子对象
        Desk desk = new Desk();

        //创建3个生产者线程(3个厨师)
        new Thread(() -> {
                while(true){
                    desk.put();
                }
            },"厨师1").start();

        new Thread(() -> {
                while(true){
                    desk.put();
                }
            },"厨师2").start();

        new Thread(() -> {
            while(true){
                desk.put();
            }
        },"厨师3").start();

        //创建2个消费者线程(2个吃货)
        new Thread(() -> {
            while(true){
                desk.get();
            }
        },"吃货1").start();

        new Thread(() -> {
            while(true){
                desk.get();
            }
        },"吃货2").start();
    }
}
import java.util.ArrayList;
import java.util.List;

public class Desk {
    private List<String> list = new ArrayList<>();

    public synchronized void put() {
        try{
            String name = Thread.currentThread().getName();
            //判断有没有包子
            if(list.size() == 0){
                list.add(name + "做的包子");
                System.out.println(name + "做了一个肉包子");
                Thread.sleep(2000);

                //唤醒别人,等待自己
                this.notifyAll();
                this.wait();
            } else{
                //有包子了,不用做了
                //唤醒别人,等待自己
                this.notifyAll();
                this.wait();
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    public synchronized void get() {
        try {
            String name = Thread.currentThread().getName();
            if (list.size() == 0){
                //没包子
                //唤醒别人,等待自己
                this.notifyAll();
                this.wait();
            } else{
                //有包子 可以吃
                System.out.println(name + "吃了" + list.get(0));
                list.clear();
                Thread.sleep(1000);
                //唤醒别人,等待自己
                this.notifyAll();
                this.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程池

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

不使用线程池的问题

  • 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新仕务来了肯定又要创建新线任处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。

线程池工作原理

线程池不会产生过多的线程,可以固定线程的数量,重复利用这些线程去处理任务;也可以控制任务数量,将任务暂时缓存起来,让线程处理,因此线程池不会因为线程过多或任务过多,导致占用系统资源过多而导致系统瘫痪,从而提高系统工作性能

image-20230926163206567

ExecutorService

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

如何得到线程池对象?

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

image-20230926163641809

import java.util.concurrent.*;

/**
 * 目标:掌握线程池的创建
 */
public class ThreadPoolTest1 {
    public static void main(String[] args) {
        //1.通过ThreadPoolExecutor创建一个线程池对象
        /*public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                                 RejectedExecutionHandler handler)*/
        ExecutorService pool = new ThreadPoolExecutor(3,5,8,
                TimeUnit.SECONDS,new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

      
    }
}

image-20230926164636390

线程池的注意事项

  1. 临时线程什么时候创建?
    新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  2. 什么时候会开始拒绝新任务?
    核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象(本质还是调用线程池的实现类ThreadPoolExecutor创建线程池对象,不推荐此方法)。

image-20230926163940068

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolTest3 {
    public static void main(String[] args) {
        //1.通过Executors创建一个线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(17);
        // Q:核心线程的数量到底配置多少呢??
        // 计算密集型的任务:核心线程数量 = CPU的核数 + 1 //计算一些东西
        // IO密集型的任务: 核心线程数量 = CPU核数 * 2 //文件、通信等
    }
}

image-20230926164022176

ExecutorService的常用方法

image-20230926164610620

a.执行Runnable任务:

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

        Runnable target = new MyRunnable();
        pool.execute(target);//线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(target);//线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(target);//线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        //从此处开始新增的任务暂时存入任务队列(4个)
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        //此时任务队列已满,且三个核心线程在执行,需要创建临时线程
        pool.execute(target);
        pool.execute(target);
        //此时任务队列已满,且三个核心线程和两个临时线程都在执行,需要拒绝任务对象
        pool.execute(target);


        pool.shutdown();//等线程池的任务全部执行完毕后,再关闭线程池
//        pool.shutdownNow();//立即关闭线程池!不管任务是否执行完毕
    }
}

b.执行Callable任务

public class ThreadPoolTest2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.通过ThreadPoolExecutor创建一个线程池
        /*public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                                 RejectedExecutionHandler handler)*/
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        //2.使用线程处理Callable(任务?)对象,并返回一个未来任务类对象,可以得到线程执行完毕后的返回值
        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));//复用

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

并发、并行、生命周期

进程

  • 正在运行的程序(软件)就是一个独立的进程。
  • 线程是属于进程的,一个进程中可以同时运行很多个线程。
  • 进程中的多个线程其实是并发和并行执行的。

并发

  • 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

image-20230926215659780

并行

  • 在同一时刻,同时有多个线程在被CPU调度执行,这就叫并行。

image-20230926215850202

拿我自己电脑举例:

在这里插入图片描述

16个逻辑处理器,每过极短时间都切换16个线程叫并发,同一时刻处理16个线程这叫并行

生命周期

  • 线程的生命周期就是线程从生到死的过程中,经历的各种状态及状态转换。
  • 理解线程这些状态有利于提升并发编程的理解能力。

Java线程的状态

  • Java总共定义了6种状态image-20230926220422012

  • 6种状态都定义在Thread类的内部枚举类中。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值