Java 线程(3)- 线程共享

2019-01-27  本文已影响0人  skeeey

当使用多线程时,当多个线程同时操作同一个变量时,由于竞争条件(race condition)可能破坏该变量的状态,导致一致性问题,而如果多线程之间依赖同一资源,则各线程之间可能会陷入 Liveness Hazards

线程安全即在多线程的环境下, 无论线程以何种方式使用该对象, 都不会引起错误, 并且对该对象的使用者而言无需额外添加同步或其它条件

确保一致性

确保线程间的一致性,最有效的方法是减少(或避免)多线程之间的数据(状态)共享,对于共享的数据(状态),则应保证在同一时刻对各线程的可见性(visible),保证一致性常用的方发包括:

常见的并发模型包括:通过加锁的方式来保证在多个进程(线程)之间的共享数据的一致性,如:Java,通过进程间的通讯共享数据的一致性(CSP,Communicating Sequential Process)如:Go

ThreadLocal

ThreadLoacl 将变量的访问限制在当前线程内,不允许变量在多个线程间共享,其内部实现是 JVM 为每个线程维护一个 ThreadLocalMap,这个 map 的 key 是 ThreadLocal 实例本身(弱引用),value 是 ThreadLocal 存储的对象

ThreadLocal 的内存泄漏的根本原因是 ThreadLocal 的生命周期跟线程一样长,所以如果线程不结束(或没有显式的调用 ThreadLocal.remove() 方法),那 ThreadLocal 就会一直存在,导致 ThreadLocal 中存储的对象得不到回收,因此造成内存泄露。

不可变对象

构造不可变对象的方法:

原子操作

单个变量的一致性问题是由于复合操作(compound actions)引起的,如:check-then-act(常见的延迟加载),read-modify-write(常见的 ++ 操作),compare-and-swap 和 put-if-absent(常见的集合操作)。Java 提供了两种方式:volatile 关键字和 java.util.concurrent.atomic,来保证对单个变量操作的原子性,其中 volatile 关键字可以保证当一个线程对 volatile 修饰的变量修改时,其修改对其他线程可见,但 volatile 不能保证复合操作的原子性。 java.util.concurrent.atomic 通过 CAS 操纵来保证对单个变量的操作的原子性

Volatile

JMM(Java Memory model)保证 volatile 变量的值不会被缓存,并且其还会保证对 volatile 变量的操作顺序(happens-before),使用 volatile 变量场景包括:

Atomic

Java 的 java.util.concurrent.atomic 支持 CAS 操作, 包括:

CAS(Compare-And-Swap,比较并交换操作),CAS 是极轻量级的操作,由处理器(硬件)直接实现,Intel 处理器通过 cmpxchg 系列指令实现,而 PowerPC 处理器通过加载并保留和条件存储的指令实现。CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值还是 A 则更新为 B,则 CAS 操作成功(read-modify-write),否则,则证明其他线程(同时)修改了地址 V,此时算法可以对该操作重新计算。

ABA 问题,一个线程 t1 从变量 V 中取出值 A,这时候另一个线程 t2 也从取出值 A,并且 t2 对变量 V 进行了一些操作变成了 B,然后 t2 又将 V 数据变成 A,这时候线程 t1 进行 CAS 操作发现内存中仍然是 A,然后 t1 操作成功。尽管线程 t1 的CAS操作成功,但是不代表这个过程就是没有问题的。

锁机制

Java 的锁机制既可以保证数据的可见性,也可以保证对数据操作的原子性,Java 通过 synchronized 块和 LockReadWriteLock 接口来提供锁机制

其中 synchronized 块是 Java 的内置加锁机制,一个 synchronized 块包括一个锁对象(monitor/intrinsic lock)和一段由其保护的代码块,对 synchronized 方法,其持有的锁对象是方法所在的对象,如果是 static 方法,则其持有的锁对象是其 Class 对象,synchronized 块具有以下特点:

在使用 synchronized 块时,要注意锁对象必须明确且一致

synchronized 块存在以下局限

由于这些局限,Java 提供了更具灵活性的锁机制:LockReadWriteLock 接口

使用锁的最佳实践:只有在 synchronized 块无法满足需求时,才考虑使用 Lock, 如果使用 Lock最后一定要释放锁

Lock lock = ...
lock.lock(); 
try { 
    // do something
} finally { 
    lock.unlock(); 
} 

避免 Liveness Hazard

常见的 Liveness Hazards 问题包括:

T1 -> has lock A -> try to request lock B -> wait forever
T2 -> has lock B -> try to request lock A -> wait forever

在实际开发过程中,以下两种方式容易产生死锁,且不易发现

    // Warning: deadlock-prone! 
    class Taxi { 
        @GuardedBy("this") private Point location, destination; 
        private final Dispatcher dispatcher; 
     
        public Taxi(Dispatcher dispatcher) { 
            this.dispatcher = dispatcher; 
        } 
     
        public synchronized Point getLocation() { 
            return location; 
        } 
     
        public synchronized void setLocation(Point location) { // hold the taxi lock
            this.location = location; 
            if (location.equals(destination)) 
                dispatcher.notifyAvailable(this); // hold the dispatcher lock, the outer method was invoked
        } 
    } 
     
    class Dispatcher { 
        @GuardedBy("this") private final Set<Taxi> taxis; 
        @GuardedBy("this") private final Set<Taxi> availableTaxis; 
     
        public Dispatcher() { 
            taxis = new HashSet<Taxi>(); 
            availableTaxis = new HashSet<Taxi>(); 
        } 
     
        public synchronized void notifyAvailable(Taxi taxi) { 
            availableTaxis.add(taxi); 
        } 
     
        public synchronized Image getImage() { // hold the dispatcher lock
            Image image = new Image(); 
            for (Taxi t : taxis) 
                image.drawMarker(t.getLocation()); // hold the taxi lock, the outer method was invoked
            return image; 
        } 
    }
public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) { 
    synchronized (fromAccount) { 
        synchronized (toAccount) { 
            // ...
        } 
    } 
}

transferMoney(myAccount, yourAccount, 10); // Thread A

transferMoney(yourAccount, myAccount, 20); // Thread B

诊断和避免死锁

可以通过生成 Thread 的 dump 文件来诊断死锁,而避免死锁可以从下面几个方面考虑:

参考

上一篇下一篇

猜你喜欢

热点阅读