java多线程之使用“实现Runnable接口”实现多线程

2. 实现 Runnable 接口

实现 Runnable 接口是更为常用的创建线程的方式,因为 Java 是单继承的,使用这种方式可以避免类继承的局限性。

操作步骤

  1. 实现Runnable接口。
  2. 重写run()方法,编写线程执行体。
  3. 执行线程需要丢入Runnable接口实现类。
  4. 调用start方法。
示例1
package com.demo01;

public class TestThread3 implements Runnable {
    // 实现Runnable接口,重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("这里多线程1-->"+i);
        }
    }

    // main方法中,将实现的接口类丢入线程
    public static void main(String[] args) {
        // 创建接口实现类
        TestThread3 t1 = new TestThread3();

        // 创建线程对象,通过线程对象来开启我们的线程代理
        // 把接口实现类丢到这个线程中
        // Thread t2 = new Thread(t1);
        // t2.start();
        // 可以直接换成这一句代码
        new Thread(t1).start();

        for (int i = 0; i < 100; i++) {
            System.out.println("这里是主线程-->"+i);
        }
    }
}
流程解释(必看***)
  1. TestThread3实现Runnable接口。
  2. 重写run方法。
  3. 创建接口实现类(这里就是TestThread3),并丢入代理线程。
  4. 调用代理线程的start方法。

为什么这里要用代理线程,不能像第一种方法直接调用start方法呢?

这里我们要注意我们这个类只是实现了Runnable接口,自身并不是线程,自然没有start方法,那我们又要运行run这个执行体线程,就只能找一个代理线程,在创建代理线程将我们这个接口实现类丢入代理线程(其实就是代理线程的有参构造),再调用代理线程的start方法。

new Thread(t1).start()这里使用到了匿名对象,即直接new了一个Thread对象,并把接口实现类丢入,但是并没有给这个线程命名,而是直接调用start方法。

代码解释
  • implements Runnable:表示 TestThread3 类实现了 Runnable 接口。Runnable 接口是 Java 中用于创建线程的一种方式,它只包含一个抽象方法 run(),任何实现该接口的类都需要实现这个方法,该方法中定义了线程要执行的任务。
  • @Override:这是一个注解,用于告诉编译器我们正在重写父接口(这里是 Runnable 接口)的方法。如果方法签名与父接口中的方法不匹配,编译器会报错,有助于避免因拼写错误等原因导致的问题。
  • public void run():实现了 Runnable 接口的 run() 方法。在这个方法中,使用 for 循环从 0 到 199 进行迭代,每次迭代都会打印出一条信息,显示当前迭代的序号。这个 run() 方法中的代码就是线程要执行的具体任务。
  • new Thread(t1):创建了一个 Thread 类的实例,将 t1(实现了 Runnable 接口的对象)作为参数传递给 Thread 类的构造函数。Thread 类是 Java 中用于表示线程的类,通过传入 Runnable 对象,我们告诉线程要执行的任务是什么。
  • .start():调用 Thread 类的 start() 方法,该方法会启动一个新的线程,并让这个新线程去执行 Runnable 对象的 run() 方法。注意,不能直接调用 run() 方法,否则不会启动新线程,而是在当前线程中执行 run() 方法的代码。
示例2

初识并发问题

package com.demo01;

// 初识并发问题
public class TestThread4 implements Runnable{

    private int ticketNumes = 10;

    // 发现问题,多个线程操作同一个资源的情况下,线程不安全
    @Override
    public void run() {
        while(true){
            System.out.println(Thread.currentThread().getName()+"买到了第"+ticketNumes--+"票");
            try {
                // 模拟延时,为了更加方便观察多个线程同时进行的效果
                // 但是使用sleep方法要处理异常
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(ticketNumes <= 0){
                break;
            }
        }
    }

    public static void main(String[] args) {
        // 创建接口实现类
        TestThread4 testThread4 = new TestThread4();

        //写代理线程,开启了三个,丢入接口实现类时可以指定线程名字
        // Thread有很多种构造方法,可以自己查文档
        // 给名字的原因是想要看到是哪个线程操作了ticketNums
        new Thread(testThread4,"x").start();
        new Thread(testThread4,"y").start();
        new Thread(testThread4,"z").start();
    }
}

运行结果:

y买到了第9票
z买到了第8票
x买到了第10票
z买到了第7票
y买到了第6票
x买到了第5票
z买到了第4票
y买到了第3票
x买到了第2票
y买到了第1票
x买到了第1票

可以发现其中x和y都买到了第一张票,但是实际条件下是不允许这种事情发生的。

发生的原因:

多个线程同时操作 ticketNumes 这个共享资源,会出现并发问题,比如可能会出现票数为负数、同一张票被多个线程重复售卖等情况。这是因为在多线程环境下,多个线程可能会同时访问和修改共享资源,导致数据不一致。

修改方法:
使用synchronized关键字
1.同步方法

run 方法中的核心操作封装到一个同步方法中,确保同一时间只有一个线程可以执行该方法。

package com.demo01;

// 初识并发问题
public class TestThread4 implements Runnable {

    private int ticketNumes = 10;

    // 同步方法,保证同一时间只有一个线程可以执行该方法
    private synchronized boolean sellTicket() {
        if (ticketNumes > 0) {
            System.out.println(Thread.currentThread().getName() + " 买到了第 " + ticketNumes + " 票");
            ticketNumes--;
            return true; // 没卖完就返回true,这样继续循环
        }
        return false; // 卖完了就返回false,这样跳出循环
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 模拟延时,为了更加方便观察多个线程同时进行的效果
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (!sellTicket()) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        // 创建接口实现类
        TestThread4 testThread4 = new TestThread4();

        // 写代理线程,开启了三个,丢入接口实现类时可以指定线程名字
        new Thread(testThread4, "x").start();
        new Thread(testThread4, "y").start();
        new Thread(testThread4, "z").start();
    }
}
代码解释
  • synchronized 修饰的 sellTicket 方法,同一时间只有一个线程可以进入该方法,从而保证了对 ticketNumes 的操作是线程安全的。
  • run 方法中,调用 sellTicket 方法进行售票操作,如果返回 false 表示票已售完,退出循环。
2. 同步代码块

也可以使用同步代码块来保证对共享资源的访问是线程安全的。

package com.demo01;

// 初识并发问题
public class TestThread4 implements Runnable {

    private int ticketNumes = 10;
    private final Object lock = new Object(); // 定义一个锁对象

    @Override
    public void run() {
        while (true) {
            synchronized (lock) { // 同步代码块,使用 lock 对象作为锁
                if (ticketNumes > 0) {
                    System.out.println(Thread.currentThread().getName() + " 买到了第 " + ticketNumes + " 票");
                    ticketNumes--;
                } else {
                    break;
                }
            }
            try {
                // 模拟延时,为了更加方便观察多个线程同时进行的效果
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 创建接口实现类
        TestThread4 testThread4 = new TestThread4();

        // 写代理线程,开启了三个,丢入接口实现类时可以指定线程名字
        new Thread(testThread4, "x").start();
        new Thread(testThread4, "y").start();
        new Thread(testThread4, "z").start();
    }
}
代码解释
  • synchronized (lock) 定义了一个同步代码块,lock 是一个对象,作为锁。同一时间只有一个线程可以获得该锁并进入同步代码块,从而保证了对 ticketNumes 的操作是线程安全的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值