(1)互斥同步(Mutual Exclusion & Synchronization)

同步:保证同一时刻共享数据被一个线程使用(在使用信号量的时候也可以是一些线程)。

互斥:互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥手段。

1)synchronized关键字

Java中最常用的互斥同步手段就是synchronized关键字,synchronized关键字经过编译后会在代码块前后生成monitorenter(锁计数器加1)与monitorexit(锁计数器减1)字节码指令,而这两个指令需要一个引用类型参数指明要锁定和解锁的对象,也就是synchronized(object/Object.class)传入的对象参数,如果没有参数指定,那就看synchronized修饰的是实例方法还是类方法,去取对应的对象实例与Class对象作为锁对象。

Java线程要映射到OS原生线程上,也就是需要从用户态转为核心(系统)态,这个转换可能消耗的时间会很长,尽管VM对synchronized做了一些优化,但还是一种重量级的操作。

2)ReentrantLock

另一个就是java.util.concurrent包下的重入锁(ReentrantLock),与synchronized相似,都具有线程重入(后面会介绍重入概念)特性,但是ReentrantLock有三个主要的不同于synchronized的功能:

等待可中断:持有锁长时间不释放,等待的线程可以选择先放弃等待,改做其他事情。

可实现公平锁:多个线程等待同一个锁时,是按照时间先后顺序依次获得锁,相反非公平锁任何一个线程都有机会获得锁。

锁绑定多个条件:是指ReentrantLock对象可以同时绑定多个Condition对象。

JDK 1.6之后synchronized与ReentrantLock性能上基本持平,但是VM在未来改进中更倾向于synchronized,所以在大部分情况下优先考虑synchronized。

(2)非阻塞同步

1)“悲观”并发策略——非阻塞同步概念

互斥同步主要问题或者说是影响性能的问题是线程阻塞与唤醒问题,它是一种“悲观”并发策略:总是会认为自己不去做相应的同步措施,无论共享数据是否存在竞争它都会去加锁。

而相反有一种“乐观”并发策略,也就是先操作,如果没有其他线程使用共享数据,那操作就算是成功了,但是如果共享数据被使用,那么就会一直不断尝试,直到获得锁使用到共享数据为止(这是最常用的策略),这样的话就线程就根本不需要挂起。这就是非阻塞同步(Non-Blocking Synchronization)

使用“乐观”并发策略需要操作和冲突检测两个步骤具有原子性,而这个原子性只能靠硬件完成,保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成。常用的指令有:测试并设置(Test-and-Set)、获取并增加(Fetch-and-Increment)、交换(Swap)、比较并交换(Compare-and-Swap,CAS)、加载链接/条件储存(Load-Linked/Store-Conditional,LL/SC)

2)CAS介绍

有三个操作数,分别是内存位置V,旧的预期值A和新值B,CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则不更新,但是都会返回V的旧值,整个过程都是一个原子过程。

img

package com.shuhai;


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 描述:
 *
 * @author 含光
 * @email jarvan_best@163.com
 * @date 2021/1/17 17:20
 * @company 数海掌讯
 */
public class CASDemo {
    private static final int THREAD_NUM = 10;//线程数目
    private static final long AWAIT_TIME = 5 * 1000;//等待时间
    public static AtomicInteger race = new AtomicInteger(0);

    public static void increase() {
        race.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exe = Executors.newFixedThreadPool(THREAD_NUM);
        for (int i = 0; i < THREAD_NUM; i++) {
            exe.execute(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            });
        }
        //检测ExecutorService 线程池任务结束并且是否关闭:一般结合shutdown 与awaitTermination 共同使用
        //shutdown 停止接收新的任务并且等待已经提交的任务
        exe.shutdown();
        //awaitTermination等待超时设置,监控ExecutorService 是否关闭
        while (!exe.awaitTermination(AWAIT_TIME, TimeUnit.SECONDS)) {
            System.out.println("线程池没有关闭");
        }
        System.out.println(race);
    }
}

通过观察incrementAndGet()方法源码我们发现用的是jdk中Unsafe类的getAndAddInt方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

通过 do{}while() 循环不断尝试将当前current加1后的新值(mext)赋值(compareAndSwapInt)给自己,如果失败的话就重新循环尝试,值到成功为止返回current值。  

3)CAS的ABA问题

这是CAS的一个逻辑漏洞,比如V值在第一次读取的时候是A值,即没有被改变过,这时候正要准备赋值,但是A的值真没有被改变过吗?

答案是不一定的,因为在检测A值这个过程中A的值可能被改为B最后又改回A,而CAS机制就认为它没有被改变过,这也就是ABA问题,解决这个问题就是增加版本控制变量,但是大部分情况下ABA问题不会影响程序并发的正确性。

(3)无同步方案

“要保障线程安全,必须采用相应的同步措施”这句话实际上是不成立的,因为有些本身就是线程安全的,它可能不涉及共享数据自然就不需要任何同步措施保证正确性。主要有两类:

1)可重入代码(Reentrant Code)

也就是经常所说的纯代码(Pure Code),可以在任何时刻中断它,之后转入其他的程序(当然也包括自身的recursion)。最后返回到原程序中而不会发生任何的错误,即所有可重入的代码都是线程安全的,而所有线程安全的代码都是可重入的

其主要特征是以下几点:

① 不依赖存储在堆(堆中对象是共享的)上的数据和公用的系统资源(方法区中可以共享的数据。比如:static修饰的变量,类的可以相关共享的数据),可以换句话说就是不含有全局变量等;

② 用到的状态由参数形式传入;

③ 不调用任何非可重入的方法。

即可以以这样的原则来判断:我们如果能预测一个方法的返回结果并且方法本身是可预测的,那么输入相同的数据,都会得到相应我们所期待的结果,就满足了可重入性的要求。

2)线程本地存储(Thread Lock Storage)

如果一段代码中所需要的数据必须与其他代码共享,那么能保证将这些共享数据放到同一个可见线程内,那么无须同步也能保证线程之间不存在竞争关系。

在Java中如果一个变量要被多线程访问,可以使用volatile关键字修饰保证可见性,如果一个变量要被某个线程共享,可以通过java.lang.ThreadLocal类实现本地存储的功能。每个线程Thread对象都有一个ThreadLocalMap(key-value, ThreadLocalHashCode-LocalValue),ThreadLocal就是当前线程ThreadLocalMap的入口。

注:这里只是简单了解概念,实际上ThreadLocal部分的知识尤为重要!之后会抽时间细细研究。