【笔记】【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);
    }
}

(注:这段代码似乎要自己编写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);
    }
}
  • 特点:平衡了简单(同步整个方法)与并发(同步尽可能短的代码路径)
  • 考虑因素:
    • 请求与释放锁需要开销,故不要讲锁块分解得过于琐碎。
    • 不要过早为了性能牺牲简单性(可能引发安全问题)
    • 耗时的计算或操作,比如网络或控制台IO,执行期间不要占有锁。

你可能感兴趣的:(笔记,java入门,java,并发编程)