Java并发编程实战读后感1,并发的来源。

133 阅读9分钟

1,并发的来源

并发的原因还是为了充分利用CPU多处理器的优势,提升程序的性能。

即便是单处理器,多线程依旧有优势,因为一个线程被阻塞后,另一个线程可以调度进来执行,而不让CPU忙等。

多线程的好处:

    1,充分利用CPU

    2,建模的简单性

    3,不必去模拟异步非阻塞IO

    4,响应灵敏的GUI

多线程的安全问题:

    1,没有充足的同步性的话,将会导致一些奇怪的结果,下面是由于竞争导致的错误

2024-11-30-00-19-37-image.png     2,解决,保证窜行性

2024-11-30-00-22-30-image.png     3,活跃性问题(死锁)

    4,性能问题

2024-11-30-00-25-27-image.png     5,框架无法保证线程安全

2024-11-30-00-29-08-image.png

第一章,线程的安全性

核心在于保护那些可变,共享的变量。

新概念,线程安全的类。

线程安全的核心就是正确性,近似等同于代码可信性。

2024-11-30-00-44-18-image.png 竞态条件

2024-11-30-10-02-05-image.png 例子:

2024-11-30-10-05-51-image.png 原子操作:

2024-11-30-10-07-26-image.png 复合操作(不安全的)

线程安全的类并不能完全保证线程安全,

例子:下面的例子由于我们期望修改状态是一个原子性的,但是它在代码中明显是分开来的,这就出现了问题。我们开发的时候往往会忘记,这里启示我们,对共享的状态(多个)的修改,必须是原子性的,if-else就应该是一个原子性的操作,在一个线程进入else存储缓存的时候,就不应该有另一个线程进入if判断了,不然可能会由于线程执行的不确定性而导致异常操作。

2024-11-30-10-15-39-image.png

2024-11-30-10-16-42-image.png

Java的内置锁:

    syncchronized,同步代码块可能带来效率问题。

重入:

2024-11-30-10-26-58-image.png 例子:父类与子类,这里有一个问题,就是为什么父类和子类的方法的锁是一样的?

难道,锁也能继承吗?

2024-11-30-10-29-08-image.png

2. 子类是否继承父类方法的锁?

Java 示例:

在 Java 中,父类方法的锁是绑定到方法所属的对象实例或类的。子类继承父类的方法时,也会继承该方法的同步行为。

父类示例:

java

复制代码

`class Parent {

public synchronized void method() {

System.out.println("Parent method");

}

}`

子类示例:

java

复制代码

`class Child extends Parent {

@Override public synchronized void method() {

System.out.println("Child method");

super.method(); // 仍然会受到锁的约束

}

}`

在这个例子中:

  • 父类的 method 是同步方法,绑定到当前对象的锁(this)。
  • 子类重写了 method,依然可以使用 synchronized 关键字,确保锁行为一致。

通过锁来保护状态:

    

2024-11-30-10-38-38-image.png

2024-11-30-10-41-36-image.png

2024-11-30-10-42-57-image.png

锁的代价:

    

2024-11-30-10-44-21-image.png

2024-11-30-10-45-53-image.png 例子:下面的的判断和修改逻辑分开了

2024-11-30-10-52-41-image.png 我们在,回想一下我们对线程安全的定义,这个方法在多线程的环境下的运行的结构应该是唯一的是确定的而不能是不确定的。感觉就应该是这样的。而导致出现问题的原因就是在某一些操作应该是互斥的,我们需要使用锁来保证代码的同步于互斥。例如,先检查和执行

改变数据就要改完,你不能边读边改(读和写互斥,保证不变性)。你也不能同时改(写和写互斥,保证原子性),先检查和执行的时候你必须保证你检查的条件不会被修改。

不变性:线程安全问题的核心。

线程的不变性条件(Thread Invariant Condition)是指在多线程程序中,需要始终保持某些条件为真,以确保系统或程序的状态一致性和正确性。这些条件描述了程序在任何时刻都必须满足的约束,线程的任何操作都不能破坏这些约束。

特性与作用

  1. 定义

    • 不变性条件是指在任意时间点,系统状态必须满足的逻辑规则,通常与数据结构的完整性和一致性相关。
    • 多线程程序中的每个线程操作可能会改变共享数据的状态,但在操作完成后,不变性条件必须继续成立。
  2. 作用

    • 防止多线程环境下的竞争条件或数据不一致。
    • 确保并发操作不会引入逻辑错误或状态污染。

线程不变性条件的示例

1. 银行账户转账场景

假设有两个账户 AB,系统需要确保以下不变性条件:

  • 总金额恒定:A.balance + B.balance == TotalAmount

在多线程环境中,如果两个线程同时执行转账操作,未正确同步可能导致临时状态中总金额不一致。问题在于如果转账操作不是原子性的,那么查账操作就可能查出错误结果。

实现示例:
import threading

class BankAccount:
    def __init__(self, balance):
        self.balance = balance
        self.lock = threading.Lock()

def transfer(from_account, to_account, amount):
    with from_account.lock, to_account.lock:
        from_account.balance -= amount
        to_account.balance += amount

