• 【笔记】【Java并发编程实战】2线程安全


    注:本文为笔者阅读《JAVA并发编程实战》(Brian Goetz等注)一书的学习笔记,如有错漏,敬请指出。

    重要概念摘录:

    概述

    • 线程安全的界定:当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步、在调用代码方无须作其他协调,这个类的行为依然是正确的,称这个类是线程安全的。(我的理解:找不出并行执行与串行执行结果相异的情况)

    • 构建并发程序要正确使用线程和锁。编写线程安全的代码,本质上是管理对状态的访问,而且通常是共享、可变的状态。

    • 一般而言,一个对象的状态就是它的数据(存储在状态变量中),还包括其他附属对象的域,包含任何会对它外部可见行为产生影响的数据。

    • 线程安全的性质取决于程序如何使用对象,而不是对象完成了什么。

    原子性

    • 自增操作(++count)并不是原子操作,它实际上是三个离散操作的简写形式:获取当前值,加1,返回新值 (read-modify-write)。若两个线程缺乏同步,会引发问题。
    • 计数上的轻微错误在基于Web的服务中是可接受的,但若计数器用于生成序列或对象唯一的标识符,多重调用返回相同的结果会导致严重的数据完整性问题。
    • 在一些偶发时段里,出现错误结果的可能性对于并发程序而言非常重要,这称为竞争条件。

    一些问题的解决方案

    在没有正确同步的情况下,若多个线程访问同一个变量,如何修复程序隐患?

    • 不要跨线程共享变量
    • 使状态变量为不可变的(我的理解:final)
    • 在任何访问状态变量的时候使用同步
      (2和3选一个)

    注意:

    • 一开始就将一个类设计成是线程安全的,比在后期重新修复它更容易。
    • 访问特定变量的代码越少,越容易确保使用恰当的同步,越容易推断出访问一个变量所需的条件。
    • 对程序的状态封装得越好,程序越容易实现线程安全,也有助于维护者保持这种线程安全性。

    一些示例代码:

    线程安全示例:无状态对象

    以下是利用Servlet进行简单的因数分解操作的程序。

    
    public class StatelessFactorizer implements Servlet {
        public void service(ServletRequest req, ServletResponse resp){
            BigInteger i=extractFromRequest(req);
            BigInteger[] factors=factor(i);
            encodeIntResponse(resp,factors);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (注:这段代码似乎要自己编写extractFromRequest()和encodeIntResponse()方法)

    • StatelessFactorizer像大多数Servlet一样是无状态的;它不包含域也没有引用其他类的域。一次特定计算的瞬时状态,会唯一地存在本地变量中,这些本地变量存储在栈中,只有执行线程才会被访问。
    • 一个访问StatelessFactorizer的线程不会影响访问同一个Servlet的其他线程的计算结果。因为两个线程不共享状态,它们如同在访问不同的实例

    充分考虑活跃度和性能

    缓存最新请求和结果的servlet

    import jakarta.servlet.ServletRequest;
    import jakarta.servlet.ServletResponse;
    import net.jcip.annotations.GuardedBy;
    
    import java.math.BigInteger;
    
    public class CachedFactorizer {
        @GuardedBy("this") private BigInteger lastNumber;
        @GuardedBy("this") private BigInteger[] lastFactors;
        @GuardedBy("this") private long hits;
        @GuardedBy("this") private long cacheHits;
    
        public synchronized long getHits(){return hits;}
        public synchronized double getCacheHitRatio(){
            return (double) cacheHits/(double)hits;
        }
    
        public void service(ServletRequest req, ServletResponse resp){
            BigInteger i=extractFromRequest(req);
            BigInteger [] factors=null;
            synchronized (this){
                ++hits;
                if(i.equals(lastNumber)){
                    ++cacheHits;
                    factors=lastFactors.clone();
                }
            }
    
            if(factors==null){
                factors=factor(i);
                synchronized (this){
                    lastNumber=i;
                    lastFactors=factors.clone();
                }
            }
            encodeIntResponse(resp,factors);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 特点:平衡了简单(同步整个方法)与并发(同步尽可能短的代码路径)
    • 考虑因素:
      • 请求与释放锁需要开销,故不要讲锁块分解得过于琐碎。
      • 不要过早为了性能牺牲简单性(可能引发安全问题)
      • 耗时的计算或操作,比如网络或控制台IO,执行期间不要占有锁。
  • 相关阅读:
    香港是如何形成现在的繁荣
    想进大厂的朋友请注意!Java多线程面试题来袭,跳槽涨薪必备法器
    如何使用ESP8266微控制器和Nextion显示器为Home Assistant展示温度传感器和互联网天气预报
    Flutter核心原理
    链表-哈希表 详解
    The babbage industrial policy forum
    C++打印CPU和内存实时使用情况
    c++ primer中文版第五版作业第三章
    采用 vue3 + vite + element-plus + tsx + decorators + tailwindcss 构建 admin 管理员后台页面
    【Android development】系列_02创建安卓应用程序
  • 原文地址:https://blog.csdn.net/julia_xueli/article/details/125339222