Tread多线程

Tread多线程

什么是线程?

  • 线程(Thread)是一个程序内部的一条执行流程。

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

多线程是什么?

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

多线程的创建方式

方式一:继承Thread

①定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法

②创建MyThread类的对象

③调用线程对象的start()方法启动线程(启动后还是执行run方法的)

//(1)让自定义的MyThread继承Thread线程类【自定义的类也就具备线程的特性】
public class MyThread extends Thread {
    //(2)想要声明自定义的线程执行的时候到底执行什么代码,主动重写父类的run方法
    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            System.out.println("【自定义线程】的run方法执行了第" + i + "次!");
        }
    }
}
public class ThreadTest1 {
    public static void main(String[] args) {
        //(3)创建自定义线程类对象并调用start方法启动线程
        MyThread myThread = new MyThread();
        myThread.start();
​
        //补:在自定义线程启动之后,继续编写代码让主线程执行
        for (int i = 1; i <= 20; i++) {
            System.out.println("【主线程】的run方法执行了第" + i + "次!");
        }
    }
}

 

方式一优缺点

  • 优点:编码简单

  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

方式二:实现Runnable接口

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

②创建MyRunnable任务对象

③把MyRunnable任务对象交给Thread处理。

public class ThreadTest2 {
    public static void main(String[] args) {
        //(3)创建MyRunnable线程任务对象 【和线程还没有关系】
        MyRunnable myRunnable = new MyRunnable();
        //(4)创建Thread线程对象,并且线程任务作为构造方法的参数传递【枪:√ 弹夹:√】
        Thread t = new Thread(myRunnable);
        t.start();
​
        //补:在自定义线程启动之后,继续编写代码让主线程执行
        for (int i = 1; i <= 20; i++) {
            System.out.println("【主线程】的run方法执行了第" + i + "次!");
        }
    }
}

 

方式二优缺点:

  • 优点:任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强。

  • 缺点:需要多一个Runnable对象。

前两种线程创建文件都存在的一个问题

假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。

解决(多线程的第三种创建方式)

利用Callable,FutureTask类型实现。

①创建任务对象

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

把Callable类型的对象封装成FutureTask对象(线程任务对象)。

②把线程任务对象封装成Thread对象。

③调用Thread对象的start方法启动线程。

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

public class ThreadTest4 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //(3)Thread类不支持直接传递一个Callable线程任务对象【封装FutureTask对象并且将Callable线程任务作为构造参数传递】
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
​
        //(4)创建Thread类对象并且将FutureTask作为参数传递
        Thread t = new Thread(futureTask);
        t.start();
​
        //★(5)通过futureTask对象获取结果
        Integer result = futureTask.get();
        System.out.println("带有返回值的线程任务执行完成后返回的结果是:" + result);
​
​
        //补:在自定义线程启动之后,继续编写代码让主线程执行
        for (int i = 1; i <= 20; i++) {
            System.out.println("【主线程】的run方法执行了第" + i + "次!");
        }
    }
}

 

方式三优缺点:

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

  • 缺点:编码复杂一点。

三种线程创建方法比较

 

 

 

Thread的常见用法

public void run()             线程的任务方法

public void start()           启动线程

public Strign getName()         获取1当前线程名称,线程名称默认是Thread-索引

public void setName(String name)     为线程设置名称

public static Thread currentThread()   获取当前执行的线程对象

public static void sleep(long time)    让当前执行的线程休眠多少毫秒后,再继续执行

public void join()            让调用这个方法的线程先执行完

public class MyRunRunable implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 200; i++) {
            //在打印的时候,想要【获取到执行当前这行代码的线程】的线程名称
            //通过★Thread.currentThread():获取当前执行此方法的线程对象
            System.out.println(Thread.currentThread().getName() + "已经跑了" + i + "米!");
        }
    }
}
​
​
public class ThreadTest5 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
​
        String threadName = Thread.currentThread().getName();
        System.out.println("【主线程名称】:" + threadName);
