线程编程: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()方法执行完毕后终止。线程创建步骤如下:-
定义一个继承自
Thread类的NewClass,并为新线程定义run()方法。 -
实例化
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[完全同步对象]
掌握这些知识对于编写高效、安全的多线程程序至关重要。在实际应用中,需要根据具体的需求和场景选择合适的编程方式和同步机制,并注意性能优化和错误处理,以确保程序的稳定性和可靠性。
超级会员免费看
31

被折叠的 条评论
为什么被折叠?



