conanan's blog conanan's blog
首页
关于
  • 分类
  • 标签
  • 归档
  • Java
  • Java Web
  • 工具

    • Maven
  • MySQL
  • Redis
  • Git
  • Vim
  • Nginx
  • Docker
GitHub

Evan Xu

前端界的小学生
首页
关于
  • 分类
  • 标签
  • 归档
  • Java
  • Java Web
  • 工具

    • Maven
  • MySQL
  • Redis
  • Git
  • Vim
  • Nginx
  • Docker
GitHub
  • 基础

    • 0 Basic
    • 1 Basic Syntax
    • 1 Basic Syntax-1 数组
    • 2 Object Orientation-1 面向对象
    • 2 Object Orientation-2 抽象类&接口
    • 3 Error & Exception
    • 2 Object Orientation-3 枚举
  • API

  • Container

  • 多线程

  • 16 9,10,11新特性
  • 17 Test
  • 18 设计原则&设计模式
  • JDBC
  • Java
  • 多线程
conanan
2021-06-24

线程安全理论

# 线程安全理论

# 如何编写线程安全的代码 🔥

核心在于对共享的、可变的状态(成员变量,包括静态的)访问操作进行管理

  • 共享:多个线程同时访问
  • 可变:变量的值在其生命周期内可变
  • 访问操作:只有在一或多个线程向这些资源做了写操作时才有可能发生
  • 管理:使用同步机制协同线程对变量的访问。可使用如:synchronized、volatile、Lock、原子变量等

注意:无状态对象一定是线程安全的(没有管理的主体了,当然线程安全)。如:大多数 Servlet 等。

# 原子性 🔥

# 竞态条件

多个线程以非互斥的方式进入临界区,当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件

常见的竞态条件类型:

  • 先检查后执行(Check-Then-Act),即基于一个可能失效的观察结果来作出判断或执行某个计算。 如:延迟初始化
    @NotThreadSafe
    public class LazyInitRace {
        private ExpensiveObject instance = null;
        public ExpensiveObject getInstance() {
            if(instance == null) {
                instance = new ExpensiveObject();
            }
            return instance;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 读取-修改-写入:基于对象之前的状态来定义对象状态的转换。 如:递增运算,i++

# 复合操作 VS 原子操作

常见的竞态条件类型其原因在于复合操作,非原子操作!

  • 当在无状态的类(本身绝对线程安全)中添加一个状态时,如果该状态由线程安全的对象管理,那么这个类还是线程安全的
  • 但是,当添加多个状态时,就比较复杂了

# 加锁机制—互斥锁—原子性 🔥

# Monitor(监视器、管程)

  • 也称作Intrinsic Lock,内置锁。Java 中可以使用同步代码块机制实现Monitor,支持原子性、可见性、有序性。
  • 每个Java对象都可以做一个实现同步的锁。线程在进入同步代码块之前自动获得锁,并在退出同步代码块时自动释放锁(无论通过正常控制路径退出,还是通过代码中抛出异常退出)
  • 获得锁的唯一途径就是进入由这个锁保护的同步代码块或方法

# 可重入

  • 当某个线程请求由其他线程持有的锁时,发出请求的线程会阻塞。但是由于Monitor是可重入的,所以如果某个线程视图获取一个已经由它自己持有的锁时,该请求就会成功
  • 重入意味着获取锁的操作粒度是线程,而不是调用

重入锁一种实现方式:

  • 为每个锁关联一个获取计数值、一个所有者线程。
  • 当计数值为0,则该锁没有被任何线程持有
  • 当一个线程请求一个未被持有的锁时,JVM将记录下锁的持有者线程、计数值置为1 同一个线程再次获取锁,则计数值递增。当线程退出同步代码时,计数值递减
  • 当计数值为0,则该锁被释放

# 活跃性 & 性能

常见的==错误==:==只有在写入共享变量时才需要使用同步锁==!!!

当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或控制台IO),一定不要持有锁!!!

# 对象共享—可见性 🔥

# 介绍 🔥

  • 也叫内存可见性(Memory Visibility)
  • 我们希望:
    • 原子性:防止某个线程正在使用对象状态,而另一个线程在同时修改状态!
    • 可见性:确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化!

# 问题 🔥

  • 单线程中,当读操作和写操作在不同的线程中执行时,我们无法确保执行读操作的线程能够实时的看到其他线程写入的值
  • 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制
  • 在没有同步的情况下,处理器、编译器、运行时都可能对操作的执行顺序进行调整!!!该操作能使JVM充分利用现代多核处理器的强大性能。如:
    • 未使用同步时,JMM允许编译器对操作进行重排序,将数值缓存在寄存器中。
    • 未使用同步时,JMM允许CPU对操作进行重排序,将数值缓存在处理器特定的缓存中。

# 非原子的64位操作 🔥

  • 当线程在没有同步的情况下读取变量时,可能会得到一个失效值(之前设置的,非随机值),这种称为最低安全性。
  • JMM要求变量的读取和写入操作都必须是原子操作!
  • 但是对于非volatile类型的64位数值变量(double和long),JVM允许将64位读或写操作分解为2个32位的操作(主要原因在于编写Java虚拟机规范时,许多主流处理器架构还不能有效地提供64位数值的原子操作)

# 锁与原子性、可见性、有序性 🔥

  • 管程锁定规则:一个unlock操作Happend Before对同一个锁的lock操作。
  • 即一个线程unlock前的操作,另一个lock线程都可以看到

# volatile 与可见性、有序性🔥

  • 稍弱的同步机制,用于确保将变量的更新操作通知到其他线程!

  • 变量声明为volatile后,编译器和运行时会注意到这个变量是共享的

    • volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方
    • 不会将该变量上的操作与其他内存操作一起重排序
  • 不建议过度依赖volatile的可见性!!!

  • 仅当volatile变量能简化代码的实现以及对同步策略的验证时,才该使用!如果在验证正确性时需要对可见性进行复杂的判断,则不要使用

  • 正确使用方式:

    • 确保它们自身状态的可见性

    • 确保它们所引用对象的状态的可见性

    • 标识一些重要的程序生命周期事件,如初始化、关闭

    • 检查某个标记以判断是否退出循环

      volatile boolean asleep;
      // ...
      while(!asleep){
          doSomeThing();
      }
      
      1
      2
      3
      4
      5
  • 当且仅当满足以下所有条件时,才应该使用volatile变量:

    • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
    • 该变量不会与其他状态变量一起纳入不变性条件中
    • 在访问变量时不需要加锁

调试技巧:

对与服务器应用程序,无论是开发、测试阶段,启动JVM时一定要指定-server命令行选项,比-client模式,JVM将进行更多优化。如:将循环中未被修改的变量提升到循环外部,因此在-client中可以执行的代码,在-server可能运行失败。

# 对象共享—发布与逸出

# 简介

  • 发布:使对象能够在当前作用域之外的代码中使用。如:
    • 将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看到该对象
  • 逸出:某个不应该发布的对象被发布时的情况

# 构造函数中逸出

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }

    void doSomething(Event e) {
    }


    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