# 不变性条件验证
account_a = BankAccount(1000)
account_b = BankAccount(2000)

# 确保操作后满足:account_a.balance + account_b.balance == 3000

通过加锁,确保任意时刻的转账操作不会破坏“不变性条件”。


2. 生产者-消费者问题

在生产者-消费者模型中,系统的核心不变性条件可能是:

  • 缓冲区的元素数目在合法范围内:0 <= buffer.size <= buffer.capacity
实现示例:
import threading
import queue

buffer = queue.Queue(maxsize=10)

def producer():
    while True:
        item = produce_item()
        buffer.put(item)  # 保证不变性:不会超过容量

def consumer():
    while True:
        item = buffer.get()  # 保证不变性:不会取空
        consume_item(item)

通过线程安全的队列 queue.Queue,自动确保不变性条件。


不变性条件的特点

  1. 全局约束

    • 通常涉及系统的共享资源或关键逻辑,如数据结构的完整性、计数器的一致性等。
  2. 操作封装

    • 多线程程序需要通过同步机制(如锁、信号量、原子操作等)来保护对共享数据的访问,避免违反不变性条件。
  3. 事务性

    • 不变性条件的检查与操作通常需要作为一个不可分割的事务来完成(原子性)。

实现线程不变性条件的技巧

  1. 使用锁(Lock)

    • 在访问共享资源前后加锁,确保操作的原子性和不变性条件。
  2. 使用原子操作

    • 使用语言或库提供的原子操作(如 CAS 或原子变量),避免显式加锁。
  3. 分解不变性条件

    • 复杂的不变性条件可以分解成多个简单子条件,分别保证每个子条件的正确性。
  4. 封装关键操作

    • 将所有对共享资源的操作封装到一个函数或模块中,避免不安全的直接访问。

违反不变性条件的后果

  • 数据不一致:共享资源的状态被破坏,例如账户金额出现负数。
  • 死锁或活锁:由于不一致的状态导致线程间的竞争无法正确解决。
  • 逻辑错误:程序的行为无法满足设计预期,可能出现崩溃或不正确的结果。

总结来说,线程的不变性条件是并发程序正确性的核心约束,确保每个线程的操作不会导致系统进入不合法状态。通过合理的同步机制和良好的代码设计,可以有效地维护不变性条件。

是的,在多线程编程中,当你先检查条件再执行操作时,必须确保检查条件不会在执行操作之前被其他线程修改。这种需求是为了避免竞态条件(Race Condition),确保程序的正确性和一致性。

核心问题:检查-执行间的竞态条件

在多线程环境中,如果检查条件和执行操作之间没有适当的同步保护,那么以下问题可能会发生:

  1. 条件被破坏
    • 一个线程在检查某个条件为真之后,另一个线程修改了共享资源,使得该条件不再成立。
  2. 错误行为
    • 由于条件已被破坏,当前线程的后续操作可能会导致逻辑错误或数据不一致。

示例:竞态条件的问题

1. 错误的先检查后执行

假设有一个共享变量 count,表示剩余资源的数量。一个线程检查是否有剩余资源,并减少资源数量:

if count > 0:  # 检查条件
    count -= 1  # 执行操作

在多线程环境中,如果两个线程同时检查条件 count > 0,它们可能同时通过检查,并都执行了 count -= 1,这可能导致 count 的值不正确(例如从 1 变成了 -1)。


解决方案:同步保护

为了避免检查-执行间的竞态条件,可以使用以下方法:

1. 使用锁(Lock)

加锁确保检查和执行操作是一个原子操作。

import threading

lock = threading.Lock()
count = 5

def safe_decrement():
    global count
    with lock:  # 加锁
        if count > 0:  # 检查和执行都在锁的保护下
            count -= 1
            print(f"Decrement successful, count = {count}")
        else:
            print("No resources left")

在这种方式中,锁确保了检查和操作之间没有其他线程干扰,避免了竞态条件。


2. 使用条件变量(Condition)

条件变量可以用来等待某些条件满足,同时确保检查和操作的原子性。

import threading

condition = threading.Condition()
count = 5

def safe_decrement():
    global count
    with condition:  # 获取条件锁
        while count <= 0:
            condition.wait()  # 等待条件满足
        count -= 1
        print(f"Decrement successful, count = {count}")
        condition.notify_all()  # 通知其他线程

条件变量不仅保护了检查和操作的原子性,还能用于等待和通知机制,提升效率。


3. 使用原子操作

在某些情况下,可以使用原子操作替代锁。例如,使用原子变量或语言提供的原子操作函数。

import threading
from threading import Lock

lock = Lock()
count = 5

def safe_increment():
    global count
    with lock:
        count += 1  # 原子操作

如果你的语言或库支持原子操作(如 C++ 中的 std::atomic 或 Python 的 multiprocessing.Value),可以直接利用这些特性简化实现。


结论

在多线程编程中,检查条件后再执行操作时,必须确保检查与执行之间是受保护的,防止其他线程篡改条件。最常见的方式是使用锁或同步原语,确保检查和操作是一个原子操作,从而避免竞态条件的问题。