1. 线程安全性

1.1 概念

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

1.2 解决方案

多个线程访问可变的变量,导致其值不正确。

修复的方式可以从下面的几个方面入手:

  1. 不在线程之间共享该变量 ThreadLocal + 无状态的类

  2. 将对象变量设置为不可变 不可变对象

  3. 同步访问变量 悲观锁 + 乐观锁

2. 原子性

2.1 概念

“读取-修改-写入”,基于对象之前的状态来定义对象状态的转换。即使是 volatile 修饰的变量,在多线程的环境里面进行自增操作,同样会发生竞态条件,所以volatile不能保证绝对的线程安全(360面试问题)。

引用书中定义:假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B完全执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指:对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。

2.1.1 java 中原子性

1、访问(读/写)某个共享变量的操作从其执行线程以外的线程来看,该操作要么已经执行结果,有么尚未执行,也就是说其他线程不会看到“该操作执行了部分的效果”。 2、访问同一组共享变量的原子操作 不能够被交错的。

  • 在java中实现原子性的两种方式:

使用CAS也是atomic包下的类。

使用锁

  • i++ 和 long/double 的非原子性

在java语言中,除long/double之外的任何类型的变量的写操作都是原子操作。

java语言中任何变量的读操作都是原子操作。

需要注意的是 原子操作 + 原子操作 != 原子操作

例如 i++ 先读后写 读跟写都是原子操作,但是 i++并不是原子操作

2.1 竞态条件

某个计算的正确性取决于多个线程的交替执行的时序。(线程的时序不同,产生的结果可能会不同)

“先检查后执行”,即通过一个可能失效的观测结果来决定下一步的操作。

首先观察到某个条件为真,然后开始执行相关的程序,但是在多线程的运行环境中,条件判断的结果以及开始执行程序中间,观察结果可能变得无效(另外一个线程在此期间执行了相关的动作),从而导致无效。常见的就是(Lazy Singleton)

  • 懒汉加载模式
/**
 * describe: 懒汉式单例模式
 *     优点:只有在需要使用LazySingleton1对象时,才真正生成一个LazySingleton1对象
 *     缺点:会因为某些Java 平台内存模型允许无序写入,使得getInstance方法可能返回
 *           一个尚未执行构造函数的对象
 * Created by tianc on 2017/4/15.
 */
public class LazySingleton {
    private static LazySingleton lazyInstance = null;
    private LazySingleton() {
    }
    public static LazySingleton getInstance(){
        if(lazyInstance == null){
            synchronized (LazySingleton.class){
                if(lazyInstance == null){
                    lazyInstance = new LazySingleton();
                }
            }
        }
        return lazyInstance;
    }
}

3. 加锁

在线程安全的定义中,多个线程间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。当不变性条件中涉及多个变量时,各个变量之间并不是互相独立的,一个变量发生变化会对其他变量的值产生约束。因此,一个变量发生改变,在同一个原子操作里面,其他相关变量也要更新。

3.1 可重入

内置锁:同步代码块(Synchronized Block)包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。关键字Synchronized修饰方法就是一种同步代码块,锁就是方法调用所在的对象,静态的Synchronized方法以Class对象作为锁。内置锁或监视锁就是以对象作为实现同步的锁。

Java内置锁,进入的唯一途径是执行进入由锁保护的同步代码块或方法。它相当于一种互斥锁。

重入锁:当一个持有锁的线程再次请求进入自己持有的锁时,该请求会成功。”重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方式,为每个锁关联一个计数器和线程持有者。

3.2 用锁来保护状态

由于锁能使其保护的代码路径以串行形式访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。

对象的内置锁与其状态之间没有内在的联系,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都要使用同一个锁来保护/同步。

4. 活性

并发应用程序按照及时方式执行的能力称为活性(liveness)[2]。一般包括三种类型的问题死锁、饿死和活锁。

4.1 死锁

线程死锁是并发程序设计中可能遇到的主要问题之一。他是指程序运行中,多个线程竞争共享资源时可能出现的一种系统状态,每个线程都被阻塞,都不会结束,进入一种永久等待状态。

4.2 饿死 

饿死(starvation)描述这样的情况:一个线程不能获得对共享资源的常规访问,并且不能继续工作,当共享资源被贪婪线程长期占有而不可用时,就会发生这样的情况。

4.3 活锁

一个线程经常对另一个线程的操作作出响应,如果另一个线程的操作也对这个线程的操作作出响应,那么就可能导致活锁(livelock)。

和死锁类似,发生活锁的线程不能进行进一步操作。但是,线程没有被锁定,它只是忙于相互响应,以致不能恢复工作

参考资料

  • 线程安全性

【java并发编程实战1】何为线程安全性

https://blog.csdn.net/u014737138/article/details/50970207

http://www.cnblogs.com/mjorcen/p/3966228.html