什么是线程安全
想象一下这样一个场景:你去酒店开了一间房,可是房间没有上锁,任何从你房间路过的人都可以推门而入(如果他们想的话)。在这样的房间里睡觉 安全吗?显然是不安全的。路不拾遗、夜不闭户这种理想状态只会出现在理想中。
同样的,在多线程的情况下,也存在线程安全的问题。我们先理解下下面的代码:
 private static int count = 0;
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每个线程让count自增100次
                    for (int i = 0; i < 100; i++) {
                        count++;
                    }
                }
            }).start();
        }
        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }counut 的输出值是否为 200?答案是否定的。因为 count++ 这个操作不是原子性的。
- 先要从主存获取count值到,然后+1,最后再把结果写到主存。
 
因为不是原子性的,就有可能被会其他线程打断,结果不确定性,这个程序就是线程不安全的。
如何保证线程安全
那么如何保证线程安全?首先说下众所周知的方法:悲观锁、乐观锁。
什么是悲观锁、乐观锁?在 java 语言里,总有一些名词看语义跟本不明白是啥玩意儿,也就总有部分面试官拿着这样的词来忽悠面试者,以此来找优越感,其实理解清楚了,这些词也就唬不住人了。
乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
CAS 机制
CAS 是英文单词 Compare And Swap (Compare And Exchange) 的缩写,翻译过来就是比较并替换。
因为经常配合循环操作,直到完成为止,所以泛指一类操作
cas(v, a, b) ,变量v,期待值a, 修改值b
更新一个变量的时候,只有当变量的预期值(计算结果值V) 和主内存当中的实际值(新值 N)相同时,才会将内存地址对应的值 N修改为 V。
ABA
简单说: 值A 变成 B 再变成 A。最终你只会看到A还是A,但是中间变成 B的过程你并不知道,这可能会产生不确定的影响。
解决:版本号( AtomicStampedReference),基础类型简单值不需要版本号每次被修改都改变版本号,每次读都顺带读出版本号。
原子操作类
轻量级锁,所谓原子操作类也称为无锁或者自旋锁,指的是 java.util.concurrent.atomic 包下,一系列以 Atomic 开头的包装类。例如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。
    private static AtomicInteger count = new AtomicInteger(0);
    ...
                    for (int i = 0; i < 100; i++) {
                        count.incrementAndGet();
                    }使用 AtomicInteger 之后,最终的输出结果同样可以保证是 200。并且在某些情况下,代码的性能会比 Synchronized 更好。
而 Atomic 操作的底层实现正是利用的 CAS 机制。
unsafe 底层实现
AtomicInteger:
public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }
public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }Unsafe:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);CAS底层的的实现方法是c++写的,底层实现:
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}- asm 表示这是汇编指令
 - LOCK_IF_MP:Multi Processors,如果是多个CPU,则前面加LOCK。CAS的关键就是这个LOCK。锁定一个北桥信号。
 - cmpxchg :compare and exchang。(非原子性指令)
 
但是这种自旋(一直霸占着CPU却不做事)是很耗 cpu 资源的,所以一般情况下,都会有线程 sleep。
CAS 的缺点
1.CPU 开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给 CPU 带来很大的压力。
2. 不能保证代码块的原子性
CAS 机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证 3 个变量共同进行原子性的更新,就不得不使用 Synchronized 了。
Synchronized
同步方法:作用于方法时,锁是当前调用该方法的对象,也就是this指向的对象。
同步方法是被共享的,那么该方法的对象相当于所有的线程来说就是唯一的,保证了锁的唯一性。
作用于静态方法时,锁是该方法所在类的class实例对象,该对象可以通过“类名.class”获取。
因为Class的相关数据存储在永久代PermGen(jdk1.8则是metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁。
同步代码块:作用于一个共享实例对象时,锁住的是所有以该对象为锁的代码块。
(这里需要注意:锁住的是该共享实例对象,而不是下面的代码块!锁的是房间门,而不是床!)
一开始的案例我们用Synchronized同步锁, 只需要在 count++ 的位置添加同步锁,让这个自加的过程捆绑具有原子性,其他线程不能插手这个过程。代码如下:
for (int i = 0; i < 100; i++) {
     synchronized (ThreadCas.class){
         count++;
     }Synchronized的实现

它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
 - Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
 - Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
 - OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为OnDeck;
 - Owner:当前已经获取到所资源的线程被称为Owner;
 - !Owner:当前释放锁的线程。
 
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
缺点
Synchronized虽然确保了线程的安全,但是在性能上却不是最优的,Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态(阻塞或者轮询),而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
尽管 Java1.6 为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
锁释放的情况
当线程执行完了代码,释放锁
或者执行发生异常,JVM会让线程自动释放锁。
如果获取到锁的线程在等待IO或者其他原因(比如sleep)被阻塞了,但是又没有释放锁,其他线程只能等着,,,因此需要一种机制可以不让等待的线程无限期的等待。(Lock出现的原因)
Lock
synchronized 无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。JDK5 增加了Lock锁。更加灵活。
Synchronized 与 Lock 的区别
实现层面不一样。synchronized是Java关键字,JVM层面实现加锁和释放锁;
lock是一个接口,在代码层面实现加锁和释放锁。
是否自动释放锁。synchronized在代码执行完或出现异常时自动释放锁;
lock不会自动释放锁,需要在finally代码块显式的释放锁 lock.unlock();
是否一直等待。synchronized会导致拿不到锁的线程一直等待;
lock可以设置尝试获取锁或者获取锁失败一定时间超时。lock.tryLock()
获取锁成功是否可知。synchronized无法得知;
lock可以通过tryLock 返回布尔值得知。
功能复杂性。synchronized 可重入、不可中断、非公平;
lock:可重入、可判断、可公平或者不公平、细分读写锁提高效率。
参考: