多线程学习笔记

多线程学习笔记

一、概念

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

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

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

  • Java虚拟机允许应用程序同时执行多个执行线程

  • 每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。

二、如何创建线程

  • 启动线程必须调用start方法而不是run方法,start方法可以开启一个线程,直接调用run方法会当初普通对象的方法执行,此时没有启动线程执行。
  • main方法是一条默认的主线程负责执行

方法一:继承Thread类

  1. 任意类继承线程类Thread。

  2. 重写run方法,描述线程任务。

    //继承Thread线程类
    public class MyThread extends Thread{
        //重写run方法,描述线程的执行任务
        @Override
        public void run() {
            for (int i = 1; i < 5; i++) {
                System.out.println("子线程Mythread:"+i);
            }
        }
    }
    
  3. 主线程创建类对象,调用start方法启动线程。

    /**
     * 线程测试一:继承Thread创建线程
     * @author 鹿先生
     * @date 2023/08/29
     */
    public class TreadTest1 {
        //main方法是一条默认的主线程负责执行
        public static void main(String[] args) {
            Thread thread = new MyThread();
            thread.start();//启动一个子线程
            //主线程的业务
            for (int i = 1; i < 5; i++) {
                System.out.println("主线程:"+i);
            }
        }
    }
    

优点:编码简单

缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展,也就是具有单继承的问题。

方法二:实现Runnable接口

基础版

  1. 任务类实现Runnable接口。

  2. 重写run方法,描述 线程任务。

    //任务类
    public class MyRunnable implements Runnable{
        //重写run方法,描述线程任务
        @Override
        public void run() {
            for (int i = 1; i <= 5; i++) {
                System.out.println("子线程MyRunable:"+i);
            }
        }
    }
    
  3. 主线程中创建任务类,并作为参数创建线程对象,启动子线程。

    /**
     * 线程测试二:实现Runnable接口
     * @author 鹿先生
     * @date 2023/08/29
     */
    public class TreadTest2 {
        //main方法是一条默认的主线程负责执行
        public static void main(String[] args) {
            Runnable runable = new MyRunnable();//创建任务类对象
            Thread thread = new Thread(runable);//使用任务类创建线程
            thread.start();//启动线程
    
            //主线程的业务
            for (int i = 1; i <= 5; i++) {
                System.out.println("主线程:"+i);
            }
        }
    }
    

简写版

/**
 * 线程测试二:实现Runnable接口(使用匿名内部类简写)
 * 三种简写方法
 * @author 鹿先生
 * @date 2023/08/29
 */
public class TreadTest2_2 {

    public static void main(String[] args) {
        //简写方法一:先使用Runnable创建匿名内部类,然后创建线程传入Runnable对象启动
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程1:"+i);
                }
            }
        };
        new Thread(runnable).start();

        //简写方法二:创建线程时,Runnable接口创建匿名内部类,然后直接启动。
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程2:"+i);
                }
            }
        }).start();


        //简写方法三:由于Runnable是函数式接口,所以我们可以使用lambda表达式简写
        new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程3:"+i);
            }
        }).start();

    }
}

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

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

方法三:实现Callable接口

  • 这个方法可以获取线程执行完后的返回值,但是编码稍微复杂一点点。
  1. 任何一个类实现Callable接口,指定返回值泛型,然后重写call方法,描述线程任务。

  2. 主线程创建该类对象,封装到FutrueTask未来任务对象中,然后创建线程对象,直接启动线程。

    //实现Callable接口,指定返回值类型
    public 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 = 1; i <= n; i++) {
                sum+=i;
            }
            return "1-"+n+"的累加和结果为:"+sum;
        }
    }
    
  3. 使用FutrueTask的get方法获取线程执行完成后的返回结果。

    /**
     * 线程测试三:实现Callable接口
     * @author 鹿先生
     * @date 2023/08/29
     */
    public class TreadTest3 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            Callable callable = new MyCallable(100);//创建Callable对象,描述线程任务
            FutureTask<String> futureTask = new FutureTask<String>(callable);//使用Callable对象封装成未来任务对象
            new Thread(futureTask).start();//启动线程执行未来任务对象的线程任务
            //注意:当线程未执行完时,futureTask.get()会阻塞在这,等待线程执行完毕,直到线程执行完,才能获取到结果
            System.out.println(futureTask.get());//打印线程执行完后的返回结果
        }
    }
    

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

