作者:
逍遥Sean
简介:一个主修Java的Web网站\游戏服务器后端开发者
主页:https://blog.csdn.net/Ureliable
觉得博主文章不错的话,可以三连支持一下~ 如有需要我的支持,请私信或评论留言!
前言
多线程编程非常复杂,本文章涵盖了应用程序员可能需要的所有工具,至少能够对线程、同步、异步等并发编程知识有一个清晰的认识。话不多说,开卷吧
线程是计算机程序的一部分,是一个进程内的独立执行路径
。在多线程编程中,一个进程可以包含多个线程
,它们可以同时并行执行不同的任务。每个线程拥有自己的栈和程序计数器
,在程序执行期间,线程可以被创建、启动、暂停、恢复、停止和销毁
。线程可以共享进程的内存空间,因此可以更高效地处理并发任务和数据共享。线程通常被用于实现服务器、数据库、游戏等应用程序,以支持多个用户或客户端同时访问和处理数据。
继承Thread类
,重写run方法,调用start方法启动线程实现Runnable接口
,实现run方法。实现Callable接口
。构造一个FutureTask任务,传入当前实现类。构造一个Thread类,传入FutureTask,调用start方法启动线程,可以使用FutureTask的get方法获得执行结果线程运行的本质就是Thread中的实现的Runnable的run方法被执行,如果没重写Thread中的run方法,就会执行构造线城时传入的Runnable接口的run方法
中断线程可以通过调用Thread类的interrupt()
方法来实现。该方法会将中断标志位设置为true。
但这并不意味着线程立即停止或中断。需要在线程的run()方法中对中断标志位进行检查,并在合适的时机退出线程。
以下是一个示例代码:
class MyThread extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// 线程要执行的任务
// 如果检测到中断标志位为true,则退出循环
}
}
}
// 中断线程
MyThread myThread = new MyThread();
myThread.start();
myThread.interrupt();
线程状态通常有以下几种:
新建状态(New)
:当我们创建一个新线程对象时,线程就处于新建状态,此时线程还没有被启动执行。
就绪状态(Runnable)
:当线程被创建后并调用了 start() 方法,线程就进入了就绪状态,此时线程已经准备好执行,只等待 CPU 调度执行。
运行状态(Running)
:当线程获得了 CPU 调度后,线程就进入了运行状态,此时线程正在执行任务。
阻塞状态(Blocking)
:当线程被某些操作阻塞时,进入了阻塞状态,如等待 I/O 操作完成,等待锁释放等。
等待状态(Waiting)
:当线程执行 wait()、join()、sleep() 等方法而进入等待状态时,线程就处于等待状态。
终止状态(Terminated)
:当线程执行完任务后或者发生了异常而导致线程终止时,线程就处于终止状态。
以下是一个简单的 Java 代码,展示了线程状态的转换过程:
public class ThreadStatusDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("Thread state: " + thread.getState()); // 新建状态
thread.start();
System.out.println("Thread state: " + thread.getState()); // 就绪状态
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread state: " + thread.getState()); // 运行状态
try {
thread.join(); // 等待子线程执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread state: " + thread.getState()); // 终止状态
}
}
守护线程(Daemon Thread)在Java多线程中是一种特殊的线程,它的作用是为其他线程提供服务。当所有非守护线程结束时,JVM会自动结束守护线程,因此,守护线程一般不会执行完毕。
守护线程的创建和普通线程一样,只需要将线程对象的setDaemon()
方法设置为true即可。守护线程的使用场景很多,例如:后台数据收集和处理、JVM垃圾回收机制、一些CPU执行周期很长的操作等。
需要注意的是,因为守护线程随时可能被停止,所以它不能访问任何可能已关闭的资源,否则会引发异常,比如文件、数据库连接等。
竞态条件(Race Condition)是指两个或多个并发进程在对共享资源进行读写时,由于执行顺序不确定而造成的程序结果出现异常的现象。通俗地讲,就是多个进程竞争同一个资源,由于执行顺序不确定,导致程序结果可能会出现错误。例如,两个进程同时对同一个全局变量进行写操作,由于执行顺序不确定,可能会导致变量值不稳定,出现结果错误的情况。在并发编程中,竞态条件是一种常见的错误,需要采取合适的措施来避免或解决。
为解决竞态条件,可以采取以下一些方案:
互斥锁:通过互斥锁来保证同一时间只有一个线程对共享资源进行访问,其他线程需要等待。这种方式可以保证数据的安全性,但会降低程序的并发性能。
信号量:类似于互斥锁,但是信号量可以控制多个线程同时访问共享资源的数量。通过在共享资源前设置信号量,来限制访问数量,从而避免竞态条件的发生。
读写锁:如果共享资源通常是读取操作,而写操作较少,可以采用读写锁。允许多个线程同时进行读操作,但写操作需要独占访问,并进行互斥控制。
原子操作:一些现代编程语言支持原子操作,保证对共享内存的操作是原子性的,不可分割的。这种方式可以避免互斥锁和信号量等同步机制的开销,但需要程序员保证原子操作的正确性。
用消息代替共享内存:尽量避免多个线程或进程共享内存进行通信,而是采用消息传递的方式。每个线程或进程处理自己的消息,避免了竞态条件的发生。
以上是一些常见的解决竞态条件的方案,不同的场景和需求下会有不同的选择。
锁对象是多线程编程中的概念,用于控制多个线程对共享资源的访问。当某个线程需要访问共享资源时,首先要尝试获取该资源对应的锁对象。如果锁对象已经被其他线程占用,则该线程就会被阻塞等待,直到对应的锁对象被释放。当一个线程成功获取锁对象后,其他线程就无法再对该资源进行访问,直到该线程释放锁对象。
在Java中,可以使用synchronized关键字来实现锁对象的功能,例如:
public class MyThread implements Runnable {
private Object lock = new Object();
public void run() {
synchronized(lock) {
// 访问共享资源的代码
}
}
}
在这个例子中,MyThread类持有一个锁对象lock,当线程需要访问共享资源时,使用synchronized关键字锁住这个锁对象。这样,当其他线程也需要访问共享资源时,会被阻塞等待,直到当前线程释放这个锁对象。
条件对象(Condition)是 Java 中的一种线程同步机制,它是一个类中的方法,用于在等待/通知模式中控制线程的执行。条件对象通常与锁对象(Lock)一起使用,用于控制多个线程的同步执行。
它通常包含一个布尔条件和相应的等待和通知方法。当一个线程需要等待某个条件满足时,它会在条件对象上等待,释放锁并进入等待状态。当另一个线程改变了条件并执行通知方法时,等待的线程会被唤醒并重新获取锁,然后重新检查条件是否已经满足。如果条件还未满足,它将再次进入等待状态。
条件对象通常用于线程间的协作,以确保一个线程只有在满足特定条件时才会执行。例如,一个生产者线程只有在队列为空时才会插入一个新元素,而一个消费者线程只有在队列非空时才会取出一个元素。在这种情况下,生产者和消费者线程可以共享同一个条件对象,并在满足特定条件时进行等待和通知。
下面是一个使用条件对象的示例代码:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
public static void main(String[] args) throws InterruptedException {
Queue<Integer> queue = new LinkedList<>();
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
lock.lock();
try {
queue.add(i);
condition.signal();
} finally {
lock.unlock();
}
}
});
Thread consumer = new Thread(() -> {
while (true) {
lock.lock();
try {
if (queue.isEmpty()) {
condition.await();
}
System.out.println("Consumed: " + queue.remove());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
producer.start();
consumer.start();
producer.join();
consumer.interrupt();
}
}
在以上示例代码中,我们使用 java.util.concurrent.locks.Condition
类来实现条件对象。我们创建了一个线程安全的队列 queue
,一个锁对象 lock
,和一个条件对象 condition
。我们将生产者线程添加元素到队列中,当元素添加到队列中后,我们通知消费者线程通过使用 condition.signal()
方法。消费者线程从队列中获取元素,如果队列为空,则通过 condition.await()
暂停线程。消费者线程在获取元素后打印消费的元素。
在这个示例中,我们使用条件对象实现了线程之间的同步。通过使用条件对象,生产者线程只有在向队列中添加元素后才通知消费者线程,而消费者线程只有在队列不为空时才进行消费操作。这种同步机制可以确保消费者线程只在有内容可以消费时才进行操作,避免了空操作的浪费。
synchronized
是Java中的关键字,用于实现线程的同步。当多个线程访问一个共享资源时,为了保证数据的一致性和正确性,需要对共享资源进行同步。使用synchronized
关键字可以确保在同一时刻只有一个线程可以访问共享资源,其他线程需要等待当前线程释放锁之后才能访问。这样可以避免多个线程同时访问同一资源导致数据出错的情况。
下面是一个Java代码示例,演示了如何使用synchronized
关键字来同步方法:
public class BankAccount {
private int balance;
public BankAccount(int balance) {
this.balance = balance;
}
// 同步存款方法
public synchronized void deposit(int amount) {
balance += amount;
}
// 同步取款方法
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
public int getBalance() {
return balance;
}
}
在上面的示例中,deposit
和withdraw
方法都被用synchronized
关键字修饰,这意味着它们在任何时候只能被一个线程访问。这可以确保对balance
这个数据成员的访问是线程安全的,因为在每个时刻只有一个线程可以执行修改操作。
请注意,同步方法只作用于对象级别的锁,也就是说,在一个实例上,只有一个线程可以执行同步方法。如果有多个实例,每个实例之间的锁是独立的,也就是说在不同的实例上可以并发调用同一个同步方法。
Java同步块是一段代码,在这里声明的对象锁用于同步访问共享数据。同步块包含两部分,一部分声明共享对象锁,另一部分是需要同步的代码段。
使用Java同步块可以防止多个线程同时访问共享数据,从而避免因并发访问而导致的数据不一致或程序崩溃的问题。Java同步块可以在多线程环境下保证数据的安全性。
Java同步块的语法如下:
synchronized (obj) {
//需要同步的代码块
}
obj是需要同步的对象,同步块中的代码只有在获得obj对象锁时才能执行,其他线程必须等待释放锁后才能执行同步块中的代码。
在Java中,volatile关键字用于修饰变量,以确保多线程并发访问该变量时的可见性和有序性。
具体来说,当一个变量被声明为volatile时,每次访问这个变量时,都会从主内存中重新读取该变量的值,并且每次修改变量的值后,都会立即将新值写入主内存中。这样可以保证多线程在访问该变量时始终读取到最新的值,避免了线程之间的数据不一致问题。
以下是一个使用volatile关键字的简单示例:
public class VolatileExample implements Runnable {
private volatile boolean isRunning = true;
public void run() {
while (isRunning) {
// do something
}
}
public void stop() {
isRunning = false;
}
}
上述示例中,我们定义了一个名为isRunning的volatile变量,该变量用于控制一个线程的运行状态。在run方法中,我们不断地循环执行某个操作,直到isRunning变量被设置为false。在stop方法中,我们将isRunning变量设置为false,从而停止线程的运行。
需要注意的是,虽然使用了volatile关键字,但是在多线程环境下仍然存在一些问题。例如,在上述示例中,线程执行的循环操作并没有对isRunning变量进行同步,因此在某些情况下可能会出现无法停止线程的情况。因此,在使用volatile关键字时,仍然需要遵循多线程编程的规范和最佳实践。
在Java中,final关键字用于表示某个变量或方法不可更改或重写,可以被看作是一种约束。
以下是一些使用final关键字的示例代码:
final int num = 10;
num = 20; // 这里会编译错误,因为num已经被声明为final,不可更改
public class Test {
public final void print() {
System.out.println("Hello World!");
}
}
class Child extends Test {
public void print() { // 这里会编译错误,因为print方法已经被声明为final,不可重写
System.out.println("Hello Java!");
}
}
public final class Test {
// ...
}
class Child extends Test { // 这里会编译错误,因为Test类已经被声明为final,不可继承
// ...
}
之前已经了解到, 除非使用锁或 volatile 修饰符,否则无法从多个线程安全地读取一
个域。
还有一种情况可以安全地访问一个共享域, 即这个域声明为 final 时。考虑以下声明:
final Map<String, Double〉accounts = new HashKap<>0;
其他线程会在构造函数完成构造之后才看到这个 accounts 变量。
如果不使用 final,就不能保证其他线程看到的是 accounts 更新后的值,它们可能都只是
看到 null , 而不是新构造的 HashMap。
当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然
需要进行同步。
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为
volatile。
java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令(而不是使用
锁) 来保证其他操作的原子性。 例如, Atomiclnteger 类提供了方法 incrementAndGet 和
decrementAndGet, 它们分别以原子方式将一个整数自增或自减。例如,可以安全地生成一个数值序列,如下所示:
public static AtomicLong nextNumber = new AtomicLong();
// In some thread...
long id = nextNumber.increinentAndGet();
incrementAndGet 方法以原子方式将 AtomicLong 自增, 并返回自增后的值。也就是说,
获得值、 增 1 并设置然后生成新值的操作不会中断。可以保证即使是多个线程并发地访问同一个实例,也会计算并返回正确的值。
有很多方法可以以原子方式设置和增减值, 不过, 如果希望完成更复杂的更新,就必须
使用 compareAndSet 方法。例如, 假设希望跟踪不同线程观察的最大值。下面的代码是不可行的:
public static AtonicLong largest = new AtomicLongO;
// In some thread...
largest.set(Math ,max (largest,get(), observed)); // Error race condition!
这个更新不是原子的。实际上,应当在一个循环中计算新值和使用 compareAndSet:
do {
oldValue = largest.get();
newValue = Math ,max (oldValue , observed);
} while (llargest.compareAndSet(oldValue, newValue));
如果另一个线程也在更新 largest,就可能阻止这个线程更新。这样一来,compareAndSet
会返回 false, 而不会设置新值。在这种情况下,循环会更次尝试,读取更新后的值,并尝试
修改。最终, 它会成功地用新值替换原来的值。这听上去有些麻烦, 不过 compareAndSet 方法会映射到一个处理器操作, 比使用锁速度更快。
在 Java SE 8 中,不再需要编写这样的循环样板代码。实际上,可以提供一个 lambda 表
达式更新变量,它会为你完成更新。对于这个例子,我们可以调用:
largest. updateAndGet(x -> Math.max(x, observed)) ;
或
largest.accumulateAndCet(observed, Math::max);
accumulateAndGet 方法利用一个二元操作符来合并原子值和所提供的参数。
还有 getAndUpdate 和 getAndAccumulate 方法可以返回原值
死锁是指在并发编程中,两个或多个进程(或线程)互相等待对方释放自己所需要的资源,而导致的一种无法继续执行的状态。
下面是一个简单的死锁案例:
假设有两个进程A和B,同时需要访问两个共享资源r1和r2,它们的执行流程如下:
进程A:申请r1资源 -> 等待r2资源 -> 等待r1资源
进程B:申请r2资源 -> 等待r1资源 -> 等待r2资源
因此,进程A等待进程B释放r2资源,而进程B等待进程A释放r1资源,二者互相等待,导致死锁。
为了避免死锁的发生,常见的方法包括:
死锁是并发编程中常见的问题,需要开发人员根据具体应用场景进行合理的设计和优化,以避免死锁的发生。
Java中,线程局部变量(Thread Local Variables)是指每个线程独立维护一份私有变量副本,访问这份副本的操作都是在当前线程中进行的。可以通过ThreadLocal类来实现线程局部变量。
在使用ThreadLocal时,需要先创建一个ThreadLocal对象,然后通过调用其get()和set()等方法来访问和操作线程局部变量。例如:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Hello World"); // 设置当前线程的局部变量值为"Hello World"
String value = threadLocal.get(); // 获取当前线程的局部变量值
需要注意的是,每个线程中的ThreadLocal对象都是独立的,互不影响。因此,不同线程中的同名ThreadLocal对象所代表的局部变量是互相独立的。另外,ThreadLocal中存储的局部变量是一种线程局部的全局变量,即该变量对于任何线程来说都是可见的,但同一个值在不同线程中的取值是不同的。
使用ThreadLocal可以避免线程安全问题,从而提高程序的效率和安全性。
废弃stop和suspend主要是因为它们的使用容易导致死锁和意外的行为
。
当一个线程调用stop方法时,它会强制终止目标线程。这可能会在目标线程的某个关键点上停止线程,导致资源无法正确释放,数据结构无法正确清理,以及其他线程无法继续执行需要的操作。相似地,当线程调用suspend方法时,它会暂停目标线程的执行,可能会导致其他线程无法执行所需的操作,从而导致死锁和其他问题。
因此,Java建议使用更安全和可控的方式来管理线程状态
,例如使用wait和notify
方法等待和通知线程。
线程安全的集合是指多线程环境下可以安全地使用的集合类,其实现了多线程访问时的同步和互斥保护,确保线程安全性。
Java中常用的线程安全的集合包括:
ConcurrentHashMap
:线程安全的哈希表,支持高并发的读写操作。CopyOnWriteArrayList
:线程安全的动态数组,支持高并发的读操作,写操作需要复制一份新数组。ConcurrentLinkedQueue
:线程安全的队列,支持高并发的入队和出队操作。ConcurrentSkipListSet
:线程安全的有序集合,支持高并发的添加、删除和查询操作。BlockingQueue
:阻塞队列,支持阻塞式的入队和出队操作,常用实现类有LinkedBlockingQueue和ArrayBlockingQueue等。使用线程安全的集合可以提高程序的并发性能,对于高并发场景下的数据处理和共享资源管理非常有用
在Java中,Callable和Future是两个非常重要的接口,用于实现多线程编程。Callable接口表示一个可以返回结果的任务,它类似于Runnable接口,但是可以返回一个结果并且可以抛出异常。Future接口表示一个异步计算的结果,可以使用它来检查计算是否完成、等待计算完成并且获取计算结果。
Callable接口定义了以下方法:
public interface Callable<V> {
V call() throws Exception;
}
其中,call()
方法表示任务执行的代码块,并且返回一个泛型值V。当任务执行完成时,它可以返回一个结果或者抛出一个异常。
Future接口定义了以下方法:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
其中,cancel()
方法用于取消任务的执行,isCancelled()
方法用于判断任务是否被取消,isDone()
方法用于判断任务是否已经执行完成。get()
方法用于获取任务执行的结果,如果任务还没有执行完成,则会阻塞当前线程直到任务完成。get(long timeout, TimeUnit unit)
方法也是用于获取任务执行的结果,但是它可以设置等待的超时时间,如果超过了指定的时间还没有获取到结果,则会抛出TimeoutException异常。
在使用Callable和Future时,通常的流程是首先创建一个Callable任务实例,然后使用ExecutorService提交给线程池执行,接着使用Future获取任务执行的结果。例如:
Callable<Integer> task = () -> {
// 执行任务的代码块
return 123;
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
Integer result = future.get();
Java执行器Executor是Java中用于管理线程池的框架。它提供了一系列的API,可以用于创建和管理线程池,控制任务数量,监控线程池状态等。使用Executor框架可以极大的简化多线程编程的工作量。
Executor框架提供了以下几个核心接口:
Executor
:最基本的线程池接口,只能执行Runnable任务,不能返回值。ExecutorService
:扩展了Executor接口,提供更多的方法,如submit()方法,可以提交Callable任务,可以返回值。ScheduledExecutorService
:扩展了ExecutorService,提供了定时执行任务的能力,如schedule()、scheduleAtFixedRate()等方法。Executor框架还提供了一些实现类,如ThreadPoolExecutor、ScheduledThreadPoolExecutor等,使用这些实现类可以非常方便地创建和管理线程池。
使用Executor框架可以充分利用多核CPU的优势,提高程序的并发性能。同时也可以避免手动创建线程带来的线程安全问题。
关于创建线程池的具体用法,有一篇专门的文章介绍:
Java使用Executors和ThreadPoolExecutor创建线程池
在Java中,常用的线程同步器类型包括:
synchronized关键字
:Java中最基本的同步机制,可以用来保护临界资源,使得同一时间只有一个线程可以访问该资源。
ReentrantLock
:Java中提供的可重入锁,与synchronized关键字类似,也可以用来保护临界资源。
Semaphore
:Java中提供的信号量,可以控制同一时间内并发访问某个资源的线程数。
CountDownLatch
:Java中提供的倒计时门栓,可以让等待某个事件发生的线程通过等待门栓来阻塞自己。
CyclicBarrier
:Java中提供的循环栅栏,可以让一组线程在到达一个同步点时阻塞,并且只有当所有线程都到达该同步点时才能继续执行。
Phaser
:Java中提供的相位器,可以让一组线程根据不同的阶段进行同步。
Exchanger
:Java中提供的交换器,可以让两个线程之间交换数据。
Swing是Java图形用户界面(GUI)库,用于创建桌面应用程序。它是基于Java AWT(Abstract Window Toolkit)开发的,但Swing提供了更多的组件、更好的外观和更好的性能。Swing应用程序可以跨平台运行在Windows、Linux和Mac OS等平台上。
Java线程是Java虚拟机(JVM)中可执行的单元。线程允许Java应用程序以并发的方式执行多个任务。Swing应用程序需要使用Java线程来管理并发任务,以防止我们的应用程序在执行长时间任务时冻结。
当我们开发Swing程序时,我们将创建一个称为事件调度线程(EDT)的线程。该线程负责处理Swing事件并管理Swing组件的用户界面更新。因此,任何对Swing组件的更新都必须在EDT上完成,否则会引发线程安全问题。我们可以使用SwingUtilities类的invokeLater()或invokeAndWait()方法来将代码提交到EDT中执行。
另外,我们可以使用SwingWorker
类来执行长时间任务,并在后台线程中执行该任务。SwingWorker类提供了诸如进度通知、取消和完成通知等功能,以方便我们了解任务的状态。同时,SwingWorker类还提供了SwingWorker#publish()和SwingWorker#process()方法,用于在EDT上实现中间结果的显示。
Java阻塞队列是一种线程安全的队列类型,在多线程环境中被广泛使用。它提供了在队列为空或已满时自动等待或阻塞的机制,从而使得多线程之间的数据交换更加方便和可靠。
常见的Java阻塞队列有以下几种:
- ArrayBlockingQueue:基于数组实现的有界阻塞队列。
- LinkedBlockingQueue:基于链表实现的有界阻塞队列。
- SynchronousQueue:没有容量的阻塞队列,每个插入操作都要等待一个对应的删除操作。
- PriorityBlockingQueue:基于堆实现的有界阻塞队列,可以按照元素的优先级进行排序。
在多线程环境中,阻塞队列通常用于协调不同线程之间的任务分发和执行
。例如,一个生产者线程可以将任务放入阻塞队列中,多个消费者线程可以从中取出任务并执行。当队列为空时,消费者线程会被自动阻塞等待生产者线程放入新任务;当队列已满时,生产者线程会被自动阻塞等待消费者线程取出任务。
使用阻塞队列可以避免手动编写线程同步代码,简化多线程编程。但同时也需要注意阻塞队列的容量和性能,不能使用过大的队列或者过多的线程,否则会导致系统资源的浪费和性能下降。