34、线程编程:Pthreads 与 Java 线程详解

线程编程:Pthreads 与 Java 线程详解

在多线程编程领域,Pthreads 和 Java 线程是两种常用的技术。下面将详细介绍它们的相关知识。

1. Pthreads 编程

在 Pthreads 编程中, pthread_setspecific() 函数用于将新数据与一个键关联。若某个线程之前已将数据与该键关联,那么之前的数据将被覆盖并丢失。

除了线程特定数据,自 C99 标准起提供了线程局部存储(TLS)机制。该机制允许使用 __thread 存储类关键字来声明变量,这样每个线程都会拥有该变量的一个独立实例。当线程终止时,该实例会被删除。 __thread 存储类关键字可应用于全局变量和静态变量,但不能用于块作用域的自动变量或非静态变量。

2. Java 线程编程
2.1 Java 线程的生成

每个正在执行的 Java 程序至少包含一个执行线程,即主线程。主线程负责执行作为启动参数传递给 Java 虚拟机(JVM)的类的 main() 方法。

主线程或之前已启动的其他用户线程可以显式创建更多用户线程。Java 的标准包 java.lang 中的预定义类 Thread 支持线程的创建,该类用于表示线程,并提供了创建和管理线程的方法。

java.lang 中的 Runnable 接口用于表示线程执行的程序代码,代码由 run() 方法提供,并由一个单独的线程异步执行。创建线程有两种方式:继承 Thread 类或使用 Runnable 接口。

  • 继承 Thread
    • 定义一个新类 NewClass ,继承自预定义的 Thread 类,并定义一个 run() 方法,该方法包含新线程要执行的语句。 NewClass 中定义的 run() 方法会覆盖 Thread 类中预定义的 run() 方法。
    • Thread 类还包含一个 start() 方法,用于创建一个执行给定 run() 方法的新线程。新创建的线程与生成它的线程异步执行, start() 方法执行并创建新线程后,控制权会立即返回给生成线程,通常在新线程终止之前,生成线程会继续执行,即生成线程和新线程并发执行。
    • 新线程在 run() 方法执行完毕后终止。线程创建步骤如下:
      1. 定义一个继承自 Thread 类的 NewClass ,并为新线程定义 run() 方法。
      2. 实例化 NewClass 的对象 nc ,并调用 nc.start() 激活线程。
graph LR
    A[定义 NewClass 继承 Thread 类] --> B[定义 run() 方法]
    B --> C[实例化 NewClass 对象 nc]
    C --> D[调用 nc.start()]
  • 使用 Runnable 接口
    • Runnable 接口定义了一个抽象的 run() 方法:
public interface Runnable {
    public abstract void run();
}
- 预定义的 `Thread` 类实现了 `Runnable` 接口,因此继承自 `Thread` 类的每个类也实现了 `Runnable` 接口。新定义的 `NewClass` 可以直接实现 `Runnable` 接口。
- 使用 `Runnable` 接口创建线程的步骤如下:
    1. 定义一个实现 `Runnable` 接口的 `NewClass`,并定义 `run()` 方法,包含新线程要执行的代码。
    2. 使用构造函数 `Thread (Runnable target)` 实例化一个 `Thread` 对象,并将 `NewClass` 的对象作为参数传递给 `Thread` 构造函数。
    3. 激活 `Thread` 对象的 `start()` 方法。
graph LR
    A[定义 NewClass 实现 Runnable 接口] --> B[定义 run() 方法]
    B --> C[实例化 NewClass 对象]
    C --> D[实例化 Thread 对象并传入 NewClass 对象]
    D --> E[调用 Thread 对象的 start()]
