多线程总结

本文详细介绍了Java中创建线程的三种方式:继承Thread类、实现Runnable接口和实现Callable接口,包括各自的优缺点和使用示例。同时,文章讨论了线程的常用方法,如设置线程名称、优先级和守护线程,并分析了线程安全问题,给出了同步代码块、同步方法和Lock锁的解决方案。此外,还提到了生产者消费者模型和线程池的概念及其应用。
摘要由CSDN通过智能技术生成

创建线程的方式

1. 继承Thread类,重写run方法

2. 实现Runnable接口,重写run方法

3. 实现Callable接口,重写run方法

方式一、继承Thread类 

特点:继承了Thread类,能够在run方法中调用Thread的方法,如getName()等

           使用该方法开启不同的线程时,需要创建多个线程对象,因此,在使用同步代码块给代码加锁的时候,不能直接在这个类中对this对象进行加锁,因为不同对象,它们的this是不同的

缺点:因为在Java中,类是单继承的,因此当使用这种方式时,就不能同时继承其他的类了

代码实现

创建线程类

public MyThread extends Thread{
    @Override
    public void run(){
        // 这里是线程的要实现的代码
        for(int i = 0 ; i <= 10 ; i++){
            System.out.println(getName()+"========="+i);
        }
    }

}

测试类

public class ThreadDemo{
    public static void main(String[] args){
        // 创建线程对象
        MyThread mt = new MyThread();
        MyThread mt1 = new MyThread();
        // 设置线程名称
        mt.setName("Thread-1");
        mt1.setName("Thread-2");
        // 开启子线程
            // 使用start方法,而不使用run方法:因为使用start方法才能开启线程,而run方法只是普通方法
        mt.start();
        mt1.start();
    }

}

方式二、实现Runnable接口

特点:

  • 避免单继承限制

                由于Java只支持单继承,如果一个类已经继承了其他类,则无法直接继承Thread类来创建线程。通过实现Runnable接口,可以避免这个限制,因为类可以实现多个接口。

  • 提高代码的重用性

                通过实现Runnable接口,将线程逻辑独立于线程类之外,使得线程类更加专注于线程的生命周期管理以及与线程相关的操作。这样可以提高代码的重用性,因为多个线程可以共享同一个Runnable实例。

  • 支持资源共享 

                当多个线程使用同一个Runnable实例时,它们可以共享同一个实例变量,从而实现资源的共享。这在某些场景下非常有用,例如线程池中的线程、多个线程访问同一个共享资源等。

  • 降低耦合性

                通过实现Runnable接口,线程对象与线程逻辑相分离,使得线程对象与特定的线程逻辑解耦。这允许在不修改线程对象的情况下,更换不同的线程逻辑,提高了代码的灵活性。

实现步骤

*   1. 自己定义一个类实现Runnable接口
*   2. 重写里面的run()方法
*   3. 创建自己的类的对象
*   4. 创建爱你一个Thread类的对象,并开启线程

代码示例

线程类和测试类分开实现

线程类

public class MyRun implements Runnable{
    @Override
    public void run() {
        // 书写线程要执行的代码
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"Hello World!!");
        }
    }
}

 测试类

public class ThreadDemo {
    public static void main(String[] args) {
        /*
        *多线程的第二种启动方式:
        *   1. 自己定义一个类实现Runnable接口
        *   2. 重写里面的run()方法
        *   3. 创建自己的类的对象
        *   4. 创建爱你一个Thread类的对象,并开启线程
        **/


        // 创建实现了Runnable接口的对象 MyRun
        // 这个对象表示要执行的任务
        MyRun myRun = new MyRun();

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

        // 设置线程名称
        t1.setName("Thread1:");
        t2.setName("Thread2:");

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

    }
}
匿名内部类实现
        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + "Hello World!!");
                }
            }
        });
        t3.start();
lambda表达式实现
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "Hello World!!");
            }
        });
        t3.start();

方式三、实现Callable接口

特点:可以获取到多线程运行的结果

区别:Callable与Runnable以及Thread最大的区别就是,Callable有返回值

实现步骤

