线程安全 ThreadSafe

一.基础

线程安全,ADT或方法在多线程中要执行正确 

具体来说,满足以下几个要求

      (1)  不违反spec、保持RI 

      (2)与多少处理器、 如何调度线程,均无关

      (3)不需要在spec中强制要求client满足某种“线程 安全”的义务

为什么要保证线程安全?

当不同线程访问同一数据或内存时,就可能会发生交错(interleaving)或竞争(Race conditions)关系。

每当这种情况发生时,程序就会发生一些意想不到的bug,而且一旦出现线程相关的bug是很难调试的。

Interleaving (交错) and Race Condition (竞争)

(1)Interleaving  交错

交错,顾名思义,就是说在线程运行的过程中,多个线程同时运行相互交错。

而且,由于线程运行一般不是连续的,那么就会导致线程间的交错。

可以说,所有线程安全问题的本质都是线程交错的问题。

(2)Race Condition   竞争

竞争是发生在线程交错的基础上的。

当多个线程对同一对象进行读写访问时,就可能会导致竞争的问题。

程序中可能出现的一种问题就是,读写数据发生了不同步。

例如,我要用一个数据,在该数据修改还没写回内存中时就读取出来了,那么就会导致程序出现问题。

程序运行时有一种情况,就是程序如果要正确运行,

必须保证A线程在B线程之前完成(正确性意味着程序运行满足其规约)。

当发生这种情况时,就可以说A与B发生竞争关系。

线程之间的“竞争条件”:作用于同一个mutable数据上的多个线程, 

彼此之间存在对该数据的访问竞争并导致interleaving,

导致postcondition可能被违反,这是不安全的。

(3)Time slicing   时间分片

虽然有多线程, 但只有一个核,每个时刻只能执行一个线程 

通过时间分片,在多个进程/线 程之间共享处理器



二.   Four ways of threadsafe  四种策略

1. Confinement 限制数据共享

保证线程安全的一个最简单也是最直接的方法就是限制数据的共享,
将可变数据放到单个线程中,避免在可变数据类型上发生竞争关系。

其中的核心思想就是不让其他线程直接读或写这个线程中的数据。

核心思想: 线程之间不共享mutable数据类型

(1)使用局部变量保证线程安全

限制数据共享主要是在线程内部使用局部变量,因为局部变量在每个函数的栈内,每个函数都有自己的栈结构,互不影响,这样局部变量之间也互不影响。

如果局部变量是一个指向对象的引用,那么就需要检查该对象是否被限制住,如果没有被限制住(即可以被其他线程所访问),那么就没有限制住数据,因此也就不能用这种方法来保证线程安全。

例:
public class Factorial {

    /**
     * Computes n! and prints it on standard output.
     * @param n must be >= 0
     */
    private static void computeFact(final int n) {
        BigInteger result = new BigInteger("1");
        for (int i = 1; i <= n; ++i) {
            System.out.println("working on fact " + n);
            result = result.multiply(new BigInteger(String.valueOf(i)));
        }
        System.out.println("fact(" + n + ") = " + result);
    }

    public static void main(String[] args) {
        new Thread(new Runnable() { // create a thread using an
            public void run() {     // anonymous Runnable
                computeFact(99);
            }
        }).start();
        computeFact(100);
    }
}

如该图,在主函数内开启了两个线程,线程调用的是相同的函数。

理论上来讲应该会发生竞争关系,然而在这里却没有。

因为在该函数中,共享的数据变量是i,n和result,这三个都是局部变量,

放在函数调用的栈内,每个函数调用时有不同的栈,因此也就有不同的i,n和result。

用图形的方式可以表示成下面这样:


由于每个函数都有自己的局部变量,那么每个函数就可以独立运行,更新它们自己的函数值,线程之间不影响结果。


(2) 避免使用全局变量

由于局部变量的特点,即使对局部变量不做任何处理,线程安全也能得到保证。然而这对于全局变量并不适用。

例1.

// This class has a race condition in it.
public class PinballSimulator {

    private static PinballSimulator simulator = null;
    // invariant: there should never be more than one PinballSimulator
    //            object created

    private PinballSimulator() {
        System.out.println("created a PinballSimulator object");
    }

    // factory method that returns the sole PinballSimulator object,
    // creating it if it doesn't exist
    public static PinballSimulator getInstance() {
        if (simulator == null) {
            simulator = new PinballSimulator();
        }
        return simulator;
    }
}

这个类的形式是属于一种单例模式,在这里,构造器是私有的,

只能通过 getInstance 方法来获取这个类的实例,而且,这个类的实例只能有一个。

这个类运行起来看似不会出现什么问题,只返回一个实例。

尽管在返回前做了一次判断,然而在多线程程序中,这还是不安全的。

试想,如果有两个线程同时访问这个函数,又紧接着对实例是否为空进行了判断,

这时,对于这两个线程来讲,结果肯定都是空的,这时连个线程都去创建该类的对象并返回,

这样我们就会得到这个对象的两个实例,这就违反了这个类的规约,程序的正确性就无法得到保证。

例2.

// is this method threadsafe?
/**
 * @param x integer to test for primeness; requires x > 1
 * @return true if x is prime with high probability
 */
public static boolean isPrime(int x) {
    if (cache.containsKey(x)) return cache.get(x);
    boolean answer = BigInteger.valueOf(x).isProbablePrime(100);
    cache.put(x, answer);
    return answer;
}

