java并发编程一:基础知识

本文深入探讨了线程安全的概念,包括竞态条件、原子性、加锁机制以及不可变对象的重要性。同时,讲解了如何通过volatile变量、ThreadLocal和安全发布策略来确保线程之间的正确交互。

一、线程安全性

1.1 什么是线程安全性?

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确行为。

1.2 原子性

i++ -> "读取-修改-写入"

1.2.1 竞态条件 和 复合操作

当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。或者说,之所以发生竞态条件是因为操作是非原子性而是一个复合操作。

public class A {
    private static A instance = null;
    public static A getInstance(){
        if(instance == null){
            instance = new A();
        }
        return instance;
    }
}复制代码

这里会存在竞态条件(先检查后执行),假设线程A1和A2同时执行getInstance方法,A1看到instance实例为空,它会去new A();A2也要判断instance是否为空,此时的instance是否为空取决于不可预测的时序(包括线程调度方式),以及A1要花多长时间new A 实例;如果A1在new操作时,轮到A2线程被调度,那么此时A2判断instance也为空,最终会出现两个A实例。 同理i++也存在这样的竞态条件(读取-修改-写入) 解决:避免某个线程修改变量时,通过某种方式防止其他线程使用这个变量,即保证操作是原子方式执行。

先检查后执行 -- 加锁实现同步
i++ -- concurrent.atomic 实现原子操作

1.3 加锁机制

1.3.1 内置锁

Synchronized

锁重入

“重入”意味着获取锁的操作粒度是“线程”,而不是“调用”,避免死锁。

二、共享对象

2.1 可见性

当读操作和写操作在不同的线程中执行时,无法确保执行读操作的线程能看到其它线程写入的值。

问题:
  1. 数据失效

public class ClassA {
    private int value;
    public int getValue() {
        return value;
    }
    public void setValue(int value) {
        this.value = value;
    }
}
复制代码

在没有同步的情况下get和set访问value,数据失效很容易出现:如果某个线程调用了set,那么另一个在调用get的线程可能会看到更新后的value值,也可能看不到。

  1. 非原子的64位操作
    非volatile类型的64位数值变量long和double,JVM允许将64位的读操作或者写操作分解为两个32位的操作,当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能读到某个值得高32位和另一个值得低32位,造成数据失效。
解决:
  1. 加锁与可见性
    加锁不仅仅具有互斥性和包括可见性,为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
  2. volatile 变量
    可见性与重排序 使用场景:确保自身状态可见,确保引用对象状态可见,标记重要程序生命周期事件发生(初始化和关闭) 某操作完成、中断或者状态标志

volatile boolean asleep;
...
while(!asleep) {
    countSomeSheep();
}
复制代码

2.2 发布与逸出

1. 发布: 发布一个对象的意思是,使对象能够在当前作用域之外的代码中使用。

  1. 将一个引用存储到其他代码可以访问的地方;
  2. 在一个非私有的方法中返回该引用;
  3. 将该对象传递到其他类的方法中等。

public static Set secrets;
public void init(){
     secrets = new HashSet();
}
复制代码

当发布某个对象时,可能间接地发布其他对象。例如如果将一个Secret对象添加到集合secrets中,那么在发布secrets的同时,也会发布Secret对象,因为任何代码都可以遍历这个集合,并获得对Secret对象的引用。

2. 逸出: 当某个不应该发布的对象被发布时,这种情况就是逸出。
对象逸出会导致对象的内部状态被暴露,可能危及到封装性,使程序难以维持稳定;若发布尚未构造完成的对象,可能危及线程安全问题。
最常见的逸出是this引用在构造时逸出,导致this引用逸出的常见错误有:

  1. 在构造函数中启动线程:
    当对象在构造函数中显式还是隐式创建线程时,this引用几乎总是被新线程共享,于是新的线程在所属对象完成构造之前就能看见它。 避免构造函数中启动线程引起的this引用逸出的方法是不要在构造函数中启动新线程,取而代之的是在其他初始化或启动方法中启动对象拥有的线程。
  2. 在构造方法中调用可覆盖的实例方法:
    在构造方法中调用那些既不是private也不是final的可被子类覆盖的实例方法时,同样导致this引用逸出。 避免此类错误的方法时千万不要在父类构造方法中调用被子类覆盖的方法。
  3. 在构造方法中创建内部类:
    在构造方法中创建内部类实例时,内部类的实例包含了对封装实例的隐含引用(深入理解 内部类),可能导致隐式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;  
    }  
}
复制代码

2.3 线程封闭

当访问共享的可变数据时,通常需要同步。一种避免使用同步的方式就是不同享数据,这叫做线程封闭。java提供了一些机制来维持线程封闭性,例如局部变量和ThreadLocal类。 线程封闭技术的一个常见应用是JDBC的Connection对象。JDBC规范不要求Connection对象时线程安全的,而要求连接池是线程安全的。线程通过线程池中获得一个Connection对象,并且用该对象来处理请求,使用完之后再返回给连接池。由于大多数请求(例如Servlet请求和EJB)都是单个线程采用同步的方式来处理,并且在Connection对象返回前,连接池不会再把它分配给其它线程,因此这种连接在处理请求时,把Connection对象封闭在线程中。

2.3.1 栈封闭

将变量封闭在方法中

2.3.2 threadLocal 为每个使用变量的线程都存有一个副本

使用场景:

  1. 一个单线程应用程序移植到多线程环境,将共享的全局变量转换为threadLocdal对象
  2. 避免调用每个方法都要传递上下文信息

2.4 不变性

如果一个对象在创建后其状态就不能被修改,那么这个对象就称为不可变对象。 不可变对象需要满足下面条件:

  1. 对象本身是final的(避免被子类化),声明属性为private 和 final
  2. 不可变对象的状态在创建后就不能再改变,不要提供任何可以修改对象状态的方法 - 不仅仅是set方法, 还有任何其它可以改变状态的方法,每次对他们的改变都是产生了新的不可变对象的对象。
  3. 不可变对象能被正确地创建(在创建过程中没有发生this引用逸出)。
  4. 如果类有任何可变对象属性, 那么当它们在类和类的调用者间传递的时候必须被保护性拷贝

不可变的对象一定是线程安全的

2.5 安全发布

转载于:https://juejin.im/post/5c348d39f265da616f7024b2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值