多线程详解

多线程详解

多线程是java学习中重要的一部分,我们会通过多线程实现同时操作同一资源的程序

进程和线程

在了解多线程之前我们先学习一些基本知识:

进程:是正在运行的程序

  • 是系统进行资源分配和调用的独立单位
  • 每个进程都有它自己的内存空间和系统资源

线程:是进程中的单个顺序控制流,是一条执行路径

  • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
  • 多线程:一个进程如果有多条执行路径,则被成为多线程程序

多线程的实现方法

方法1:继承Thread类

  • 定义一个类MyThread继承Thread类
  • 在MyThread类中重写run()方法
  • 创建MyThread类的对象
  • 启动线程

两个小问题:

  • 为什么要重写run()方法?
    • 因为run()是用来封装被线程执行的代码
  • run()方法和start()方法的区别?
    • run():封装线程执行的代码,直接调用,相当于普通方法的调用
    • start():启动线程,然后由JVM调用此线程的run()方法

下面给出示例代码:

public class Demo1 {
    public static void main(String[] args) {
        //首先创建对象
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        //调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
        my1.start();
        my2.start();
    }
}
public class MyThread extends Thread {
    //重写run方法,实现线程化
    @Override
    public void run() {
        for (int i=0;i<500;i++){
            System.out.println(i);
        }
    }
}

方法2:实现Runnable接口

  • 定义一个类MyRunnable实现Runnable接口
  • 在MyRunnable类里重写run方法
  • 创建MyRunnable类的对象
  • 创建Thread类的对象,参数是MyRunnable对象
  • 启动线程

采用接口的好处:

  • 避免了Java单继承的局限性
  • 适合多个相同的程序的代码去处理同一个资源的情况,把线程和程序的代码以及数据有效分离,较好得体现了面向对象的设计思想

下面给出示例代码:

public class Demo1 {
    public static void main(String[] args) {
        //首先创造MyRunnable类
        MyRunnable my = new MyRunnable();

        //然后创建Thread对象
        Thread t1 = new Thread(my);
        Thread t2 = new Thread(my);

        //可以采用带线程名的方法创造对象
        Thread t3 = new Thread(my,"吕小布");

        //下面正常运行即可
        t1.start();
        t2.start();
        t3.start();
    }
}
public class MyRunnable implements Runnable{
    //重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //这里没有继承Thread,所以不能直接使用getName
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

设置和获得多线程名称

Thread类中设置和获得多线程名称的方法:

  • void setName(String name):将此线程的名称更改为参数name
  • String getName():返回此线程的名称
  • 通过构造方法也可以设置线程名称

如何获得main()方法的线程名称?

  • public static Thread currentThread():返回当前正在执行的线程对象的引用

下面给出示例代码:(setName和getName)

public class Demo1 {
    public static void main(String[] args) {
        //首先创建对象
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        //我们可以通过setName()方法改变线程名
        my1.setName("吕小布");
        my2.setName("吕大布");

        //调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
        my1.start();
        my2.start();
    }
}
public class MyThread extends Thread{
    //重写run方法,实现线程化
    @Override
    public void run() {
        for (int i=0;i<500;i++){
            //我们可以采用getName()方法获得线程名
            System.out.println(getName() + ":" + i);
        }
    }
}

下面给出示例代码:(通过构造方法重写线程名以及获得main的线程名)

public class Demo1 {
    public static void main(String[] args) {
        //我们可以直接采用带参构造创造有线程名的线程
        //但注意:需要在MyThread里重写构造方法并super父类name
        MyThread my1 = new MyThread("吕小布");
        MyThread my2 = new MyThread("吕大布");


        //调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
        my1.start();
        my2.start();

        //这里再多讲一个Thread的Static方法
        //currentThread()是static方法,可以获得其当前运行的线程
        System.out.println(Thread.currentThread().getName());
    }
}
public class MyThread extends Thread{
    //重写构造方法
    public MyThread(){

    }

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

    //重写run方法,实现线程化
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            //我们可以采用getName()方法获得线程名
            System.out.println(getName() + ":" + i);
        }
    }
}

线程调度

线程有两种调度模型:

  • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
  • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么随机选择一个,优先级高的线程获取的CPU时间片相对较高

Java采用的是抢占式调度模型

假如计算机只有一个CPU,那么CPU在某一时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性的。

Thread类中设置和获取优先级的方法:

  • public final int getPriority():返回此线程的优先级
  • public final void setPriority(int newPriority):更改此线程的优先级

线程默认优先级是5,线程优先级范围是1~10;线程优先级高仅仅代表线程获得时间片的几率高。

下面给出示例代码:

