java多线程

java多线程

概念

进程 :

是程序的基本执行实体.
线程:

线程是操作系统能够进行运算的调度的最小单位.它被包含在进程 之中,是进程中的实际运算单位.
线程简单理解 :

应用软件中相互独立,可以同时运行的功能.

过去的代码都是单线程代码,从上到下依次运行.
多线程程序特点:

CPU可以在多个程序中来回执行,把内存创建等,等待时间充分利用起来.

多线程的应用场景

让多个功能同时运行.
例如:

在软件的耗时操作:在拷贝,迁移大文件时同时 进行其他操作.
多线程作用:

多线程可以让程序同时做很多事情.提高效率.

并发和并行

并发:

在同一时刻,有多个指令在单个CPU交替 执行.

并行:

在同一时刻,有多个指令在多个CPU上同时 执行.

CPU线程数表示计算机最大同时可以运行线程个数,超过并行.

多线程的实现方式(三种)

方式一:

java 复制代码
		/*
       * 多线程的第一种启动方式:
       * 1.自己定义一个类继承Thread类
       * 2.重写run方法
       * 3.创建子类对象,并启动线程
       * */

		

自己定义类继承Thread类和重写run方法

java 复制代码
public class MyThread extends Thread {
   //2.重写run方法
    @Override
    public void run() {
        //书写线程要执行的代码
        for (int i = 0; i < 50; i++) {
            System.out.println("hello,world");
        }
    }
}
java 复制代码
//创建子类对象,启动线程(一个对象表示一个线程)
//多个线程需要创建多个对象
MyThread myThread1 = new MyThread();
//开启线程
//不能直接用对象调用run方法
myThread1.start();

给线程命名(setName方法)

java 复制代码
//给线程命名
myThread1.setName("线程1");
myThread2.setName("线程2");

获取线程名字(getName方法)

java 复制代码
System.out.println(getName()+"hello,world");

方式二:

java 复制代码
/*
       * 多线程的第二种启动方式
       * 1.自己定义一个类实现Runnable接口
       * 2.重写里面run方法
       * 3.创建自己类的对象
       * 4.创建Thread类的对象
       * 5.开启线程
       * */

自己定义一个类实现Runnable接口

java 复制代码
public class MyRun implements Runnable {
    @Override
    public void run() {
        //书写线程要执行的代码
        for (int i = 0; i < 50; i++) {
            /*
            * 不能直接调用getName方法(没有继承Thread)
            * 调用getName方法前要获取当前线程对象
            * 获取方法:Thread.currentThread();
            * */
            Thread t = Thread.currentThread();
            System.out.println(t.getName()+"hello,world");
        }
    }
}
java 复制代码
//创建自己类的对象
        //表示多线程要执行的任务
        MyRun mr=new MyRun();

        //创建线程对象(直接用Thread创建,不需要再继承)
        //参数表示:要执行的任务
        Thread t1=new Thread(mr);
        //创建第二个线程(参数相同:执行任务一样)
        Thread t2=new Thread(mr);

        //设置线程名称
        t1.setName("线程1");
        t2.setName("线程2");

        //开启线程
        t1.start();
        t2.start();

方式三:

java 复制代码
		/*
       * 多线程第三种实现方式:
       * 特点:可以获取多线程运行的结果(前面两种都不可以)
       * 步骤:
       *    1.创建一个类实现Callable接口
       *    2.重写call方法(是有返回值的,表示多线程的运行结果)
       *    3.创建实现类的对象(表示多线程要执行的任务)
       *    4.创建Future接口实现类(FutureTask)的对象(作用管理多线程运行的结果)
       *    5.创建Thread对象
       *    6.启动线程.
       * 	7.用FutureTask的对象调用get方法获取结果
       * */

创建一个类实现Callable接口

java 复制代码
//Callable的泛型是指结果的类型
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum+=i;
        }
        return sum;
    }
}
java 复制代码
//3.创建实现类的对象(表示多线程要执行的任务)
        MyCallable mc=new MyCallable();

        //4.创建Future接口实现类(FutureTask)的对象(作用管理多线程运行的结果)
        //参数表示:要管理那个任务的结果
        FutureTask<Integer> ft=new FutureTask<>(mc);

        Thread thread=new Thread(ft);
        //启动线程
        thread.start();

        //获取多线程运行的结果
        Integer i = ft.get();
        System.out.println(i);

有多条线程时

要创建多个线程结果管理者对象