private static Map<Integer,Boolean> cache = new HashMap<>();

在对cache进行是否包含x检测时,如果在这个时候也同样有两个线程同时对其进行访问,

如果一个线程包含了 x ,而后另一个线程紧接着将其中的 x 删除掉了,

这样一来,第一个线程的cache.get(x) 方法就不能得到 x 的值,即程序出现了问题,无法满足正确性。

因此,如果仅使用限制数据类型的方法来保证线程安全,就要避免使用全局变量。


2.Immutability(共享不可变数据)

不可变数据类型,指那些在整个程序运行过程中,指向内存的引用是一直不变的,通常使用final来修饰。

不可变数据类型通常来讲是线程安全的,但也可能发生意外。

使用不可变数据类型和不可变引用,避免多线程之间的race condition

但是,程序在运行过程中,有时为了优化程序结构,默默地将这个引用更改了。

此时,客户端程序员是不知道它被更改了,对于客户端而言,这个引用还是不可变的,但其实已经被悄悄更改了。

这时就会发生一些线程安全问题。

解决方案就是给这些不可变数据类型再增加一些限制:

  • 所有的方法和属性都是私有的。
  • 不提供可变的方法,即不对外开放可以更改内部属性的方法。
  • 没有数据的泄露,即返回值而不是引用。
  • 不在其中存储可变数据对象。

这样就可以保证线程的安全了。


3.Using Thread safe Data Types(共享线程安全的可变数据)

如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。

一般来说,JDK同时提供两个相同功能的类,一个是thread safe,另一个不是。

原因:thread safe的类一般性能上受影响。

在java中最典型的线程安全的两个类型就是 StringBuffer 和 StringBuilder

StringBuffer在使用的过程中是线程安全的,但是速度慢;

StringBuilder在使用过程中线程不安全,但速度快。

官方建议尽量使用 StringBuilder ,以提高速度。

可以看到,java提供了线程安全的数据类型,但不仅仅这一种,还有很多,其中最主要的一个大类就是 Collection 类,像下面这样:

private static Map<Integer,Boolean> cache =Collections.synchronizedMap(new HashMap<>());

java提供了一个包装类,可以将线程不安全的类型进一步封装,变成线程安全的数据类型。

此外,需要注意的一点就是,在包装数据类型的时候,千万记住不要将引用留在外面,像这样:

private final Set<String> roomNumbers = new HashSet<>();
private final Set<String> floorplan =Collections.synchronizedSet(roomNumbers);

虽然使用了线程安全的封装类,但将 new HashSet<>() 的引用留在了外面,那么就可能发生一些不愉快的事情了。

在使用synchronizedMap(hashMap)之后,不要再把参数hashMap共享给其他线程,不要保留别名,一定要彻底销毁。

注意:即使在线程安全的集合类上,使用iterator也是不安全的,除非使用lock机制


4.Synchronization(同步机制共享)

                                  ---最复杂也最有价值的thread safe策略

前三种策略的核心思想: 
—–> 避免共享 
—–> 即使共享,也只能读/不可写(immutable) 

—–> 即使可写 (mutable),共享的可写数据应自己具备在多线程之间协调的能力,即“使用线程安全的mutable ADT”

但是很多时候,无法满足上述三个条件…

程序员来负责多线程之间对mutable数据的共享操作,通过“同步”策略,避免多线程同时访问数据

使用锁机制,获得对数据的独家mutation权,其他线程被阻塞,不得访问

(1) Synchronized Blocks and Methods

Lock是Java语言提供的内嵌机制

每个object都有相关联的lock

Object lock = new Object();

synchronized (lock) { // thread blocks here until lock is free
// now this thread has the lock
balance = balance + 1;
// exiting the block releases the lock
}

Lock保护共享数据

注意:要互斥,必须使用同一个lock进行保护

a.Monitor pattern 监视器模式
用ADT自己做lock
synchronized(this).
/** SimpleBuffer is a threadsafe EditBuffer with a simple rep. */
public class SimpleBuffer implements EditBuffer {
    private String text;
    ...
    public SimpleBuffer() {
        synchronized (this) {
            text = "";
            checkRep();
        }
    }
    public void insert(int pos, String ins) {
        synchronized (this) {
            text = text.substring(0, pos) + ins + text.substring(pos);
            checkRep();
        }
    }
    public void delete(int pos, int len) {
        synchronized (this) {
            text = text.substring(0, pos) + text.substring(pos+len);
            checkRep();
        }
    }
    public int length() {
        synchronized (this) {
            return text.length();
        }
    }
    public String toString() {
        synchronized (this) {
            return text;
        }
    }
}

在每个函数内部,加上 synchronized (this) 这样的话,就是将该对象锁住,

每个对象一个时间段只能调用这个对象中的一个方法,而不能同时调用多个,这样就保证了线程的安全性。

对synchronized的方法,多个线程执行它时不允许interleave,也就是说“按原子的串行方式执行”

Synchronized Statements/Block


任何共享的mutable变量/对象必须被lock所保护

涉及到多个mutable变量的时候,它们必须被同一个lock所保护

同时,使用 synchronized 的话,也要避免死锁,即任何两个线程之间,

对对象进行锁的顺序需要一致,如果相反,就很可能会出现死锁的现象。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值