*   1. 创建一个类MyCallable,实现Callable()
*   2. 重写call (是有返回值的,表示多线程运行的结果)
*
*   3. 创建MyCallable的对象(表示 多线程要执行的任务)
*   4. 创建FutureTask的对象(作用:管理多线程运行的结果)
*   5. 创建Thread类的对象

代码样例

线程类

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 求 1- 100 之间的和
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

 测试类

public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
        *  多线程的第三种实现方式
        *       特点: 可以获取到多线程运行的结果
        *
        *   1. 创建一个类MyCallable,实现Callable()
        *   2. 重写call (是有返回值的,表示多线程运行的结果)
        *
        *   3. 创建MyCallable的对象(表示 多线程要执行的任务)
        *   4. 创建FutureTask的对象(作用:管理多线程运行的结果)
        *   5. 创建Thread类的对象
        *
        * **/

        //        *   3. 创建MyCallable的对象(表示 多线程要执行的任务)
        MyCallable mc = new MyCallable();
        //        *   4. 创建FutureTask的对象(作用:管理多线程运行的结果)
        FutureTask<Integer> task = new FutureTask<>(mc);
        //        *   5. 创建Thread类的对象
        Thread t1 = new Thread(task);
        // 启动线程
        t1.start();

        // 获取多线程运行的结果
        Integer res = task.get();
        System.out.println("res = " + res);
    }
}

线程常用方法

总汇

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

String getName(): 获取线程名称

static Thread currentThread(); : 获取当前线程对象

static void sleep(long time) :让线程休眠time毫秒

setPriority(int newPriority): 设置线程的优先级 

final int getPriority():            获取线程的优先级

final void setDaemon(boolean on):        设置为守护线程

public static void yield()  礼让线程、出让线程 

public static void join()  礼让线程、出让线程

使用

设置线程名称

给线程设置名称的方法:

①线程对象.setName
② 在继承Thread的类中重新含有设置名称的构造方法,然后在新建对象的时候,在形参部分传入线程名称即可,

线程优先级

前言

计算机当中线程的调用方式:
1. 抢占式调度   
        
   多个线程抢夺 CPU 的执行权,CPU在某个时候执行某条线程是不确定的,执行多少时间也是不确定的         特点:主打一个   随机性
2. 非抢占式调度  

   所有的线程轮流地执行,执行的时间也是差不多的

在Java中,采用的是 抢占式翘度的方式:随机

方法

setPriority(int newPriority): 设置线程的优先级
final int getPriority():获取线程的优先级
线程优先级:默认和main线程的是5 ,最大是10,最小是1
         // main线程的优先级
        System.out.println("Thread.currentThread().getPriority() = " + Thread.currentThread().getPriority());

        Thread t1 = new Thread(mr,"飞机");
        Thread t2 = new Thread(mr,"大炮");

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

设置守护线程

 细节

当其他的非守护线程执行完毕后,守护线程陆续结束

通俗说法:
       当女神线程结束了,那么备胎也没有什么存在的必要了
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("女神");
        t2.setName("备胎");

        // 将t2 设置为守护线程
        t2.setDaemon(true);

        t1.start();
        t2.start();

 礼让线程

public static void join()  礼让线程、出让线程
 public static void yield()  礼让线程、出让线程

 yield():主动礼让,礼让后还能和其他线程争抢CPU资源

        /*
        * public static void yield()  礼让线程、出让线程
        * */
        MyThread1 t1 = new MyThread1();
        MyThread1 t2 = new MyThread1();

        t1.setName("飞机");
        t2.setName("坦克");

        // 将t2 设置为守护线程
        t2.setDaemon(true);

        t1.start();
        t2.start();

join:表示把 t1 这个线程插入到当前线程(执行在哪个线程上,当前线程就表示哪个线程)之前

      /*
        * public static void join()  礼让线程、出让线程
        * */
        MyThread1 t1 = new MyThread1();

        t1.setName("tomato");
        t1.start();
        // join 表示把 t1 这个线程插入到当前线程(执行在哪个线程上,当前线程就表示哪个线程)之    前
        // 当前线程:main线程
        t1.join();


        // 执行main中的线程
        for (int i = 0; i < 11; i++) {
            System.out.println("main线程:"+i);
        }