2.2 Thread 类的其他方法
  • join() 方法 :Java 线程可以通过调用 t.join() 等待另一个 Java 线程 t 终止。该方法有三种变体:

    • void join() :调用线程会被阻塞,直到目标线程终止。
    • void join (long timeout) :调用线程会被阻塞,直到目标线程终止或给定的时间间隔 timeout (以毫秒为单位)过去。
    • void join (long timeout, int nanos) :行为与 void join (long timeout) 类似,但可以使用额外的纳秒参数更精确地指定时间间隔。
    • 如果目标线程尚未启动,调用线程不会被阻塞。
  • isAlive() 方法 Thread 类的 isAlive() 方法用于获取线程的执行状态。如果目标线程已启动但尚未终止,该方法返回 true ;否则返回 false join() isAlive() 方法对调用线程没有影响。

  • 线程命名方法 :可以使用 void setName (String name) String getName () 方法为特定线程分配和获取名称,也可以在创建线程时使用构造函数 Thread (String name) 分配名称。

  • 静态方法 Thread 类定义了一些静态方法,这些方法会影响调用线程或提供程序执行的相关信息:

    • static Thread currentThread() :返回调用线程的 Thread 对象引用,后续可用于调用 Thread 对象的非静态方法。
    • static void sleep (long milliseconds) :阻塞调用线程的执行,直到指定的时间间隔过去,之后线程再次准备好执行,并可被分配到执行核心或处理器。
    • static void yield() :指示 JVM 将处理器分配给具有相同优先级的另一个线程。如果存在这样的线程,JVM 的调度器可以让该线程执行。在没有时间片调度的 JVM 实现中,如果线程执行长时间运行且不阻塞的计算,使用 yield() 方法会很有用。
    • static int enumerate (Thread[] th_array) :生成程序中所有活动线程的列表,返回值指定收集在参数数组 th_array 中的 Thread 对象的数量。
    • static int activeCount() :返回程序中活动线程的数量,可在调用 enumerate() 方法之前确定参数数组的所需大小。

下面是一个并行矩阵乘法的 Java 示例:

// 示例代码,用于并行矩阵乘法
// 假设 ReadMatrix() 是静态方法,用于读取矩阵
class MatMult implements Runnable {
    private int[][] in1;
    private int[][] in2;
    private int[][] out;
    private int row;

    public MatMult(int[][] in1, int[][] in2, int[][] out, int row) {
        this.in1 = in1;
        this.in2 = in2;
        this.out = out;
        this.row = row;
    }

    @Override
    public void run() {
        for (int j = 0; j < in2[0].length; j++) {
            for (int k = 0; k < in1[0].length; k++) {
                out[row][j] += in1[row][k] * in2[k][j];
            }
        }
    }