public class Demo1 {
    public static void main(String[] args) {
        //首先创建对象
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();
        MyThread my3 = new MyThread();

        //我们可以通过setName()方法改变线程名
        my1.setName("刘备");
        my2.setName("关羽");
        my3.setName("张飞");

        //我们先来查看未设置时优先级度数以及最高最低优先级(最低1,最高10,默认5)
        System.out.println(my1.getPriority());
        System.out.println(Thread.NORM_PRIORITY);
        System.out.println(Thread.MIN_PRIORITY);
        System.out.println(Thread.MAX_PRIORITY);

        //然后我们通过修改优先级控制线程顺序
        my1.setPriority(10);
        my2.setPriority(5);
        my3.setPriority(1);


        //调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
        my1.start();
        my2.start();
        my3.start();
    }
}
public class MyThread extends Thread{
    //重写run方法,实现线程化
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            //我们可以采用getName()方法获得线程名
            System.out.println(getName() + ":" + i);
        }
    }
}

线程控制

下面给出线程控制相关方法:

方法名 说明
static void sleep(long millis) 使当前正在执行的线程停留指定的毫秒数
void join() 等待这个线程死亡
void setDaemon(boolean on) 将该线程标记为守护线程,当运行的线程都是守护线程时,程序终止

下面给出示例代码:(sleep方法)

public class Demo1 {
    public static void main(String[] args) {
        //首先创建对象
        Thread1 my1 = new Thread1();
        Thread1 my2 = new Thread1();
        Thread1 my3 = new Thread1();

        //我们可以通过setName()方法改变线程名
        my1.setName("刘备");
        my2.setName("孙权");
        my3.setName("曹操");


        //调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
        //这里数据出来就会每隔1s出来一次,且每次不是同个进程连续执行
        my1.start();
        my2.start();
        my3.start();
    }
}
//这里我们讲解sleep
public class Thread1 extends Thread{
    //重写run方法,实现线程化
    @Override
    public void run() {
        for (int i=0;i<500;i++){
            //我们可以采用getName()方法获得线程名
            System.out.println(getName() + ":" + i);
            //我们在输出后,让他们等到1s(注意这里单位是ms)
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

下面给出示例代码:(join方法)

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        //首先创建对象
        Thread2 my1 = new Thread2();
        Thread2 my2 = new Thread2();
        Thread2 my3 = new Thread2();

        //我们可以通过setName()方法改变线程名
        my1.setName("刘备");
        my2.setName("孙权");
        my3.setName("曹操");


        //调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
        my1.start();
        //这里使用join()方法,我们使用join之后,只有这个进程结束之后,才继续运行其他部分
        my1.join();
        my2.start();
        my3.start();
    }
}
public class Thread2 extends Thread{
    //重写run方法,实现线程化
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            //我们可以采用getName()方法获得线程名
            System.out.println(getName() + ":" + i);
        }
    }
}

下面给出示例代码:(setDaemon方法)

public class Demo3 {
    public static void main(String[] args) {
        //首先创建对象
        Thread3 my2 = new Thread3();
        Thread3 my3 = new Thread3();

        //我们可以通过setName()方法改变线程名
        my2.setName("关羽");
        my3.setName("张飞");

        //我们设置主线程main改名为刘备
        Thread.currentThread().setName("刘备");

        //我们把关羽和张飞设置为守护线程
        my2.setDaemon(true);
        my3.setDaemon(true);

        //调用线程(注意:使用start方法启动线程,其中start方法会执行run方法)
        my2.start();
        my3.start();

        //我们给主线程设置一些动作
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }

        //然后当我们运行后会发现,主线程完全运行,然后守护线程逐渐死亡不运行完毕
    }
}
public class Thread3 extends Thread{
    //重写run方法,实现线程化
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            //我们可以采用getName()方法获得线程名
            System.out.println(getName() + ":" + i);
        }
    }
}

线程生命周期

这里我们给出示例图(来自b站黑马程序员)

image-20220627185559961

案例:卖票

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

下面给出示例代码:

public class SellTicketDemo {
    public static void main(String[] args) {
        //创建SellTicket对象
        SellTicket st = new SellTicket();

        //创建售卖机
        Thread sell1 = new Thread(st,"售卖机1号");
        Thread sell2 = new Thread(st,"售卖机2号");
        Thread sell3 = new Thread(st,"售卖机3号");

        //开始运行即可
        sell1.start();
        sell2.start();
        sell3.start();
    }
}
public class SellTicket implements Runnable{
    //这里设置共有ticket
    public static int ticket = 100;

    //重构方法

    @Override
    public void run() {
        //因为程序一直运行,需要在死循环里进行
        while (true){
            //当票还有时,售卖,ticket减一
            if (ticket>0){
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
                ticket--;
            }
        }
    }
}

