您的位置:首页 > 新闻 > 热点要闻 > 多线程基础

多线程基础

2025/6/3 0:57:48 来源:https://blog.csdn.net/qq_42742845/article/details/141071076  浏览:    关键词:多线程基础

线程的创建

1. 通过继承Thread类实现多线程

通过继承Thread类来自定义一个线程类, 并重写run()方法. run()方法是一个线程的线程体, 也就是开启这个线程后它要执行的部分. 之后, 直接在main方法中通过new()的方式创建两个线程实例, 再通过start()方法, 让这两个线程运行起来. 运行起来之后就开始执行run()方法中的代码了.

为了方便我们看到结果, 使用getName()方法来获取当前线程的name. 这个方法是父类Thread中的方法, 直接调用即可.

public class ThreadTest extends Thread {public ThreadTest() {System.out.println(this.getName());}/*** 线程的线程体*/@Overridepublic void run() {System.out.println(this.getName() + "线程开始");for (int i = 0; i < 20; i++) {System.out.println(this.getName() + " " + i);}System.out.println(this.getName() + "线程结束");}public static void main(String[] args) {System.out.println("主线程开始");// 创建线程ThreadTest t1 = new ThreadTest();ThreadTest t2 = new ThreadTest();// 启动线程t1.start();t2.start();System.out.println("主线程结束");}
}

2. 通过实现Runnable接口实现多线程

通过继承Thread类的方式确实可以帮助我们创建自己的线程, 但是由于java单继承的特点, 我们自定义的线程类无法再继承其他的类, 这样会给我们带来一些限制. 此时我们可以采取实现Runnable接口的方式创建我们自己的线程. 查看源码可知, Thread类本身也是继承了Runnable接口的.

// Thread类源码
public
class Thread implements Runnable {...
}

Runnable接口中只有一个方法, 就是run()方法, 可以看出, 这就是我们多线程的核心方法.

// Runnable接口源码
public interface Runnable {public abstract void run();
}

在我们的测试类中, 依然是对run()方法的重写, 不同的是由于我们没有继承Thread类, 于是就没有了getName()方法. 不过也可以使用Thread.currentThread()方法得到当前的线程的对象实体, 再调用getName()方法就可以了.

Thread.currentThread()方法是Thread类中的一个静态方法, 返回的就是当前线程的对象实体.

// Thread类源码
/*** Returns a reference to the currently executing thread object.** @return  the currently executing thread.*/         
public static native Thread currentThread();

由于我们的测试类只是实现了Runnable接口, 所以它并没有start()方法, 那么怎么让它启动呢? 这时我们就需要把测试方法包装成Thread对象. 方式就是Thread t1 = new Thread(new RunnableTest());.

这里还有一点需要注意的是虽然我们的测试类的构造方法Runnable()是我们自定义线程的构造方法, 但是在我们new RunnableTest()时, 这个构造方法是在主线程(即main)中执行的, 也就是说line3这段代码的输出结果是main. 其原因就是这段代码是在main线程中执行的, 此时我们的自定义线程还没开启呢.

我们的自定义线程通过start()方法启动之后, 就开始执行run()方法, 在run()方法中(即线程体中)执行的Thread.currentThread().getName()得到的才是我们自定义线程的名字.

public class RunnableTest implements Runnable {public RunnableTest() {System.out.println(Thread.currentThread().getName());}/*** 当前线程的线程体*/@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程开始");for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + " " + i);}System.out.println(Thread.currentThread().getName() + "线程结束");}public static void main(String[] args) {System.out.println("主线程开始");Thread t1 = new Thread(new RunnableTest());Thread t2 = new Thread(new RunnableTest());t1.start();t2.start();System.out.println("主线程结束");}
}

另外, 如果我们自定义的线程类只需要使用一次, 那么可以使用匿名内部类的方式书写如下:

Thread t1 = new Thread(new Runnable(){@Overridepublic void run() {...}
});

Runnable接口中只有一个方法, 这种接口我们称之为函数式接口, 那么就可以使用lambda表达式来书写. 如果方法体只有一行, {}都可以省略.

Thread t3 = new Thread(() -> {...
});

线程的声明周期

在这里插入图片描述

新生状态(New)

用 new 关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用 start 方法进入就绪状态。

就绪状态(Runnable)

处于就绪状态的线程已经具备了运行条件,但是还没有被分配到 CPU,处于“线程就绪队列”,等待系统为其分配 CPU。就绪状态并不是执行状态,当系统选定一个等待执行的 Thread 对象后,它就会进入执行状态。一旦获得 CPU,线程就进入运行状态并自动调用自己的 run 方法。有 4 中原因会导致线程进入就绪状态:

  1. 新建线程:调用 start()方法,进入就绪状态;

  2. 阻塞线程:阻塞解除,进入就绪状态;

  3. 运行线程:调用 yield()方法,直接进入就绪状态;

  4. 运行线程:JVM 将 CPU 资源从本线程切换到其他线程。

运行状态(Running)

在运行状态的线程执行自己 run 方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

阻塞状态(Blocked)

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有 4 种原因会导致阻塞:

  1. 执行 sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。

  2. 执行 wait()方法,使当前线程进入阻塞状态。当使用 nofity()方法唤醒这个线程后,它进入就绪状态。

  3. 线程运行时,某个操作进入阻塞状态,比如执行 IO 流操作read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。

  4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

死亡状态(Terminated)

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它 run()方法内的全部工作; 另一个是线程被强制终止,如通过执行 stop()destroy()方法来终止一个线程(注:stop()/destroy()方法已经被 JDK 废弃,不推荐使用)。

当一个线程进入死亡状态以后,就不能再回到其它状态了。

线程的使用

1. 终止线程

如果我们想在一个线程中终止另一个线程我们一般不使用 JDK 提供的 stop()/destroy()方法(它们本身也被 JDK 废弃了)这种方式过于粗暴, 线程会直接挂掉, 就算有后续的收尾工作也不会执行。

通常的做法是提供一个 boolean 型的终止变量,当这个变量值为 false 时,则终止线程的运行。

public class StopThreadTest implements Runnable{private boolean flag = true;@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程开始");int i = 0;while(flag) {System.out.println(Thread.currentThread().getName() + " " + i++);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//模拟线程的收尾工作System.out.println(Thread.currentThread().getName() + "线程结束");}public void stop() {this.flag = false;}public static void main(String[] args) throws IOException {System.out.println("主线程开始");StopThreadTest st = new StopThreadTest();Thread t1 = new Thread(st);t1.start();System.in.read();st.stop();System.out.println("主线程结束");}
}

2. 暂停线程

sleep()方法

**作用: **让正在运行的线程休眠一段时间并进入阻塞状态, 等待休眠结束后再进入就绪队列.

public class SleepThreadTest implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程开始");for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {// 让线程暂停, 并设置时间为1000, 单位是毫秒(millis)Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "线程结束");}public static void main(String[] args) {System.out.println("主线程开始");Thread t0 = new Thread(new SleepThreadTest());t0.start();System.out.println("主线程结束");}
}

yield()方法

**作用: **让正在运行的线程停止运行, 让出cpu, 进入就绪队列排队.

注意: yield()方法是Thread类的一个静态方法. 使用yield()方法使线程重新排队后, 该线程有一定几率会被调度程序再次选中, 所以该方法让出cpu是有可能失败的.

