本文章将带你初步了解java语言中的线程基础概念和常见用法,帮助你快速入门和学习线程!
进程与线程
我们都知道前端中的javascript是一个单线程的语言,所有代码只能在上一个代码执行完才能继续执行。
简单来说,进程好比一个工厂,线程是好比工厂内的一条流水线,想提高工厂的效益,我们可以多增加几个流水线(线程)。
java比较强大,它是一个多线程的语言,因此代码效率可以很高。
我们先看一段简单的代码:
public class Java_Thread {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()); // main
}
}
- 这段Java代码的主要作用是打印出当前正在执行的主线程的名称。
- Thread.currentThread()是一个静态方法,它返回对当前正在执行的线程的引用,即主线程的引用。
- getName()方法获取这个线程的名称
在这段java程序运行时,默认产生一个进程,这个进程会有一个主线程(如main),所有代码都在主线程中运行。
线程的创建
创建一个自己的线程非常容易,首先,我们需要定义一个继承Thread的类。然后,重写线程的run方法即可(我们在编译器内点击Ctrl + O ,可以快速重写线程内的run方法)。Thread.currentThread().getName()可以获取线程的名称。
接下来,我们声明自己的线程,并启动线程(执行其start()方法即可启动)
public class Java_Thread {
public static void main(String[] args) {
// 创建自定义线程对象
MyThread thread = new MyThread();
// 执行自定义线程
thread.start();
// 主线程
System.out.println(Thread.currentThread().getName());
}
}
//声明自定义线程类
class MyThread extends Thread{
// 重写运行指令
@Override
public void run() {
System.out.println("我的线程"+Thread.currentThread().getName());
}
}
![image.png](https://cdn.nlark.com/yuque/0/2023/png/21865277/1699237333219-14f5acf0-05aa-462c-a17b-6b7dd38d7dbb.png#averageHue=%23302f2e&clientId=u820503c1-7a78-4&from=paste&height=108&id=u7cbfd118&originHeight=135&originWidth=667&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=12086&status=done&style=none&taskId=uaa936cdd-90b9-4ac5-b40b-9f8706ee3ec&title=&width=533.6)
代码中,thread.start()的执行在System.out.println(Thread.currentThread().getName())代码的运行之前,但通过运行结果,我们会发现“main”先打印,“我的线程Thread-0”被后打印。
这说明了,不同线程之间执行是有先后顺序的,由于主线程从一开始就在,所以在这个示例中主线程最先执行。
线程的生命周期
同vue、react框架一样,线程也有生命周期的概念。如图,Java中的线程生命周期主要有六个阶段,分别是:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)和终止(Terminated)。
我们以如下代码为例,简述其声明周期过程
public class Java_Thread {
public static void main(String[] args) {
// 创建自定义线程对象
MyThread thread = new MyThread();
// 执行自定义线程
thread.start();
// 主线程
System.out.println(Thread.currentThread().getName());
}
}
//声明自定义线程类
class MyThread extends Thread{
// 重写运行指令
@Override
public void run() {
System.out.println("我的线程"+Thread.currentThread().getName());
}
}
- 新建状态(New):创建了MyThread的实例,但还未调用start()方法,线程处于新建状态。
- 就绪状态(Runnable):调用了thread.start()方法后,线程进入就绪状态,等待CPU调度。
- 运行状态(Running):由于该代码示例中没有等待状态,因此一旦线程进入就绪状态并被调度后,线程将直接进入运行状态,执行MyThread的run()方法。
- 终止状态(Terminated):当MyThread的run()方法执行完毕后,线程将进入终止状态,表示线程已经执行完毕。
由于上述代码示例中没有显式地让线程进入等待状态,因此它不包含等待状态。如果要让线程进入等待状态,可以在MyThread的run()方法中使用Thread类的wait()方法。
线程的并行与串行
并行
正常情况下,子线程的运行是独立的,互不干扰,谁先抢到cpu资源,谁先执行。
public class Java_Thread {
public static void main(String[] args) {
MyThread1 thread1 = new MyThread1();
MyThread2 myThread2 = new MyThread2();
thread1.start();
myThread2.start();
System.out.println("主线程执行完毕");
}
}
class MyThread1 extends Thread{
@Override
public void run() {
System.out.println("我的线程1:"+Thread.currentThread().getName());
}
}
class MyThread2 extends Thread{
@Override
public void run() {
System.out.println("我的线程2:"+Thread.currentThread().getName());
}
}
也正因如此,“线程1”和“线程2”的执行顺序是不确定的。这种运行方式成为“并行”。那么,我们自然而然可以做到让不同线程之间按顺序执行,即“串行”。
串行
要实现串行非常简单,只需要给线程对象执行join()方法即可。
休眠机制
线程在执行过程中可以进行休眠,等待一定时机后继续执行的能力(可以与其他线程进行数据交互)
public class Java_Thread {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(3000);
System.out.println("main线程执行完毕");
}
}
如上述代码实现了一个3秒的线程休眠,3s后才会打印“main线程执行完毕”。我们通过休眠实现一个时间打印器:
线程的其他创建写法
之前的demo中,我们定义一个线程都采用了继承Thread类的方法。这种方法写起来较为繁琐,我们看看另外两种方便的创建方法。
Lambda表达式
() -> { }
在这个Lambda表达式中,箭头符号"->"表示Lambda表达式的开始,箭头左侧是要传递的参数列表,箭头右侧是Lambda表达式的主体。
public class Thread_01 {
public static void main(String[] args) {
// 构建线程对象时,可以把逻辑传递给这个对象
// 传递逻辑 () -> {}
Thread T = new Thread(() -> {
System.out.println("线程T");
});
T.start();
System.out.println("main线程执行完毕");
}
}
实现Runnable接口
可以将实现了Runnable接口的对象作为参数传递给Thread类的构造函数,然后调用start()方法启动线程。
public class Thread_01 {
public static void main(String[] args) {
// 构建线程对象时,可以传递实现了Runnable接口得类的对象,一般使用匿名类
Thread t5 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程");
}
});
}
}
线程池
线程是进程中的实际运作单位,而线程池是使用池化技术管理和使用线程的机制。它的存在价值就是降低资源消耗:
通过重复利用已创建的线程,避免线程的创建和销毁所造成的资源消耗。
javjava中有四种常见得线程池
- 固定大小的线程池
- 动态创建的线程池
- 单一线程池
- 定时线程池
固定大小的线程池
当有新任务到达时,如果线程池中有空闲线程,则立即执行任务。如果线程池已满,则任务会被放在队列中等待。这种线程池适用于处理大量短期且并发量不大的任务。
我们来看一个图解,如下图,在线程池对象中有三个线程1、2、3。A,B分别提交了一个任务给线程池对象。由于线程1,2,3空闲,因此,线程池对象对象把任务先分给1,2线程处理;此时,1,2线程处于工作状态(红色),3线程处于空闲状态(绿色)。
随后,C提交了一个新任务,由于1,2线程未结束,任务必然由3线程处理
如果此时再来一个D任务,由于1,2,3线程均处于工作状态,此时,任务D只能处于等待状态。
假设线程2的任务比较简单,线程2先处理完任务,此时,D任务就会交给线程2处理。
我们用代码展示上述的过程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Thread_Pool {
public static void main(String[] args) {
// 1.创建固定数量的线程对象
// ExecutorService 是线程服务对象
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i= 0 ;i<5;i++){
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
}
那么,代码的运行结果应该只会打印三种线程的名称
观察打印结果,和我们的图解过程是一致的。
动态创建的线程池
这是一个可以缓存空闲线程的线程池,当有新任务到达时,如果线程池中有空闲线程,则立即执行任务。如果线程池中没有空闲线程,则创建新的线程执行任务。这种线程池适用于处理大量短期且并发量较大的任务。
ExecutorService executorService = Executors.newCachedThreadPool();
单一线程池
这是一个只有一个线程的线程池,适用于需要顺序执行且无并发需求的任务。这种线程池可以避免多线程竞争和死锁等问题,提高程序的稳定性和性能。
ExecutorService executorService = Executors.newSingleThreadExecutor()
定时线程池
这是一个可以定时执行任务的线程池,可以按照指定的时间间隔执行任务。这种线程池适用于需要定时执行的任务,如定时清理缓存、定时发送邮件等。
// 定时线程池(ScheduledThreadPool)
ExecutorService executorService = Executors.newScheduledThreadPool(3);
了解即可,我们不用做深入研究!
线程的同步与异步
线程的串行与并行和异步与同步是两个不同的概念。
串行和并行是描述线程的执行方式的:
- 串行:在程序中,一个线程完成一项任务后,另一个线程才开始执行。这就像一条直线,一个任务完成后,下一个任务才开始。
- 并行:在程序中,多个线程可以同时执行。这就像多条直线,它们并行前进,没有先后顺序。
而异步与同步是描述线程之间通信方式的:
- 异步:当一个线程在等待资源时,它不会阻塞,而是继续执行其他任务。当资源可用时,它会通过回调函数获取结果。
- 同步:当一个线程在等待资源时,它会阻塞,直到资源可用。
当然,作为初学者,我们不用理解的太深,我们简单看一个demo就行!
我们来实现一个火车站买票的程序。首先,我们需要一个售票的窗口的类Ticket,然后我们开三个线程(代表三个人去抢票)
package thread;
public class Ticket implements Runnable {
private int num = 10;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (num <= 0) {
break;
}
System.out.println(Thread.currentThread().getName() + "买到了第" + num + "票");
num --;
}
}
}
class TicketTest{
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket,"Person1");
Thread thread2 = new Thread(ticket,"Person2");
Thread thread3 = new Thread(ticket,"Person3");
thread1.start();
thread2.start();
thread3.start();
}
}
这段代码的主要功能是模拟一个售票系统。在Ticket类中,有一个变量num,表示可售出的票的数量。当num小于或等于0时,售票活动结束。
在TicketTest类中,创建了三个Thread对象,这三个线程都使用同一个Ticket对象作为参数。
由于这三个线程共享同一个Ticket对象(也就是共享同一个num变量),因此在每个线程的run方法中,循环会一直执行直到num小于等于0为止。
这段代码的一个主要问题是,由于线程可能会同时修改num变量,因此可能会出现数据竞争的问题,我们来看一下代码运行结果
问题似乎不言而喻!
为了解决数据竞争的问题,我们可以使用synchronized关键字来确保在同一时刻只有一个线程可以访问共享资源(即num变量)。我们展示一种写法:
讲到这里,相信你对线程的同步异步问题已经有了初步的认识。