    public static void main(String[] args) {
        int[][] in1 = ReadMatrix();
        int[][] in2 = ReadMatrix();
        int[][] out = new int[in1.length][in2[0].length];

        Thread[] threads = new Thread[in1.length];
        for (int i = 0; i < in1.length; i++) {
            threads[i] = new Thread(new MatMult(in1, in2, out, i));
            threads[i].start();
        }

        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
2.3 Java 线程的同步

Java 程序中的线程访问共享地址空间,为避免多个线程同时访问变量时出现竞态条件,需要应用适当的同步机制。Java 提供了同步块和同步方法来保证访问共享数据的线程之间的互斥。

  • 同步方法 :同步方法可以避免两个或多个线程同时执行该方法。例如,实现一个同步的计数器增量操作:
public class Counter {
    private int value = 0;
    public synchronized int incr() {
        value = value + 1;
        return value;
    }
}

Java 通过为每个 Java 对象分配一个隐式互斥变量来实现同步。由于每个类直接或间接继承自 Object 类,而 Object 类有一个隐式互斥变量,因此每个类的对象都隐式拥有自己的互斥变量。

当线程 t 激活对象 Ob 的同步方法时,会有以下效果:
- 开始执行同步方法时, t 会隐式尝试锁定 Ob 的互斥变量。如果互斥变量已被另一个线程 s 锁定,线程 t 会被阻塞。当锁定线程 s 释放互斥变量时,被阻塞的线程 t 会再次准备好执行。只有在成功锁定 Ob 的互斥变量后,才会执行被调用的同步方法。
- 当 t 离开被调用的同步方法时,会隐式释放 Ob 的互斥变量,以便其他线程可以锁定它。

  • 同步块 :除了同步方法,Java 还提供了同步块。同步块以 synchronized 关键字开头,并在括号中指定用于同步的任意对象。通常使用包含同步块的方法所属的对象进行同步。例如,实现计数器增量操作的同步块:
public int incr() {
    synchronized (this) {
        value = value + 1;
        return value;
    }
}
  • 完全同步对象 :Java 的同步机制可用于实现完全同步对象(也称为原子对象),这些对象可以被任意数量的线程访问,而无需额外的同步。为避免竞态条件,同步必须在对象对应类的方法中进行。该类必须具备以下属性:
    • 所有方法必须声明为同步方法。
    • 不允许有无需使用本地方法即可访问的公共入口。
    • 所有入口必须由类的构造函数一致初始化。
    • 在出现异常的情况下,对象也必须保持一致状态。

下面以 ExpandableArray 类为例,展示完全同步类的概念:

// 简化的 ExpandableArray 类示例
import java.lang.System;

class ExpandableArray {
    private Object[] data;
    private int size;

    public ExpandableArray() {
        data = new Object[10];
        size = 0;
    }

    public synchronized void add(Object obj) {
        if (size == data.length) {
            Object[] newData = new Object[data.length * 2];
            System.arraycopy(data, 0, newData, 0, data.length);
            data = newData;
        }
        data[size++] = obj;
    }
}
  • 死锁问题 :使用完全同步类可以避免竞态条件的发生,但当线程与不同对象同步时,可能会导致死锁。以 Account 类的 swapBalance() 方法为例,当两个线程 A B 同时执行 swapBalance() 方法时,可能会发生死锁:
    • 时间 T1 :线程 A 调用 a.swapBalance(b) 并锁定对象 a 的互斥变量。
    • 时间 T2 :线程 A 调用对象 a getBalance() 方法并执行该函数;同时,线程 B 调用 b.swapBalance(a) 并锁定对象 b 的互斥变量。
    • 时间 T3 :线程 A 调用 b.getBalance() 并被阻塞,因为对象 b 的互斥变量之前已被线程 B 锁定;线程 B 调用对象 b getBalance() 方法并执行该函数。
    • 时间 T4 :线程 B 调用 a.getBalance() 并被阻塞,因为对象 a 的互斥变量之前已被线程 A 锁定。
graph LR
    A[T1: A 调用 a.swapBalance(b) 锁定 a] --> B[T2: A 调用 a.getBalance()]
    B --> C[T2: B 调用 b.swapBalance(a) 锁定 b]
    C --> D[T3: A 调用 b.getBalance() 阻塞]
    D --> E[T3: B 调用 b.getBalance()]
    E --> F[T4: B 调用 a.getBalance() 阻塞]

为避免死锁,可以采用回退策略或让每个线程使用相同的锁定顺序。可以使用 Java 方法 System.identityHashCode() 获得对象的唯一排序,也可以使用任何其他唯一的对象排序。

在同步 Java 方法时,为使程序高效且安全,需要考虑以下几点:
- 同步操作开销较大,因此同步方法应仅用于可能被多个线程同时调用且可能操作公共对象数据的方法。如果应用程序确保某个方法在每个时间点都由单个线程执行,则可以避免同步以提高效率。
- 应将同步限制在关键区域,以减少锁定的时间间隔。对于较大的方法,可以考虑使用同步块而不是同步方法。
- 为避免不必要的序列化,不应使用同一对象的互斥变量来同步不同的、不连续的关键部分。
- 一些 Java 类(如 Hashtable Vector StringBuffer )内部已经进行了同步,这些类的对象无需额外的同步。
- 如果对象需要同步,应将对象数据放入私有或受保护的实例字段中,以防止外部的非同步访问。所有访问实例字段的对象方法都应声明为同步方法。
- 当不同线程以不同顺序访问多个对象时,可以通过让每个线程使用相同的锁定顺序来防止死锁。

2.4 可变锁粒度的同步

为说明 Java 同步机制的使用,考虑一个具有可变锁粒度的同步类 MyMutex 。该类允许通过显式获取和释放 MyMutex 类的对象来同步任意对象访问,实现了类似于 Pthreads 中互斥变量的锁定机制。

MyMutex 类使用一个实例字段 OwnerThread 来指示当前哪个线程获取了同步对象。

class MyMutex {
    private Thread OwnerThread;

    public void getMyMutex() {
        while (true) {
            if (tryGetMyMutex()) {
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized boolean tryGetMyMutex() {
        if (OwnerThread == null) {
            OwnerThread = Thread.currentThread();
            return true;
        }
        return false;
    }

    public synchronized void freeMyMutex() {
        if (OwnerThread == Thread.currentThread()) {
            OwnerThread = null;
        }
    }
}

如果 getMyMutex() 方法被声明为同步方法,当线程 T1 激活 getMyMutex() 方法时,会在进入方法之前锁定 MyMutex 类同步对象的隐式互斥变量。如果另一个线程 T2 持有同步对象的显式锁, T2 无法使用 freeMyMutex() 方法释放该锁,因为这需要锁定由 T1 持有的隐式互斥变量,从而导致死锁。可以在 getMyMutex() 方法中使用同步块来避免使用额外的 tryGetMyMutex() 方法。

MyMutex 类的对象可用于显式保护关键部分,例如用于保护计数器操作的 Counter 类:

class Counter {
    private int value = 0;
    private MyMutex mutex = new MyMutex();

    public void increment() {
        mutex.getMyMutex();
        try {
            value++;
        } finally {
            mutex.freeMyMutex();
        }
    }
}
2.5 静态方法的同步

基于隐式对象互斥变量实现的同步块和同步方法适用于所有相对于对象激活的方法。类的静态方法不是相对于对象激活的,因此没有隐式对象互斥变量。不过,静态方法也可以进行同步,后续可以进一步探讨其实现方式。

通过以上内容,我们详细了解了 Pthreads 编程和 Java 线程编程的相关知识,包括线程的生成、同步机制以及可能出现的问题和解决方法。掌握这些知识对于编写高效、安全的多线程程序至关重要。

线程编程:Pthreads 与 Java 线程详解

3. 静态方法同步的实现方式

虽然类的静态方法没有隐式对象互斥变量,但可以通过类级别的锁来实现同步。因为 Java 中每个类都有一个对应的 Class 对象,这个 Class 对象可以作为锁来保证静态方法的同步。

以下是一个静态方法同步的示例:

public class StaticSyncExample {
    private static int staticCounter = 0;

    public static synchronized void incrementStaticCounter() {
        staticCounter++;
    }

    public static int getStaticCounter() {
        return staticCounter;
    }
}

在上述代码中, incrementStaticCounter() 方法被声明为 synchronized 静态方法。当一个线程调用这个方法时,它会尝试获取 StaticSyncExample 类对应的 Class 对象的锁。如果锁已经被其他线程持有,该线程会被阻塞,直到锁被释放。

也可以使用同步块来实现静态方法的同步,示例如下:

public class StaticSyncExampleWithBlock {
    private static int staticCounter = 0;

    public static void incrementStaticCounter() {
        synchronized (StaticSyncExampleWithBlock.class) {
            staticCounter++;
        }
    }

    public static int getStaticCounter() {
        return staticCounter;
    }
}

在这个示例中,使用 synchronized (StaticSyncExampleWithBlock.class) 同步块来保证 staticCounter 的线程安全。

4. 线程编程的性能优化

在多线程编程中,性能优化是一个重要的方面。以下是一些常见的性能优化策略:

4.1 减少锁的持有时间

锁的持有时间越长,其他线程等待的时间就越长,从而降低了程序的并发性能。因此,应该尽量减少锁的持有时间。例如,将一些不需要同步的操作放在同步块之外:

public class Counter {
    private int value = 0;

    public int incrementAndGet() {
        int result;
        // 非同步操作
        // ...
        synchronized (this) {
            value++;
            result = value;
        }
        // 非同步操作
        // ...
        return result;
    }
}
4.2 减小锁的粒度

锁的粒度是指锁所保护的数据范围。如果锁的粒度太大,会导致多个线程同时竞争同一个锁,从而降低并发性能。可以将大的锁拆分成多个小的锁,以提高并发度。例如,在一个多线程的哈希表实现中,可以为每个桶分配一个独立的锁:

import java.util.LinkedList;

public class FineGrainedHashTable<K, V> {
    private static final int NUM_BUCKETS = 16;
    private final LinkedList<Entry<K, V>>[] buckets;
    private final Object[] locks;

    public FineGrainedHashTable() {
        buckets = new LinkedList[NUM_BUCKETS];
        locks = new Object[NUM_BUCKETS];
        for (int i = 0; i < NUM_BUCKETS; i++) {
            buckets[i] = new LinkedList<>();
            locks[i] = new Object();
        }
    }

    public void put(K key, V value) {
        int bucketIndex = key.hashCode() % NUM_BUCKETS;
        synchronized (locks[bucketIndex]) {
            LinkedList<Entry<K, V>> bucket = buckets[bucketIndex];
            for (Entry<K, V> entry : bucket) {
                if (entry.key.equals(key)) {
                    entry.value = value;
                    return;
                }
            }
            bucket.add(new Entry<>(key, value));
        }
    }

    public V get(K key) {
        int bucketIndex = key.hashCode() % NUM_BUCKETS;
        synchronized (locks[bucketIndex]) {
            LinkedList<Entry<K, V>> bucket = buckets[bucketIndex];
            for (Entry<K, V> entry : bucket) {
                if (entry.key.equals(key)) {
                    return entry.value;
                }
            }
            return null;
        }
    }

    private static class Entry<K, V> {
        K key;
        V value;

        Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
}
4.3 使用无锁数据结构

无锁数据结构可以避免锁带来的性能开销,提高并发性能。例如,Java 中的 AtomicInteger AtomicLong 等原子类就是无锁数据结构的典型代表。以下是一个使用 AtomicInteger 的示例:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        counter.incrementAndGet();
    }

    public int get() {
        return counter.get();
    }
}
5. 线程编程的错误处理

在多线程编程中,错误处理是一个容易被忽视但非常重要的方面。以下是一些常见的错误处理策略:

5.1 捕获线程中的异常

当线程中抛出未捕获的异常时,线程会终止,但这可能不会被主线程及时发现。可以通过为线程设置 UncaughtExceptionHandler 来捕获线程中的异常:

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            throw new RuntimeException("Exception in thread");
        });

        thread.setUncaughtExceptionHandler((t, e) -> {
            System.out.println("Caught exception in thread " + t.getName() + ": " + e.getMessage());
        });

        thread.start();
    }
}
5.2 避免死锁和饥饿

