FunTester Java 并发编程基础(上)

FunTester · 2024年02月18日 · 2093 次阅读

介绍

Java 是一种功能强大、用途广泛的编程语言。Java 并发是指多个线程同时执行程序,共享资源和数据。通过 synchronized 关键字、Lock 接口等实现线程同步,避免竞态条件和数据不一致问题。并发编程提高系统性能和资源利用率,然而并发编程带来了同步、线程安全等挑战,以及避免死锁和竞争条件等常见陷阱。

基本概念

首先,为了理解和使用 Java 中的并发编程打下基础。并发编程对于利用现代多核处理器的强大功能以及创建能够同时并行执行任务的响应灵敏且高效的应用程序至关重要。

概念 解释
Thread 线程代表 Java 程序内的独立执行路径。线程允许并发和并行执行代码。Java 通过 Thread 类支持多线程。
Runnable 该 Runnable 接口用于定义可由线程执行的代码。它提供了一种封装线程应执行的任务或作业的方法。
Synchronization 块和方法等同步机制 synchronized 用于控制对代码关键部分的访问,防止多个线程同时访问它们。
Locks and Mutexes 锁(例如,ReentrantLock)是用于管理对共享资源的访问的显式机制,允许线程获取和释放锁以进行受控访问。
Race Conditions 当两个或多个线程同时访问共享数据时,就会出现竞争条件,最终结果取决于执行顺序,从而导致不可预测的行为。正确的同步可以防止竞争情况。
Data Race 数据竞争是一种特定类型的竞争条件,其中两个或多个线程同时访问共享数据,并且至少其中一个线程修改数据。数据竞争可能会导致未定义的行为,应该避免。
Deadlocks 当两个或多个线程被阻塞,等待永远不会被释放的资源时,就会发生死锁。识别和避免死锁对于并发编程至关重要。
Atomic Operations 原子操作是线程安全的操作,可以在不受其他线程干扰的情况下执行。Java 提供了原子类,例如 AtomicInteger 和 AtomicReference。
Thread Local Storage 线程本地存储允许每个线程拥有自己的变量副本,该副本与其他线程隔离。它对于存储特定于线程的数据很有用。
Volatile 该 volatile 关键字确保对变量的更改对所有线程都可见。它用于多个线程无需同步访问的变量。
Java Memory Model (JMM) JMM 定义了线程如何与内存交互的规则和保证,确保一个线程所做的更改对其他线程的可见性。

Thread 和 Runnable

Thread类是创建和管理线程的基础类。它允许使用者在应用程序中定义和运行并发任务或进程。线程代表独立的执行逻辑,可以同时执行任务,从而可以在程序中实现并行性。

/**  
 * 测试线程  
 */  
class TestThread extends Thread {  

    /**  
    * 重写run方法  
    */  
    @Override  
    void run() {  
        for (int i = 1; i < 3; i++) {  
            println("线程: ${Thread.currentThread().getName()} 计数: $i")//输出线程名和计数  
        }  
    }  

    static void main(String[] args) {  
        //创建两个线程  
        TestThread thread1 = new TestThread()  
        TestThread thread2 = new TestThread()  
        //启动线程  
        thread1.start()  
        thread2.start()  
        println "主线程结束"  
    }  
}

控制台输出:

主线程结束
线程: Thread-1 计数: 1
线程: Thread-0 计数: 1
线程: Thread-1 计数: 2
线程: Thread-0 计数: 2

Runnable 接口是一个函数式接口,表示可以由线程并发执行的任务或代码段。它提供了一种定义线程应运行的代码的方法,而无需显式扩展该类 Thread。实现 Runnable 接口可以更好地分离关注点并提高代码的可重用性。

/**  
 * 测试runnable接口类  
 */  
class TestRunnable implements Runnable {  

    /**  
    * 重写run方法  
    */  
    @Override  
    void run() {  
        for (int i = 1; i < 3; i++) {  
            println("线程: ${Thread.currentThread().getName()} 计数: $i")//输出线程名和计数  
        }  
    }  

    static void main(String[] args) {  
        //创建两个接口实例  
        TestRunnable runnable1 = new TestRunnable()  
        TestRunnable runnable2 = new TestRunnable()  
        //创建两个线程  
        Thread thread1 = new Thread(runnable1)  
        Thread thread2 = new Thread(runnable2)  
        //启动线程  
        thread1.start()  
        thread2.start()  
        println "主线程结束"  
    }  
}

控制台输出:

主线程结束
线程: Thread-1 计数: 1
线程: Thread-0 计数: 1
线程: Thread-0 计数: 2
线程: Thread-1 计数: 2

Thread 状态表示线程在其生命周期中可能处于的不同阶段或条件:

