Java中的多线程入门解读
Java中的多线程
一. 线程的创建
在Java中使用线程有两种方法:
- 继承
Thread
类 - 实现
Runnable
接口
1. 继承Thread
类 使用 线程
例:
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
cat.start();
}
}
class Cat extends Thread {
@Override
public void run() {
while (true) {
System.out.println("喵");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
解读
- 当一个类继承了
Thread
类时, 该类就可以当作线程使用 - 我们会重写其中的
run()
方法, 写入自己需要的东西 Thread
类 之中的run()
方法也是实现了Runnable
中的run()
方法
2. 实现Runnable
接口 来 使用线程
因为Java是单继承的, 在某些情况下一个类已经继承了一个另外一个类, 这个时候再继承Thread
显然不太可能, 这个时候就可以实现Runnable
接口来使用多线程
例:
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
new Thread(dog).start();
}
}
class Dog implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("汪 " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
解读
- 没有
start()
方法, 所以可以将其放入Thread
中, 再调用start()
方法 - 这里其实使用了一个设计模式: 代理模式
// 线程代理类, 模拟了一个最简单的Thread
class ThreadProxy implements Runnable {
private Runnable target = null;
@Override
public void run() {
if (target != null) {
target.run(); // 动态绑定机制
}
}
public ThreadProxy(Runnable target) {
this.target = target;
}
public void start() {
start0();
}
public void start0() {
run();
}
}
通过代码可以看出, 其实是在start0()
中 帮助 Dog
类 运行 run()
方法
这就是代理模式(静态代理模式)
二. 多线程机制说明
- 我们的
main
函数 其实是一个线程, 可以看作由java进程创建的一个线程, 而Cat
又是main
进程
创建的一个子线程 - 子线程
Thread-0
和main
线程可以同时执行, 不会阻塞 - 可以利用
Jconsole
来监控进程执行情况 - 主线程
main
结束, 其子线程可能还在继续执行, 但是如果一个进程下所有的线程全部结束, 那么这个进程也会跟着结束 - 主线程和子线程还可以继续创建子线程
Cat
的start()
方法与run()
方法的区别
只有执行start
方法 才算是创建一个新的线程, 如果直接调用Cat
的run()
方法, 并不算创建线程. 而只是在main
线程 中 调用了一个方法, 所以此时还是只有main
线程一个
public synchronized void start() {
start0();
}
而start0()
是一个native
本地方法, 是由JVM
调用, 底层采用c/c++
实现
private native void start0();
调用start0()
方法 之后, 线程变为可运行状态, 具体什么时候执行,
取决于CPU的调度与资源
总结:
- 从Java的设计上来看, 通过继承
Thread
和 实现Runnable
接口来创建线程其实本质上没有区别, 因为Thread
也实现了Runnable
接口 - 实现
Runnable
接口的方式更加适合多个线程共享一个资源的情况, 并且避免了单继承的局限性
三. 线程的使用
1. main
线程中控制子线程
如果希望主线程中去控制子线程, 可以设置一个Boolean变量来控制run()
方法的终止, 就可以
通知 子线程结束
public class Main {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
t1.start();
Thread.sleep(1000);
t1.setLoop(false);
}
}
class T1 extends Thread {
private boolean loop = true;
@Override
public void run() {
while (loop) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程" + Thread.currentThread().getName() + " 运行中..");
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
2. 常用方法
第一组方法
主要用于线程的基本信息和操作
setName()
设置线程名称getName()
返回该线程的名称start()
使得该线程开始执行, Java虚拟机底层调用该线程的start0()
方法run()
调用该线程对象的run()
方法, 注意这里并没有创建新的线程setPriority()
更改线程的优先级getPriority()
获取线程的优先级sleep()
在指定的毫秒数内让当前执行的线程休眠interrupt()
结束休眠
第二组方法
线程的礼让和插队
-
yield()
线程的礼让, 让出占用的cpu, 让其他线程执行, 但是礼让的时间不确定, 而且也不一定礼让成功 -
join()
线程的插队, 插队的线程一旦插入成功, 则肯定先执行完插入的线程的所有任务, 再执行本身的任务 (在t1中调用t2.join(), 则t2插入到t1中, 先执行完t2)
案例: main
线程中创建子线程, 子线程每隔一秒输出一次hello, 输出20次, 主线程每隔一秒输出一次hi, 输出20次
当主线程运行5次之后, 就让子线程先输出完
public class Main {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
t1.start();
for (int i = 0; i < 20; i++) {
Thread.sleep(100);
System.out.println("hello");
if (i == 5) {
t1.join();
}
}
}
}
class T1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hi");
}
}
}
四. 用户线程与守护线程
用户线程: 也叫工作线程, 当线程的任务执行完或者线程以被通知的方式结束
守护线程: 一般是为工作线程服务的, 当所有的用户线程结束, 守护线程自动结束
常见的守护线程: 垃圾回收机制
例:
public class Main {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
t1.setDaemon(true); // 设置为守护线程, 注意先设置再start()
t1.start();
for (int i = 0; i < 20; i++) {
Thread.sleep(100);
System.out.println("hello");
}
}
}
class T1 extends Thread {
@Override
public void run() {
for (; ;) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hi");
}
}
}
五. 线程的生命周期与七大状态
JDK中Thread 定义了枚举类型代表线程的状态
官方将进程状态分成六种, 但是如果如图, 将Runnable
状态 细化为 Ready
和 Running
, 就可以分为七种状态, 这也就是我们常说的线程的七大状态
六. 线程同步机制
synchronized
在多线程编程中, 一些敏感的数据不允许被多个线程同时访问, 此时就可以使用同步访问技术,
保证数据在任何同一时刻, 最多有一个线程访问, 以此保证数据的完整性
实现同步的方法
- 同步代码块
synchronized (对象) { // 得到对象的锁, 才能操作同步代码
// 需要被同步的代码
}
- 声明方法为同步方法
public synchronized void m(String name) {
// 需要被同步的代码
}
案例: 多窗口售票问题
将售卖方法设置为同步方法, 这样的话同一时刻只会有一个窗口售卖
public class Main {
public static void main(String[] args) throws InterruptedException {
SellTicket sellTicket = new SellTicket();
Thread thread1 = new Thread(sellTicket);
Thread thread2 = new Thread(sellTicket);
Thread thread3 = new Thread(sellTicket);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
class SellTicket implements Runnable {
private int ticketNum = 100;
@Override
public void run() {
while (true) {
boolean ok = sell();
if (!ok) {
break;
}
}
}
public synchronized boolean sell() {
if (ticketNum <= 0) {
System.out.println("票已经售卖完");
return false;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("售票处" + Thread.currentThread().getName() + "售出一张票, 剩余票数为 " + (--ticketNum));
return true;
}
}
七. 线程的互斥锁与死锁
互斥锁
Java语言中, 引入了对象互斥锁的概念, 来保证共享数据操作的完整性
- 每个对象都对应于一个可称为"互斥锁" 的标记, 这个标记用来保证在任意时刻, 只能有一个线程访问
该对象
2.关键字synchronized
与对象的互斥锁联系, 当某个对象用synchronized
修饰时, 表明该对象在任意时刻只能由一个线程访问 - 同步的局限性: 导致程序的执行效率变低
- 同步方法(非静态)的锁可以是this, 也可以是其他对象; 同步方法(静态)的锁为当前对象本身
利用售票代码改写
public boolean sell() {
synchronized (this) {
if (ticketNum <= 0) {
System.out.println("票已经售卖完");
return false;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("售票处" + Thread.currentThread().getName() + "售出一张票, 剩余票数为 " + (--ticketNum));
return true;
}
}
这就是同步代码块
注意synchronized
后面的括号中可以填其他的对象, 表明关联的是这个对象的互斥锁
但是如果是静态的方法和代码块, 就只能关联到这个类本身
public static void m1() {
synchronized (SellTicket.class) {
System.out.println("m1");
}
}
注意:
xxx.class
和this
的区别, 前者关联的是类, 锁的是这个类, 后者锁的是这个类new
出的 对象!
所以静态同步只能锁该类,
换言之, 当每次都new 一个对象的情况下, 下面的代码是没有意义的
public void m1() {
synchronized (this) {
System.out.println("m1");
}
}
因为每一次创建线程都会new
一个对象, 这样的话就代表每个对象都锁自己, 是没意义的
死锁
多个线程占用了对方的锁资源, 不肯相让, 就会导致卡在这里, 造成死锁
案例
class DeadLockDemo extends Thread {
static Object o1 = new Object();
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入1");
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入3");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入4");
}
}
}
}
}
分析
- 如果线程1
flag = true
, 就会持有o1
的对象锁, 但是如果o2
的对象锁拿不到, 就会堵塞 - 如果线程2
flag = false
, 就会持有o2
的对象锁, 但是如果o1
的对象锁拿不到, 就会堵塞
这个时候两个线程就有可能死锁, 分别持有对方想要的资源, 但是不肯相让
锁的释放
以下情况下会释放锁
- 当前线程的同步方法/同步代码块执行结束
- 当前线程在同步代码块/同步方法中遇到
break/return
- 当前线程在同步代码块/同步方法中发现了未处理的Error/Exception, 导致异常结束
- 当前线程在同步代码块/同步方法中执行了线程对象的
wait()
方法, 当前线程暂停
以下情况下不会释放
- 线程执行同步代码块/同步方法时, 调用
yield/join
方法 - 线程执行同步代码块/同步方法时, 其他线程调用了该线程的
suspend()
方法(被挂起)
(这里已经不推荐使用了)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)