likes
comments
collection
share

【一文通关】Java多线程基础(2)- Thread 类与线程的创建

作者站长头像
站长
· 阅读数 15

Thread 类与线程的创建

使用Thread的子类

在Java语言中,可以使用Thread类或子类创建线程对象。

编写Thread类的子类时,需要重写父类的run()方法, 目的是规定线程的具体操作,否则线程就不操作,因为父类的run()方法中没有任何的操作语句

使用Thread类

使用Thread的子类的好处是:

  • 可以在子类中增加新的变量,使线程具有某种属性, 可以在子类中增加新的成员变量,使线程具有某种功能。但是,Java不支持多继承, Thread的子类不能再扩展其他的类。

创建线程的另一个途径就是用Thread类直接创建线程对象。使用Thread创建线程通常使用的构造方法是: Thread(Runnable target)。 该构造方法中的参数是一个Runnable类型的接口, 因此,参数必须传递一个实现Runnable接口的实例。

线程调用start方法后,一旦轮到它来享用CPU资源, 目标对象就会自动调用接口中的run方法

//使用Thread创建线程

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyThread());
        thread.start();
    }
}
class MyThread implements Runnable{
    @Override
    public void run(){
        for (int i = 0; i < 15; i++) {
            System.out.println("汽车" + i);
        }
    }
}

我们知道线程间可以共享相同的内在单元(包括代码与数据)并利用这些共享单元来实现数据交换、实时通信与必要 的同步操作。

对于 Thread(Runnable target)构造方法创建的线程,轮到它来享用 CPU 资源时,目标对象就会自动调用接口中的 run()方法,因此,对于使用同一目标对象的线程,目标对象的成员变量自然就是这些线程共享的数据单元

另外,创建目标对象的类在必要时还可以是某个特定类的子类,因此,使用Runnable接口比使用Thread的子类更具有灵活性。

下面例子中使用 Thread类创建两个模拟猫和狗的线程,猫和狗共享房屋中的一桶水,即房屋是线程的目标对象,房屋中的一桶水被猫和狗共享。猫和狗轮流喝水(狗喝的多,猫喝的少),当水被喝尽时,猫和狗进入死亡状态。猫或狗在轮流喝水的过程中,主动休息片刻(让 Thread类调用sleep(int n)进入中断状态),而不是等到被强制中断喝水。

package thread;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        House house = new House();
        house.setWaterCount(10);
        Thread dog = new Thread(house); // dog,cat使用同一目标对象house
        dog.setName("dog");
        Thread cat = new Thread(house); // dog,cat使用同一目标对象house
        cat.setName("cat");
        dog.start();
        cat.start();
    }
}
class House implements Runnable{
    private int waterCount;
    public void setWaterCount(int waterCount){
        this.waterCount = waterCount;
    }
    @Override
    public void run(){
        while (true){
            String name = Thread.currentThread().getName();
            if(name.equals("dog")){
                System.out.println(name+ "喝水");
                waterCount -= 2;
            } else if (name.equals("cat")) {
                System.out.println(name+ "喝水");
                waterCount -= 1;
            }
            System.out.println("剩余" + waterCount + "水");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(waterCount<=0) return;
        }
    }
}

需要注意的是,在这个例子中,由于两个线程共享了同一个目标对象,因此存在线程安全问题。当两个线程同时对水缸中的水进行修改时,可能会出现数据不一致的情况,需要使用同步机制来避免这个问题。

目标对象与线程的关系

从对象和对象的关系角度上看, 目标对象和线程的关系有两种。

  1. 目标对象和线程完全解耦

    上面的例子, House对象里面并没有cat和dog属性, 也就是说,House类创建的对象不包含对cat和dog线程对象的引用。在这种情况下, 目标对象经常需要获得线程的名字因为无法获得线程对象的引用

    String name = Thread.currentThread().getName(); 来获得当前占用CPU的线程的name。

  2. 目标对象组合线程(弱耦合)

    目标对象可以组合线程,即将线程作为自己的成员,使用Thread.currentThread()方法来获得当前线程对象的引用。

    package thread;
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            House house = new House();
            house.setWaterCount(10);
            // 这里需要开启猫狗线程
            house.cat.start();
            house.dog.start();
        }
    }
    class House implements Runnable{
        private int waterCount;
        public void setWaterCount(int waterCount){
            this.waterCount = waterCount;
        }
        Thread dog;
        Thread cat;
        House(){
            this.dog = new Thread(this);
            this.cat = new Thread(this);
        }
        @Override
        public void run(){
            while (true){
                Thread thread = Thread.currentThread();
                if(thread == dog){
                    System.out.println("dog喝水");
                    waterCount = waterCount - 2;
                } else if (thread == cat) {
                    System.out.println("cat喝水");
                    waterCount = waterCount - 1;
                }
                System.out.println("剩余" + waterCount);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                if(waterCount<=0) return;
            }
        }
    }
    

关于run方法启动的次数

在Java中,一个线程的run()方法只会被执行一次。当我们创建一个线程对象并调用start()方法时,JVM会在后台自动创建一个新的线程,并在新线程中调用run()方法。如果我们再次调用start()方法,JVM会抛出IllegalThreadStateException异常,提示线程已经启动过了,不能再次启动。

因此,在正常情况下,一个线程的run()方法只会被执行一次。但是在某些情况下,我们可以在run()方法中使用循环语句,使线程在一定条件下可以重复执行run()方法。例如,在网络编程中,我们可以在run()方法中使用一个无限循环来不断地接收客户端的请求,直到服务器关闭连接为止。

需要注意的是,在使用循环语句时,我们需要注意线程安全问题,避免出现数据不一致的情况。另外,如果在run()方法中使用了wait()sleep()等方法,线程也会暂停执行,直到被唤醒或时间到期才会继续执行run()方法。