缺点:编码复杂一点。

三、线程的常用方法

1.方法

常用方法

// 1.线程的任务方法
public void run()
// 2.启动线程
public void start()
// 3.获取当前线程的名称,线程名称默认是Thread-索引
public String getName()
// 4.为线程设置名称
public void setName(String name)
// 5.获取当前执行的线程对象
public static Thread currentThread()
// 6.让当前执行的线程休眠多少毫秒后,再继续执行
public static void sleep(long time)
// 7.让调用这个方法的线程先执行完,再继续执行其他代码
public final void join()

构造方法

// 1.可以为当前线程指定名称
public Thread(String name)
// 2.封装Runnable对象成为线程对象
public Thread(Runnable target)
// 3.封装Runnable对象成为线程对象,并指定线程名称
public Thread(Runnable target,String name)

2.案例

线程类

//继承Thread线程类
public class MyThread extends Thread{

    //无参构造
    public MyThread() {
    }

    //有参构造
    public MyThread(String name) {
        super(name);//调用父类构造函数Thread(String name)创建线程时指定线程名称
    }

    //重写run方法,描述线程的执行任务
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            //打印线程名称
            System.out.println(Thread.currentThread().getName()+"线程:"+i);
        }
    }
}

主线程类

/**
 * 线程常用方法案例
 * @author 鹿先生
 * @date 2023/08/30
 */
public class TreadTest1 {
    public static void main(String[] args) throws Exception {
        MyThread thread1 = new MyThread();
        thread1.setName("1号线程");
        thread1.start();//启动一个子线程

        MyThread thread2 = new MyThread("2号线程");
        thread2.start();//启动一个子线程
        thread2.join();//必须等到2号线程执行完,程序才会往下走(也就是说3号线程永远在2号线程后面)

        MyThread thread3 = new MyThread("3号线程");
        thread3.start();//启动一个子线程


        //获取主线程名
        String mainName = Thread.currentThread().getName();
        //主线程的业务
        for (int i = 1; i <= 5; i++) {
            if(i==5)Thread.sleep(5000);//当i等于5时,线程休息5秒钟
            System.out.println(mainName+"线程:"+i);
        }
    }
}

注意:Thread类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用。

四、线程安全

1. 线程安全问题

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

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

    • 存在多个线程在同时执行
    • 同时访问一个共享资源
    • 存在修改该共享资源

2. 用程序模拟线程安全问题

  • 小明和小红同时取钱,银行会亏十万,这就发生了线程安全问题。

1.账户类

//账户类
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 toriMoney(Double wantMoney) {
        String name = Thread.currentThread().getName();//获取线程名
        if (money >= wantMoney) {//余额大于十万元,就取走
            System.out.println(name + "来取钱了,要取十万元");
            money -= wantMoney;
            System.out.println(name + "取钱成功了,还剩余额:" + money);
        } else {
            System.out.println(name + "取钱失败:余额不足!");
        }
    }
}

2.线程类

//继承Thread线程类
public class MyThread extends Thread {

    private Account acc;

    //构造函数传入待操作账户和线程名
    public MyThread(Account acc, String name) {
        super(name);//利用父类Thread构造方法指定线程名
        this.acc = acc;
    }

    //重写run方法,描述线程的执行任务
    @Override
    public void run() {
        acc.toriMoney(100000.0);//取钱
    }
}

3.两个账户同时取钱