死锁和饥饿是多线程编程中常见的问题。可以通过以下方法来避免:
- 死锁 :使用一致的锁顺序,避免不同线程以不同顺序获取锁。例如,在前面的 Account 类的 swapBalance() 方法中,可以通过统一的锁顺序来避免死锁。
- 饥饿 :合理设置线程的优先级,避免某些线程长时间得不到执行。

6. 总结

本文详细介绍了 Pthreads 编程和 Java 线程编程的相关知识,涵盖了线程的生成、同步机制、静态方法同步、性能优化以及错误处理等方面。

编程类型 关键知识点
Pthreads 编程 pthread_setspecific() 函数、线程局部存储(TLS)机制
Java 线程编程 线程生成(继承 Thread 类、使用 Runnable 接口)、 Thread 类的方法、同步机制(同步方法、同步块、完全同步对象)、可变锁粒度同步、静态方法同步
graph LR
    A[线程编程] --> B[Pthreads 编程]
    A --> C[Java 线程编程]
    B --> B1[pthread_setspecific()]
    B --> B2[TLS 机制]
    C --> C1[线程生成]
    C --> C2[Thread 类方法]
    C --> C3[同步机制]
    C --> C4[可变锁粒度同步]
    C --> C5[静态方法同步]
    C1 --> C11[继承 Thread 类]
    C1 --> C12[使用 Runnable 接口]
    C3 --> C31[同步方法]
    C3 --> C32[同步块]
    C3 --> C33[完全同步对象]