这里我们稍微给出编译结果:

售卖机2号正在售卖第1张票
售卖机2号正在售卖第2张票
售卖机1号正在售卖第1张票
售卖机3号正在售卖第1张票
售卖机1号正在售卖第4张票
售卖机2号正在售卖第3张票
售卖机1号正在售卖第6张票

结论:

我们会发现相同的票会出现很多次,这是因为当一个线程运行时,另一个进程可能也同步运行,他们先后顺序执行输出操作,未执行ticket--的操作,导致出现多次相同票。

多线程安全问题

多线程出现问题的环境:

  • 是否是多线程环境
  • 是否存在共享数据
  • 是否有多条语句操作共享数据

那么我们如何解决多线程安全问题呢?

  • 基本思想:让程序没有安全问题的环境

如何实现?

  • 把多条语句操作共享数据的代码锁起来,让任意时刻只有一个线程执行
  • Java提供了同步代码块和同步方法来解决

同步代码块

锁多条语句操作共享数据,可以通过同步代码块来实现:

synchronized(任意对象){
    多条语句操作共享数据的代码
}

这里我们同样给出同步方法的格式:

//同步方法(这里的对象是 this)
public synchronized 返回类型 方法名(){
    
}
//同步静态方法(这里的对象是 类名.class)
public static synchronized 返回类型 方法名(){
    
}

同步的好处和弊端:

  • 好处:解决了多线程的数据安全问题
  • 弊端:当线程很多时,因为每个线程都要判断是否上锁,降低程序运行速率

下面给出代码示例:(同步代码块)

public class SellTicketDemo {
    public static void main(String[] args) {
        //创建SellTicket对象
        Demo78.SellTicket st = new SellTicket();

        //创建售卖机
        Thread sell1 = new Thread(st,"售卖机1号");
        Thread sell2 = new Thread(st,"售卖机2号");
        Thread sell3 = new Thread(st,"售卖机3号");

        //开始运行即可
        sell1.start();
        sell2.start();
        sell3.start();
    }
}
public class SellTicket implements Runnable{
    //这里设置共有ticket
    public static int ticket = 100;
    //注意:这里需要创建private的Object对象,使所以线程共用一把锁
    private Object obj = new Object();
    //重构方法

    @Override
    public void run() {
        //因为程序一直运行,需要在死循环里进行
        while (true){
            //采用同步代码块来实现锁方法
            synchronized (obj){
                if (ticket>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
                    ticket--;
                }
            }
        }
    }
}

下面给出代码示例:(同步方法)

public class SellTicketDemo {
    public static void main(String[] args) {
        //创建SellTicket对象
        SellTicket st = new SellTicket();

        //创建售卖机
        Thread sell1 = new Thread(st,"售卖机1号");
        Thread sell2 = new Thread(st,"售卖机2号");
        Thread sell3 = new Thread(st,"售卖机3号");

        //开始运行即可
        sell1.start();
        sell2.start();
        sell3.start();
    }
}
public class SellTicket implements Runnable{
    //这里设置共有ticket
    public static int ticket = 100;
    private Object obj = new Object();

    //重构方法
    @Override
    public void run() {
        //因为程序一直运行,需要在死循环里进行
        while (true){
            //采用同步方法来实现锁方法
            //这里可以采用SellMethod方法中任意一个
            sellMethod3();
        }
    }

    //sellMethod1:直接把内容带入方法中,在方法中使用同步代码块
    private void sellMethod1() {
        synchronized (obj){
            if (ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
                ticket--;
            }
        }
    }

    //sellMethod2:直接把方法变成同步方法(这里的锁是class本身,即this)
    private synchronized void sellMethod2() {
            if (ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
                ticket--;
        }
    }

    //sellMethod3:直接把方法变成静态同步方法(这里的锁是class本身,即类名.class,即SellTicket.class)
    private static synchronized void sellMethod3() {
            if (ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
                ticket--;
            }
    }
}

线程安全的类

我们给出一些线程安全的类,你可以在多线程的编译过程中直接使用以下类:

  • StringBuffer
    • 线程安全,可变的字符序列
    • JDK5之后由StringBuilder代替,在无多线程情况下使用StringBuilder,在多线程情况下使用StringBuffer
  • Vector
    • 线程安全,数组
    • JDK1.2之后由ArrayList代替,在无多线程情况下使用ArrayList,在多线程情况下使用Vector
  • Hashtable
    • 该类实现了一个哈希表,它将键映射到值;任何非null对象都可以作为键或值
    • JDK1.2之后由HashMap代替,在无多线程情况下使用HashMap,在多线程情况下使用Hashtable

Lock锁

同步代码块和同步方法等同于Lock锁,但我们无法清晰看出锁的印记

