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

相关推荐
ftswsfb2 分钟前
现代C++:现代C++?
开发语言·c++
乌萨奇也要立志学C++5 分钟前
【C++详解】C++ 智能指针:使用场景、实现原理与内存泄漏防治
开发语言·c++
minji...12 分钟前
C++ 详细讲解vector类
开发语言·c++
LiuYaoheng16 分钟前
【Android】View 的基础知识
android·java·笔记·学习
勇往直前plus23 分钟前
Sentinel微服务保护
java·spring boot·微服务·sentinel
星辰大海的精灵24 分钟前
SpringBoot与Quartz整合,实现订单自动取消功能
java·后端·算法
小鸡脚来咯26 分钟前
一个Java的main方法在JVM中的执行流程
java·开发语言·jvm
江团1io027 分钟前
深入解析三色标记算法
java·开发语言·jvm
天天摸鱼的java工程师36 分钟前
RestTemplate 如何优化连接池?—— 八年 Java 开发的踩坑与优化指南
java·后端
m0_7381207239 分钟前
CTFshow系列——PHP特性Web97-100
开发语言·安全·web安全·php·ctfshow