线程安全

在一个类内部封装了所有必要的同步,外部对其进行顺序或并发(交替调度执行)的一系列操作(不需要额外的同步)都不会导致该类都实例处于无效状态,则这个类就是线程安全的。

无状态类(不可变性)

无状态对象(不包含变量,也不应用其他对象的域)永远是线程安全的,因为一个线程访问该对象,访问的状态变量都存储在该线程的栈中,而线程间不共享各自栈中的状态,因此不会影响其他线程访问该对象的正确性。
无状态类必须满足三个条件
1 创建时没有逸出
2 所有域都是final类型
3 创建后状态不能再被修改

public class StateLessObject {
    public int add(int a, int b) {
        int c = a + b;
        return c;
    }
}

原子性

若有操作a和b,从执行a的线程角度来看,当其他线程执行b时,要么b全部执行,要么b没有执行,则a和b互为原子操作。

原子变量不使用锁或其他同步机制来保护对其值的并发访问。所有操作都是基于CAS原子操作的。他保证了多线程在同一时间操作一个原子变量而不会产生数据不一致的错误,并且他的性能优于使用同步机制保护的普通变量,譬如说在多线程环境 中统计次数就可以使用原子变量。
利用原子变量保存状态,并且在单一原子操作中更新相互关联的状态变量,这样对于整个类来说便是线程安全的了

public class Counting {
    private final AtomicLong count = new AtomicLong(0);
    public long getCount() {
        return count.get();
    }
    public void addOne() {
        count.incrementAndGet();
    }
}
在java中可以用AtomicReference来管理其他类型的变量

CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。CAS 是一种无锁的非阻塞算法的实现,

1 若线程从内存中读取i的值记录为k,若此时i = 0,则k = 0;
2 记录准备要给i写入的新值 j = k+1 = 1
3 更新内存中的i,此时先进行i和k比较,若i的值还等于k,则写入j,否则不执行任何操作(该操作对应操作系统的一条硬件操作指令,本身具有原子性)

CAS的应用——自旋锁
public class SpinLock {
  private AtomicReference<Thread> sign =new AtomicReference<>();

  public void lock(){
    Thread current = Thread.currentThread();
    while(!sign .compareAndSet(null, current)){
    }
  }

  public void unlock (){
    Thread current = Thread.currentThread();
    sign .compareAndSet(current, null);
  }
}

ABA问题:若线程A将要执行上面的3操作时,线程B把i加1,之后又将i减1,然后A开始执行3时,i依然等于k,于是A会将新值j更新到内存。这种问题对于基本类型值来说影响不大,但对于引用类型就会产生很大影响。Java为了解决ABA问题,提供AtomicStampedReference类进行引用但版本控制,每次有线程修改了引用的值,就会进行版本的更新。

锁(可见性)

可见性:当线程A修改了对象的状态后,其他线程也能看到改变。在没有同步情况下,编译器等对代码的执行可能由重排序现象,因此多线程下需要用同步机制来保证可见性。

内部锁

synchronized块:由锁对象和代码块组成,静态synchronized锁是从Class对象获取,其他就是该方法所在对象本身。

  • 每个java对象都可用于同步的锁,即内部锁(监视器锁),具有互斥性,最多只能有一个线程拥有该对象锁,同一时间就只能有一个线程运行特定锁保护的代码块。
  • 内部锁是可重入的,即线程可以多次获取自己已经占用的锁,如线程A请求一个未占用的锁,此时JVM记录锁占有者,并将请求计数+1,若A再次请求该锁,计数递增;每次A退出同步块,则计数器递减,直到计数器归零,锁被释放。
若锁不可重入,则下面会出现死锁
public class View {
    public synchronized void fun() {
        System.out.println("parent");
    }
}
public class TextView extends View {
    public synchronized void fun() {
        System.out.println("child");
        super.fun();
    }
}
  • 内部锁也可以用来保证内存可见性,为了保证所有线程能看到共享变量的最新值,读取和写入都必须用同一把锁进行同步

锁保护的变量:每个共享的可变变量都需要由唯一确定的锁保护,每次访问该变量时,都必须要获取该锁。比如将变量封装private,且get和set方法都用synchronized修饰。对于每个涉及多个变量的约束,需要用同一个锁来保护其所有变量。

活跃度和性能: 若线程长时间占有锁,则会引发活跃度和性能问题,因此执行耗时操作(网络,io)时,不要占有锁

私有锁对象:相比使用对象内部锁(或其他公共可访问锁),外部代码无法得到它,因此可以更好地规避活跃性风险。

public class PrivateLock {
    private final Object myLock = new Object();
    void fun() {
        synchronized(myLock) {
            // 私有锁同步的代码块
        }
    }
}

显式锁

