多线程

本文介绍了Java中多线程的实现方式,包括继承Thread类、实现Runnable接口以及利用Callable和Future接口。对比了各种实现方式的优缺点,并详细讲解了线程的生命周期、线程安全问题、同步机制如synchronized和Lock锁,以及生产者消费者模型和等待唤醒机制。最后讨论了线程的六种状态和线程调度策略。
摘要由CSDN通过智能技术生成

概述

  • 线程:
    • 线程是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中实际运作单位
    • 简单理解:应用软件中互相独立,可以同时运行的功能
  • 进程
    • 进程是程序的基本执行实体

 

多线程

什么是多线程

  • 有了多线程,我们就可以让程序同时做多件事情
     

作用

  • 提高效率
     

应用场景

只要你想让多个事情同时运行就需要用到多线程

  • 软件中的耗时操作
    • 拷贝、迁移大文件
    • 加载大量的资源文件
  • 所有的聊天软件
  • 所有的后台服务器

 

并发和并行

  • 并发:
    • 在同一时刻,有多个指令在单个CPU上交替执行
  • 并行:
    • 在同一时刻,有多个指令在多个CPU上同时执行

 

多线程的实现方式

 

继承Thread类的方式进行实现

  • 1.自己定义一个类继承Thread
  • 2.重写run方法
  • 3.创建子类对象,并启动线程
public class MyThread extends Thread {
    @Override
    public void run() {
        //线程要执行的代码
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "你好,猫猫");
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
    
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("线程1");
        t2.setName("线程2");

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

 

实现Runnable接口的方式进行实现

  • 1.自己定义一个类实现Runnable接口
  • 2.重新里面的run方法
  • 3.创建自己的类的对象
  • 4.创建一个Thread类对象,并开启线程
public class MyRun implements Runnable {
    @Override
    public void run() {
        //书写线程要执行的代码
        for (int i = 0; i < 100; i++) {
            //先获取到当前线程的对象
            /*Thread t = Thread.currentThread();
            System.out.println(t.getName() + "你好,猫猫!");*/
            System.out.println(Thread.currentThread().getName() + "你好,猫猫!");
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        //创建MyRun对象
        //表示多线程要执行的任务
        MyRun mr = new MyRun();

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

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

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

 

利用Callable接口和Future接口方式实现

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

  • 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 = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable mc = new MyCallable();

        FutureTask<Integer> ft = new FutureTask<>(mc);

        Thread t1 = new Thread(ft);

        t1.start();

        Integer result = ft.get();
        //5050
        System.out.println(result);
    }
}

 

三种实现方式对比

优点缺点
继承Thread类编程比较简单,可以直接使用Thread类中的方法可以扩展性较差,不能再继承其他的类
实现Runnable接口/实现Callable接口扩展性强,实现该接口的同时还可以继承其他的类编程相对复杂,不能直接使用Thread类中的方法
  • 继承Thread类/实现Runnable接口无法获得多线程的结果
  • 实现Callable接口可以获取多线程的结果

 

常见的成员方法

方法名称说明
String getName()返回此线程的名称
void setName(String name)设置线程的名字(构造方法也可以设置名字)
static Thread currentThread()获取当前线程的对象
static void sleep(long time)让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority)设置线程的优先级
final int getPriority()获取线程的优先级
final void setDaemon(boolean on)设置为守护线程
public static void yield()出让线程 / 礼让线程
public static void join()插入线程 / 插队线程

 

public class MyThread extends Thread {
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + "@" + i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        /*
        void setName(String name)
        细节:
            1。如果我们没有给线程设置名字,线程也是有默认的名字的
                格式:Thread-X(X序号,从0开始)
            2。如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置

        static Thread currentThread()
        细节:
            当JVM虚拟机启动之后,会自动启动多条线程
            其中有一条线程就叫做main线程
            他的作用就是去调用main方法,并执行里面的代码
            在以前,我们写的所有的代码,其实都是运行在main线程当中
        static void sleep(long time)
        细节:
            1。哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
            2。方法的参数:就表示睡眠的时间,单位毫秒
            3。当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
        */

        Thread t = Thread.currentThread();
        String name = t.getName();
        //main
        System.out.println(name);


        System.out.println("11111");
        Thread.sleep(5000);
        System.out.println("22222");


        MyThread t1 = new MyThread("朵朵");
        MyThread t2 = new MyThread("钢镚");

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

线程优先级