线程安全

小引

需求:

        某电影院目前正在上映国产大片,共有10张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

 代码实现

线程类

public class MyThread extends Thread{
    // static 表示这个类所有对象共享该属性
    static  int ticket = 0;
    @Override
    public void run() {
        while (true){
                if (ticket < 20){
                    try {
                        Thread.sleep(10);
                        ticket++;
                        System.out.println(getName()+"  正在卖票    "+ticket);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    System.out.println(getName()+"  票卖完了....");
                    break;
                }
        }
    }
}

 测试类

public class ThreadDemo {
    public static void main(String[] args) {

        // 创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        // 起名
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        // 开启
        t1.start();
        t2.start();
        t3.start();
    }
}

分析

        按这种方式执行,卖票的情况 可能出现:当卖完最后一张票的时候,还会有其他窗口(线程)进行卖票。因为在线程类中有个暂停10毫秒的方法(就算没有,也可能出现),当最后一个线程卖完票,还没来得及增加票数并打印卖票信息的时候,其他两个窗口(线程)以及抢到了CPU的资源,就在这段时间它们也去增加票数并打印卖票信息了。这就造成了线程安全问题。

        简单地来说就是,由于共享资源ticket是这三个窗口都能访问的,而在访问的时候没有对这三个窗口进行限制(没有加锁等操作),导致超卖了。

 解决

思想:给线程访问共享资源的这段代码加锁

方式:加   synchronized

1. 同步代码块方式

        特点1: 锁默认打开,有一个线程进去了,锁自动关闭

        特点2: 里面的代码全部执行完毕,线程出来,锁自动打开

2. 同步方法方式

        特点1:同步方法是锁住方法里面所有的代码

        特点2:锁对象不能自己指定

                非静态:this

                静态:当前类的字节码文件对象

同步代码块方式 

public class MyThread extends Thread{

    // static 表示这个类所有对象共享该属性
    static  int ticket = 0;
    @Override
    public void run() {
        while (true){
            synchronized (MyThread.class){
                if (ticket < 20){
                    try {
                        Thread.sleep(10);
                        ticket++;
                        System.out.println(getName()+"  正在卖票    "+ticket);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    System.out.println(getName()+"  票卖完了....");
                    break;
                }
            }
        }
    }
}
细节
①同步代码块不能写在循环的外面:因为如果写在循环外面,一旦有一个线程对象获取到了锁,该对象就会一直执行,直至循环条件结束。这样就会导致其他线程对象就算抢夺到了CPU 的执行权,也没能得到锁,不能参与执行,也就没有意义了

②锁对象:可以是任意一个对象,但一定要是唯一的。因此这里不能用this,因为this代表当前对象;在测试类中创建对象时,有三个线程类对象,即三个对象都是当前对象,导致对象不唯一,达不到锁的效果

 class对象是唯一的

同步方法方式

 技巧:

        先写同步代码块,将其改为同步方法

步骤

// 1. 循环
// 2. 同步代码块
//3. 判断共享数据是否到末尾,如果到了末尾的操作
//4. 判断共享数据是否到末尾,如果没有到末尾的操作

线程类(通过实现Runnable方式) 

public class MyRunnable implements Runnable{
    // 因为MyRunnable是作为参数传入Thread中进行创建线程的,因此只需要传一次,故不会出现像extends Thread实现创建线程的方式那样创建多个线程对象
    // 所以也就不用加static实现共享变量了
    int ticket = 0;