public class YieldThreadTest implements Runnable{@Overridepublic void run() {for (int i = 0; i < 20; i++) {if (i == 0 && "Thread-1".equals(Thread.currentThread().getName())) {// 暂停线程让出cpu, 但是可能会失败Thread.yield();}System.out.println(Thread.currentThread().getName() + " " + i);}}public static void main(String[] args) {Thread t0 = new Thread(new YieldThreadTest());Thread t1 = new Thread(new YieldThreadTest());t0.start();t1.start();}
}

3. 联合线程

线程A邀请线程B优先执行, 在线程B执行完之后线程A才能继续执行, 类似于方法的嵌套调用. "邀请"的方式是调用线程Bjoin()方法.

join() 方法的使用

join()方法就是指调用该方法的线程在执行完 run()方法后,再执行 join ()方法后面的代码, 即将两个线程合并,用于实现同步控制.

在下面的实例代码中, 主线程(main)和在执行的过程中执行了t0.join(), 联合了Thread-0线程, 于是main线程停止执行, 并等待Thread-0执行完再接着执行. 于此同时Thread-1线程不受任何影响. 依然是与其他线程并发执行.

public class JoinTreadTest {public static void main(String[] args) throws InterruptedException{Thread t0 = new Thread(new ThreadA());Thread t1 = new Thread(new ThreadB());t0.start();t1.start();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + " "+ i);if (i == 2) {t0.join();}Thread.sleep(1000);}}
}class ThreadA implements Runnable {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
class ThreadB implements Runnable {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

联合线程实例

使用线程联合的方式实现同步.

public class JoinDemo {public static void main(String[] args) {System.out.println("爸爸和儿子买烟的故事");Thread t = new Thread(new FatherTread());t.start();}
}class FatherTread implements Runnable {@Overridepublic void run() {System.out.println("爸爸想抽烟, 发现烟抽完了");System.out.println("爸爸让儿子去买一包红塔山");Thread t = new Thread(new SonThread());System.out.println("等待儿子买烟回来");t.start();try {t.join();} catch (InterruptedException e) {e.printStackTrace();System.out.println("儿子丢了, 爸爸出门找儿子");System.exit(1);}System.out.println("爸爸抽上了烟, 并把零钱给了儿子");}
}
class SonThread implements Runnable {@Overridepublic void run() {System.out.println("儿子出门买烟");System.out.println("儿子买烟需要十分钟");for (int i = 0; i < 10; i++) {System.out.println("第" + i + "分钟");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

4. Thread类其他常用方法

4.1 获取线程名称

  • this.getName()获取当前线程名称, 适用于继承Thread类实现的多线程方式.
  • Thread.currentThread.getName()先获取当前线程的实例, 再获取当前线程名称.

4.2 设置线程名称

  • 通过构造方法设置线程名称

    Thread类有一个public Thread(String name)的构造方法, 子类继承Thread类之后, 可以在子类的构造方法的第一行调用父类的这个构造方法super(name)

  • 通过Thread类的setName()方法设置线程名称

    无论是通过继承Thread类, 还是通过实现Runnable接口实现的多线程, 都可以在线程对象创建完成后, 直接调用Thread类的普通方法setName()来为线程设置名称.

4.3 判断当前线程是否存活

thread.isAlive()方法: 判断当前线程是否处于活动状态.

线程创建并启动之后(执行thread.start()方法), 到执行完自己的run()(线程体)之前, 都属于活动状态.

线程的优先级

1. 什么是线程优先级

每一个线程都是有优先级的,我们可以为每个线程定义线程的优先级,但是这并不能保 证高优先级的线程会在低优先级的线程前执行。线程的优先级用数字表示,范围从 1 到 10, 一个线程的缺省优先级是 5。 Java的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关, 如非特别需要,一般无需设置线程优先级。

注意:线程的优先级,不是说哪个线程优先执行,如果设置某个线程的优先级高。那就 是有可能被执行的概率高。并不是优先执行。

2. 线程优先级的使用

  • thread.getPriority()方法可以获取线程的优先级
  • thread.setPriority()方法可以设置线程的优先级

**注意: **线程优先级必须在线程启动之前(start())设置, 启动之后设置是无效的.

守护线程

1. 什么是守护线程

在 Java 中有两类线程:

  • User Thread(用户线程):就是应用程序里的自定义线程。

  • Daemon Thread(守护线程):服务于用户线程的线程, 比如垃圾回收线程,就是最典型的守护线程。

    守护线程特点: 守护线程会随着用户线程死亡而死亡

2. 守护线程与用户线程的区别

用户线程,不随着主线程的死亡而死亡。

用户线程只有两种情况会死掉,

  1. run()方法执行过程中异常终止。
  2. 正常把run()方法执行完毕,线程死亡。

守护线程,随着用户线程的死亡而死亡,当用户线程死亡守护线程也会随之死亡.

3. 守护线程的使用

使用setDaemon()方法设置守护线程

public class DaemonThreadTest {public static void main(String[] args) {Thread t = new Thread(new UserThread());t.start();}
}/*** 守护线程*/
class DaemonThread implements Runnable {@Overridepublic void run() {for (int i = 0; i < 20; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}
}/*** 用户线程*/
class UserThread implements Runnable {@Overridepublic void run() {Thread daemonThread = new Thread(new DaemonThread());// 将该线程设为守护线程daemonThread.setDaemon(true);daemonThread.start();for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " " + i);try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}}
}

线程同步

1. 什么是线程同步

同步是指程序中用于控制不同线程间操作发生相对顺序的机制。

1.1 线程冲突

当多个线程同时操作某个资源时很容易就会出现冲突. 如果多个线程同时执行写操作就容易出现覆盖的情况.

1.2 线程同步的概念

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时 访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用.

2. 实现线程同步

2.1 synchronized关键字

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突 的问题。Java 语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线 程同时访问造成的这种问题。这套机制就是 synchronized 关键字。

// synchronized 语法结构: 
public synchronized void accessVal(int newVal); synchronized(锁对象){ 同步代码
} 

synchronized 关键字使用时需要考虑的问题:

  • 需要对那部分的代码在执行时具有线程互斥的能力(线程互斥:并行变串行)。
  • 需要对哪些线程中的代码具有互斥能力(通过 synchronized 锁对象来决定)。 它包括两种用法:synchronized 方法和 synchronized 块。

2.2 synchronized关键字的使用

  • synchronized 方法

    通过在方法声明中加入 synchronized 关键字来声明,语法如下:

    public synchronized void method() {}
    

    synchronized 在方法声明时使用:放在访问权限修饰符(public)之后,返回类型声明(void) 之前, 也可以放在最前面, 但是不太规范。这时同一个对象下synchronized 方法在多线程中执行时,该方法是同步的,即一次 只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

  • synchronized 块

    synchronized 方法的缺陷:如果直接将一整个方法声明为 synchronized 将会大大影响效率。

    Java 为我们提供了更好的解决办法,那就是synchronized块, 可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率.

    synchronized(锁对象){ ...
    } 
    

synchronized关键字不能被继承

按对象锁的类型分类
使用this作为锁

在不同线程中, 调用相同对象的synchronized(){} 会发生互斥.

本质是对同步代码所在的对象加锁.

  • 语法结构
synchronized(this) {// 同步代码
}
// 等同于对普通方法加synchronized
public synchronized void method() {}
使用字符串作为锁

所有使用统一字符串为对象锁的synchronized(){}代码块都会同步.

本质是对字符串常量对象加锁

  • 语法结构
synchronized("String") {// 同步代码
}
使用Class作为锁

在不同的线程中, 使用统一Class对象的synchronized(){} 代码块会同步执行.

本质是对类的class对象加锁,

  • 语法结构
synchronized(XX.class) {// 同步代码
}
// 等同于对类的静态方法加synchronized修饰
public synchronized static void staticMethod() {// 同步代码
}

总结

无论是使用何种类型的对象锁, 本质都是对对象加锁, 字符串和类对象的本质也是对象, 在对象的内存结构中, 对象头的位置, 存储量该对象的锁信息, 加锁的实质就是修改这个锁信息, 当某个线程需要获取锁时, 会先判断对象是否已经加锁, 如果没有就自己加锁, 然后执行同步代码, 如果已被加锁, 就需要等待锁被释放, 再去尝试加锁.

使用自定义对象作为锁
synchronized关键字的使用方式分类
  • 修饰普通方法: 锁当前对象
  • 修饰静态方法: 锁当前对象所属类的类对象
  • 包裹代码块: 小括号里写什么就对什么加锁

3. wait 和 notify

方法名所用
final void wait()表示线程一直等待, 直到得到其他线程通知
void wait(long timeout)线程等待指定的毫秒数
final void wait(long timeout, int nanos)线程等待指定的毫秒, 微秒时间
final void notify()唤醒一个处于等待状态的线程
final void notifyAll()唤醒同一个对象上, 所有调用wait()方法的线程, 优先级高的先运行

以上方法斗是java.lang.Object类的方法;

都只能在同步方法或者同步代码块中使用, 否则会抛出异常.

死锁

1. 死锁的概念

1.1 死锁的定义

多个线程由于竞争资源而造成的一种互相等待的状态, 若无外力作用, 这些线程将无法推进.

1.2 死锁产生的原因

  • 系统资源的竞争
  • 线程推进顺序不合理

1.3 死锁产生的必要条件

  • 互斥资源–在一段时间内只能被一个线程使用的资源, 具有排他性
  • 不可剥夺–线程获得的资源在使用完之前不能被其他线程强行夺走, 只能主动释放
  • 请求保持–线程已经保持了至少一个资源, 同时又在请求新的资源
  • 循环等待–这些陷入死锁的线程中存在一个循环等待链

| final void wait(long timeout, int nanos) | 线程等待指定的毫秒, 微秒时间 |
| final void notify() | 唤醒一个处于等待状态的线程 |
| final void notifyAll() | 唤醒同一个对象上, 所有调用wait()方法的线程, 优先级高的先运行 |

以上方法斗是java.lang.Object类的方法;

都只能在同步方法或者同步代码块中使用, 否则会抛出异常.

死锁

1. 死锁的概念

1.1 死锁的定义

多个线程由于竞争资源而造成的一种互相等待的状态, 若无外力作用, 这些线程将无法推进.

1.2 死锁产生的原因

  • 系统资源的竞争
  • 线程推进顺序不合理

1.3 死锁产生的必要条件

  • 互斥资源–在一段时间内只能被一个线程使用的资源, 具有排他性
  • 不可剥夺–线程获得的资源在使用完之前不能被其他线程强行夺走, 只能主动释放
  • 请求保持–线程已经保持了至少一个资源, 同时又在请求新的资源
  • 循环等待–这些陷入死锁的线程中存在一个循环等待链

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com