JavaSE进阶笔记:10 线程

101 阅读14分钟

1.线程概述

1.1线程

  • 线程(Thread)是一个程序的一条执行路径;
  • 启动程序后,main方法的执行就是一条单独的执行路径;
  • 程序如果只有一条执行路径,便是单线程程序

1.2.多线程

从软硬件上实现多条执行流程的技术;

2.创建线程的方法

2.1多线程实现方案一:继承Thread

/**
 * 多线程实现方案一:继承Thread
 * 重写run(),start()启动
 * 优点:代码简单
 * 缺点:无法继承其他类,不利于扩展
 */
public class ThreadDemo01 {
    public static void main(String[] args) {
        Thread thread = new MyThread();
        //子线程必须放在主线程前,不然就认为是单一线程,主线程必然先跑
        //启动线程;
        thread.start();
        for (int i = 0; i < 3; i++) {
            System.out.println("主线程:"+i);
        }
        //每次线程执行的先后不一定
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("子线程:"+i);
        }
    }
}

2.2多线程实现方案二:实现Runnable

/**
 * 多线程实现方案二:实现Runnable
 * 重写run(),创建一个任务对象交给Thread启动
 * 优点:可以继承其他类和实现接口,利于扩展
 * 缺点:多一层对象包装,无法直接返回值
 */
public class ThreadDemo02 {
    public static void main(String[] args) {
        //创建一个任务对象
        MyRunnable myRunnable = new MyRunnable();
        //任务对象交给Thread
        Thread thread = new Thread(myRunnable);
        //启动
        thread.start();

        for (int i = 0; i < 3; i++) {
            System.out.println("主线程:"+i);
        }
    }
}
class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("子线程:"+i);
        }
    }
}

2.3线程实现方案二:Runnable(匿名内部类)

/**
 * 多线程实现方案二:Runnable(匿名内部类)
 * 语法形式 重写run()
 * 优点:进一步简化
 * 缺点:无法直接返回值,不适合需要返回执行结果的业务场景
 */
public class ThreadDemo02Other {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println("子线程:"+i);
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();

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

        //简化写法2(lambda表达式)
        new Thread(() -> {
                for (int i = 0; i < 3; i++) {
                    System.out.println("子线程3:"+i);
                }
            }
        ).start();


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

2.4多线程实现方案三:实现Callable接口,结合FutureTask完成

/**
 * 多线程实现方案三:实现Callable接口,结合FutureTask完成
 * 语法形式:
 *      重写call(),创建创建Callable任务对象
 *      Callable任务对象交给FutureTask对象,转交给Thread
 * 优点:扩展性强,可以得到线程执行结果
 * 缺点:编程相对复杂
 */
public class ThreadDemo03 {
    public static void main(String[] args) {
        //1.创建Callable任务对象
        MyCallable call = new MyCallable(10);
        //2.Callable任务对象交给FutureTask对象
        //FutureTask对象的作用1:是Runnable对象(实现Runnable接口),可以转交给Thread
        //FutureTask对象的作用2:线程执行结束后,调用get方法得到返回结果
        FutureTask<String> futureTask = new FutureTask<>(call);
        Thread thread = new Thread(futureTask);
        thread.start();

        MyCallable call2 = new MyCallable(20);
        FutureTask<String> futureTask2 = new FutureTask<>(call2);
        Thread thread2 = new Thread(futureTask2);
        thread2.start();

        try {
            //如果第一个子线程没执行完,会在此等待,直到拿到结果,继续顺序执行
            String s = futureTask.get();
            System.out.println(s);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            String s = futureTask2.get();
            System.out.println(s);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//泛型设置返回对象类型
class MyCallable implements Callable<String>{
    private int n;

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

    //重写call()任务方法
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum+=i;
        }
        return "子线程的和为"+sum;
    }
}

3.线程常用API

/**
 * 线程常用API
 */
public class ThreadDemo01 {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        //设置线程名
        t1.setName("子线程1号");
        System.out.println(t1.getName());
        t1.start();

        Thread t2 = new MyThread("子线程2号");
        System.out.println(t2.getName());
        t2.start();
        
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
            if (i == 1){
                try {
                    //休眠3s
                    Thread.sleep(1000 * 3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        //每次线程执行的先后不一定
    }
}
class MyThread extends Thread{
    public MyThread() {
    }

    //设置命名构造器
    public MyThread(String name) {
        //为当前线程对象命名,送给父类有参构造器初始化名称
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            //获得线程名
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
        }
    }
}

4.线程安全

4.1线程安全概述

多个线程同时操作同一个资源,可能出现业务安全问题;

4.2 安全案例:取款

/**
 * 安全案例:取款
 */
public class ThreadTest {
    public static void main(String[] args) {
        Account ace = new Account("ACE000", 100000);

        new DrawThread("明",ace).start();
        new DrawThread("红",ace).start();
        /**
         * 红取钱成功,突出:100000.0
         * 账号剩余:0.0
         * 明取钱成功,突出:100000.0
         * 账号剩余:-100000.0
         */
    }
}

public class DrawThread extends Thread{
    private Account account;

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