java 复制代码
//创建多线程运行的参数对象
        MyCallable mc = new MyCallable(list);

        //创建多线程运行结果的管理者对象
        //线程1
        FutureTask<Integer> ft1 = new FutureTask<>(mc);
        //线程2
        FutureTask<Integer> ft2 = new FutureTask<>(mc);

        //创建线程对象
        Thread t1 = new Thread(ft1);
        Thread t2 = new Thread(ft2);

        //设置线程名字
        t1.setName("抽奖箱1");
        t2.setName("抽奖箱2");

        //开启线程
        t1.start();
        t2.start();

        //获取线程返回值
        //线程1返回值
        Integer max1 = ft1.get();
        //线程2返回值
        Integer max2 = ft2.get();

三种方法的选择


如果不需要获取运行结果

选择前两类.
前两种的区别:
第一种方式可可扩展性差的原因:
java中只能单继承,不支持多继承 .
第二种方法:

如果要调用Thread中的方法,需要先获取线程对象再调用方法,
获取方法:Thread.currentThread();

Thread中常见的成员方法


getName方法细节:

1.如果我们没有给线程设置名字,线程也有默认名字
格式 :Thread-X(线程序号,从0开始)
setName方法细节:

除了setName方法给线程设置名字,还可以通过构造方法设置名字

java 复制代码
		//子类不能继承父类的构造方法(需要重新创造)
        //快捷方法alt+insert
        MyThread t1 = new MyThread("线程1");
        MyThread t2 = new MyThread("线程2");

获取当前线程对象方法的细节:

java 复制代码
//那条线程执行这个方法,此时就获取那条线程的对象
        //此时没有创建线程对象,所以获取的是执行main方法的线程
        Thread t = Thread.currentThread();
        String name = t.getName();
        System.out.println(name);//main
        /*
        * 细节:
        *       当JVM虚拟机启动后,会自动启动多条线程
        *       其中一条就是调用main方法,并执行里面的代码(名字就是main)
        *       在之前,我们写的所有代码,都是运行在main方法当中
        * */

让线程休眠方法细节:

1.那条线程执行到这个方法,那条线程就会在这里停留对应的时间

2.停留时间与方法参数有关:方法的参数就表示停留的时间.单位毫秒

3.时间到了后,线程会自动醒来继续执行后续代码.

java 复制代码
//停留10s再执行输出语句
Thread.sleep(10000);
System.out.println(11111);

让自己创建线程休眠

java 复制代码
@Override
    public void run() {
        //让自己创建的线程休眠
        //此时异常不能抛(父类中run方法没有抛,所以子类中也不能抛)
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (int i = 0; i < 50; i++) {
            System.out.println(getName()+"@"+i);
        }
    }

线程的优先级

线程的调度:

1.抢占式调度(强调随机性):

多条线程抢占CPU的执行权,执行的线程不确定,执行时间也不确定.

2.非抢占式调度:所有线程轮流执行,执行时间也差不多.
Java中采取的就是抢占式调度.
优先级表示抢占到CPU的概率
java中线程优先级分为10档,最小时1,最大是10,没有设置默认为5

优先级最大不能表示100%抢占到CPU,只是概率大.

java 复制代码
MyThread t1 = new MyThread("飞机");
MyThread t2 = new MyThread("坦克");

t1.setPriority(10);
t2.setPriority(1);

守护线程的细节 :
应用场景:

如:在QQ中聊天时,发送了一个文件当中途退出QQ传输文件这个线程就没必要再继续下去了.

java 复制代码
/*
         * 守护线程细节:当其他的非守护线程结束,守护线程也会陆续结束.
         * 守护线程不会一下子结束,会陆续结束          
         * */

        MyThread1 t1 = new MyThread1("女神");
        MyThread2 t2 = new MyThread2("备胎");

        //把第二个设置为守护线程
        t2.setDaemon(true);
        
        t1.start();
        t2.start();

礼让线程

线程的执行是随机的.

礼让线程作用:让线程尽可能均匀执行.

java 复制代码
@Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+":"+i);

            //出让线程(出让当前CPU的执行权)
            Thread.yield();
        }
    }

插入线程细节 :
作用:

将一个线程插入再另一个线程前,等插入的线程执行完,再执行后面的线程

