【一文通关】Java多线程基础(2)- Thread 类与线程的创建
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;
}
}
}
需要注意的是,在这个例子中,由于两个线程共享了同一个目标对象,因此存在线程安全问题。当两个线程同时对水缸中的水进行修改时,可能会出现数据不一致的情况,需要使用同步机制来避免这个问题。
目标对象与线程的关系
从对象和对象的关系角度上看, 目标对象和线程的关系有两种。
-
目标对象和线程完全解耦
上面的例子, House对象里面并没有cat和dog属性, 也就是说,House类创建的对象不包含对cat和dog线程对象的引用。在这种情况下, 目标对象经常需要获得线程的名字因为无法获得线程对象的引用
String name = Thread.currentThread().getName(); 来获得当前占用CPU的线程的name。
-
目标对象组合线程(弱耦合)
目标对象可以组合线程,即将线程作为自己的成员,使用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()
方法。
转载自:https://juejin.cn/post/7227732942326546491