线程创建方式:继承 `Thread` 类与实现 `Runnable` 接口的对比

线程创建方式:继承 Thread 类与实现 Runnable 接口的对比:一场关于“基因”与“外挂”的精彩对决

各位看官,大家好!今天咱们来聊聊Java多线程这块“硬骨头”上的两块“肥肉”——创建线程的两种主要方式:继承 Thread 类和实现 Runnable 接口。这两种方式就像武林中的两大门派,各有千秋,各有拥趸。咱们今天就来扒一扒它们的底裤,看看谁更胜一筹。

一、故事的开端:为什么要创建线程?

在深入探讨这两种方式之前,咱们先简单回顾一下为什么要创建线程。想象一下,你是一个餐厅的老板,只有一个服务员。如果同时来了10桌客人,服务员只能一桌一桌地服务,其他客人只能眼巴巴地等着,客户体验极差。但是如果你雇佣了10个服务员,每人服务一桌,效率就大大提高了。

在计算机世界里,线程就相当于这些服务员。一个进程就像一个餐厅,而线程就是餐厅里的服务员。通过创建多个线程,我们可以让程序同时执行多个任务,提高程序的运行效率,更好地利用CPU资源。

二、第一位选手:继承 Thread 类——“基因”突变

这种方式就像给一个人直接注入了“超能力基因”,让他天生就拥有了执行任务的能力。

  1. 如何操作?

    你需要创建一个类,继承 java.lang.Thread 类,并重写 run() 方法。这个 run() 方法就是线程要执行的任务。

    class MyThread extends Thread {
       @Override
       public void run() {
           // 这里写线程要执行的任务
           System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行...");
       }
    }
    
    public class Main {
       public static void main(String[] args) {
           MyThread thread1 = new MyThread();
           thread1.start(); // 启动线程
       }
    }

    这段代码创建了一个名为 MyThread 的类,它继承了 Thread 类并重写了 run() 方法。在 main 方法中,我们创建了一个 MyThread 对象,并调用 start() 方法来启动线程。注意,是 start() 方法,而不是 run() 方法。调用 start() 方法后,JVM 会创建一个新的线程,并在新的线程中执行 run() 方法。如果直接调用 run() 方法,那只是普通的函数调用,并不会创建新的线程。

  2. 优点:

    • 简单易懂: 这种方式非常直观,代码也很简洁,容易理解。
    • 可以直接使用 Thread 类的方法: 继承了 Thread 类,自然可以使用 Thread 类提供的各种方法,例如 getName() 获取线程名称, sleep() 让线程休眠等。
  3. 缺点:

    • 单继承的限制: Java是单继承的语言。如果你的类已经继承了其他的类,就无法再继承 Thread 类了。这就像一个人已经有了父母,就不能再认干爹干妈了。
    • 耦合度高: 线程的任务代码和线程的控制代码紧密耦合在一起,不利于代码的维护和扩展。这就像把发动机和方向盘焊在一起,想换个方向盘都得把发动机一起拆下来。
    • 不符合面向对象的设计原则: 线程的任务应该是一个独立的实体,而不是类本身的一个属性。这就像把服务员和服务员的职责混为一谈,服务员应该是一个独立的对象,而不是餐厅的一个属性。

三、第二位选手:实现 Runnable 接口——“外挂”加身

这种方式就像给一个普通人安装了一个“外挂”,让他拥有了执行任务的能力,但本质上他还是一个普通人。

  1. 如何操作?

    你需要创建一个类,实现 java.lang.Runnable 接口,并实现 run() 方法。然后,你需要创建一个 Thread 对象,并将 Runnable 对象作为参数传递给 Thread 对象的构造函数。

    class MyRunnable implements Runnable {
       @Override
       public void run() {
           // 这里写线程要执行的任务
           System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行...");
       }
    }
    
    public class Main {
       public static void main(String[] args) {
           MyRunnable runnable = new MyRunnable();
           Thread thread1 = new Thread(runnable);
           thread1.start(); // 启动线程
       }
    }

    这段代码创建了一个名为 MyRunnable 的类,它实现了 Runnable 接口并实现了 run() 方法。在 main 方法中,我们创建了一个 MyRunnable 对象,然后将它作为参数传递给 Thread 对象的构造函数。最后,我们调用 start() 方法来启动线程。

  2. 优点:

    • 可以实现多个接口: Java允许一个类实现多个接口,这使得你的类可以拥有更多的功能。这就像一个人可以同时拥有多个技能证书,例如驾驶证、英语证书、编程证书等等。
    • 降低耦合度: 线程的任务代码和线程的控制代码分离,更符合面向对象的设计原则,有利于代码的维护和扩展。这就像把发动机和方向盘分开,想换个方向盘只需要更换方向盘,不需要拆卸发动机。
    • 易于资源共享: 多个线程可以共享同一个 Runnable 对象,从而实现资源共享。这就像多个服务员可以共享同一个餐厅的厨房,共同完成顾客的订单。
    • 更符合面向对象的设计原则: 将线程的任务抽象成一个独立的 Runnable 对象,更加符合面向对象的设计原则。
  3. 缺点:

    • 代码稍显复杂: 相比于继承 Thread 类,代码稍微复杂一些。
    • 不能直接使用 Thread 类的方法: 需要通过 Thread.currentThread() 方法来获取当前线程对象,才能使用 Thread 类的方法。

四、擂台赛:优缺点大PK

为了更直观地比较这两种方式的优缺点,咱们用一个表格来总结一下:

特性 继承 Thread 实现 Runnable 接口
单继承限制
耦合度
资源共享 困难 容易
代码复杂度 简单 稍复杂
面向对象设计原则 不符合 符合

五、深入剖析:资源共享的奥秘