​
        MyRunRunable myRunRunable = new MyRunRunable();
        //线程起名方式(1):通过线程对象调用setName方法传递名称
        Thread t1 = new Thread(myRunRunable);
        t1.setName("张二狗");
        //思考:模拟两个人跑 => 两个线程跑【跑的逻辑一样 所以使用同一个线程任务】不会干扰【底层:线程栈 线程执行过程中产生的变量数据都在线程栈中保存】
//线程起名方式(2):通过new Thread构造方法的时候,将参数一作为线程任务,参数二作为线程名称
        Thread t2 = new Thread(myRunRunable, "刘铁柱");
        t1.start();
        //t1.join(); 【让调用此方法的线程先执行完:插队】
        t2.start();
    }
}

 

线程安全

什么是线程安全问题?

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

取钱的线程安全问题

场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?

 

 

 

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

  • 存在多个线程在同时执行

  • 多个线程同时访问一个共享资源

  • 存在修改共享资源的情况

 

定义一个账号类

package com.itheima.safe;
​
import java.time.LocalTime;
​
public class Account {
    private String accoundId;
    private Integer money;
    //取钱:takeMoney
    public void takeMoney(Integer money) {
        //获取当前取钱的线程名称
        String name = Thread.currentThread().getName();
        System.out.println(LocalTime.now() + " " + name + "准备开始取钱!");
        if (this.money >= money) {
            System.out.println(LocalTime.now() + " " + name + "取出了" + money + "元!");
            this.money -= money;
        } else {
            System.out.println(LocalTime.now() + " " + name + "余额不足!");
        }
        System.out.println(LocalTime.now() + " 账户的余额是:" + this.money + "元!");
    }
​
    public String getAccoundId() {
        return accoundId;
    }
​
    public void setAccoundId(String accoundId) {
        this.accoundId = accoundId;
    }
​
    public Integer getMoney() {
        return money;
    }
​
    public void setMoney(Integer money) {
        this.money = money;
    }
​
    public Account() {
    }
​
    public Account(String accoundId, Integer money) {
        this.accoundId = accoundId;
        this.money = money;
    }
}

 


定义线程类


public class TakeMoneyRunnable implements Runnable {
    //线程任务需要访问到Account账户对象【将账户对象作为线程任务的构造方法 并且只给出一个有参构造】
    private Account account;
    public TakeMoneyRunnable(Account account) {
        this.account = account;
    }
    @Override
    public void run() {
        account.takeMoney(100000);
    }
}

 

 

测试类

package com.itheima.safe;
​
public class TakeMoneyThreadTest {
    public static void main(String[] args) {
        Account account = new Account("CHINA-BANK-62261728738", 100000);
        //创建线程任务【由于两个线程的逻辑一样 只需要一个线程任务】
        TakeMoneyRunnable takeMoneyRunnable = new TakeMoneyRunnable(account);
        //创建线程对象并且传递线程任务和线程名称
        Thread t1 = new Thread(takeMoneyRunnable, "张二狗");
        Thread t2 = new Thread(takeMoneyRunnable, "王美丽");
        t1.start();
        t2.start();
    }
}
​

 

线程同步(解决线程安全)

线程同步的思想

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

同步代码块

作用:把访问共享资源的核心代码给上锁,以此保证线程安全。

 

 

 

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

对线程安全改造

 public void takeMoney(Integer money) {
        String name = Thread.currentThread().getName();
        System.out.println(LocalDateTime.now() + "" + name + "准备开始取钱");
       //(同步代码块)加锁
        synchronized (this) {
            if (this.money >= money) {
                System.out.println(LocalDateTime.now() + "" + name + "取出了" + money + "元");
                this.money -= money;
            } else {
                System.out.println(LocalDateTime.now() + "" + name + "余额不足");
            }
            System.out.println(LocalDateTime.now() + "账户的余额是" + this.money + "元");
        }
    }

 

同步锁的注意事项

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

锁对象的使用规范

  • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。

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

同步方法

作用:把访问共享资源的核心代码给上锁,以此保证线程安全。

 

 

 

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

对线程安全改造