java 复制代码
 MyThread t1 = new MyThread("飞机");

        t1.start();

        //把线程t1插在当前线程之前
        //t1线程:飞机
        //当前线程:main线程
        t1.join();

        for (int i = 0; i < 10; i++) {
            //当前执行在main线程下
            System.out.println("main线程"+i);
        }

线程的生命周期

在 Java 中,当一个线程的run方法执行完毕后,这个线程就进入了终止状态。处于终止状态的线程不会再被调度获取 CPU 的执行权来执行run方法中的内容了。

java 复制代码
		MyThread t1 = new MyThread("窗口1");
        MyThread t2 = new MyThread("窗口2");
        MyThread t3 = new MyThread("窗口3");


        t3.start();
        t1.start();
        t2.start();
MyThread t1 = new MyThread("窗口1");
        MyThread t2 = new MyThread("窗口2");
        MyThread t3 = new MyThread("窗口3");


        //首先进入运行状态获取第一张票,但没有执行权
        t3.start();
        //进入运行状态获取第第二张张票,但没有执行权
        t1.start();
        //进入运行状态获取第第三张张票,并且抢到执行权先执行
        t2.start();
        
        //票从第一张开始卖
        /*
          窗口1卖出第3张票
          窗口1卖出第4张票
          窗口1卖出第5张票
          窗口3卖出第1张票
          窗口2卖出第2张票
          窗口1卖出第6张票
          */

线程安全问题

线程在执行代码的时候,CPU的执行权随时会被抢走(代码可能执行了一半).
解决方法:

把操作共享数据的代码锁起来.

(锁):表示锁对象

java 复制代码
/*
     * 锁对象是任意的如:Object类型的
     * 但是要保证锁对象的唯一性(static)
     * static Object lock = new Object();
     * */

    //锁对象(一般写当前类的字节码文件对象)
    //synchronized (MyThread.class)
    static Integer lock = 0;

    @Override
    public void run() {

        while (true) {
            //同步代码块:把共享数据的代码锁起来
            synchronized (lock) {
                if (counter < 100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    counter++;
                    System.out.println("正在卖第" + counter + "张票");
                } else {
                    break;
                }
            }
        }

同步代码块的细节

synchronized ()中的锁对象必须是唯一的.

如果锁不唯一,不能起到效果.
注意:

锁对象一般写当前类的字节码文件对象(这个对象是唯一的).

java 复制代码
synchronized (MyThread.class)

同步方法

如果要把一个方法中所有代码锁起来时 ,此时没必要用同步代码块,可以直接将synchronized关键字直接加到方法上.

java 复制代码
//前面不需要写static,因为此时MyRunnable表示线程要执行的任务
    //只会创建一次所以不需要添加static表示共享.
    int ticket = 0;

    @Override
    public void run() {
        while (true) {
            if (method()) break;
        }
    }

    private synchronized boolean method() {
        if (ticket == 100) {
            return true;
        } else {
            ticket++;
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "在卖第" + ticket + "张票");
        }
        return false;
    }

StringBuffer和StringBuilder区别

StringBuffer的和StringBuilder功能一样都是用于字符串的拼接.
区别:

StringBuffer中方法都是添加了synchronized 关键字的同步方法.在多线程情况下可以保证数据安全.

StringBuilder方法适用于不考虑多线程数据安全的情况.

Lock对象

Lock实现提供比使用synchronized方法和语句可以获得更加广泛的锁定操作

Lock中提供了获得锁和释放锁的方法

void lock(): 获得锁

void unlock(): 释放锁

java 复制代码
//创建Lock对象
    //Lock是一个接口,不能直接创建对象要创建接口的实现对象
    //因为对象的反复创建,为了保证锁的唯一性添加static
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //同步代码块:把共享数据的代码锁起来
            //synchronized (MyThread.class) {
            lock.lock();//上锁
            try {
                if (counter == 100) {
                    break;
    
                } else {
                    Thread.sleep(100);
                    counter++;
                    System.out.println("正在卖第" + counter + "张票");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                //finally:保证开锁代码,一定被执行.
                lock.unlock();//开锁(释放锁)
            }
            //}
            
        }

死锁

死锁不是一个知识点是一个错误.
死锁是指:

在程序中出现了锁的嵌套.

例:

出现死锁时程序会卡死,不会继续往下执行.

生产者和消费者(等待唤醒机制)

生产者消费者模式是一个十分经典的多线程协作模式
情况一 :

情况二 :

等待唤醒机制

上述过程涉及的方法

如果有多个等待的线程:notify()方法会随机唤醒一个,所以notifyAll()方法更常用.
消费者代码

