线程创建方式:继承 Thread
类与实现 Runnable
接口的对比:一场关于“基因”与“外挂”的精彩对决
各位看官,大家好!今天咱们来聊聊Java多线程这块“硬骨头”上的两块“肥肉”——创建线程的两种主要方式:继承 Thread
类和实现 Runnable
接口。这两种方式就像武林中的两大门派,各有千秋,各有拥趸。咱们今天就来扒一扒它们的底裤,看看谁更胜一筹。
一、故事的开端:为什么要创建线程?
在深入探讨这两种方式之前,咱们先简单回顾一下为什么要创建线程。想象一下,你是一个餐厅的老板,只有一个服务员。如果同时来了10桌客人,服务员只能一桌一桌地服务,其他客人只能眼巴巴地等着,客户体验极差。但是如果你雇佣了10个服务员,每人服务一桌,效率就大大提高了。
在计算机世界里,线程就相当于这些服务员。一个进程就像一个餐厅,而线程就是餐厅里的服务员。通过创建多个线程,我们可以让程序同时执行多个任务,提高程序的运行效率,更好地利用CPU资源。
二、第一位选手:继承 Thread
类——“基因”突变
这种方式就像给一个人直接注入了“超能力基因”,让他天生就拥有了执行任务的能力。
-
如何操作?
你需要创建一个类,继承
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()
方法,那只是普通的函数调用,并不会创建新的线程。 -
优点:
- 简单易懂: 这种方式非常直观,代码也很简洁,容易理解。
- 可以直接使用
Thread
类的方法: 继承了Thread
类,自然可以使用Thread
类提供的各种方法,例如getName()
获取线程名称,sleep()
让线程休眠等。
-
缺点:
- 单继承的限制: Java是单继承的语言。如果你的类已经继承了其他的类,就无法再继承
Thread
类了。这就像一个人已经有了父母,就不能再认干爹干妈了。 - 耦合度高: 线程的任务代码和线程的控制代码紧密耦合在一起,不利于代码的维护和扩展。这就像把发动机和方向盘焊在一起,想换个方向盘都得把发动机一起拆下来。
- 不符合面向对象的设计原则: 线程的任务应该是一个独立的实体,而不是类本身的一个属性。这就像把服务员和服务员的职责混为一谈,服务员应该是一个独立的对象,而不是餐厅的一个属性。
- 单继承的限制: Java是单继承的语言。如果你的类已经继承了其他的类,就无法再继承
三、第二位选手:实现 Runnable
接口——“外挂”加身
这种方式就像给一个普通人安装了一个“外挂”,让他拥有了执行任务的能力,但本质上他还是一个普通人。
-
如何操作?
你需要创建一个类,实现
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()
方法来启动线程。 -
优点:
- 可以实现多个接口: Java允许一个类实现多个接口,这使得你的类可以拥有更多的功能。这就像一个人可以同时拥有多个技能证书,例如驾驶证、英语证书、编程证书等等。
- 降低耦合度: 线程的任务代码和线程的控制代码分离,更符合面向对象的设计原则,有利于代码的维护和扩展。这就像把发动机和方向盘分开,想换个方向盘只需要更换方向盘,不需要拆卸发动机。
- 易于资源共享: 多个线程可以共享同一个
Runnable
对象,从而实现资源共享。这就像多个服务员可以共享同一个餐厅的厨房,共同完成顾客的订单。 - 更符合面向对象的设计原则: 将线程的任务抽象成一个独立的
Runnable
对象,更加符合面向对象的设计原则。
-
缺点:
- 代码稍显复杂: 相比于继承
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服务器。
-
使用继承
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
对象。 -
使用实现
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
接口是更好的选择。
希望这篇文章能够帮助你更好地理解这两种线程创建方式,并在实际开发中做出更明智的选择。记住,代码就像艺术品,选择最适合你的“颜料”和“画笔”,才能创作出更精彩的作品!
感谢各位看官的耐心阅读,咱们下期再见!