从 0 到 1!Java 并发编程基础全解析,零基础入门必看!

写在前面

博主在之前写了很多关于并发编程深入理解的系列文章,有博友反馈说对博主的文章表示非常有收获但是对作者文章的某些基础描述有些模糊,所以博主再根据最能接触到的基础,为这类博友进行扫盲!当然,后续仍然会接着进行创作且更倾向于实战Demo,希望令友友们有期待更希望有收获!

>>>线程简介

什么是线程

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位 。打个比方,如果把进程看作是一个工厂,那么线程就是工厂里的一条生产线。一个进程中可以并发多个线程,每条线程并行执行不同的任务,就如同一个工厂中可以有多条生产线同时工作,各自生产不同的产品。

在 Java 中,线程拥有自己独立的运行栈和程序计数器,这保证了线程在执行时的独立性。但同一进程中的多个线程会共享进程的堆内存和方法区内存,就像同属一个工厂的生产线共享原材料仓库和生产工艺一样。例如,在一个 Java Web 应用程序中,会有多个线程同时处理不同用户的请求,这些线程共享应用程序的内存空间和资源。

为什么要使用多线程

  1. 提高程序执行效率:相较于进程,线程的创建和切换开销更小。进程创建时需要分配独立的内存空间、文件描述符等资源,而线程创建时仅需分配少量的栈空间和寄存器等资源,切换时也只需保存和恢复少量的寄存器内容,因此线程的创建和切换速度更快,能有效减少系统开销,提高程序执行效率。以一个文件处理程序为例,若使用单线程,在读取文件内容时,线程会处于阻塞状态,CPU 资源被闲置;而采用多线程,可在一个线程读取文件时,另一个线程对已读取的数据进行处理,从而充分利用 CPU 资源,提升程序整体执行效率。
  1. 充分利用多处理器资源:在多核处理器环境下,多线程能使程序充分利用多个处理器核心。每个线程可被分配到不同的处理器核心上并行执行,如同多个工人同时在不同的生产线上工作,极大地提高了程序的并行处理能力。例如,在进行大数据分析时,可将数据处理任务分解为多个子任务,每个子任务由一个线程负责,并在不同的处理器核心上执行,从而加快数据分析速度。
  1. 方便数据共享:同一进程内的多个线程共享进程的内存空间和资源,数据共享变得非常便捷。线程间可以直接访问共享内存中的数据,无需像进程间通信那样借助复杂的机制。比如在一个图形绘制程序中,负责绘制图形的线程和负责处理用户输入的线程可以共享图形数据,方便实时更新图形显示。
  1. 提高程序响应性:在一些需要实时响应用户操作的应用程序中,多线程可使程序在处理耗时任务时,依然能快速响应用户的其他操作。例如,在一个音乐播放器应用中,主线程负责处理用户的播放、暂停、切换歌曲等操作,而播放音乐的任务则由一个单独的线程完成。这样,当用户在播放音乐时进行其他操作,如调整音量,主线程能及时响应,不会因为音乐播放的耗时操作而出现卡顿。

线程优先级

在 Java 中,每个线程都有一个优先级,它是一个整数,范围从 1 到 10。优先级越高的线程,在竞争 CPU 资源时越有可能被优先调度执行,但这并不意味着低优先级的线程就不会被执行,只是执行的机会相对较少。线程优先级的默认值为 5。

Java 的Thread类中定义了三个常量来表示线程优先级:

  • Thread.MIN_PRIORITY:表示最低优先级,值为 1。
  • Thread.NORM_PRIORITY:表示普通优先级,值为 5。
  • Thread.MAX_PRIORITY:表示最高优先级,值为 10。

下面通过一个简单的代码示例来展示线程优先级的设置和使用:

public class ThreadPriorityDemo {public static void main(String[] args) {// 创建线程1并设置最低优先级Thread thread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("线程1,优先级:" + Thread.currentThread().getPriority());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}});thread1.setPriority(Thread.MIN_PRIORITY);// 创建线程2并设置最高优先级Thread thread2 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("线程2,优先级:" + Thread.currentThread().getPriority());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}});thread2.setPriority(Thread.MAX_PRIORITY);// 启动两个线程thread1.start();thread2.start();}
}

在上述代码中,创建了两个线程thread1和thread2,分别设置它们的优先级为最低和最高。运行程序后,可以观察到线程 2 输出的次数可能会比线程 1 多,这体现了优先级对线程调度的影响。但由于线程调度的不确定性,在不同的运行环境和次数下,结果可能会有所不同。

线程的状态

在 Java 中,线程共有六种状态,这些状态反映了线程在其生命周期内的不同运行情况,它们定义在Thread类的State枚举中,通过getState()方法可以获取线程当前的状态。

1.新建(NEW):当使用new关键字创建一个线程对象后,线程就处于新建状态。此时线程还没有开始运行,仅仅是一个对象实例,系统没有为其分配运行所需的资源。例如:

Thread thread = new Thread();System.out.println(thread.getState()); // 输出:NEW

2.可运行(RUNNABLE):调用线程的start()方法后,线程进入可运行状态。处于此状态的线程位于可运行线程池中,等待线程调度器选中并分配 CPU 资源。一旦获得 CPU 时间片,线程就会进入运行中状态。可运行状态实际上包含了就绪(ready)和运行中(running)两种子状态,在 Java 中统一用RUNNABLE表示。例如:

Thread thread = new Thread(() -> {// 线程任务逻辑
});
thread.start();
System.out.println(thread.getState()); // 输出:RUNNABLE

3.终结(TERMINATED):当线程的run()方法执行完毕,或者因异常退出run()方法时,线程就进入终结状态,此时线程的生命周期结束,不再具备运行能力。例如:

Thread thread = new Thread(() -> {// 线程任务逻辑
});thread.start();while (thread.isAlive()) {// 等待线程执行完成
}System.out.println(thread.getState()); // 输出结果:TERMINATED

4.阻塞(BLOCKED):当线程试图获取一个被其他线程持有的锁时,如果获取失败,线程就会进入阻塞状态。在阻塞状态下,线程不会被分配 CPU 执行时间,直到它获得锁。例如:

public class BlockedState {private static final Object lock = new Object();public static void main(String[] args) {// 线程1获取锁并休眠Thread thread1 = new Thread(() -> {synchronized (lock) {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});// 线程2尝试获取已被占用的锁Thread thread2 = new Thread(() -> {synchronized (lock) {// 等待锁释放}});thread1.start();// 确保线程1先获取锁try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}thread2.start();System.out.println("线程2的状态:" + thread2.getState()); // 输出:BLOCKED}
}

5.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(如通知或中断)才能继续执行。例如,调用Object类的wait()方法、Thread类的join()方法等,会使线程进入等待状态。处于等待状态的线程不会被分配 CPU 执行时间,直到被唤醒。例如:

public class WaitingState {private static final Object lock = new Object();public static void main(String[] args) {// 创建等待线程Thread waitingThread = new Thread(() -> {synchronized (lock) {try {lock.wait();System.out.println("等待线程被唤醒");} catch (InterruptedException e) {e.printStackTrace();}}});waitingThread.start();// 主线程短暂休眠try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 创建通知线程new Thread(() -> {synchronized (lock) {lock.notify();System.out.println("已发送唤醒通知");}}).start();}
}

6.有时限的等待(TIMED_WAITING):该状态与等待状态类似,但线程会在指定的时间后自动返回,而无需其他线程的通知。例如,调用Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis)等方法时,线程会进入有时限的等待状态。例如:

public class TimedWaitingState {public static void main(String[] args) {Thread thread = new Thread(() -> {try {Thread.sleep(2000);  // 线程休眠2秒System.out.println("线程休眠结束");} catch (InterruptedException e) {e.printStackTrace();}});thread.start();  // 启动线程try {Thread.sleep(100);  // 主线程短暂休眠} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程状态:" + thread.getState());  // 输出TIMED_WAITING状态}
}

Daemon 线程

Daemon 线程,即守护线程,是一种特殊的线程。它的作用是为其他线程的运行提供服务,就像一个默默在后台工作的助手。守护线程与用户线程相对,用户线程用于执行具体的业务逻辑,而守护线程则在后台执行一些辅助性的任务,如垃圾回收线程就是一个典型的守护线程,它负责回收不再使用的内存空间,保证程序的内存使用效率。

守护线程和用户线程的主要区别在于,当 JVM 中所有的用户线程都结束时,无论守护线程是否完成任务,JVM 都会直接退出,而不会等待守护线程执行完毕。这是因为守护线程本身就是为用户线程服务的,当用户线程都不存在了,守护线程也就没有存在的必要了。

在 Java 中,可以通过setDaemon(true)方法将一个线程设置为守护线程,但这个设置必须在线程启动之前进行,否则会抛出IllegalThreadStateException异常。例如:

public class DaemonThreadDemo {public static void main(String[] args) {// 创建并启动守护线程Thread daemonThread = new Thread(() -> {while (true) {try {Thread.sleep(1000);System.out.println("守护线程正在运行");} catch (InterruptedException e) {e.printStackTrace();}}});daemonThread.setDaemon(true);daemonThread.start();// 主线程休眠3秒try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主线程结束");}
}

在上述代码中,创建了一个守护线程daemonThread,并将其设置为守护线程。主线程睡眠 3 秒后结束,此时尽管守护线程还在运行,但由于所有用户线程(这里的主线程是用户线程)都已结束,JVM 会直接退出,守护线程也随之结束。

>>>启动和终止线程

构造线程

在 Java 中,构造线程主要有两种方式:继承Thread类和实现Runnable接口。这两种方式各有特点,适用于不同的场景。

  • 继承Thread:通过继承Thread类,并重写其run()方法来定义线程的执行逻辑。这种方式的优点是代码实现简单直观,直接使用Thread类的方法,无需额外的对象来代理线程执行。例如:
public class MyThread extends Thread {@Overridepublic void run() {// 线程任务逻辑System.out.println("Thread running by extending Thread class");}
}

在上述代码中,MyThread类继承自Thread类,并重写了run()方法,在run()方法中定义了线程要执行的任务。

  • 实现Runnable接口:实现Runnable接口,将线程的执行逻辑封装在run()方法中,然后将实现了Runnable接口的对象作为参数传递给Thread类的构造函数来创建线程。这种方式的优势在于,一个类可以在继承其他类的同时实现Runnable接口,避免了 Java 单继承的限制,并且更适合多个线程共享同一个资源的场景。例如:
public class MyRunnable implements Runnable {@Overridepublic void run() {// 线程执行任务System.out.println("Runnable接口实现的线程正在运行");}
}

使用时,可以这样创建线程:

public class Main {public static void main(String[] args) {// 创建Runnable实现类实例MyRunnable task = new MyRunnable();// 创建并启动线程Thread worker = new Thread(task);worker.start();}
}

在这个例子中,MyRunnable类实现了Runnable接口,然后通过Thread类的构造函数将MyRunnable对象包装成一个线程。

启动线程

当我们构造好线程对象后,需要调用start()方法来启动线程。调用start()方法后,线程并不会立即开始执行,而是进入就绪状态,被纳入线程调度器的管理范围,等待 CPU 调度。一旦获得 CPU 时间片,线程就会执行其run()方法中的代码。

start()方法的作用是通知 Java 虚拟机,该线程已经准备好,可以被调度执行了。它会触发一系列底层操作,包括创建操作系统级别的线程、分配资源等。例如:

Thread thread = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("线程正在运行:" + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}
});
thread.start();

在上述代码中,创建了一个线程对象thread,并调用start()方法启动线程。线程启动后,会执行run()方法中的循环,每隔 100 毫秒输出一次信息。

需要注意的是,不能对同一个线程对象多次调用start()方法,否则会抛出IllegalThreadStateException异常。因为start()方法的设计规定,一个线程只能启动一次,多次调用会导致线程生命周期管理的混乱。另外,不要直接调用线程的run()方法,虽然调用run()方法也会执行线程中的代码,但它不会启动新线程,只是在当前线程中执行run()方法的逻辑,这与通过start()方法启动线程的效果完全不同。

理解过期的 suspend ()、resume () 和 stop ()

在早期的 Java 版本中,Thread类提供了suspend()、resume()和stop()方法来控制线程的执行,但这些方法现在已经被标记为过期,不再推荐使用,主要原因如下:

  • suspend()方法:该方法用于暂停线程的执行,但它存在严重的问题。当一个线程调用suspend()方法后,线程会暂停执行,但它并不会释放已经持有的锁资源。这可能导致其他线程在等待获取这些锁时被阻塞,从而引发死锁问题。例如:
public class SuspendDeadlock {private static final Object lock = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (lock) {System.out.println("线程1获得锁");Thread.currentThread().suspend(); // 暂停当前线程System.out.println("线程1恢复执行");}});Thread thread2 = new Thread(() -> {synchronized (lock) {System.out.println("线程2获得锁");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}thread1.resume(); // 尝试恢复线程1System.out.println("已发送恢复信号");}});thread1.start();thread2.start();}
}

在上述代码中,线程 1 获取锁后调用suspend()方法暂停自己,线程 2 随后尝试获取锁并恢复线程 1,但由于线程 1 持有锁,线程 2 无法获取锁,从而导致死锁。

  • resume()方法:resume()方法用于恢复被suspend()暂停的线程。它必须与suspend()方法成对使用,但由于suspend()方法存在死锁风险,resume()方法也因此受到牵连,不再推荐使用。
  • stop()方法:stop()方法用于立即终止线程的执行。它会使线程立即停止正在执行的任务,包括catch或finally语句中的代码,并抛出ThreadDeath异常。这种强制终止线程的方式非常危险,因为它不会保证线程的资源能够正常释放,可能导致数据不一致、文件未关闭、数据库连接未释放等问题。例如,在一个对文件进行读写操作的线程中,如果使用stop()方法终止线程,可能会导致文件数据损坏或丢失。

安全地终止线程

为了安全地终止线程,通常不建议使用上述过期的方法,而是采用更温和、安全的方式,例如使用标志位来控制线程的终止。这种方式的核心思想是在线程内部定义一个标志变量,通过修改这个标志变量的值来通知线程何时停止执行。

  1. 使用自定义标志位:在run()方法中,通过一个while循环和一个标志位来控制线程的执行。当标志位被设置为true时,while循环结束,线程正常退出。例如:
public class SafeThreadTermination {private static class MyTask implements Runnable {private volatile boolean stopRequested = false;@Overridepublic void run() {while (!stopRequested) {// 执行线程任务System.out.println("Thread is running");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Thread terminated");}public void requestStop() {stopRequested = true;}}public static void main(String[] args) {MyTask task = new MyTask();Thread workerThread = new Thread(task);workerThread.start();try {// 主线程等待3秒Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}// 请求停止工作线程task.requestStop();}
}

在上述代码中,MyTask类实现了Runnable接口,在run()方法中通过while (!stopFlag)循环来判断是否继续执行任务。stop()方法用于将stopFlag设置为true,从而终止线程。

2. 使用interrupt()方法:interrupt()方法用于中断线程。它并不会立即终止线程,而是设置线程的中断标志。当线程处于阻塞状态(如调用sleep()、wait()等方法)时,调用interrupt()方法会使线程抛出InterruptedException异常,从而有机会在捕获异常后进行相应的处理,如终止线程。例如:

public class InterruptThread {public static void main(String[] args) {// 创建并启动新线程Thread thread = new Thread(() -> {// 检查线程中断状态while (!Thread.currentThread().isInterrupted()) {try {System.out.println("线程正在执行任务");Thread.sleep(1000); // 暂停1秒} catch (InterruptedException e) {// 恢复中断状态并退出循环Thread.currentThread().interrupt();System.out.println("接收到中断信号,准备终止线程");break;}}});thread.start();// 主线程休眠3秒后中断工作线程try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}thread.interrupt();}
}

在这个例子中,线程在while循环中不断检查自身的中断标志。当调用interrupt()方法后,线程的中断标志被设置,由于线程处于睡眠状态,会抛出InterruptedException异常。在捕获异常后,重新设置中断标志(这是一种常见的处理方式,以确保上层代码能够正确处理中断),并跳出循环,从而安全地终止线程。

>>>线程间通信

volatile 和 synchronized 关键字

在 Java 并发编程中,volatile和synchronized关键字是保证线程安全和线程间通信的重要手段,它们有着不同的作用和特性。

  • volatile关键字:主要有两个作用,一是保证可见性,二是禁止指令重排。当一个变量被volatile修饰后,任何线程对它的修改都会立即被其他线程看到,这是因为volatile修饰的变量在写操作时,会将修改后的值立即刷新到主内存,读操作时会直接从主内存读取,而不是从线程的工作内存中读取旧值。例如:
public class VolatileExample {// 使用volatile修饰确保多线程环境下的可见性private volatile int value;// 设置值的方法public void setValue(int value) {this.value = value;}// 获取值的方法public int getValue() {return value;}
}

在上述代码中,value变量被volatile修饰,当一个线程调用setValue方法修改value的值时,其他线程调用getValue方法能立即获取到最新的值。同时,volatile还能禁止指令重排,确保程序按照代码编写的顺序执行,避免在多线程环境下由于指令重排导致的并发问题。比如在双重检查锁定(DCL)实现单例模式时,如果不使用volatile修饰单例对象,可能会因为指令重排导致其他线程获取到未初始化的单例对象。

  • synchronized关键字:主要用于实现同步互斥,保证同一时刻只有一个线程能够进入被synchronized修饰的代码块或方法,从而避免多线程对共享资源的竞争访问。它可以修饰方法或代码块。当修饰方法时,整个方法都是同步的;当修饰代码块时,只有代码块中的内容是同步的。例如:
public class SynchronizedExample {private int count;// 线程安全的计数器递增方法public synchronized void increment() {count++;}// 获取当前计数值public int getCount() {return count;}
}

在这个例子中,increment方法被synchronized修饰,当一个线程调用increment方法时,其他线程必须等待该线程执行完increment方法,释放锁后才能进入,从而保证了count变量的操作是线程安全的。

volatile和synchronized的主要区别在于:volatile主要用于保证变量的可见性和禁止指令重排,它不会对代码块或方法进行加锁,不能保证原子性操作;而synchronized通过加锁机制实现同步互斥,既能保证可见性,也能保证原子性,但会带来一定的性能开销,因为加锁和解锁操作涉及到线程状态的切换和资源的竞争。

等待 / 通知机制

在 Java 中,线程间的等待 / 通知机制是通过Object类的wait()、notify()和notifyAll()方法来实现的,这三个方法用于协调多个线程对共享资源的访问,实现线程间的通信和协作。

  • wait()方法:当一个线程调用对象的wait()方法后,该线程会释放对象的锁,并进入等待状态,直到被其他线程调用notify()或notifyAll()方法唤醒,或者等待时间超时(如果使用带超时参数的wait(long timeout)方法)。wait()方法必须在synchronized块中调用,否则会抛出IllegalMonitorStateException异常。例如:
public class WaitNotifyExample {private static final Object lock = new Object();public static void main(String[] args) {Thread waitingThread = new Thread(() -> {synchronized (lock) {try {System.out.println("等待线程开始执行");lock.wait();System.out.println("等待线程被唤醒");} catch (InterruptedException e) {e.printStackTrace();}}});waitingThread.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}Thread notifyingThread = new Thread(() -> {synchronized (lock) {System.out.println("通知线程准备唤醒");lock.notify();}});notifyingThread.start();}
}

在上述代码中,线程 1 获取锁后调用lock.wait()方法,进入等待状态并释放锁。线程 2 在 1 秒后获取锁,调用lock.notify()方法唤醒线程 1,线程 1 被唤醒后重新获取锁并继续执行。

  • notify()方法:随机唤醒一个在该对象上等待的线程。当调用notify()方法时,会从等待该对象锁的线程中选择一个唤醒,被唤醒的线程会进入可运行状态,等待获取对象的锁,一旦获取到锁,就会继续执行wait()方法之后的代码。
  • notifyAll()方法:唤醒所有在该对象上等待的线程。与notify()方法不同,notifyAll()会将所有等待该对象锁的线程都唤醒,这些线程都会进入可运行状态,竞争对象的锁,最终只有一个线程能获取到锁并继续执行,其他线程则继续等待。

需要注意的是,wait()、notify()和notifyAll()方法都依赖于对象的监视器(锁),只有获取了对象的锁才能调用这些方法。并且,在使用等待 / 通知机制时,通常需要结合条件判断来确保线程在合适的时机等待和唤醒,避免不必要的等待和竞争。例如在生产者 - 消费者模型中,生产者线程在缓冲区满时调用wait()方法等待,消费者线程从缓冲区取走数据后调用notify()或notifyAll()方法唤醒生产者线程。

等待 / 通知的经典范式

等待 / 通知的经典范式是一种在多线程编程中常用的代码结构,用于实现线程间的协作和同步,它遵循一定的模式和规范,能够有效地避免死锁和竞态条件等问题。下面是一个典型的等待 / 通知经典范式的代码示例:

public class WaitNotifyPattern {private static final Object lock = new Object();private static boolean condition = false;public static void main(String[] args) {Thread waiterThread = new Thread(() -> {synchronized (lock) {while (!condition) {try {System.out.println("等待线程进入等待状态");lock.wait();System.out.println("等待线程收到通知");} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("等待线程执行后续操作");}});Thread notifierThread = new Thread(() -> {synchronized (lock) {condition = true;System.out.println("通知线程更新状态并发送通知");lock.notify();}});waiterThread.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}notifierThread.start();}
}

在这个范式中,包含以下几个关键步骤:

  1. 条件判断:在wait()方法前,使用while循环对条件进行判断。这是因为wait()方法可能会被虚假唤醒(在没有其他线程调用notify()或notifyAll()方法的情况下被唤醒),通过while循环可以确保只有在条件真正满足时才继续执行后续代码,避免错误的执行。
  1. 等待:调用wait()方法使线程进入等待状态,并释放对象的锁。这样其他线程就有机会获取锁并修改条件。
  1. 通知:当条件满足时,另一个线程获取锁,修改条件后调用notify()或notifyAll()方法通知等待的线程。被通知的线程会从wait()方法处返回,重新获取锁后继续执行。

这种范式确保了线程间的协作和同步,使得在多线程环境下,线程能够按照预期的顺序和条件进行执行,有效地解决了线程间通信和资源竞争的问题 。

输入 / 输出流

在 Java 中,线程间可以通过输入输出流进行通信,这种方式常用于网络编程和文件处理等场景。输入输出流提供了一种在不同线程之间传递数据的机制,通过将数据写入输出流,另一个线程可以从对应的输入流中读取数据,从而实现线程间的数据交换和通信。

以 Socket 通信为例,客户端和服务器端的线程可以通过 Socket 的输入输出流进行数据传输。服务器端创建一个 ServerSocket 来监听指定端口,当有客户端连接时,服务器端会创建一个新的 Socket 与客户端进行通信,并为这个 Socket 分配输入输出流。客户端通过 Socket 连接到服务器端后,也能获取到对应的输入输出流。例如:

  • 服务器端代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;public class Server {public static void main(String[] args) {try (ServerSocket serverSocket = new ServerSocket(8888)) {System.out.println("服务器启动成功,正在等待客户端连接...");Socket clientSocket = serverSocket.accept();System.out.println("客户端连接成功");BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);new Thread(() -> {try {String inputLine;while ((inputLine = in.readLine()) != null) {System.out.println("收到客户端消息:" + inputLine);out.println("服务器响应:" + inputLine);}} catch (IOException e) {e.printStackTrace();}}).start();} catch (IOException e) {e.printStackTrace();}}
}
  • 客户端代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;public class Client {public static void main(String[] args) {try (Socket socket = new Socket("localhost", 8888)) {// 初始化输入输出流BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter out = new PrintWriter(socket.getOutputStream(), true);BufferedReader consoleIn = new BufferedReader(new InputStreamReader(System.in));// 创建线程处理服务器消息new Thread(() -> {try {String serverResponse;while ((serverResponse = in.readLine()) != null) {System.out.println("服务器响应: " + serverResponse);}} catch (IOException e) {e.printStackTrace();}}).start();// 处理用户输入String userInput;while ((userInput = consoleIn.readLine()) != null) {out.println(userInput);}} catch (IOException e) {e.printStackTrace();}}
}

在上述代码中,服务器端和客户端分别创建了输入输出流来进行数据的读写。服务器端的线程负责读取客户端发送的消息并回复,客户端的线程负责读取服务器端的回复并显示。通过这种方式,服务器端和客户端的线程实现了基于输入输出流的通信。

thread.join () 的使用

在 Java 多线程编程中,thread.join()方法是一个非常有用的方法,它的作用是让当前线程等待调用join()方法的线程执行完毕后再继续执行。例如,在一个主线程中启动了多个子线程,有时需要确保这些子线程都执行完成后,主线程再继续执行后续的操作,这时就可以使用join()方法。

join()方法的工作原理是:当一个线程 A 调用另一个线程 B 的join()方法时,线程 A 会进入等待状态,直到线程 B 执行完毕或者等待超时(如果使用带超时参数的join(long millis)方法)。在等待过程中,线程 A 会释放 CPU 资源,不会占用 CPU 时间片,直到线程 B 执行结束或者超时时间到达,线程 A 才会重新进入可运行状态,继续执行后续的代码。例如:

public class JoinExample {public static void main(String[] args) {// 创建并启动线程1Thread thread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("线程1运行中:" + i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}});// 创建并启动线程2Thread thread2 = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("线程2运行中:" + i);try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}}});thread1.start();thread2.start();// 等待所有线程执行完成try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主线程继续执行:所有子线程已完成");}
}

在上述代码中,主线程启动了thread1和thread2两个子线程,然后调用thread1.join()和thread2.join()方法,主线程会等待thread1和thread2执行完毕后才会继续执行最后一行输出语句。通过join()方法,实现了主线程与子线程之间的协作,确保了程序按照预期的顺序执行 。

ThreadLocal 的使用

ThreadLocal是 Java 中一个用于线程本地存储的类,它为每个线程提供了独立的变量副本,使得每个线程都可以独立地访问和修改自己的变量副本,而不会影响其他线程的变量副本。这在多线程编程中非常有用,可以有效地避免线程安全问题,特别是在一些需要在多个方法之间传递线程特定数据的场景。

ThreadLocal的工作原理是:每个线程都有一个ThreadLocalMap对象,当线程通过ThreadLocal对象的get()方法获取变量时,实际上是从该线程的ThreadLocalMap中获取对应的值;当通过set()方法设置变量时,也是将值存储到该线程的ThreadLocalMap中。这样,每个线程都拥有自己独立的变量副本,互不干扰。例如,在数据库连接管理中,使用ThreadLocal可以为每个线程创建独立的数据库连接,避免多线程竞争同一个数据库连接带来的问题。示例代码如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;public class DatabaseConnectionUtil {private static final String URL = "jdbc:mysql://localhost:3306/mydb";private static final String USER = "root";private static final String PASSWORD = "password";private static final ThreadLocal<Connection> threadLocalConnection = ThreadLocal.withInitial(() -> {try {return DriverManager.getConnection(URL, USER, PASSWORD);} catch (SQLException e) {throw new RuntimeException("Failed to create database connection", e);}});public static Connection getConnection() {return threadLocalConnection.get();}public static void closeConnection() {Connection connection = threadLocalConnection.get();if (connection != null) {try {connection.close();} catch (SQLException e) {System.err.println("Error closing database connection: " + e.getMessage());} finally {threadLocalConnection.remove();}}}
}

在上述代码中,threadLocalConnection是一个ThreadLocal对象,它为每个线程提供独立的数据库连接。getConnection()方法用于获取当前线程的数据库连接,closeConnection()方法用于关闭当前线程的数据库连接,并移除ThreadLocal中的连接对象,避免内存泄漏。通过使用ThreadLocal,每个线程都可以独立地管理自己的数据库连接,提高了程序的线程安全性和性能 。

>>>博主总结

以上就是 Java 并发编程基础的核心内容。从线程的基础概念到启动终止,再到线程间通信与应用实例,每个知识点都环环相扣。掌握这些内容,能帮助我们编写出高效、稳定的多线程程序,更能让我们在面对复杂业务场景时,灵活运用并发技术提升程序性能。并发编程的世界充满挑战,但也乐趣无穷,希望大家在实践中不断探索,攻克难题,成为 Java 并发编程的高手!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/82069.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

el-input宽度自适应方法总结

使用 style 或 class 直接设置宽度 可以通过内联样式或 CSS 类来直接设置 el-input 的宽度为 100%&#xff0c;使其自适应父容器的宽度 <template><div style"width: 100%;"><el-input style"width: 100%;" v-model"input">…

解决 Supabase “permission denied for table XXX“ 错误

解决 Supabase “permission denied for table” 错误 问题描述 在使用 Supabase 开发应用时&#xff0c;你可能会遇到以下错误&#xff1a; [Nest] ERROR [ExceptionsHandler] Object(4) {code: 42501,details: null,hint: null,message: permission denied for table user…

java每日精进 5.20【MyBatis 联表分页查询】

1. MyBatis XML 实现分页查询 1.1 实现方式 MyBatis XML 是一种传统的 MyBatis 使用方式&#xff0c;通过在 XML 文件中编写 SQL 语句&#xff0c;并结合 Mapper 接口和 Service 层实现分页查询。分页需要手动编写两条 SQL 语句&#xff1a;一条查询分页数据列表&#xff0c;…

linux taskset 查询或设置进程绑定CPU

1、安装 taskset larkubuntu&#xff1a;~$ sudo apt-get install util-linux larkubuntu&#xff1a;~$ taskset --help 用法&#xff1a; taskset [选项] [mask | cpu-list] [pid|cmd [args...]] 显示或更改进程的 CPU 关联性。 选项&#xff1a; -a&#xff0c; --all-tasks…

Python应用字符串格式化初解

大家好!在 Python 编程中&#xff0c;字符串格式化是一项基础且实用的技能。它能让你更灵活地拼接字符串与变量&#xff0c;使输出信息更符合需求。本文将为和我一样的初学者详细介绍 Python 字符串格式化的常用方法。 定义: 字符串格式化就是将变量或数据插入到字符串中的特定…

EasyRTC嵌入式音视频通信SDK一对一音视频通信,打造远程办公/医疗/教育等场景解决方案

一、方案概述​ 数字技术发展促使在线教育、远程医疗等行业对一对一实时音视频通信需求激增。传统方式存在低延迟、高画质及多场景适配不足等问题&#xff0c;而EasyRTC凭借音视频处理、高效信令交互与智能网络适配技术&#xff0c;打造稳定低延迟通信&#xff0c;满足基础通信…

SEO长尾词优化精准布局

内容概要 长尾关键词作为SEO策略的核心要素&#xff0c;其价值在于精准捕捉细分需求与低竞争流量入口。相较于短尾词的高泛化性&#xff0c;长尾词通过语义扩展与场景化组合&#xff0c;能够更高效地匹配用户搜索意图&#xff0c;降低优化成本的同时提升转化潜力。本文将从词库…

【MySQL】第7节|Mysql锁机制与优化实践以及MVCC底层原理剖析

锁等待分析 我们通过检查InnoDB_row_lock相关的状态变量来分析系统上的行锁的争夺情况 示例场景 假设有两个用户同时操作账户表 accounts&#xff08;主键为 id&#xff09;&#xff1a; 1. 用户A&#xff1a;执行转账&#xff0c;锁定账户 id1 并等待3秒&#xff1a; BEG…

基于规则引擎与机器学习的智能Web应用防火墙设计与实现

基于规则引擎与机器学习的智能Web应用防火墙设计与实现 引言&#xff1a;智能防御的必然选择 在2023年OWASP最新报告中&#xff0c;传统Web应用防火墙&#xff08;WAF&#xff09;对新型API攻击的漏报率高达67%&#xff0c;而误报导致的正常业务拦截损失每年超过2.3亿美元。面…

GIM发布新版本了 (附rust CLI制作brew bottle流程)

GIM 发布新版本了&#xff01;现在1.3.0版本可用了 可以通过brew upgrade git-intelligence-message升级。 初次安装需要先执行 brew tap davelet/gim GIM 是一个根据git仓库内文件变更自动生成git提交消息的命令行工具&#xff0c;参考前文《GIM: 根据代码变更自动生成git提交…

PyQt5高效布局指南:QTabWidget与QStackedWidget实战解析

&#x1f50d; 问题背景 当界面控件过多时&#xff0c;直接平铺会导致窗口拥挤、用户体验下降。PyQt5提供了两种高效容器控件&#xff1a; QTabWidget&#xff1a;选项卡式布局&#xff0c;支持直接切换不同功能模块QStackedWidget&#xff1a;堆栈式布局&#xff0c;需配合导…

《2.2.1顺序表的定义|精讲篇》

上一节学习了线性表的逻辑结构&#xff0c;线性表需要实现哪些基本运算/操作&#xff1f;在本节中&#xff0c;我们将学习顺序表的定义、顺序表的特性&#xff0c;以及如何用代码来实现顺序表。下个小节我们会介绍基于顺序存储&#xff08;这种存储结构&#xff09;如何用代码具…

【 大模型技术驱动智能网联汽车革命:关键技术解析与未来趋势】

大模型技术驱动智能网联汽车革命&#xff1a;关键技术解析与未来趋势 关键词总结&#xff1a; 大模型技术&#xff1a;LLM、VLM、MLLM、Transformer架构核心场景&#xff1a;智能驾驶、智能座舱、智能网联关键技术&#xff1a;端到端系统、BEVOCC网络、多模态融合、强化学习挑…

Rocketmq broker 是主从架构还是集群架构,可以故障自动转移吗

RocketMQ Broker的架构与故障转移机制 RocketMQ的Broker架构同时采用了主从架构和集群架构&#xff0c;并且支持故障自动转移。下面详细说明&#xff1a; 一、架构类型 1. 集群架构 RocketMQ天然支持分布式集群部署 一个RocketMQ集群包含多个Broker组(每组有主从) 不同Bro…

从零开始建立个人品牌并验证定位变现性的方法论——基于开源AI大模型、AI智能名片与S2B2C商城生态的实证研究

摘要&#xff1a;本文提出一种融合开源AI大模型、AI智能名片与S2B2C商城小程序源码的"最小测试闭环"方法论&#xff0c;通过技术赋能实现个人品牌定位的精准验证与变现路径优化。以某美妆领域自由职业者为例&#xff0c;其通过开源AI大模型完成能力图谱构建与资源匹配…

SQL进阶之旅 Day 2:高效的表设计与规范:从基础到实战

【SQL进阶之旅 Day 2】高效的表设计与规范&#xff1a;从基础到实战 开篇 在数据库开发中&#xff0c;一个良好的表设计不仅能够提高查询效率&#xff0c;还能避免冗余数据和一致性问题。本文作为"SQL进阶之旅"系列的第2天&#xff0c;将重点介绍高效的表设计与规范…

Java—— IO流的应用

带权重的点名系统 案例要求 文件中有学生的信息&#xff0c;每个学生的信息独占一行。包括学生的姓名&#xff0c;性别&#xff0c;权重 要求每次被抽中的学生&#xff0c;再次被抽中的概率在原先的基础上降低一半。 本题的核心就是带权重的随机 分析 权重&#xff0c;权重和…

Docker中部署Alertmanager

在 Docker 中部署 Alertmanager&#xff08;通常与 Prometheus 告警系统配合使用&#xff09;的步骤如下&#xff1a; 一、拉取镜像prom/alertmanager docker pull prom/alertmanager二、 创建 Alertmanager 配置文件 首先准备Alertmanager的配置文件 alertmanager.yml(如存…

【大模型面试每日一题】Day 27:自注意力机制中Q/K/V矩阵的作用与缩放因子原理

【大模型面试每日一题】Day 27&#xff1a;自注意力机制中Q/K/V矩阵的作用与缩放因子原理 &#x1f4cc; 题目重现 &#x1f31f;&#x1f31f; 面试官&#xff1a;请解释Transformer自注意力机制中Query、Key、Value矩阵的核心作用&#xff0c;并分析为何在计算注意力分数时…

AI+能碳管理系统:全生命周期碳管理

在"双碳"目标的时代背景下&#xff0c;AI赋能的能碳管理系统正在重新定义企业碳管理的边界与深度。这套系统犹如一位不知疲倦的碳管家&#xff0c;从原材料采购到产品报废&#xff0c;在每一个价值环节编织起精密的碳管理网络&#xff0c;实现从微观设备到宏观战略的…