在Java编程中,多线程是提升应用性能和响应效率的重要手段。理解多线程的基本概念、实现方式及同步机制,对于开发高并发、高可靠的企业级应用至关重要。本文将深入剖析Java多线程的基础知识,包括并发与并行的差异、线程实现方式、同步锁的应用以及线程间的通信,帮助在校大学生和初级开发者构建扎实的多线程编程能力。
多线程的基本概念
Java多线程编程是实现并行处理的核心技术之一,其基础在于理解并发与并行的区别。并行指的是多个CPU实例或多个机器同时执行处理逻辑,是真正的“同时”运行。而并发则是通过CPU调度算法,让多个任务在同一个CPU上交错执行,给人一种同时运行的错觉。并发常常涉及公共资源的访问,对这些资源的协调处理是并发编程的难点。
进程与线程
进程是操作系统中运行的一个应用程序,它拥有独立的内存空间和系统资源。多进程指的是同时运行多个任务程序,这在操作系统中是通过进程调度实现的。而线程则是操作系统能够进行运算调度的最小单位,它可以共享进程的资源,如内存、文件句柄等。多线程指的是在同一个进程中运行多个线程,它们可以独立执行不同的任务,从而提升程序的执行效率。
多线程的应用场景
多线程在实际开发中有着广泛的应用场景,例如服务器同时处理多个用户请求、教学电脑控屏软件同时共享屏幕给多个设备等。这些场景都需要多线程来实现并行处理,提高响应速度和系统性能。
Java程序运行原理
Java程序的运行是基于Java虚拟机(JVM)的。当执行一个Java命令时,系统会启动JVM,从而启动一个应用程序进程。该进程会自动启动一个“主线程”,并调用某个类的main方法来开始程序的执行。
JVM的启动方式
JVM的启动是多线程的。在JVM启动时,至少会启动垃圾回收线程和主线程。垃圾回收线程负责管理内存,而主线程则负责执行程序的主逻辑。因此,JVM的启动过程本身就是多线程的体现。
实现多线程的两种方式
Java提供了两种实现多线程的方式:继承Thread类和实现Runnable接口。这两种方式各有优劣,适用于不同的场景。
继承Thread类
- 定义一个类继承Thread。
- 重写run方法。
- 将线程需要执行的任务写在run方法中。
- 创建线程对象。
- 调用start()方法开启新线程。
这种方式的优点在于可以直接使用Thread类中的方法,代码较为简洁。但其弊端是无法继承其他类,因为Java不支持多继承。如果已经有一个父类,就不能再使用这种方式。
实现Runnable接口
- 定义一个类实现Runnable接口。
- 实现run方法。
- 将线程需要执行的任务写在run方法中。
- 创建Runnable的子类对象。
- 创建Thread对象,并传入Runnable对象。
- 调用start()方法开启新线程。
这种方式的优点在于可以继承其他类,因为接口可以多实现。但其弊端在于需要获取线程对象才能调用Thread中的方法,使得代码相对复杂。
两种实现方式的区别
| 特性 | 继承Thread类 | 实现Runnable接口 |
|---|---|---|
| 继承限制 | 无法继承其他类 | 可以继承其他类 |
| 代码复杂度 | 相对简单 | 相对复杂 |
| 线程共享 | 直接共享 | 需要通过对象传递 |
继承Thread类适合简单场景,而实现Runnable接口更适合需要共享资源或继承其他类的复杂场景。选择哪种方式,取决于具体的应用需求和代码结构。
线程同步(同步锁)
在多线程环境中,线程同步是保证数据一致性的重要手段。当多个线程并发执行时,如果存在共享资源,就可能引发数据竞争,导致不一致或错误结果。
同步代码块
同步代码块使用synchronized关键字加上一个锁对象来定义,这种代码块被称为同步代码块。多个同步代码块如果使用相同的锁对象,则它们是同步的,同一时间只能执行一段代码,直到该段代码执行完毕,其他线程才能继续执行。
同步锁的应用
同步锁的应用场景包括共享数据的修改、资源的访问等。例如,在买票系统中,多个售票窗口同时访问共享的票数变量,就需要使用同步锁来防止竞态条件。
线程同步的实例
以下是一个买票系统的示例,展示了如何使用同步锁来防止多个线程同时修改共享资源:
public class Demo4 {
public static void main(String[] args) {
// 创建4个售票窗口
TicketThread t1 = new TicketThread("A");
TicketThread t2 = new TicketThread("B");
TicketThread t3 = new TicketThread("C");
TicketThread t4 = new TicketThread("D");
// 开启线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class TicketThread extends Thread {
// 共享定义100张票
static int tickets = 100;
public TicketThread(String name) {
super(name);
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized ("nihao") {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "已售票号" + tickets--);
} else {
break;
}
}
}
}
}
在这个例子中,synchronized关键字使用了一个锁对象“nihao”,确保多个线程在访问共享资源时不会同时进行,从而避免了数据竞争。
同步线程之间的通信
在多线程环境中,线程通信是实现线程协作的重要手段。当多个线程并发执行时,CPU的调度是随机的,为了实现有规律的执行,可以使用等待唤醒机制。
通信的必要性
多个线程并发执行时,默认情况下,CPU是随机切换线程的。为了实现有规律的执行,例如每个线程执行一次打印,就需要使用通信机制。
通信的实现方式
线程通信主要通过wait()和notify()方法实现。wait()方法用于让线程等待,notify()方法用于唤醒等待的线程。这两个方法必须在同步代码块中执行,且使用同步锁对象来调用。
线程通信的实例
以下是一个实现ABABAB型输出的示例,展示了如何使用wait()和notify()方法进行线程通信:
public class Demo1 {
public static void main(String[] args) {
// 创建打印对象
final Shuchu sc = new Shuchu();
new Thread(() -> {
while (true) {
try {
sc.scw1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true) {
try {
sc.scw2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
class Shuchu {
private int flag = 1;
public void scw1() throws InterruptedException {
synchronized (this) {
while (flag != 1) {
this.wait();
}
System.out.print("1");
System.out.print("2");
System.out.print("3");
System.out.print("4");
System.out.println("5");
flag = 2;
this.notify();
}
}
public void scw2() throws InterruptedException {
synchronized (this) {
while (flag != 2) {
this.wait();
}
System.out.print("A");
System.out.print("B");
System.out.print("C");
System.out.print("D");
System.out.println("E");
flag = 1;
this.notify();
}
}
}
在这个例子中,flag变量用于控制线程的执行顺序,wait()和notify()方法用于实现线程间的通信,确保输出顺序为ABABAB。
线程通信的关键点
在使用wait()和notify()方法进行线程通信时,有几个关键点需要注意:
- wait()方法和notify()方法必须在同步代码块中调用,且使用同一个同步锁对象。
- wait()方法可以带参数,表示等待的时间,如果没有参数则一直等待。
- sleep()方法在同步中不释放锁,而wait()方法在同步中释放锁。这意味着在使用wait()方法时,线程会释放锁,允许其他线程执行,而在使用sleep()方法时,线程不会释放锁,其他线程无法进入同步代码块。
面试题:sleep()和wait()的区别
- sleep()必须传入参数,参数是时间,时间到了能自己醒来。
- wait()不是必须要传入参数,如果没有参数就等待,如果传入参数则在时间到了后等待。
- sleep()在同步中不释放锁,而wait()在同步中释放锁。
总结与建议
Java多线程编程是提升应用性能和响应效率的核心技术之一。理解并发与并行的区别、线程的实现方式、同步锁的应用以及线程通信机制,对于开发高并发、高可靠的企业级应用至关重要。在校大学生和初级开发者应注重同步机制和线程通信的实践,以提升代码的安全性和可靠性。
在实践中,继承Thread类适合简单的多线程场景,而实现Runnable接口更适合需要共享资源或继承其他类的复杂场景。选择哪种方式,取决于具体的应用需求和代码结构。
此外,线程通信是实现线程协作的重要手段,wait()和notify()方法的正确使用可以确保线程按照预期顺序执行。建议在学习和实践中多加练习,熟悉这些概念和方法的使用,以提升自己的多线程编程能力。
关键字列表:Java多线程, 同步锁, 线程通信, 并发与并行, 线程实现方式, JVM启动, 线程同步, 线程执行顺序, sleep与wait区别, 线程安全