线程状态 描述
NEW 线程处于NEW已创建但尚未开始执行时的状态。它尚未具备运行资格,尚未获取任何系统资源。
RUNNABLE 线程处于RUNNABLE可以运行的状态,Java 虚拟机(JVM)已经为它的执行分配了资源。但是,它当前可能尚未执行。
BLOCKED 线程处于BLOCKED等待获取监视器锁以进入同步块或方法的状态。它被另一个持有锁的线程阻塞。
WAITING 线程处于WAITING等待满足特定条件才能继续执行的状态。它可能会无限期地等待,直到收到另一个线程的通知。
TIMED_WAITING 与状态类似WAITING,状态中的线程TIMED_WAITING正在等待特定条件。但它有一个超时时间,RUNNABLE超时后会自动过渡到。
TERMINATED 线程处于TERMINATED已完成执行或已显式终止时的状态。一旦终止,线程就无法重新启动或再次运行。

线程生命周期相关方法:

方法 描述
start() 通过调用线程的方法来启动线程的执行run()。当start()调用时,线程从NEW状态转换到RUNNABLE状态,并开始并发执行。这是启动新线程的主要方法。
wait() 用于使线程自愿放弃其持有的监视器锁。应该从同步块或方法中调用它。线程进入该WAITING状态并释放锁,直到收到另一个线程的通知。
notify()/notifyAll() wait()用于唤醒在同一对象上使用该方法正在等待的一个/所有线程。它允许一个/所有等待线程转换回状态RUNNABLE,让它们有机会继续。
join() 允许一个线程等待另一线程完成。当一个线程调用join()另一个线程时,它将阻塞,直到目标线程完成执行。
yield() 向 JVM 表明当前线程愿意让出当前的 CPU 时间以允许其他线程运行。这是一个提示,实际行为取决于 JVM 的实现。
sleep() 将当前线程的执行暂停指定的时间(以毫秒为单位)。它允许您在程序中引入延迟,通常用于计时目的。
interrupt() 通过设置线程的中断状态来中断线程的执行。它可用于请求线程正常终止或以自定义方式处理中断。如果线程正在等待、睡眠或阻塞,InterruptedException则会抛出异常。如果您在中断线程级别捕获异常,请通过调用手动设置其中断状态Thread.currentThread().interrupt()并抛出异常,以便在更高级别进行处理。

同步关键字

synchronized关键字用于创建同步代码块,确保Thread一次只有一个可以执行它们。它提供了一种控制对程序关键部分的访问的方法,防止多个线程同时访问它们。

要进入同步块,必须获取对象监视器上的锁。对象的监视器是一种同步机制,提供 Object 实例的锁定功能。执行此操作后,块中包含的所有代码都可以进行独占和原子操作。退出 synchronized 块后,锁将返回到对象的监视器以供其他线程获取。如果无法立即获取锁,则执行 Thread 会等待,直到获取到这个锁。

/**  
 * 测试线程,用于测试synchronize关键字  
 */  
class TestThread extends Thread {  

    static int count = 0//计数器,用于统计循环次数,这里是共享资源  

    static Object lock = new Object()//锁对象,用于同步代码块  

    /**  
     * 重写run方法  
     */  
    @Override  
    void run() {  
        for (int i = 0; i < 10; i++) {  
            synchronized (lock) { // 同步代码块  
                count++//计数器加1  
            }  
        }  
    }  

    static void main(String[] args) {  
        TestThread thread1 = new TestThread()//创建线程实例  
        TestThread thread2 = new TestThread()//创建线程实例  
        thread1.start()//启动线程  
        thread2.start()//启动线程  
        thread1.join()//等待线程1结束  
        thread2.join()//等待线程2结束  
        println "count = $count"//输出count的值  
        println "主线程结束"  

    }  
}

控制台输出:

count = 20
主线程结束

synchronized关键字也可以在方法级别上指定。对于非静态方法,锁是从该方法所属的对象实例的监视器中获取的;对于静态方法,则是从该方法所属类的 Class 对象的监视器中获取的。

/**  
 * 计数器加1  
 * @return  
 */  
synchronized def increase() {  
    count++//计数器加1  
}  

/**  
 * 计数器减1  
 * @return  
 */  
static synchronized decrease() {  
    count--//计数器减1  
}

锁是可重入的,因此如果线程已经持有锁,它可以再次成功获取锁。

/**  
 * 这个方法演示synchronize重入, 一个线程可以多次进入同一个对象的synchronized方法  
 * @return  
 */  
synchronized def step() {  
    step1()  
    step2()  
}  

synchronized def step1() {  

}  

synchronized def step2() {  

}

wait/notify/notifyAll

wait()使用, notify(),方法同步访问功能/资源的最常见模式notifyAll()是条件循环。让我们看一个示例,演示如何使用wait()notify()协调两个线程来打印备用数字:

public class WaitNotifyExample {
    private static final Object lock = new Object();
    private static boolean isOddTurn = true;