    @Override
    public void run() {
        account.drawMoney(100000);
    }
}

public class Account {
    private String cardId;
    private double money;

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        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("账号剩余:"+this.money);
        }else {
            System.out.println(name+"取钱失败,余额不足");
        }
    }

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

5.线程同步

5.1 线程同步核心思想

  • 加锁:
    • 共享资源上锁,每次只能一个线程访问,结束后解锁,其他线程竞争再进一个;
    • 达到多线程先后依次访问共享资源

5.2 同步代码块

  • 作用:有线程安全问题的核心代码上锁

  • 原理:每次一个线程进入

  •   synchronized(同步锁对象){
      	使用共享资源的核心代码
      }
    
  • 锁对象要求:对于同时执行的线程来说是同一个对象;

    • 理论上可以选用任意的唯一对象(如常量),但不好会影响无关线程;
    • 建议使用共享资源作为锁对象
    • 实例方法建议使用this
    • 静态方法建议用字节码(类名.class)对象作为锁对象
    /**
     * 静态方法建议用字节码(类名.class)对象作为锁对象
     */
    public static void run(){
        synchronized (Account.class) {
            
        }
    }


/**
     * this == acc 共享账号
     * 取钱方法
     */
    public void drawMoney(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+"取钱失败,余额不足");
            }
        }
    }


/**
* 不好,会影响无关线程;
 */    
//取钱方法
    public void drawMoney(double money){
        String name = Thread.currentThread().getName();
        synchronized ("任意唯一对象:如常量") {
            if (this.money >= money){
                System.out.println(name+"取钱成功,突出:"+money);
                this.money -= money;
                System.out.println("账号剩余:"+this.money);
            }else {
                System.out.println(name+"取钱失败,余额不足");
            }
        }
    }

5.3 同步方法

  • 作用:有线程安全问题的核心方法上锁

  • 格式:

    修饰符 synchronized 返回值类型 方法名称(形参列表){
    		使用共享资源的核心代码
    }
    
  • 同步方法底层原理:

    • 底层有隐式锁对象的,锁的范围是整个方法
    • 实例方法,默认用this作为锁对象,前提是代码要高度面向对象
    • 静态方法:默认用字节码(类名.class)对象作为锁对象

5.4 同步代码块和同步方法比较

  • 同步代码块:效率更高点
  • 同步方法:更简洁明了,用的更多

5.5 Lock锁

  • 为了更清晰表达加锁和释放锁,JDK5后增加了历新的对象Lock更灵活和方便;
  • Lock可以提供更广泛的锁定操作;
  • Lock是接口,采用它的实现类ReentrantLock()构建锁对象
public class AccountLock {
    private String cardId;
    private double money;
    private final Lock lock = new ReentrantLock();

    /**
     * Lock锁更灵活
     */
    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("账号剩余:" + this.money);
            } else {
                System.out.println(name + "取钱失败,余额不足");
            }
        } finally {
            //解锁
            lock.unlock();
        }
    }

6.线程通信

6.1 线程通信概念

  • 线程间互相发送数据,通常通过共享一个数据的方式实现;
  • 线程间根据数据情况,判断执行声明,以及通知其他线程怎么做
  • 线程通信前提:多个线程操作同一个共享资源时进行通信,且要保证线程安全

6.2 线程通信常见模型

  • 生产者消费者模型:生产者生产数据,消费者消费数据
  • 要求:
    • 生产者生产完数据,唤醒消费者,等待自己;
    • 消费者消费完,唤醒生产者,等待自己

6.3 案例:生产者消费者模型

方法名作用
void wait()让活动在当前对象的线程无限等待(释放之前占有的锁)
void notify()唤醒当前对象正在等待的线程(只提示唤醒,不会释放锁)
void notifyAll()唤醒当前对象全部正在等待的线程(只提示唤醒,不会释放锁)
/**
 * 案例:生产者消费者模型
 */
public class ThreadDemo {
    public static void main(String[] args) {
        Account account = new Account("wind998", 0);

        //两人取钱
        new DrawThread("婷姐",account).start();
        new DrawThread("小火柴",account).start();

        //三人存钱
        new DepositThread("婷哥",account).start();
        new DepositThread("天机星",account).start();
        new DepositThread("地魁星",account).start();
    }
}

/**
 * 取钱线程
 */
public class DrawThread extends Thread{
    private Account account;

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