资源共享是多线程编程中的一个重要概念。多个线程可能需要访问和修改同一块内存区域,这就是资源共享。实现 Runnable 接口的方式更容易实现资源共享。

咱们来看一个例子:

class TicketSeller implements Runnable {
    private int tickets = 10; // 共享的票数

    @Override
    public void run() {
        while (true) {
            synchronized (this) { // 同步代码块,保证线程安全
                if (tickets > 0) {
                    try {
                        Thread.sleep(100); // 模拟卖票的耗时操作
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 卖出一张票,剩余 " + --tickets + " 张");
                } else {
                    System.out.println("票已售罄!");
                    break;
                }
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        TicketSeller seller = new TicketSeller(); // 创建一个共享的 TicketSeller 对象
        Thread thread1 = new Thread(seller, "窗口 1");
        Thread thread2 = new Thread(seller, "窗口 2");
        Thread thread3 = new Thread(seller, "窗口 3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

在这个例子中,TicketSeller 类实现了 Runnable 接口,并维护了一个 tickets 变量,表示剩余的票数。多个线程共享同一个 TicketSeller 对象,因此它们共享同一个 tickets 变量。为了保证线程安全,我们使用了 synchronized 关键字来同步代码块,防止多个线程同时访问和修改 tickets 变量。

如果使用继承 Thread 类的方式,每个线程都会创建一个独立的 TicketSeller 对象,每个对象都有自己的 tickets 变量,就无法实现资源共享了。

六、实战演练:一个更复杂的例子

为了更好地理解这两种方式的应用场景,咱们来看一个更复杂的例子:模拟一个简单的Web服务器。

  1. 使用继承 Thread 类的方式:

    import java.io.*;
    import java.net.*;
    
    class WebServerThread extends Thread {
       private Socket socket;
    
       public WebServerThread(Socket socket) {
           this.socket = socket;
       }
    
       @Override
       public void run() {
           try (
                   BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                   PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
           ) {
               String request = in.readLine();
               System.out.println("Received request: " + request);
    
               // 模拟处理请求
               String response = "HTTP/1.1 200 OKrnContent-Type: text/htmlrnrn<html><body><h1>Hello, World!</h1></body></html>";
               out.println(response);
    
               socket.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
    }
    
    public class WebServer {
       public static void main(String[] args) throws IOException {
           ServerSocket serverSocket = new ServerSocket(8080);
           System.out.println("Server started on port 8080");
    
           while (true) {
               Socket socket = serverSocket.accept();
               new WebServerThread(socket).start(); // 为每个客户端连接创建一个新的线程
           }
       }
    }

    这个例子中,WebServerThread 类继承了 Thread 类,负责处理客户端的请求。WebServer 类负责监听端口,接受客户端的连接,并为每个连接创建一个新的 WebServerThread 对象。

  2. 使用实现 Runnable 接口的方式:

    import java.io.*;
    import java.net.*;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    class WebServerTask implements Runnable {
       private Socket socket;
    
       public WebServerTask(Socket socket) {
           this.socket = socket;
       }
    
       @Override
       public void run() {
           try (
                   BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                   PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
           ) {
               String request = in.readLine();
               System.out.println("Received request: " + request);
    
               // 模拟处理请求
               String response = "HTTP/1.1 200 OKrnContent-Type: text/htmlrnrn<html><body><h1>Hello, World!</h1></body></html>";
               out.println(response);
    
               socket.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
    }
    
    public class WebServer {
       public static void main(String[] args) throws IOException {
           ServerSocket serverSocket = new ServerSocket(8080);
           System.out.println("Server started on port 8080");
    
           ExecutorService executor = Executors.newFixedThreadPool(10); // 使用线程池
    
           while (true) {
               Socket socket = serverSocket.accept();
               executor.execute(new WebServerTask(socket)); // 将任务提交给线程池
           }
       }
    }

    这个例子中,WebServerTask 类实现了 Runnable 接口,负责处理客户端的请求。WebServer 类负责监听端口,接受客户端的连接,并将任务提交给线程池。使用线程池可以更好地管理线程,避免创建过多的线程导致系统资源耗尽。

    在这个例子中,使用实现 Runnable 接口的方式更加灵活,可以使用线程池来管理线程,提高程序的性能和稳定性。

七、最佳实践:如何选择?

那么,在实际开发中,我们应该如何选择这两种方式呢?

  • 优先选择实现 Runnable 接口的方式: 除非你真的需要继承 Thread 类,并且不关心单继承的限制,否则优先选择实现 Runnable 接口的方式。
  • 使用线程池: 在高并发的场景下,使用线程池来管理线程是一个更好的选择。线程池可以重用线程,避免频繁创建和销毁线程的开销,提高程序的性能。
  • 注意线程安全: 在多线程编程中,一定要注意线程安全问题。使用 synchronized 关键字、Lock 接口等方式来同步代码块,防止多个线程同时访问和修改共享资源。

八、总结:没有绝对的胜者,只有更合适的选择

总而言之,继承 Thread 类和实现 Runnable 接口这两种方式各有优缺点,没有绝对的胜者。选择哪种方式取决于具体的应用场景和需求。

  • 如果你只是想简单地创建一个线程,并且不关心单继承的限制,可以选择继承 Thread 类。
  • 如果你需要实现多个接口,或者需要更好地控制线程的生命周期,或者需要实现资源共享,那么实现 Runnable 接口是更好的选择。

希望这篇文章能够帮助你更好地理解这两种线程创建方式,并在实际开发中做出更明智的选择。记住,代码就像艺术品,选择最适合你的“颜料”和“画笔”,才能创作出更精彩的作品!

感谢各位看官的耐心阅读,咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注