/**
 * 测试线程安全问题
 */
public class ThreadTest {
    public static void main(String[] args) {
        //创建一个账户,余额十万元
        Account account = new Account("ICBC-110", 100000);
        //小明小红线程同时取钱
        new MyThread(account, "小明").start();
        new MyThread(account, "小红").start();
    }
}

五、线程同步(线程安全的解决办法)

1.线程思想概述

  • 线程同步是解决线程安全问题的方案(放生线程安全问题时就是因为线程时异步的,所以我们选择同步就没问题了)。
  • 思想;让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
  • 线程同步的原理:就是加锁,每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动释放锁,然后其他线程才能再加锁进来。

2.方式一:同步代码块

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

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

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

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

  • 字符串常量在常量池只会保留一份。

synchronized(同步锁){
    访问共享资源的核心代码
}

案例:

//账户类
public class Account {
	......

    //静态代码块实现线程同步,对于静态方法,锁对象使用类的字节码
    public static void count(){
        synchronized (Account.class){
            System.out.println("我是静态方法。");
        }
    }

	......

    //取钱
    public void toriMoney(Double wantMoney) {
        //静态代码块实现线程同步,建议使用共享资源作为锁对象(这个地方小明和小红的共享资源就是账户,也就是当前的账户对象)
        //而且这样锁的范围小一些,不会出bug锁住其他账户
        //多个线程操作同一个共享资源,同一时刻只有一个线程能获取锁,也就是一个一个来。
        synchronized (this) {
            String name = Thread.currentThread().getName();//获取线程名
            if (money >= wantMoney) {//余额大于十万元,就取走
                System.out.println(name + "来取钱了,要取十万元");
                money -= wantMoney;
                System.out.println(name + "取钱成功了,还剩余额:" + money);
            } else {
                System.out.println(name + "取钱失败:余额不足!");
            }
        }
    }
}

3.方式二:同步方法(推荐)

原理:把访问共享资源的核心方法给上锁,以此保证线程安全。

  • 同步方法其实底层也是有隐式锁对象的,也就是默认有锁对象,锁的范围是整个方法代码。
  • 如果是普通方法,锁对象就是当前对象;如果是静态方法,锁对象就是当前类的字节码。
修饰符 synchronized 返回值类型 方法名称(形参列表){
    操作共享资源的代码
}

案例:

//账户类
public class Account {
	......

    //同步方法实现线程同步,对于静态方法,锁对象默认使用类的字节码
    public synchronized static void count() {
        System.out.println("我是静态方法。");
    }

	......

    //取钱
    public synchronized void toriMoney(Double wantMoney) {
        //同步方法实现线程同步,synchronized对于实例方法,锁对象默认就是当前对象
        String name = Thread.currentThread().getName();//获取线程名
        if (money >= wantMoney) {//余额大于十万元,就取走
            System.out.println(name + "来取钱了,要取十万元");
            money -= wantMoney;
            System.out.println(name + "取钱成功了,还剩余额:" + money);
        } else {
            System.out.println(name + "取钱失败:余额不足!");
        }
    }
}

优点:可读性比同步代码块好。

缺点:同步方法锁的范围比同步代码块的范围更大,类似提前排队,性能略差,但是对于当今社会的计算机性能而言,可以忽略不记。

4.方式三:Lock锁

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

案例:

//账户类
public class Account {
	......
        
    //使用final修饰表示锁唯一,不能二次修改;并且在对象的属性里创建lock对象属性表示一个对象一个锁。
    private final Lock lk = new ReentrantLock();

	......