java 复制代码
public class Foodie extends Thread{
    //消费者

    @Override
    public void run() {
        /*
         * 书写多线程的套路
         * 1.循环
         * 2.同步代码块(可以该为同步方法/Lock锁)
         * 3.判断共享数据是否到末尾(到末尾的情况)
         * 4.判断共享数据是否到末尾(没到末尾的情况,执行核心逻辑)
         * */

        while(true){
            //Desk.lock:在控制生产者和消费者的执行类中定义了
            synchronized (Desk.lock){
                if(Desk.count==0){
                    break;
                }else {
                    //判断桌子上是否有面条
                    if(Desk.foodFlag==0){
                        //如果没有,等待
                        //不能直接用wait方法等待,要用锁对象调用
                        try {
                            Desk.lock.wait();//让当前线程和锁进行绑定
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        //Desk.lock.notifyAll();//唤醒这把锁对应的所有线程

                    }else {
                        //把吃的总数-1
                        Desk.count--;
                        //如果有,开吃
                        System.out.println("吃货在吃面条,还能吃"+Desk.count+"碗");
                        //吃完唤醒Cook继续做
                        Desk.lock.notifyAll();
                        //修改桌子状态
                        Desk.foodFlag=0;
                    }
                }
            }
        }
    }
}

生产者代码

java 复制代码
public class Cook extends Thread {
    //生产者
    @Override
    public void run() {
        /*
         * 书写多线程的套路
         * 1.循环
         * 2.同步代码块(可以该为同步方法/Lock锁)
         * 3.判断共享数据是否到末尾(到末尾的情况)
         * 4.判断共享数据是否到末尾(没到末尾的情况,执行核心逻辑)
         * */

        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    //判断桌子上是否有食物
                    if (Desk.foodFlag == 1) {
                        try {
                            //有,就等待
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        //如果没有,就制作食物
                        System.out.println("厨师做了一碗面条");
                        //修改桌子上的食物状态
                        Desk.foodFlag = 1;
                        //唤醒消费者开吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

控制生产者和消费者的执行

java 复制代码
public class Desk {
    /*
    * 作用:
    * 控制生产者和消费者的执行
    * */

    //表示桌子上是否有面条: 0:没有面条   1:桌子上有面条
    public static int foodFlag=0;

    //吃货最多可以吃10碗面
    public static int count=10;

    //锁对象
    public static Object lock=new Object();
}

测试类代码

java 复制代码
 		//创建线程对象
        Cook c=new Cook();
        Foodie f=new Foodie();

        //给线程设置名字
        c.setName("厨师");
        f.setName("吃货");

        //开启线程
        c.start();
        f.start();

上述代码用厨师和吃货分别代表一个线程,模拟了多线程协作模式.

等待唤醒机制(阻塞队列方式实现)


阻塞队列的继承体系图

实现步骤

1.创建阻塞队列的实现类

java 复制代码
//1.创建阻塞队列(接口)的实现类
//使用数组实现类要指定大小(构造方法的参数)
ArrayBlockingQueue<String> queue=new ArrayBlockingQueue<>(1);

2.通过创建对象的方式将queue传递给生产者和消费者
生产者

java 复制代码
public class Cook extends Thread{
    //消费者
    
    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue){
        this.queue = queue;
    }

    @Override
    public void run() {

    }
}

消费者

java 复制代码
 ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {

    }
}

3.补全生产者和消费者的run方法
生产者

java 复制代码
public class Cook extends Thread{
    //消费者

    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        while(true){
            //不断把面条放到阻塞队列中
            try {
                //队列的添加方法
                //put方法底层带有锁,不要再添加锁,否则形成锁的嵌套(死锁)
                queue.put("面条");
                System.out.println("厨师做了一碗面条");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

消费者

java 复制代码
public class Foodie extends Thread {
    ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            //不断从队列获取面条
            try {
                //队列获取方法
                //take方法底层带有锁,不要再添加锁,否则形成锁的嵌套(死锁)
                queue.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

测试类代码

java 复制代码
/*
        * 阻塞队列实现等待唤醒机制
        * 细节:
        *     生产者和消费者使用同一个阻塞队列
        * */

        //1.创建阻塞队列(接口)的实现类
        //使用数组实现类要指定大小(构造方法的参数)
        ArrayBlockingQueue<String> queue=new ArrayBlockingQueue<>(1);

        //2.通过创建对象的方式将queue传递给生产者和消费者
        Cook c=new Cook(queue);
        Foodie f=new Foodie(queue);

        //3.开启线程
        c.start();
        f.start();

线程的6种状态

没有蓝色的状态,蓝色的状态是为了方便理解.

线程池


线程池作用:

实现线程的复用.

线程池的代码实现


创建线程池的两种方式

第一种(无上限)

java 复制代码
		//1.获取线程池对象(无上限)
       ExecutorService pool1 = Executors.newCachedThreadPool();

        //2.提交任务
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());

        //3.销毁线程(池内所有线程随之消失)
        //线程池一般不关闭
        pool1.shutdown();

第二种(有上限)

java 复制代码
		//1.获取线程池对象(无上限)
		//参数指定最大线程数
        ExecutorService pool1 = Executors.newFixedThreadPool(3);

        //2.提交任务
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());

        //3.销毁线程(池内所有线程随之消失)
        //线程池一般不关闭
        pool1.shutdown();

自定义线程池

自定义线程池路实现思路


临时线程创建的时机

核心线程都在处理任务,队列也排满时.
任务执行的顺序

不一定是先提交,先执行.如:此时任务4,5,6在排队,任务7,8已经执行了.
当任务数超过核心线程数+队列长度+临时线程数

超出的任务会触发任务拒绝策略.

四种任务拒绝策略
自定义线程池的代码实现

创建自定义线程池

java 复制代码
/*
        * ThreadPoolExecutor tpe=new ThreadPoolExecutor
        * (核心线程数,最大线程数,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
        * 参数一:核心线程数目               不能小于0
        * 参数二:最大线程数                 不能小于等于0, 最大线程数>=核心线程数
        * 参数三:空闲线程最大存活时间         不能小于0
        * 参数四:时间单位                   用TimeUnit指定
        * 参数五:任务队列(阻塞队列)                  不能为null
        * 参数六:创建线程工厂                不能为null
        * 参数七:
         *       任务的拒绝策略  是ThreadPoolExecutor的静态内部类   不能为null
          *      创建方式new ThreadPoolExecutor(外部类).内部类
          *
          *      定义内部类的情况:
          *      内部类依赖外部类存在,单独出现没有意义
          *      内部类本身是一个单独的个体
         */

        //创建自定义线程池
        ThreadPoolExecutor tpe=new ThreadPoolExecutor(
                3,//核心线程数目不能小于0
                6,//最大线程数 不能小于等于0, 最大线程数>=核心线程数
                60,//空闲线程最大存活时间
                TimeUnit.SECONDS,//时间单位=>此时是秒
                new ArrayBlockingQueue<>(3),//任务队列(阻塞队列)不能为null
                Executors.defaultThreadFactory(),//创建线程工厂不能为null
                new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
        );

提交任务

java 复制代码
//提交任务
tpe.submit(new MyRunnable());

关闭线程池

java 复制代码
//关闭线程池
 tpe.shutdown();

线程池的大小

thread dump工具是用来测CPU计算时间和等待时间的.

获取JVM虚拟机可用处理器数目

java 复制代码
//获取JVM虚拟机可用线程数
int count=Runtime.getRuntime().availableProcessors();
System.out.println(count);
根据公式创建线程池大小的两种情况

最大并行数 =JVM虚拟机可用线程数.
类型一(项目是CPU密集型)
CPU密集型 :计算比较多,读取本地文件或数据库的操作比较少.
公式 :最大并行数+1
类型二(项目是I/O密集型)
CPU密集型 :计算比较少,读取本地文件或数据库的操作比较多.
公式:如图

多线程后续需要学习知识

P167

相关推荐
诚丞成15 分钟前
计算世界之安生:C++继承的文水和智慧(上)
开发语言·c++
Smile灬凉城66627 分钟前
反序列化为啥可以利用加号绕过php正则匹配
开发语言·php
lsx20240638 分钟前
SQL MID()
开发语言
Dream_Snowar42 分钟前
速通Python 第四节——函数
开发语言·python·算法
西猫雷婶43 分钟前
python学opencv|读取图像(十四)BGR图像和HSV图像通道拆分
开发语言·python·opencv
鸿蒙自习室44 分钟前
鸿蒙UI开发——组件滤镜效果
开发语言·前端·javascript
星河梦瑾44 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富1 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想1 小时前
JMeter 使用详解
java·jmeter
言、雲1 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库