多线程编程是现代软件开发中的一个重要概念,它允许程序同时执行多个任务,提高了程序的性能和响应性。本博客深入探讨了多线程编程的关键概念、原理和最佳实践。
进程
线程
多线程
并发
并行
我们可以编写Java程序来查看自己电脑的CPU核数
Code:
Runtime runtime = Runtime.getRuntime();
//获取当前操作系统的cpu核数
int cpuNums = runtime.availableProcessors();
System.out.println("当前CPU核数=" +cpuNums);
输出: 当前CPU核数=6
在Java启动时,会默认创建两个线程:
main
线程:这是Java应用程序的主线程,是程序的入口点。main
线程执行main
方法中的代码,负责程序的初始化和执行。GC
线程:这是Java虚拟机(JVM)内部的垃圾回收线程。它负责在后台自动回收不再使用的内存,以确保程序的内存管理。创建线程的方法主要有以下两种:
Thread
类: 您可以创建一个自定义的类,继承自Thread
类,并重写run
方法来定义线程的执行逻辑。然后,通过创建该类的实例并调用start
方法来启动线程。Runnable
接口: 您可以创建一个实现了Runnable
接口的类,实现run
方法来定义线程的执行逻辑。然后,通过创建该类的实例,将其传递给Thread
类的构造函数,并调用start
方法来启动线程。这两种方式都可以用于创建线程,但使用Runnable
接口通常更灵活,因为它允许多个线程共享相同的Runnable
实例,实现了解耦和代码复用。
以下是图示,展示了这两种线程创建方式的关系:
Code:
public class MyThread extends Thread {
@Override
public void run() {
while(true){
System.out.println("喵喵,我是小猫咪" + (++times));
}
}
public static void main(String[] args) { //main线程
//创建一个线程对象
MyThread myThread = new MyThread();
myThread.start(); //开启新线程
//main线程业务代码
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
输出:
main 0
喵喵,我是小猫咪1
main 1
main 2
喵喵,我是小猫咪2
。。。
分析Code:
main
线程和myThread
线程,并且同时执行任务。优点:
Thread
类创建线程相对简单,只需创建一个继承自Thread
的子类并重写run
方法,然后实例化子类对象并调用start
方法即可启动线程。缺点:
Thread
类,就不能再继承其他类。这可能限制了您的代码组织和设计选择。Thread
类的方式不太适合多个线程之间共享相同的资源,因为每个线程都是一个独立的对象,不容易在多个线程之间共享数据。Thread
类的方式可能不太适合,因为线程池更适合管理实现Runnable
接口的任务。为什么使用start()方法启动线程,而不是直接调用run()方法?
myThread.start()
:创建一个新线程,实现多线程执行。myThread.run()
:相当于调用了一个方法,而没有真正的创建一个线程。
start()源码分析:
以下是start()
方法的底层源码,其中最核心的是start0()
方法。
public synchronized void start() {
。。。
。。。
start0(); //最核心的代码
}
private native void start0();
首先调用最核心的代码,即调用start0()方法;
所有线程并不会马上执行,只是将线程状态改为可运行状态,具体什么时候执行,取决于CPU。
为什么推荐通过实现Runnable来创建线程?
- 因为Java是单继承机制,在某些情况下已经继承了其他父类,这时在继承Thread类来创建线程显然不可能了。
- Java设计者就提供了另外一种方式创建线程,通过实现Runnable接口创建线程。
Code:
模拟抢票系统可以用多线程来模拟,每个线程代表一个用户尝试抢票。下面是一个简单的Java示例,使Runnable
接口来实现一个基本的抢票系统模拟:
public class TicketSystem implements Runnable {
private int totalTickets = 10; // 总票数
@Override
public void run() {
while (totalTickets > 0) {
if (totalTickets > 0) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 抢到了第 " + totalTickets + " 张票");
totalTickets--;
} else {
System.out.println("票已售罄");
}
try {
Thread.sleep(100); // 模拟用户抢票间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TicketSystem ticketSystem = new TicketSystem();
Thread user1 = new Thread(ticketSystem, "小明");
Thread user2 = new Thread(ticketSystem, "张三");
Thread user3 = new Thread(ticketSystem, "李四");
user1.start();
user2.start();
user3.start();
}
}
输出:
李四抢到第10票
小明抢到第9票
。。。
张三抢到第2票
小明抢到第2票
李四抢到第1票
李四抢到第0票
小明抢到第-1票
分析Code:
从输出结果上可以看到有多线程的抢票系统存在以下几个问题:
多个线程同一时刻访问了同一个资源,造成线程不安全
优点:
Runnable
接口比继承Thread
类更灵活。一个类可以实现多个接口,因此您可以在一个类中实现Runnable
接口,同时还可以继承其他类或扩展其他功能。Runnable
接口创建线程更容易与其他类协作。Runnable
接口,多个线程可以共享相同的实例变量,这使得资源共享更容易。Runnable
接口的线程更容易集成到线程池中,线程池可以更好地管理和重用线程。缺点:
Thread
类,实现Runnable
接口的方式稍微复杂一些,需要在类中实现run
方法,并且需要创建Runnable
对象并传递给Thread
类的构造函数。Runnable
对象,然后将它传递给Thread
对象。为什么有线程同步机制?
- 为了确保多个线程可以安全地访问和操作共享的资源,以防止出现竞态条件(Race Condition)和数据不一致的情况。如上述的抢票系统存在的问题
如上图所示:
每个线程需要使用共享资源时,先尝试获取锁,如果锁被其他线程占用,则将该线程加入到队列中,等待获取锁,
使用完共享资源后,释放锁,线程重新排序队列,四个Person对象重新抢夺这个锁(已经使用的线程仍然可以继续排序争夺锁) 。
在Java中可以使用synchronized关键字创建同步方法/同步块实现线程同步机制。
同步方法:
同步方法可以通过在方法声明中添加synchronized
关键字来实现。
这会使得该方法在被多个线程访问时,只有一个线程能够执行该方法,其他线程需要等待该线程执行完成后才能继续执行。
Code:
使用同步方法解决抢票问题
public class TicketSystem {
private int totalTickets = 10; // 总票数
// 使用同步方法确保线程安全
public synchronized void buyTicket(String threadName) {
if (totalTickets > 0) {
System.out.println(threadName + " 抢到了第 " + totalTickets + " 张票");
totalTickets--;
} else {
System.out.println("票已售罄");
}
}
public static void main(String[] args) {
TicketSystem ticketSystem = new TicketSystem();
Thread user1 = new Thread(() -> {
ticketSystem.buyTicket("张三");
});
Thread user2 = new Thread(() -> {
ticketSystem.buyTicket("小明");
});
Thread user3 = new Thread(() -> {
ticketSystem.buyTicket("李四");
});
user1.start();
user2.start();
user3.start();
}
}
在这个示例中,buyTicket
方法被定义为同步方法,确保了线程安全。多个用户线程(张三、小明、李四)同时尝试调用buyTicket
方法,但只有一个线程能够成功抢到票,其他线程会等待。
同步块:
同步块可以通过在代码块前添加**synchronized**
关键字来实现。
这会使得该代码块在被多个线程访问时,只有一个线程能够执行该代码块,其他线程需要等待该线程执行完成后才能继续执行。
Code:
使用同步块解决抢票问题
public class TicketSystem implements Runnable {
private int totalTickets; // 总票数
private int interval; // 抢票间隔(毫秒)
public TicketSystem(int totalTickets, int interval) {
this.totalTickets = totalTickets;
this.interval = interval;
}
@Override
public void run() {
while (true) {
synchronized (this) { // 使用同步块确保线程安全
if (totalTickets > 0) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 抢到了第 " + totalTickets + " 张票");
totalTickets--;
} else {
System.out.println("票已售罄");
break; // 所有票已售完,退出循环
}
}
try {
Thread.sleep(interval); // 模拟用户抢票间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
int totalTickets = 10;
int interval = 100;
TicketSystem ticketSystem = new TicketSystem(totalTickets, interval);
Thread user1 = new Thread(ticketSystem, "张三");
Thread user2 = new Thread(ticketSystem, "小明");
Thread user3 = new Thread(ticketSystem, "李四");
user1.start();
user2.start();
user3.start();
}
}
这个示例中,我将总票数和抢票间隔作为构造函数参数传递给 TicketSystem
类,使代码更具通用性。
多个线程各自占有一些共享的资源,并且相互等待其他线程占有的资源才能运行,从而导致两个线程都在等待对象释放资源。
在 Java 中,Lock
锁是一种用于多线程编程的机制,它提供了比传统的 synchronized
关键字更灵活和强大的线程同步和互斥控制方式。
Lock
接口定义了一套用于获取和释放锁的方法,可以手动开启和关闭锁。ReentrantLock
。Code:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketSystem implements Runnable {
private int totalTickets = 10; // 总票数
private Lock lock = new ReentrantLock(); // 创建一个ReentrantLock锁
@Override
public void run() {
while (true) {
try {
lock.lock(); // 获取锁
if (totalTickets > 0) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " 抢到了第 " + totalTickets + " 张票");
totalTickets--;
} else {
System.out.println("票已售罄");
break; // 所有票已售完,退出循环
}
} finally {
lock.unlock(); // 释放锁
}
try {
Thread.sleep(100); // 模拟用户抢票间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TicketSystem ticketSystem = new TicketSystem();
Thread user1 = new Thread(ticketSystem, "张三");
Thread user2 = new Thread(ticketSystem, "小明");
Thread user3 = new Thread(ticketSystem, "李四");
user1.start();
user2.start();
user3.start();
}
}
在这个示例中,我们使用了 ReentrantLock
锁来控制对共享资源的访问。在 run
方法中,通过 lock.lock()
获取锁,在访问共享资源后使用 lock.unlock()
释放锁,以确保线程安全。
优点
Lock
锁的优势在于它提供了更多的控制,例如可以设置锁的超时时间、使用条件变量等.缺点
synchronized
关键字相比,使用 Lock
锁需要手动释放锁,因此需要在 finally
块中确保锁的释放,以防止出现死锁等问题。方法 | 说明 |
---|---|
setName() | 设置线程名称 |
getName() | 返回该线程名称 |
start() | 该线程开始执行,JVM调用start0()方法 |
run() | 调用线程对象run()方法 |
setPriority() | 更改线程的优先级 |
getpriority() | 获取线程的优先级 |
sleep() | 线程休眠 |
interrupt() | 中断线程 |
yield() | 线程礼让,但是不能保证线程礼让成功 |
join() | 线程插队 |
Code:
public class ThreadStop implements Runnable {
private boolean isRunning = true; //线程停止标志位
@Override
public void run() {
int i = 0;
while (isRunning) {
System.out.println("正在执行..." + i++);
}
}
//暂停线程方法
public void stopThread() {
isRunning = false;
}
public static void main(String[] args) throws InterruptedException {
ThreadStop threadStop = new ThreadStop();
//创建线程,启动线程
new Thread(threadStop).start();
Thread.sleep(2000);
threadStop.stopThread();
}
}
线程在执行任务时会不断检查自己的终止状态,如果isRunning=false,则终止线程。
在main方法中,启动了一个线程,并在2秒后将isRunning=false,终止线程。
线程中断是指一个线程向另一个线程发出信号,请求其停止正在执行的操作。
这个信号由一个布尔标志来表示,通常称为线程的中断状态(interrupt status)。
当线程的中断状态被设置为**true
**时,线程会收到一个中断请求。
Code:
class MyRunnable implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// 线程执行任务
try {
Thread.sleep(1000); // 模拟工作
} catch (InterruptedException e) {
// 响应中断请求,可以进行清理工作
Thread.currentThread().interrupt(); // 重新设置中断状态
}
}
}
}
public class ThreadInterruptExample {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); //开启线程
// 在某个时刻中断线程
try {
Thread.sleep(5000);
thread.interrupt(); // 发送中断信号,设置为true
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程在执行任务时会不断的检查自己的中断状态,如果中断状态为true,则推出任务执行。
在“main”方法中,启动了一个线程,并且在5秒后发送中断请求。
线程一旦插队成功,则肯定先执行插入的线程所有的任务,再去执行其他线程的任务。
Code:
public class ThreadStop implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("vip线程来了," + i);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadStop threadStop = new ThreadStop();
Thread thread = new Thread(threadStop, "A");
thread.start();
//main线程
for (int i = 0; i < 500; i++) {
if (i == 100) {
thread.join(); //线程插队
}
System.out.println("main线程," + i);
}
}
}
main线程在循环100次后,插入vip线程,等待vip线程执行完后,再继续执行main线程的任务。
Code:
public class ThreadStop implements Runnable {
@Override
public void run() {
System.out.println("正在执行" + Thread.currentThread().getName() + "线程");
Thread.yield(); //线程礼让
System.out.println("停止" + Thread.currentThread().getName() + "线程");
}
public static void main(String[] args) throws InterruptedException {
ThreadStop threadStop = new ThreadStop();
new Thread(threadStop, "A").start();
new Thread(threadStop, "B").start();
}
}
输出:
正在执行A线程
正在执行B线程
停止B线程
停止A线程
在main
方法中,我们创建了两个线程实例(线程"A"和线程"B"),它们都使用相同的ThreadStop
对象作为任务,并启动这两个线程。
由于两个线程共享ThreadStop
对象,它们运行相同的run()
方法。
由于Thread.yield()
方法的存在,这两个线程在执行过程中可能会主动让出CPU时间,以便其他线程有机会运行。
public class ThreadStop {
public static void main(String[] args) throws InterruptedException {
God god = new God();
Person person = new Person();
//创建上帝线程
Thread godThread = new Thread(god);
godThread.setDaemon(true); //设置为守护线程
godThread.start();
//创建用户线程
Thread personThread = new Thread(person);
personThread.start();
}
}
class God implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("正在执行守护线程");
}
}
}
class Person implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("用户岁数=" + i);
}
System.out.println("======say goodbye======");
}
}
输出:
正在执行守护线程
用户岁数=0
用户岁数=1
用户岁数=2
用户岁数=3
用户岁数=4
用户岁数=5
用户岁数=6
用户岁数=7
用户岁数=8
用户岁数=9
用户岁数=10
用户岁数=11
用户岁数=12
用户岁数=13
用户岁数=14
用户岁数=15
用户岁数=16
用户岁数=17
用户岁数=18
用户岁数=19
======say goodbye======
正在执行守护线程
正在执行守护线程
正在执行守护线程
正在执行守护线程
正在执行守护线程
正在执行守护线程
当用户线程执行完毕后,守护线程也会紧随其后。
多线程编程是现代软件开发不可或缺的一部分,但也存在复杂性和挑战。
通过深入理解多线程的原理和最佳实践,开发人员可以更好地利用多核处理器,提高程序性能和响应性,同时避免潜在的线程安全问题。
本博客提供了一个较为基础的多线程编程指南,帮助开发人员入门这一重要领域的技能。