    //取钱
    public void toriMoney(Double wantMoney) {
        try {
            lk.lock();//加锁
            String name = Thread.currentThread().getName();//获取线程名
            if (money >= wantMoney) {//余额大于十万元,就取走
                System.out.println(name + "来取钱了,要取十万元");
                money -= wantMoney;
                System.out.println(name + "取钱成功了,还剩余额:" + money);
            } else {
                System.out.println(name + "取钱失败:余额不足!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lk.unlock();//无论业务是否成果执行,都会解锁,代码健壮性更强
        }
    }
}

六、线程通信(了解)

  • 线程通信:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
  • 线程通信的常见模型(生产者与消费者模型)
    • 生产者线程负责生产数据
    • 消费者线程负责消费生产者生产的数据。
    • 注意:生产者生产完数据应该等待自己,通知消费者;消费者消费完数据也应该等待自己,再通知生产者生产!(等待就是释放锁,不再竞争cpu资源)
  • 线程通信的前提是保证线程安全。
  • Object类的等待和唤醒方法(这些方法必须使用锁对象调用):
//让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAll()方法
void wait()
//唤醒正在等待的单个线程
void notyfy()
//唤醒正在等待的所有线程
void notifyAll()
  • 线程等待和唤醒是操作系统中线程同步的重要机制之一。线程等待是指线程在执行过程中暂停执行,等待某个事件发生后继续执行;而唤醒则是等待的相反操作,它让一个处于等待状态的线程重新获得执行权
  • 当一个线程获取到锁后,其他线程会进入等待状态(锁释放后会自动唤醒其他线程)。

案例

桌子类

//桌子实体类
public class Desk {
    private List<String> list = new ArrayList<>();//桌子上放包子的地方

    //put和get两个方法都添加了synchronized,它们锁的是同一个对象,就是桌子对象,会同时锁住三个厨师和两个吃货。
    //先唤醒其他线程,然后再等待
    //完成线程任务就需要释放锁,所以需要先唤醒其他线程,然后等待当前线程。

    //生产包子
    public synchronized void put() {
        String name = Thread.currentThread().getName();//获取线程名
        if (list.isEmpty()){
            //没有包子,需要做包子
            list.add(name+"做的肉包子!");
            System.out.println(name+"做了一个肉包子!");
            try {
                Thread.sleep(3000);
                this.notifyAll();//唤醒所有等待线程
                this.wait();//当前线程释放锁,等待
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else {
            //有包子,不用做
            this.notifyAll();//唤醒所有等待线程
            try {
                this.wait();//当前线程释放锁,等待
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    //吃包子
    public synchronized void get() {
        String name = Thread.currentThread().getName();//获取线程名
        if (!list.isEmpty()){
            //有包子,可以吃
            System.out.println(name+"吃了"+list.get(0));
            list.clear();
            try {
                Thread.sleep(1000);
                this.notifyAll();//唤醒所有等待线程
                this.wait();//当前线程释放锁,等待
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else {
            //没有包子
            this.notifyAll();//唤醒所有等待线程
            try {
                this.wait();//当前线程释放锁,等待
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

主线程类

//包含五个线程:三个生产者线程和两个消费者线程
//线程通信案例
public class ThreadTest {
    public static void main(String[] args) {
        //创建一个桌子对象
        Desk desk = new Desk();

        // 启动五个线程
        new Thread(() -> {
            while (true) {
                desk.put();//生产包子
            }
        }, "厨师1").start();

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

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

        new Thread(() -> {
            while (true) {
                desk.get();//吃包子
            }
        }, "吃货1").start();

        new Thread(() -> {
            while (true) {
                desk.get();//吃产包子
            }
        }, "吃货2").start();
    }
}

注意

  • 这个案例咱们手动唤醒和等待其他线程,更好的理解线程通信。(其实每次任务执行完,锁释放后,其他线程就会自动被唤醒,不需要我们手动唤醒;线程的等待和唤醒都是自动的)。
  • 下面的写法跟上面的写法效果一样。
//桌子实体类
public class Desk {
    private List<String> list = new ArrayList<>();//桌子上放包子的地方

    //put和get两个方法都添加了synchronized,它们锁的是同一个对象,就是桌子对象,会同时锁住三个厨师和两个吃货。
    //先唤醒其他线程,然后再等待
    //完成线程任务就需要释放锁,所以需要先唤醒其他线程,然后等待当前线程。

    //生产包子
    public synchronized void put() {
        String name = Thread.currentThread().getName();//获取线程名
        if (list.isEmpty()){
            //没有包子,需要做包子
            list.add(name+"做的肉包子!");
            System.out.println(name+"做了一个肉包子!");
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    //吃包子
    public synchronized void get() {
        String name = Thread.currentThread().getName();//获取线程名
        if (!list.isEmpty()){
            //有包子,可以吃
            System.out.println(name+"吃了"+list.get(0));
            list.clear();
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

七、线程池

1.概述

  • 线程池是一个可以复用线程的技术。
  • 不使用线程池的问题:用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能
  • 在线程池中,咱们的线程叫工作线程(WorkThread),需要处理的任务会排队进入任务队列(WorkQueue)依次被工作线程处理,这个任务必须要实现Runnable接口或者callable接口。
  • 线程池创建后会一直存活,除非手动关闭,核心线程只要被创建,就会一直存在。

2.线程池的创建

  • JDK5.0起提供了代表线程池的接口:ExecutorService;常用的实现类是ThreadPoolExecutor。

  • 对于核心线程数量如何选择

    • 计算密集型的任务:核心线程数量 = CPU的的核数(电脑逻辑处理器个数) + 1;

    • IO密集型的任务: 核心线程数量 = CPU核数 * 2;

  • 如何得到线程池对象

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

      //创建线程池
      ExecutorService pool = new ThreadPoolExecutor(
          //核心线程数3,最大线程数5,临时线程剔除8秒剔除
          3, 5, 8, TimeUnit.SECONDS,
          //任务队列采用数组的阻塞队列,大小为4(也可以创建链表的阻塞队列,那样任务可以无限存放);使用默认的线程工厂
          new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
          //任务拒绝策略是:任务队列满了,无法处理该任务时,抛出异常
          new ThreadPoolExecutor.AbortPolicy());
      
    • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

      //创建线程池
      ExecutorService pool = Executors.newFixedThreadPool(3);
      
  • 构造器

    ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 
                       BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 
    
    • 参数一:corePoolSize:指定线程池的核心线程的数量
    • 参数二:maximumPoolSize:指定线程池的最大线程数量
    • 参数三:keepAliveTime:指定临时线程的存活时间
    • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
    • 参数五:workQueue:指定线程池的任务队列
    • 参数六:threadFactory:指定线程池的线程工厂
    • 参数七:handler:指定线程池的任务拒绝策略(线程都很忙,任务队列也满了的时候,新任务来了该怎么处理)
  • 线程池的注意事项

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

3.处理Runnable任务

1.ExecutorService的常用方法

//1.执行Runnable任务
void execute(Runnable command)
//2.执行Callable任务,返回未来任务对象,用于获取线程返回的结果
Future<T> submit(Callable<T> task)
//3.等全部任务执行完毕后,再关闭线程池
void shutdown()
// 4.立即关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
List<Runnable> shutdownNow()

2.任务拒绝策略

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

3.案例:

Runnable类

public class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("打印线程名称:"+Thread.currentThread().getName());
//        try {
//            Thread.sleep(Integer.MAX_VALUE);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
    }
}

主线程类

//创建线程池
public class ThreadTest {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService pool = new ThreadPoolExecutor(
                //核心线程数3,最大线程数5,临时线程剔除8秒剔除
                3, 5, 8, TimeUnit.SECONDS,
                //任务队列采用数组的阻塞队列,大小为4(也可以创建链表的阻塞队列,那样任务可以无限存放);使用默认的线程工厂
                new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                //任务拒绝策略是:任务队列满了,无法处理该任务时,抛出异常
                new ThreadPoolExecutor.AbortPolicy());

        MyRunnable runnable = new MyRunnable();

        //线程池启动后,会一直存活,除非手动关闭

        //创建3个核心线程处理任务
        pool.execute(runnable);
        pool.execute(runnable);
        pool.execute(runnable);
        //这4个任务加入任务队列等待
        pool.execute(runnable);
        pool.execute(runnable);
        pool.execute(runnable);
        pool.execute(runnable);
        //创建两个临时线程
        pool.execute(runnable);
        pool.execute(runnable);
        //这个时候根据任务拒绝策略来处理任务,这里是抛出异常
//        pool.execute(runnable);

//        pool.shutdown();//等线程执行完再关闭线程池
        List<Runnable> list = pool.shutdownNow();//立即关闭线程池
        list.stream().forEach(System.out::println);
    }
}

4.处理Callable任务

callable类

//实现Callable接口,指定返回值类型
public 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 = 1; i <= n; i++) {
            sum+=i;
        }
        return Thread.currentThread().getName()+"求出1-"+n+"的累加和结果为:"+sum;
    }
}

主线程类

//线程池处理Callable任务
public class ThreadTest2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程池
        ExecutorService pool = new ThreadPoolExecutor(
                //核心线程数3,最大线程数5,临时线程剔除8秒剔除
                3, 5, 8, TimeUnit.SECONDS,
                //任务队列采用数组的阻塞队列,大小为4(也可以创建链表的阻塞队列,那样任务可以无限存放);使用默认的线程工厂
                new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                //任务拒绝策略是:任务队列满了,无法处理该任务时,抛出异常
                new ThreadPoolExecutor.AbortPolicy());

        //线程池处理Callable任务返回未来任务对象
        Future<String> future1 = pool.submit(new MyCallable(100));
        Future<String> future2 = pool.submit(new MyCallable(200));
        Future<String> future3 = pool.submit(new MyCallable(300));
        Future<String> future4 = pool.submit(new MyCallable(400));//这个地方会复用线程
        //调用未来任务对象获取线程返回结果
        System.out.println(future1.get());
        System.out.println(future2.get());
        System.out.println(future3.get());
        System.out.println(future4.get());
    }
}

5.使用Executors得到线程池

  • Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
  • 这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
//1.创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newFixedThreadPool(int nThreads)
//2.创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新的线程。
public static ExecutorService newSingleThreadExecutor()
//3.线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉
public static ExecutorService newCachedThreadPool()
//4.创建一个线程池,可以实现再给定的延迟后运行任务,或者定期执行任务。
public static ScheduleExecutorService newScheduleThreadPool(int corePoolSize)

案例:

//创建线程池
ExecutorService pool = Executors.newFixedThreadPool(3);

注意:

  • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险,出现OOM内存溢出(因为newFixedThreadPool和newSingleThreadExecutor的最大任务数默认是Integer.MAX_VALUE,newCachedThreadPool的最大线程数也是Integer.MAX_VALUE,容易内存溢出)。

八、并发、并行

  • 正在运行的程序(软件)就是一个独立的进程
  • 线程是属于进程的,一个进程中可以同时运行很多个线程。
  • 进程中的多个线程其实是并发和并行执行的。
  • 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发
  • 在同一时刻上,同时有多个线程在被CPU调度执行,这就是并行
  • 多线程到底是怎样执行的:并发和并行同时执行的。

九、线程的生命周期

  • 线程的生命周期从生到死的过程中,经历的各种状态及状态转换。
  • java总共定义了6种状态,6种状态都定义在Thread的内部枚举类中。
线程状态 说明
NEW(新建) 线程刚被创建,但并未启动
Runnable(可运行) 线程已经调用了start(),等待CPU调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态。
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒。
Time Waiting(计时等待) 同waiting状态,有几个方法(sleep,wait)有超时参数,调用它们将进入Timed Waiting状态。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止run方法而死亡。

多线程学习笔记_第1张图片

你可能感兴趣的:(java,java)