ThisEscape 构造函数中doSomething其实是this.doSomething,在还未构造完时,即发布了一个尚未构造完成的对象!即使发布对象的语句位于构造函数最后一行!不要在构造函数中使this引用逸出!!!

只有当构造函数返回时(执行完毕),this引用才应该从线程中逸出!

采用工厂方法改造后的:

public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        }
    }

    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }

    void doSomething(Event e) {
    }

    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}

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

# 避免同步—对象不共享、线程封闭

# 简介

访问共享的可变数据时,通常需要使用同步,一种避免使用同步的方式就是不共享数据!

如果仅在单线程内访问数据,就不需要同步,该技术称为线程封闭!它是实现线程安全性最简单方法之一!

如JDBC规范并不要求Connection对象必须是线程安全的,但是在服务器应用中,由于大多数请求(如Servlet)都是由单个线程采用同步的方式来处理,并且在Connection对象返回之前,连接池都不会把它分配给其他线程。因此这种管理模式隐含地将Connection对象封闭在线程中。

# 栈封闭—局部变量 🔥

在封闭栈中,只能通过局部变量才能访问对象,因此线程间不存在对象共享

# ThreadLocal 🔥

ThreadLocal能使线程中的某个值与保存值的对象关联起来。常用于防止对可变的单实例变量或全局变量进行共享!

public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    private ThreadLocal<Connection> connectionHolder
        = new ThreadLocal<Connection>() {
        public Connection initialValue() {
            try {
                return DriverManager.getConnection(DB_URL);
            } catch (SQLException e) {
                throw new RuntimeException("Unable to acquire Connection, e");
            }
        };
    };

    public Connection getConnection() {
        return connectionHolder.get();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 满足同步—不变性

线程安全性是不可变对象的固有属性之一!即不可变对象一定是线程安全的!

但是即使对象中所有的域都是final类型的,这个对象也可能是可变的!不变对象的域中保存的是可变对象的引用!

例如使用 volatile 发布不可变对象(对象的封装可以更好更方便保障原子性)

/**
 * OneValueCache
 * <p/>
 * Immutable holder for caching a number and its factors
 *
 * @author Brian Goetz and Tim Peierls
 */
@Immutable
public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i,
                         BigInteger[] factors) {
        lastNumber = i;
        lastFactors = Arrays.copyOf(factors, factors.length);
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i))
            return null;
        else
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}
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
/**
 * VolatileCachedFactorizer
 * <p/>
 * Caching the last result using a volatile reference to an immutable holder object
 *
 * @author Brian Goetz and Tim Peierls
 */
@ThreadSafe
public class VolatileCachedFactorizer extends GenericServlet implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}
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

# 安全发布

如何保障使用对象的线程能够看到该对象处于已发布状态?则对象的引用和对象的状态必须都对其他线程可见!常用模式:

  • 静态初始化函数中初始化一个对象的引用

    静态初始化函数由JVM在类初始化阶段执行,由于JVM内部的同步机制,该初始化方法的对象必定被安全发布!

    public static Holder holder = new Holder(1);
    
    1
  • 将对象的引用保存到volatile类型的域或AtomicReferance对象中

  • 将对象的引用保存到某个正确构造对象的final类型域中。如上面的不变性

  • 将对象的引用保存到一个由锁保护的域中

编辑
上次更新: 2021/07/07, 14:41:01
最近更新
01
线程生命周期
07-06
02
并发简史
06-24
03
简介
06-21
更多文章>
Theme by Vdoing | Copyright © 2019-2021 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×