  • 线程的调度
    • 抢占式调度(随机性)
    • 非抢占式调度
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr, "朵朵");
        Thread t2 = new Thread(mr, "钢镚");

        //5
        //System.out.println(t1.getPriority());
        //5
        //System.out.println(t2.getPriority());
        //5 main
        //System.out.println(Thread.currentThread().getPriority());

		//不是绝对的,概率问题
        t1.setPriority(1);
        t2.setPriority(10);

        t1.start();
        t2.start();
    }
}
  • 设置守护线程
    • 细节:当其他的非守护线程执行完毕之后,守护线程会陆续结束
    • 通俗易懂:当"女神线程"结束了,那么"备胎线程"也没有寻找的必要了,陆续结束(不会立马结束)
public class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}

public class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

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

        //第二线程设置为守护线程(备胎线程)
        t2.setDaemon(true);

        t1.start();
        t2.start();
    }
}
  • 礼让线程
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + "@" + i);
            //表示出让当前CPU的执行权
            Thread.yield();
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("朵朵");
        t2.setName("钢镚");

        t1.start();
        t2.start();
    }
}
  • 插入线程
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.setName("朵朵");
        t.start();

        //表示把t这个线程,插入到当前线程之前
        //t:朵朵
        //当前线程:main线程
        t.join();
        
        //执行在main线程当中的
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程" + i);
        }
    }
}

 

线程的生命周期

在这里插入图片描述

  • 问:sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?
  • 答:不会,sleep方法时间到了之后处于就绪状态,会继续抢CPU的执行权,抢到了才会执行代码

 

线程安全问题

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

public class MyThread extends Thread {
    //表示这个类所有的对象,都共享ticket数据
    static int ticket = 0;

    @Override
    public void run() {
        while (true) {
            if (ticket < 100) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(getName() + "正在卖" + ticket + "张票");
            } else {
                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();
    }
}

买票引发的安全问题

  • 1.相同的票出现了多次
    • 线程执行时,有随机性
  • 2.出现了超出范围的票
    • 线程执行时候,有随机性
  • 解决:把if里操作共享数据的的代码锁起来,有线程进来之后,就算其他的线程抢夺到了cpu的执行权,也得等着。

 

同步代码块

  • 把操作共享数据的代码锁起来
  • 格式:
	synchronized(){
		操作共享数据的代码
	}
  • 特点1:锁默认打开,有一个线程进去了,锁自动关闭
  • 特点2:里面的代码全部执行完毕,线程出来,锁自动打开
public class MyThread extends Thread {
    //表示这个类所有的对象,都共享ticket数据
    static int ticket = 0;

    //锁对象,一定要是唯一的
    //static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            //同步代码块
            //synchronized (obj) {
            synchronized (MyThread.class) {
                if (ticket < 1000) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(getName() + "正在卖" + ticket + "张票");
                } else {
                    break;
                }
            }
        }
    }
}

 

同步方法

  • 就是把synchronized关键字加到方法上
  • 格式:
	修饰符 synchronized 返回值类型 方法名(方法参数) {}
  • 特点1: 同步方法是锁住方法里面所有的代码
  • 特点2: 锁对象不能自己指定
    • 非静态:this
    • 静态:当前类的字节码文件对象
public class MyRunnable implements Runnable {
    int ticket = 0;