掌握这些知识对于编写高效、安全的多线程程序至关重要。在实际应用中,需要根据具体的需求和场景选择合适的编程方式和同步机制,并注意性能优化和错误处理,以确保程序的稳定性和可靠性。

内容概要:本文围绕SecureCRT自动化脚本开发在毕业设计中的应用,系统介绍了如何利用SecureCRT的脚本功能(支持Python、VBScript等)提升计算机、网络工程等相关专业毕业设计的效率质量。文章从关键概念入手,阐明了SecureCRT脚本的核心对象(如crt、Screen、Session)及其在解决多设备调试、重复操作、跨场景验证等毕业设计常见痛点中的价值。通过三个典型应用场景——网络设备配置一致性验证、嵌入式系统稳定性测试、云平台CLI兼容性测试,展示了脚本的实际赋能效果,并以Python实现的交换机端口安全配置验证脚本为例,深入解析了会话管理、屏幕同步、输出解析、异常处理和结果导出等关键技术细节。最后展望了低代码化、AI辅助调试和云边协同等未来发展趋势。; 适合人群:计算机、网络工程、物联网、云计算等相关专业,具备一定编程基础(尤其是Python)的本科或研究生毕业生,以及需要进行设备自动化操作的科研人员; 使用场景及目标:①实现批量网络设备配置的自动验证报告生成;②长时间自动化采集嵌入式系统串口数据;③批量执行云平台CLI命令并分析兼容性差异;目标是提升毕业设计的操作效率、增强实验可复现性数据严谨性; 阅读建议:建议读者结合自身毕业设计课题,参考文中代码案例进行本地实践,重点关注异常处理机制正则表达式的适配,并注意敏感信息(如密码)的加密管理,同时可探索将脚本外部工具(如Excel、数据库)集成以增强结果分析能力。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值