//同步方法加锁(修饰符后面,放回值类型前面)
    public synchronized void takeMoney(Integer money) {
        String name = Thread.currentThread().getName();
        System.out.println(LocalDateTime.now() + "" + name + "准备开始取钱");
​
            if (this.money >= money) {
                System.out.println(LocalDateTime.now() + "" + name + "取出了" + money + "元");
                this.money -= money;
            } else {
                System.out.println(LocalDateTime.now() + "" + name + "余额不足");
            }
            System.out.println(LocalDateTime.now() + "账户的余额是" + this.money + "元");
    }
}

 

同步方法底层原理

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

  • 如果方法是实例方法:同步方法默认用this作为的锁对象。

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

1.同步方法是如何保证线程安全的?

  • 对出现问题的核心方法使用**synchronized修饰**

  • 每次只能一个线程占锁进入访问

2.同步方法的同步锁对象的原理?

  • 对于实例方法默认使用**this作为锁对象。**

  • 对于静态方法默认使用**类名.class对象作为锁对象。**

Lock锁

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。

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

对线程安全改造

private static final Lock LOCK = new ReentrantLock();
    public void takeMoney(Integer money) {
​
        LOCK.lock();
​
        try {
            String name = Thread.currentThread().getName();
            System.out.println(LocalDateTime.now() + "" + name + "准备开始取钱");
            if (this.money >= money) {
                System.out.println(LocalDateTime.now() + "" + name + "取出了" + money + "元");
                this.money -= money;
            } else {
                System.out.println(LocalDateTime.now() + "" + name + "余额不足");
            }
            System.out.println(LocalDateTime.now() + "账户的余额是" + this.money + "元");
        }finally {
            LOCK.unlock();
        }
        }
​

 

线程池

了解线程池

什么是线程池

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

不使用线程池的问题

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

线程池的工作原理

 

 

 

创建线程池

如何得到线程池对象?

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

 

 

 

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

ThreadPoolExecutor**构造器**

 

 

 

  • 参数一:corePoolSize : 指定线程池的核心线程数量。

  • 参数二:maximumPoolSize:指定线程池的最大线程数量。

  • 参数三:keepAliveTime :指定临时线程的存活时间。

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

  • 参数五:workQueue:指定线程池的任务队列。

  • 参数六:threadFactory:指定线程池的线程工厂。

  • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

public class PoolDemo1 {
    public static void main(String[] args) {
        //基于ThreadPoolExecutor的构造方法创建线程池对象
        //核心线程:【线程任务:Cpu密集型(运算):当前机器Cpu的核心数+1 Runtime.getRuntime().availableProcessors()+1】
        //核心线程:【线程任务:IO密集型(读写):当前机器Cpu的核心数*2 Runtime.getRuntime().availableProcessors()*2】
        //ThreadFactory:线程工厂 【Exectors.defaultThreadFactory】 获取默认的线程工厂
        ThreadPoolExecutor pool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors() + 1, 15, 40L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
​
        //可以基于线程池规范接口的execute方法提交线程任务交给线程池执行
        for (int i = 1; i <= 25; i++) {
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "执行了线程任务!");
                }
            });
        }
​
        //AbortPolicy:默认丢弃新任务并且抛出异常
        //DiscardPolicy:默认丢弃新任务并且不抛出异常
        //DiscardOldestPolicy:默认将等待时间最长的任务丢弃,并且让新任务添加到队列中
        //CallerRunsPolicy:使用主线程执行新任务绕过当前线程池
//线程池一旦提交任务就持久运行【想要关闭调用shutdown/shutdownNow】
        pool.shutdown();
    }
}

 

线程池的注意事项

1、临时线程什么时候创建?

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

2、什么时候会开始拒绝新任务?

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

线程池如何处理Runnable任务?

  • 使用ExecutorService的方法:

  • void execute(Runnable target)

线程池如何处理Callable任务,并得到任务执行完后返回的结果?

  • 使用ExecutorService的方法:

  • Future<T> submit(Callable<T> command)

并发,并行

并发的含义

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

并行的理解

在同一个时刻上,同时有多个线程在被CPU调度执行。

简单说说多线程是怎么执行的?

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

  • 并行:同一个时刻多个线程同时在执行。

 

线程的生命周期

Java线程的状态

  • Java总共定义了6种状态

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

 

 

 

 

 

posted @ 2023-03-08 20:09  好学的耀耀  阅读(101)  评论(0编辑  收藏  举报