    @Override
    public void run() {
        //1。循环
        while (true) {
            //2。同步代码块
            synchronized (MyRunnable.class) {
                //3。判断共享数据是否到了末尾,如果到了末尾
                if (ticket == 100) {
                    break;
                } else {
                    //4。判断共享数据是否到了末尾,如果没有到末尾
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket++;
                    System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
                }
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
  • 将同步代码块改成同步方法
  • 选中while里的语句 --> option+commend+M -->抽取出方法
  • 添加synchronized
public class MyRunnable implements Runnable {
    int ticket = 0;

    @Override
    public void run() {
        //1。循环
        while (true) {
            //2。同步代码块
            synchronized (MyRunnable.class) {
                if (method()) break;
            }
        }
    }

    //this
    private synchronized boolean method() {
        //3。判断共享数据是否到了末尾,如果到了末尾
        if (ticket == 100) {
            return true;
        } else {
            //4。判断共享数据是否到了末尾,如果没有到末尾
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
        }
        return false;
    }
}

 

Lock锁

  • 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁
  • 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
  • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
  • Lock中提供了获得锁和释放锁的方法(手动上锁、手动释放锁)
    • void lock():获得锁
    • void unlock():释放锁
  • Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
  • ReentrantLock的构造方法
    • ReentrantLock():创建一个ReentrantLock的实例
public class MyThread extends Thread {
    static int ticket = 0;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {
                if (ticket == 100) {
                    break;
                } else {
                    Thread.sleep(10);
                    ticket++;
                    System.out.println(getName() + "正在卖" + ticket + "张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

 

死锁

public class MyThread extends Thread {
    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        while (true) {
            if ("线程A".equals(getName())) {
                synchronized (objA) {
                    System.out.println("线程A拿到了A锁,准备拿B锁");
                    synchronized (objB) {
                        System.out.println("线程A拿到了B锁,准备执行完一轮");
                    }
                }
            } else if ("线程B".equals(getName())) {
                if ("线程B".equals(getName())) {
                    synchronized (objB) {
                        System.out.println("线程B拿到了B锁,准备拿A锁");
                        synchronized (objA) {
                            System.out.println("线程B拿到了A锁,准备执行完一轮");
                        }
                    }
                }
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("线程A");
        t2.setName("线程B");

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

        //线程A拿到了A锁,准备拿B锁
        //线程B拿到了B锁,准备拿A锁
    }
}

 

生产者和消费者

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

  • 生产者 – 生产数据
  • 消费者 – 消费数据
  • 假设生产者是厨师,用来制作食物
  • 假设消费者是吃货,用来进食食物
  • 核心思想:利用桌子来控制线程的执行

 

等待唤醒机制

  • 生产者
    • 判断桌子上是否有食物
      • 有:等待
      • 没有:制作食物
    • 把食物放在桌子上
    • 叫醒等待的消费者开吃
  • 消费者
    • 判断桌子上是否有食物
      • 如果没有就等待
      • 如果有就开吃
    • 吃完之后,唤醒厨师继续做

 

常见方法
方法名称说明
void wait()当前线程等待,直到被其他线程唤醒
void notify随机唤醒单个线程
void notifyAll唤醒所有线程
//作用:控制生产者和消费者的执行
public class Desk {

    //是否有食物 0:没有    1:有
    //boolean的弊端 只有两个结果,只能控制两条线程轮流执行
    //如果以后需求变了,需要三条四条线程,无法满足,考虑到后面的通用性,用int
    public static int foodFlag = 0;

    //总个数
    //吃货最多可以吃十碗
    public static int count = 10;

    //锁对象
    public static Object lock = new Object();
}
public class Cook extends Thread {
    @Override
    public void run() {
        /*
            1。循环
            2。同步代码块
            3。判断共享数据是否到了末尾(先写到了末尾的情况)
            4。判断共享数据是否到了末尾(没有到末尾的情况,执行题目的核心逻辑)
        * */
        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 {
                        //如果没有,就制作食物
                        System.out.println("厨师做了一碗面条");
                        //修改桌子上的食物状态
                        Desk.foodFlag = 1;
                        //叫醒等待的吃货开吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}
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();
                        }
                        //唤醒跟这把锁绑定的所有线程
                        Desk.lock.notifyAll();

                    } else {
                        //如果有,就开吃
                        //把吃的总数-1
                        Desk.count--;
                        System.out.println("吃货正在吃面条,还能再吃" + Desk.count + "碗!!!");

                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();

                        //修改桌子的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程的对象
        Cook c = new Cook();
        Foodie f = new Foodie();

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

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

 

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

  • put数据时:放不进去,会等着,也叫做阻塞
  • take数据时:取出第一个数据,取不到会等着,也叫做阻塞
  • 细节:
    • 生产者和消费者必须使用同一个阻塞队列

 

阻塞队列的继承结构

在这里插入图片描述

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);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        //1。创建阻塞队列的对象
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

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

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

 

线程的六种状态

在这里插入图片描述

  • 新建状态(NEW) --> 创建线程对象
  • 就绪状态 (RUNNABLE) --> start方法
  • 阻塞状态 (BLOCKED) --> 无法获得锁对象
  • 等待状态 (WAITING) --> wait方法
  • 计时等待 (TIMED_WAITING) --> sleep方法
  • 结束状态 (TERMINATED) --> 全部代码运行完毕
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值