于是Java给出Lock类清晰给出加锁和释放锁的方法

  • void lock():获得锁
  • void unlcok():释放锁

Lock是接口,不能直接实例化,所以我们采用它的实现类ReentrantLock来实例化

ReentrantLock构造方法:

  • ReentrantLock():创建一个Reentrantlock实例

下面给出代码示例:

public class SellTicketDemo {
    public static void main(String[] args) {
        //创建SellTicket对象
        SellTicket st = new SellTicket();

        //创建售卖机
        Thread sell1 = new Thread(st,"售卖机1号");
        Thread sell2 = new Thread(st,"售卖机2号");
        Thread sell3 = new Thread(st,"售卖机3号");

        //开始运行即可
        sell1.start();
        sell2.start();
        sell3.start();
    }
}
public class SellTicket implements Runnable{
    //这里设置共有ticket
    public static int ticket = 100;
    //设置锁Lock(Lock是接口,所以需要采用继承类ReentrantLock来实现)
    public Lock lock = new ReentrantLock();

    //重构方法

    @Override
    public void run() {
        //因为程序一直运行,需要在死循环里进行
        while (true){
            //这里锁上
            lock.lock();
            //当票还有时,售卖,ticket减一
            if (ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + (101-ticket) + "张票");
                ticket--;
            }
            //这里开锁
            lock.unlock();
        }
    }
}

生产者消费者模式概述

生产者消费者模式一个非常经典的多线程协作模式

实际上主要包含两类线程:

  • 一类是生产者线程用于生产数据
  • 一类是消费者线程用于消费数据

同时需要一个公共仓库

  • 生产者生产数据后放置于共享数据区,并不关心消费者行为
  • 消费者只需要从共享数据区获得数据,并不关心生产者行为

为了体现生产和消费的等待和唤醒状态,Java提供了Object类的一些方法:

方法名 说明
void wait() 导致当前线程等待,直到其他线程采用唤醒方法(notify或notifyAll)
void notify() 唤醒正在等待对象监视器的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程

生产者消费者模式案例

生产者消费者案例包含的类:

  • 奶箱类(Box):定义一个成员变量,表示第X瓶奶,提供存储牛奶和获取牛奶的方法
  • 生产者类(Producer):实现Runnable接口,重写run方法,调用存储牛奶的方法
  • 消费者类(Customer):实现Runnable接口,重写run方法,调用获得牛奶的方法
  • 测试类(BoxDemo):里面包含main方法
    • 创建奶箱对象,这里是共享数据区域
    • 创建生产者对象,把奶箱作为参数传递
    • 创建消费者对象,把奶箱作为参数传递
    • 创建两个线程对象,分别把生产者对象和消费者对象作为参数传递
    • 启动线程

下面给出示例代码:

public class BoxDemo {
    public static void main(String[] args) {
        //创造一个奶盒
        Box b = new Box();

        //创造生产者对象,把奶盒当作公共资源放进去
        Producer p = new Producer(b);

        //创造顾客对象,把奶盒当作公共资源放进去
        Customer c = new Customer(b);

        //创造线程
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);

        //开始线程运行
        t1.start();
        t2.start();
    }
}
public class Box {
    //定义成员变量,表示第x瓶奶
    private int milk;

    //定义Box状态
    private boolean state = false;

    //提供存储牛奶的方法(要使用synchronized指定一次只能执行一个)
    public synchronized void put(int milk){
        //如果还有奶,暂时不需要存入,我们就将这个线程挂起
        if (state){
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        //如果没有奶,我们需要把第x瓶奶放入
        this.milk = milk;
        System.out.println("送奶工将第" +this.milk + "瓶奶放入奶箱");

        //然后我们修改奶瓶状态,使其他线程苏醒
        state = true;
        notifyAll();
    }

    //提供获得牛奶的方法(要使用synchronized指定一次只能执行一个)
    public synchronized void get(){
        //如果没有奶,我们需要等待,将这个线程挂起
        if (!state){
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        //如果有奶,我们取奶
        System.out.println("用户拿到第" + this.milk + "瓶奶");

        //然后我们修改状态,并使其他线程苏醒
        state = false;
        notifyAll();
    }
}
public class Producer implements Runnable{
    private Box b;
    public Producer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        //依次放入i瓶奶
        for (int i = 1; i <= 5; i++) {
            b.put(i);
        }
    }
}
public class Customer implements Runnable{
    private Box b;
    public Customer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        while (true){
            b.get();
        }
    }
}

结束语

好的,关于Java相关的多线程我们就讲解到这里

posted @ 2022-07-06 15:33  秋落雨微凉  阅读(103)  评论(0编辑  收藏  举报