    public static void main(String[] args) {
        Thread oddThread = new Thread(() -> {
            for (int i = 1; i <= 10; i += 2) {
                synchronized (lock) {
                    while (!isOddTurn) {
                        try {
                            lock.wait(); // Wait until it's the odd thread's turn
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    System.out.println("Odd: " + i);
                    isOddTurn = false; // Satisfy the waiting condition
                    lock.notify(); // Notify the even thread
                }
            }
        });

        Thread evenThread = new Thread(() -> {
            for (int i = 2; i <= 10; i += 2) {
                synchronized (lock) {
                    while (isOddTurn) {
                        try {
                            lock.wait(); // Wait until it's the even thread's turn
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                    System.out.println("Even: " + i);
                    isOddTurn = true; // Satisfy the waiting condition
                    lock.notify(); // Notify the odd thread
                }
            }
        });

        oddThread.start();
        evenThread.start();
    }
}

控制台输出:

Odd: 1
Even: 2
Odd: 3
Even: 4
Odd: 5
Even: 6
Odd: 7
Even: 8
Odd: 9
Even: 10

注意事项:

  • 为了在一个对象上使用wait()notify()notifyAll(),需要首先获取该对象的锁——我们的两个线程在该lock对象上同步以获取其锁。
  • 始终在检查正在等待的条件的循环内等待。如果另一个线程在等待开始之前满足条件,这可以解决计时问题,并且还可以保护您的代码免受虚假唤醒 - 我们的两个线程都在由标志控制的循环内等待isOddTurn
  • notify()在调用/之前始终确保满足等待条件notifyAll()。如果不这样做将导致通知,但没有线程能够逃脱其等待循环——我们的两个线程都满足isOddTurn另一个线程继续的标志。

volatile

当一个变量被声明为 volatile 时,它保证对该变量的任何读写操作都直接在主内存上执行,确保原子更新和对所有线程的变化可见性。换句话说,JMM 对于事件 “写入 volatile 变量” 和任何后续 “读取 volatile 变量” 都应用了 “happens-before” 关系。因此,变量的任何后续读取都将看到最近写入的值。

ThreadLocal 类

ThreadLocal是一个提供线程局部变量的类。线程局部变量是每个线程唯一的变量,这意味着访问变量的每个线程ThreadLocal都会获得该变量自己的独立副本。当使用者有需要为每个线程隔离和单独维护的数据时,这非常有用,而且还可以减少对共享资源的争用,这通常会导致性能瓶颈。它通常用于存储用户会话、数据库连接和线程特定状态等值,而无需在方法之间显式传递它们。

ThreadLocal以下是如何使用存储和检索线程特定数据的简单示例:

public class ThreadLocalExample {
/**  
 * 创建一个ThreadLocal对象,使用withInitial方法设置初始值  
 */  
static ThreadLocal<Long> threadLocal = ThreadLocal.withInitial(() -> System.currentTimeMillis())  

static void main(String[] args) {  
    for (i in 0..<3) {//创建3个线程  
        new Thread({//每个线程都会打印出threadLocal的值  
            println threadLocal.get()  
        }).start()  
        sleep(1000)//休眠1秒  
    }  
}
}

控制台输出:

1708155410499
1708155411511
1708155412516

不可变对象

不可变对象是指其状态在创建后无法修改的对象。一旦不可变对象被初始化,其内部状态在其整个生命周期中保持不变。此属性使不可变对象本质上是线程安全的,因为它们不能被多个线程同时修改,从而消除了同步的需要。

创建不可变对象涉及几个关键步骤:

  1. 创建类final:防止继承并确保该类不能被子类化。
  2. 将所有字段声明为final:将所有实例变量标记为,以final确保它们仅初始化一次,通常在构造函数中初始化。
  3. 无 setter 方法:不提供允许修改对象状态的 setter 方法。
  4. 安全发布this构建过程中引用不会逃逸。
  5. 没有可变对象:如果类包含对可变对象(可以更改其状态的对象)的引用,请确保这些引用不公开或允许外部修改。
  6. 将所有字段设为私有:通过将字段设为私有来封装字段以限制直接访问。
  7. 在修改状态的方法中返回一个新对象:不修改现有对象,而是创建一个具有所需更改的新对象并返回它。

/**  
 * 不可变对象  
 */  
final class ImmutablePerson {  

    final String name  

    final int age  

    final List<ImmutablePerson> family  

    ImmutablePerson(String name, int age, List<ImmutablePerson> family) {  
        this.name = name  
        this.age = age  
        List<ImmutablePerson> copy = new ArrayList<>(family)// 构造函数中的参数family是一个可变的集合,所以需要进行防御性复制  
        this.family = Collections.unmodifiableList(copy)// 通过unmodifiableList方法,返回一个不可变的集合  
//        在构造方法中,“this”不会传递到任何地方  
    }  

    String getName() {  
        name  
    }  

    int getAge() {  
        age  
    }  

    // 没有set方法,所以对象的状态不会改变  

    // 通过返回一个新的对象来实现修改  
    ImmutablePerson withAge(int newAge) {  
        return new ImmutablePerson(this.name, newAge)  
    }  

    // 为简单起见,没有 toString、hashCode 和 equals 方法  
}
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册