Java5.0之后提供的ReentrantLock(实现Lock接口),与synchronized一样都可保证互斥和内存可见性,并具有相同的可重入加锁语义,但与内部锁机制不同的是,它具有更多的灵活性,提供可轮训,定时,中断的锁获取操作(避免死锁发生),提供两种锁公平性的选择。所有加锁,解锁方式都是显式的。注意一定要按照规范在finally释放Lock,因为它不会自动释放锁。

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 更新对象状态
    // 捕获异常
} finally {
    lock.unlock();
}
  • 公平性:
    公平锁:请求该锁时,若锁可用,前面已经有线程请求过该锁,则当前线程按照顺序排队等待
    非公平锁:请求该锁时,若锁可用,则当前线程可直接获得该锁
    在激烈的锁竞争情况下,即持有锁的时间短,请求锁的平均时间短,非公平锁性能要好;而当持有锁的时间长,或请求锁的平均间隔长,则使用公平锁比较好。
  • 内部锁和显式锁选取
    内部锁:简洁,且线程转储(thread dump)可识别死锁。没特殊需求还是首选synchronized
    显式锁:注意finally加unlock,定时,轮询,中断,公平队列,非块结构锁(ConcurrentHashMap)
  • 读写锁(ReentrantReadWriteLock)
    互斥(独占锁:写/写,写/读,读/读)过分地限制了并发性,读写锁允许一个资源能被多个读者访问,或被一个写者访问,但这两者不能同时进行。在锁持有时间比较长且大部分操作都不会改变锁守护的资源时,使用读写锁比独占锁具有更好地性能。
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReentrantReadWriteLock实现思想:
1 为读,写两个锁提供可重进入的加锁语义。写锁只有唯一一个所有者
2 可以构造为非公平或公平的,在公平锁时,等待时间最长的线程获得选择权,若锁由读者获得,一个线程请求写锁时,则读者不允许获得读锁,直到写这处理完且已经释放了写锁;在非公平锁时,线程访问顺序不定
3 写者降级为读者是可以的,但读者不可升级为写者(否则会导致死锁)

Volatile变量

是同步的一种弱形式,若a被声明为volatile类型,则编译器与运行时对其操作不会与其他内存操作重排序,也不会缓存在寄存器或其他地方,只在主存读取写入,保证了不同线程对该变量操作的内存可见性。

加锁能保证原子性和可见性,volatile只能保证可见性。比如自增操作时,多线程环境下volatile不能保证结果正确,volatile通常用来作为标记变量使用

发布逸出

安全发布:一个对象发布当时的状态对所有访问线程可见,且其引用也是都可见的
从构造函数内部获取的this对象,都是未完成构造的对象,会导致逸出,只有当构造函数返回后,才处于稳定的状态。可以考虑使用工厂方法防止this引用在构造期间逸出。

public class SafeListener {
    private final EventListener listener;
    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                fun();
            }
        }
    }
    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener;
        source.registerListener(safe.listener);
        returen safe;
    }
}

如果一个对象是可变的,它就必须被安全的发布,通常发布线程和消费线程都必须同步化,对象的引用和对象的状态必须同时对其他线程可见
1 通过静态初始化器初始化对象的引用。因为静态初始化器由JVM在类的初始化阶段执行,由于JVM内部的同步,确保了对象的安全发布

public static Holder holder = new Holder();

2 对象的引用存储到volatile或AtomicRefrence,或被正确创建的对象的final域中,或者由锁保护的域中(Hashtable,ConcurrentMap这些线程安全容器)

线程封闭

线程封闭技术是实现线程安全最简单的方式之一,比如安卓的视图操作只允许主线程操作,JDBC连接池中的Connection只限制在处于请求处理期间的线程中

栈限制:本地变量被限制在线程本地中,存在于执行线程的栈,其他线程无法访问该栈,比如变量放在方法内作为局部变量,而不是放在方法外部。

ThreadLocal:提供了get和set访问器,为每个使用它的线程维护单独的一份数据拷贝,因此get总是返回当前执行线程通过set设置的最新值

在并发程序使用共享对象

1 线程限制:通过限制在一个线程中,只能被占用它的线程修改
2 共享只读:共享的只读对象,没有额外同步,可以被多个线程访问,但是任何线程都不能修改它
3 共享线程安全:在对象内部进行同步,通过公共接口访问
4 被守护:被守护的对象只能通过特定锁访问,比如被线程安全对象封装的对象,被特定锁保护的已发布对象。即使一个对象不是线程安全的,但也可以通过一些机制让它安全地用于多线程程序。把数据封装在对象内部,把对数据的访问限制在对象的方法上,更容易确保线程在访问数据时能获得正确的锁
比如Collections.synchronizedList方法通过装饰器模式,将非线性安全的ArrayList等封装,装饰器将相关接口每个方法都实现为同步方法。
5 必须保证对象的所有同步方法用的锁,都是同一个锁

public class ListHelper<E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());

    public boolean putIfAbsent(E x) {
        synchronized(list) {
            boolean absent = !list.contains(x);
            if(absent) {
                list.add(x);
            }
            return absent;
        }
    }
}