    @Override
    public void run() {
        while (true) {
            account.drawMoney(10000);
            try {
                Thread.sleep(1000 * 3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 存钱线程
 */
public class DepositThread extends Thread{
    private Account account;

    public DepositThread(String name, Account account) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        while (true) {
            account.deposit(10000);
            try {
                Thread.sleep(1000 * 3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


/**
 * 实体和方法
 */
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 synchronized void drawMoney(double money) {
        String name = Thread.currentThread().getName();
        try {
            if (this.money >= money) {
                System.out.println(name+"取钱成功,取出:"+money);
                this.money -= money;
                System.out.println("账号剩余:"+this.money);
                //唤起其他线程
                this.notifyAll();
                //必须用锁对象让线程挂起
                this.wait();
            } else {
                //钱不够,花钱线程挂起,唤起,存钱线程
                //注意先后顺序,先唤醒再挂起,不然无法后继
                //唤起其他线程
                this.notifyAll();
                //必须用锁对象让线程挂起
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void deposit(double money) {
        String name = Thread.currentThread().getName();
        try {
            if (this.money == 0) {
                this.money += money;
                System.out.println(name+"存钱成功,存入:"+money+"账号剩余:"+this.money);
                //唤起其他线程
                this.notifyAll();
                //必须用锁对象让线程挂起
                this.wait();
            } else {
                //钱够
                //唤起其他线程
                this.notifyAll();
                //必须用锁对象让线程挂起
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

7.线程池

7.1 概述

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

7.1.1使用线程池的原因

创建新线程开销很大,每个请求都创建新线程处理,严重影响系统性能

7.1.2 线程池工作原理

  • 设置复数的核心线程(工作线程);
  • 任务请求进入任务队列(WorkQueue)排队等待,最大可执行数量为核心线程的数量
  • 任务接口
    • Runnable;Callable
  • 假设:有三台售货机,最多同时有三个人上前操作,其余人排队等待;

7.1.3 线程池代表接口

JDK5提供了代表线程池的接口:ExceutorService

7.1.4 得到线程池对象方法

  • 方式一:ExceutorService实现类ThreadPoolExecutor自创建一个线程池对象;
  • 方式二:Executors(线程池工具类)调用方法返回不同特点的线程池对象

7.1.5 构造器参数说明

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 
参数释义注意
corePoolSize指定线程池的线程数量(核心线程)不能小于0;
需要随时应对派遣任务;
maximumPoolSize指定线程池可支持的最大线程数最大数量 >= 核心线程数量
keepAliveTime指定临时线程最大存活时间不能小于0
临时线程 = 最大数量- 核心线程数量
unit指定存活时间的单位(天、时、分、秒)时间单位
workQueue指定任务队列不能为null
内部按需求设置可以等待的任务量
threadFactory指定用某线程工厂创建线程不能null
handler拒绝任务的策略不能为null
所有线程都忙,任务队列满后的拒绝新任务的方式

7.1.6 新任务拒绝策略

策略意义
ThreadPoolExecutor.AbortPolicy()丢弃任务并抛出异常,默认策略
ThreadPoolExecutor.DiscardPolicy()丢弃任务,不抛异常,不推荐
ThreadPoolExecutor.DiscardOldestPolicy()抛弃队列中等待最久的任务,把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy()由主线程(main老板直接服务)负责调用任务的run()方法,绕过线程池直接执行;

7.2 常见面试题

7.2.1 临时线程什么时候创建

新任务提交时,核心线程都在忙,任务队列满了,还有临时线程创建名额,此时才会创建临时线程;

7.2.2 什么时候会拒绝任务?

所有线程都忙,任务队列满了,新任来了才会拒绝;

7.3 线程池处理Runnable任务

/**
 * 线程池执行Runnable任务
 */
public class ThreadPoolDemo01 {
    public static void main(String[] args) {
        /**
         public ThreadPoolExecutor(
         int corePoolSize,
         int maximumPoolSize,
         long keepAliveTime,销毁临时线程必须等线程不被占用开始计算
         TimeUnit unit,
         BlockingQueue<Runnable> workQueue,
         ThreadFactory threadFactory,
         RejectedExecutionHandler handler) {
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,6
                , TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),new ThreadPoolExecutor.AbortPolicy());

        MyRunnable target = new MyRunnable();
        //pool.execute()执行任务,没有返回值,一般用来执行Runnable任务
        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);
        //上述创建临时线程
        /**
         * 所有线程都被占用,等待队列满了
         * 新的11号任务来了,需要执行拒绝策略
         * Task com.yanfeng.d8_threadpool.MyRunnable@6d6f6e28 rejected
         * from java.util.concurrent.ThreadPoolExecutor@135fbaa4
         */
        pool.execute(target);

        /**
         * 关闭线程(开发中一般不用)
         */
        //立即关闭,即使线程没处理完,会丢失任务
        pool.shutdownNow();
        //全部任务执行完后,关闭(建议用这个)
        pool.shutdown();
    }
}

7.4线程池处理Callable任务

/**
 * 线程池执行Callable任务
 * 线程池开始执行,程序不会自行关闭
 */
public class ThreadPoolDemo02 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         public ThreadPoolExecutor(
         int corePoolSize,
         int maximumPoolSize,
         long keepAliveTime,销毁临时线程必须等线程不被占用开始计算
         TimeUnit unit,
         BlockingQueue<Runnable> workQueue,
         ThreadFactory threadFactory,
         RejectedExecutionHandler handler) {
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,6
                , TimeUnit.SECONDS,new ArrayBlockingQueue<>(5),new ThreadPoolExecutor.AbortPolicy());

        //submit()方法处理Callable任务,返回结果,有未来任务对象接收,用时从中取
        Future<String> submit = pool.submit(new MyCallable(100));
        Future<String> submit2 = pool.submit(new MyCallable(200));
        Future<String> submit3 = pool.submit(new MyCallable(300));
        Future<String> submit4 = pool.submit(new MyCallable(400));
        String s = submit.get();
        System.out.println(s);
        System.out.println(submit2.get());
        System.out.println(submit3.get());
        System.out.println(submit4.get());
    }
}

7.5 Executors工具类实现线程池

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

/**
 * Executors工具类实现线程池
 * 大型系统不允许使用Executors创建线程
 */
public class ThreadPoolDemo03 {
    public static void main(String[] args) {
        /**
         * newFixedThreadPool
         * newSingleThreadExecutor
         * 存在问题:
         *      没有任务限制,没有拒绝策略,任务队列无限排,会出现内存溢出
         */
        //创建固定线程的线程池(全核心线程);某线程因异常结束,会创建新线程补上
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        //只有一个固定线程,因异常结束,会创建新线程补上
        ExecutorService executorService1 = Executors.newSingleThreadExecutor();

        /**
         * newCachedThreadPool 线程无限增加,会出现内存溢出
         * newScheduledThreadPool
         * 存在问题:
         *    没有控制任务数量,
         */
        //来了任务直接创建线程
        ExecutorService executorService2 = Executors.newCachedThreadPool();
        //核心线程数量控制,但最大线程数量不定
        ExecutorService executorService3 = Executors.newScheduledThreadPool(3);
    }
}

8.定时器

  • 定时器是一种控制任务延时调用,或者周期调用的技术;
  • 作用:闹钟、定时邮件发送;

8.1 Timer定时器

特点Timer是单线程,处理多个任务顺序执行,存在延时与设置定时器的时间有出入;
到了设定执行时间,前面任务还没执行完,就会延后执行
存在问题可能因某个任务异常是Timer线程死掉,影响后续任务执行

8.2 ScheduledExecutorService定时器

  • ScheduledExecutorService是JDK1.5中引入并发包,目的是弥补Timer的缺陷,ScheduledExecutorService内部为线程池;
  • 优点:基于线程池,某个任务执行情况不会影响其他定时任务执行;
/**
 * ScheduledExecutorService定时器
 * 定时任务间互不影响
 */
public class TimerDemo02 {
    public static void main(String[] args) {
        //1.创建ScheduledExecutorService线程池做定时器
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);

        /**
         * ScheduledFuture<?> scheduleAtFixedRate
         * (Runnable command,long initialDelay,long period,TimeUnit unit);
         * (任务,延时?后执行,隔?后重复执行,时间单位)
         */
        //2.开启定时任务;周期调度方法
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行A:"+new Date());
                try {
                    Thread.sleep(1000 * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },0,2, TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行B:"+new Date());
                System.out.println(10/0);
            }
        },0,2, TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行C:"+new Date());
            }
        },0,2, TimeUnit.SECONDS);
    }
}

9.并发和并行

  • 正在运行的程序是一个独立进程,线程属于进程,
  • 多个进程其实是并发与并行同时进行;
    • 并发是一次执行一个,再换下一个
    • 并行是有多个在一起执行,可以看做执行一组线程
    • 并发与并行同时进行;一次执行一组,再换下一组

9.1并发

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

9.2 并行

  • 同一时刻,同时有多个线程在被CPU处理;
  • 依靠CPU逻辑处理器

9.3 并发和并行含义总结

  • 并发:CPU分时轮询的执行线程
  • 并行:同一时刻同时执行

10.线程的生命周期

10.1 Java线程的6中状态

线程生命周期6种状态.png

10.2 注意:

注意
sleep不会让出自己的锁,时间到了,直接运行
wait会让出锁,时间到,抢锁,抢到运行;
会让出锁,被唤醒后需要抢锁,抢到运行;

RecordDate:2021/08/20