    @Override
    public void run() {

        while (true){
                if (method()) break;
            }
    }
    // 锁对象:this--> mr
    private synchronized boolean method() {
        if (ticket == 100){
            return true;
        }else {
            try {
                Thread.sleep(10);
                ticket ++;
                System.out.println(Thread.currentThread().getName()+"   在卖第 "+ticket+"张票");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

Lock锁方式

线程类

public class MyThread extends Thread{

    // static 表示这个类所有对象共享该属性
    static  int ticket = 0;

    // 使用Lock接口下的ReentrantLock
    // 因为使用的是继承Thread类的方式创建的多线程,因此在创建该类的对象时,会创建多个,
    // 多个线程对象有着自己的Lock属性,因此为了实现锁的效果,需要将lock使用static修饰
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
//            synchronized (MyThread.class){
            lock.lock();
            try {
                if (ticket < 20){
                        Thread.sleep(10);
                        ticket++;
                        System.out.println(getName()+"  正在卖票    "+ticket);
                }else{
                    System.out.println(getName()+"  票卖完了....");
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

测试类

public class ThreadDemo {
    public static void main(String[] args) {

        // 创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        // 起名
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        // 开启
        t1.start();
        t2.start();
        t3.start();
    }
}

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

特点:该模式是一个十分经典的多线程写作的模式

作用:在Java中,线程的执行是有随机性的,而这个等待唤醒机制就要打破这种随机规则,它会让两个线程轮流执行

情景

情景一:消费者等待

 *   消费者(消费数据):
 *          1. 判断桌子上是否有食物
 *          2. 如果没有就等待(wait)
*   生产者(生产数据):
 *          1. 制作食物
 *          2. 把食物放在桌子上
 *          3. 叫醒等待的消费者(notify)

情景二:生产者等待

*   消费者(消费数据):
*          1.判断桌子上是否有食物
*          2.如果没有,等待
*          3. 如果有,开吃
*          4. 吃完之后,唤醒厨师继续做
*
*   生产者(生产数据):
*          1. 判断桌子上是否有食物
*          2. 没有,制作食物
*          3. 有,等待
*          4. 把食物放在桌子上
*          5 叫醒等待的消费者(notify)

完整过程

*   生产者(生产数据):
*          1. 判断桌子上是否有食物
*          2. 没有,制作食物
*          3. 有,等待
*          4. 把食物放在桌子上
*          5 叫醒等待的消费者(notify)
*
*   消费者(消费数据):
*          1.判断桌子上是否有食物
*          2.如果没有,等待
*          3. 如果有,开吃
*          4. 吃完之后,唤醒厨师继续做

 

代码实现

生产者类(厨师类)

步骤

* 1. 循环
* 2. 同步代码块(可改写为同步方法)
* 3. 判断共享数据是否到达末尾(到了末尾)
* 3. 判断共享数据是否到达末尾(没到末尾,执行核心逻辑)
public class Cook extends Thread{
    @Override
    public void run() {
       while (true){
           synchronized (Desk.lock){
               if (Desk.count == 0){
                   break;
               }else{
                   // 判断桌子上是否有食物
                   if (Desk.foodFlag==1){
                      //如果有,等待
                       try {
                           Desk.lock.wait();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                   }else{
//                       Desk.count++;
                       // 如果没有,制作食物
                       if (Desk.count>0){
                           System.out.println("厨师又做了一碗面条");
                       }else{
                          System.out.println("厨师做了一碗面条");
                       }
                       // 修改桌子上的食物状态
                        Desk.foodFlag = 1;
                       // 叫消费者来吃,唤醒
                        Desk.lock.notifyAll();
                   }
               }
           }
       }
    }
}

 

消费者类(吃货类)

步骤

* 1. 循环
* 2. 同步代码块(可改写为同步方法)
* 3. 判断共享数据是否到达末尾(到了末尾)
* 3. 判断共享数据是否到达末尾(没到末尾,执行核心逻辑)
public class Foodie extends Thread{
    @Override
    public void run() {
        while (true){
            synchronized (Desk.lock){
                if (Desk.count == 0){
                    break;
                }else{
                    // 先判断桌子上是否有面条
                    if (Desk.foodFlag == 0){
                    // 如果没有,等待
                        //让当前线程跟锁进行绑定
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else{
                        // 吃完后要将面条总数减一
                        Desk.count--;
                        // 如果有,开吃
                        if (Desk.count==0){
                            System.out.println("吃货吃饱了,嗝~,吃不下了");
                        }else{
                            System.out.println("吃货在吃面条,还能吃 "+Desk.count+"碗!!");
                        }
                        //吃完后唤醒厨师继续做
                        Desk.lock.notifyAll();
                        // 修改桌子状态
                        Desk.foodFlag = 0;
                    }


                }
            }
        }
    }
}

 

平台类(桌子类)

作用:控制生产者和消费者的执行

public class Desk {

    // 是否有面条:
        // 0: 没有面条
        // 1:有面条
    public static int  foodFlag = 0;

    // 总个数
    public static int count = 10;

    // 锁对象
    public static Object lock = new Object();
}
测试类
public class ProducterAndComsummer {
    public static void main(String[] args) {
        // 创建线程对象
        Cook cook = new Cook();
        Foodie foodie = new Foodie();

        // 设置线程名
        cook.setName("厨师 ");
        foodie.setName("吃货 ");

        // 开启线程
        cook.start();
        foodie.start();
    }
}

阻塞队列方式

代码实现

厨师类
public class Cook extends Thread{
    // 创建阻塞队列
    ArrayBlockingQueue<String> queue ;

    public Cook(ArrayBlockingQueue<String> queue){
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true){
            //  厨师不断地把面条放到阻塞队列中
            try {
                queue.put("面条");
                System.out.println("厨师煮了一碗面条");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}
吃货类
public class Foodie extends Thread {
    // 创建阻塞队列
    ArrayBlockingQueue<String> queue ;

    public Foodie(ArrayBlockingQueue<String> queue){
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true){
            //  厨师不断地把面条放到阻塞队列中
            try {
                String food = queue.take();
                System.out.println("吃货吃了一碗"+food);
                System.out.println();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}
测试类
public class ThreadDemo {
    public static void main(String[] args) {
    // 1. 创建阻塞队列
    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1) ;

    // 2. 创建线程的对象,并把阻塞队列传递过去
        Cook cook = new Cook(queue);
        Foodie foodie = new Foodie(queue);

        cook.start();
        foodie.start();

    }
}
细节

 生产者和消费者必须使用同一个阻塞队列

线程生命周期

生命周期状态

状态

创建 :start()

就绪:不停地抢CPU资源

运行:运行代码

阻塞:sleep()或其他阻塞方式

死亡:run()执行完

图解

 所有状态及产生状态的原因

 代码中的生命周期

 

线程池(重点)

线程池类实现

步骤

1. 获取线程池对象

2. 提交任务

3. 所有的任务全部执行完毕,关闭线城池

public class MyThreadPoolDemo {
    /*
        public static ExecutorService newFixedThreadPool(int nTheads);
        public static ExecutorService newCachedThreadPool();
     */
    public static void main(String[] args) {
        // 1. 获取线程池对象
//        ExecutorService pool = Executors.newCachedThreadPool();
        ExecutorService pool = Executors.newFixedThreadPool(3);

        // 2.提交任务
        pool.submit(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"------------"+i);
            }
        });
        pool.submit(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"------------"+i);
            }
        });

        // 3. 所有的任务全部执行完毕,关闭线城池
        pool.shutdown();
    }
}

 

自定义线程池

自定义线程池参数

corePoolSize:                核心线程数

maximumPoolSize:        最大线程数

keepAliveTime:              临时线程数空闲存活时间(值)

TimeUnit:                        临时线程数空闲存活时间单位(单位)

BlockingQueue<Runnable>: 阻塞队列类型

ThreadFactory:                线程工厂(线程创建方式)

RejectedExecutionHandler: 拒绝线程处理器(拒绝策略)

线程池参数理解

 将饭店和线程池联系起来,理解如下 

各个参数的限制

 

临时线程的错误理解

 

临时线程的正确理解 

 阻塞队列的理解

  拒绝策略的理解

拒绝策略

类型

AbortPolicy

DiscardPolicy

DiscardOldestPolicy

CallerRunsPolicy 

说明

 本次多线程的总结是观看黑马视频的总结,其视频链接如下:

黑马Java进阶教程,全面剖析Java多线程编程,含抢红包、抽奖实战案例_哔哩哔哩_bilibili

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值