从 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。

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

java 复制代码
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关键字创建一个线程对象后,线程就处于新建状态。此时线程还没有开始运行,仅仅是一个对象实例,系统没有为其分配运行所需的资源。例如:

java 复制代码
Thread thread = new Thread();

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

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

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

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

java 复制代码
Thread thread = new Thread(() -> {
    // 线程任务逻辑
});

thread.start();

while (thread.isAlive()) {
    // 等待线程执行完成
}

System.out.println(thread.getState()); // 输出结果:TERMINATED
 

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

java 复制代码
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 执行时间,直到被唤醒。例如:

java 复制代码
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)等方法时,线程会进入有时限的等待状态。例如:

java 复制代码
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异常。例如:

java 复制代码
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类的方法,无需额外的对象来代理线程执行。例如:
java 复制代码
public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程任务逻辑
        System.out.println("Thread running by extending Thread class");
    }
}
 

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

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

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

java 复制代码
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 虚拟机,该线程已经准备好,可以被调度执行了。它会触发一系列底层操作,包括创建操作系统级别的线程、分配资源等。例如:

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()方法后,线程会暂停执行,但它并不会释放已经持有的锁资源。这可能导致其他线程在等待获取这些锁时被阻塞,从而引发死锁问题。例如:
java 复制代码
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(); // 尝试恢复线程1
                System.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循环结束,线程正常退出。例如:
java 复制代码
public class SafeThreadTermination {

    private static class MyTask implements Runnable {
        
        private volatile boolean stopRequested = false;

        @Override
        public 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,从而终止线程。

  1. 使用 interrupt() 方法:interrupt()方法用于中断线程。它并不会立即终止线程,而是设置线程的中断标志。当线程处于阻塞状态(如调用sleep()、wait()等方法)时,调用interrupt()方法会使线程抛出InterruptedException异常,从而有机会在捕获异常后进行相应的处理,如终止线程。例如:
java 复制代码
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修饰的变量在写操作时,会将修改后的值立即刷新到主内存,读操作时会直接从主内存读取,而不是从线程的工作内存中读取旧值。例如:
java 复制代码
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修饰的代码块或方法,从而避免多线程对共享资源的竞争访问。它可以修饰方法或代码块。当修饰方法时,整个方法都是同步的;当修饰代码块时,只有代码块中的内容是同步的。例如:
java 复制代码
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异常。例如:
java 复制代码
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()方法唤醒生产者线程。

等待 / 通知的经典范式

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

java 复制代码
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 连接到服务器端后,也能获取到对应的输入输出流。例如:

  • 服务器端代码
java 复制代码
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();
        }
    }
}
 
  • 客户端代码
java 复制代码
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 才会重新进入可运行状态,继续执行后续的代码。例如:

java 复制代码
public class JoinExample {
    public static void main(String[] args) {
        // 创建并启动线程1
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("线程1运行中:" + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 创建并启动线程2
        Thread 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可以为每个线程创建独立的数据库连接,避免多线程竞争同一个数据库连接带来的问题。示例代码如下:

java 复制代码
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 并发编程的高手!

相关推荐
Thanks_ks12 天前
Java 并发编程挑战:从原理到实战的深度剖析与解决方案
java 并发编程·线程安全优化·死锁与竞态条件·线程池调优·并发容器实战·无锁编程技术·jvm 并发监控
熬夜学编程的小王13 天前
【Linux篇】高并发编程终极指南:线程池优化、单例模式陷阱与死锁避坑实战
linux·单例模式·线程池·线程安全
风清扬201720 天前
面试现场“震”情百态:HashMap扩容记
线程池·线程安全·arraylist·扩容机制·redis集群·标签: hashmap·concurrenthashmap
在努力的韩小豪2 个月前
SpringMVC和SpringBoot是否线程安全?
spring boot·后端·springmvc·线程安全·bean的作用域
ling__wx2 个月前
List、Set 和 Map 的区别及常见实现类、线程安全集合(总结图表)
java·list·set·map·集合·线程安全
郑州吴彦祖7723 个月前
探索Java多线程的核心概念与实践技巧,带你从入门到精通!
java·多线程·线程安全
简 洁 冬冬3 个月前
集合类不安全问题
线程安全
自信不孤单4 个月前
Linux线程安全
linux·多线程·条件变量·线程安全·同步··互斥
无问8177 个月前
Javaee:线程安全问题和synchronized关键字
java·线程安全