多线程与高并发-volatile与CAS

1.volatile

我们来看一下这个小程序,写了一个方法啊,首先定义了一个变量布尔类型等于true,这里模拟的是一个服务器的操作,我的值为true你就给我不间断的运行,什么时候为false你再停止。 测试new Thread启动一个线程,调用m方法,睡了一秒,最后running等于false,运行方法他是不会停止的。 如果你要把volatile打开,那么结果就是启动程序一秒之后他就会m end停止。(volatile就是不停的追踪这个值,时刻看什么时候发生了变化)。

/**
 * volatile 关键字,使一个变量在多个线程间可见
 * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未 必知道
 * 使用volatile关键字,会让所有线程都会读到变量的修改值
 * 在下面的代码中,running是存在于堆内存的t对象中
 * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个 copy,并不会每次都去
 * 读取堆内存,这样,当线程修改running的值之后,t1线程感知不到,所以不会停止运行
 * 使用volatile,将会强制所有线程都会去堆内存中读取running的值
 * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能 替代synchronized
 */
public class T01_HelloVolatile {
    /*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行 结果的区别
    void m() {
        System.out.println("m start");
        while (running) {
        }
        System.out.println("m end!");
    }
    public static void main(String[] args) {
        T01_HelloVolatile t = new T01_HelloVolatile();
        new Thread(t::m, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running = false;
    }
}

解析没有volatile时,线程t1读取的running的值是线程空间copy的值,main线程中若修改的话,t1是感知不到的,加上volatile后,会让t1线程每次从内存中获取值,这样一旦running的值发生改变就会读取到。

1.1volatile的作用

A::保证线程的可见性
大家知道java里面是有堆内存的,堆内存是所有线程共享里面的内存,除了共享的内存之外呢,每个线程都有自己的专属的区域,都有自己的工作内存,如果说在共享内存里有一个值的话,当我们线程,某一个线程都要去访问这个值的时候,会将这个值copy一份,copy到自己的这个工作空间里头,然后对这个值的任何改变,首先是在自己的空间里进行改变,什么时候写回去,就是改完之后会马上写回去。什么时候去检查有没有新的值,也不好控制。
在这个线程里面发生的改变,并没有及时的反应到另外一个线程里面,这就是线程之间的不可见 ,对这个变量值加了volatile之后就能够保证 一个线程的改变,另外一个线程马上就能看到。
大家可以去查这个词:MESI ,他的本质上是使用了cpu的一个叫做 高速缓存一致性协议。
B::禁止指令重排序
指令重排序也是和cpu有关系,每次写都会被线程读到,加了volatile之后。cpu原来执行一条指令的时候它是一步一步的顺序的执行,但是现在的cpu为了提高效率,它会把指令并发的来执行,第一个指令执行到一半的时候第二个指令可能就已经开始执行了,这叫做流水线式的执行。在这种新的架构的设计基础之上呢想充分的利用这一点,那么就要求你的编译器把你的源码编译完的指令之后呢可能进行一个指令的重新排序。
这个是通过实际工程验证了,不仅提高了,而且提高了很多。

1.2 DCL单例模式

我们来聊一聊什么是单例,单例的意思就是我保证你在JVM的内存里头永远只有某一个类的一个实例,
其实这个很容易理解,在我们工程当中有一些类真的没有必要new好多个对象,比如说权限管理者。
单例最简单的写法就是下面这种写法,是说我有一个类,定义了这个类的一个对象,然后一个对象呢是在个类的内部的,同时我把Mgr01()这个类的构造方法设置成private意思就是别的不要去new我,只有我自己能new,理论上来说我就只有自己一个实例了,通过getInstance()访问这个实例,所以无论你调用多少次的getInstanc()本质上它就只有这一个对象,这种写法非常简洁也很容易理解,由JVM来保证永远只有这一个实例。

/**
 * 饿汉式
 * 类加载到内存后,被实例化一个单例 JVM保证线程安全
 * 缺点,不管用到与否,类装载时就完成实例化
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01() {
    }

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
    }
}

但是有的人吹毛求疵,他会说我还没开始用这个对象呢,没用这个对象调这个方法你干嘛把他初始化了,你能不能什么时候开始用,调这个方法的时候你再给我初始化。所以呢,下面代码这个是和上一种一样的写法。

public class Mgr02 {
    private static final Mgr02 INSTANCE;

    static {
        INSTANCE = new Mgr02();
    }

    private Mgr02() {
    }

    public static Mgr02 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr02 m1 = Mgr02.getInstance();
        Mgr02 m2 = Mgr02.getInstance();
        System.out.println(m1 == m2);
    }
}

还有人提出了要求,么时候我开始调用这个getInstace()的时候,我才对它进行初始化。当然,这个不要对它进行初始化两次,只能初始化一次才对,不然就成了俩对象了吗,所以上来之后先判断 INSTANCE == null的话我才初始化。不过,更加吹毛求疵的事情又来了,我不单要求你我用的时候才进行初始化,我还要求你线程安全。显然我们下面03这个是不保证线程安全的,所以你多个线程访问的时候它一定会出问题,下来你自己可以实验实验。结果中会存在多个不同的单例。

/**
 *懒汉式
 */
public class Mgr03 {
    private static Mgr03 INSTANCE;

    private Mgr03() {
    }

    public static Mgr03 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(Mgr03.getInstance().hashCode())).start();
        }
    }
}

所要怎么做呢,我们要加一个synchronized解决,加一把锁嘛,public static synchronized 这句话一旦加上就没问题了,因为这个里面从头到尾就只有一个线程运行,第一个线程发现它为空给它new了,第二个线程他无论怎么访问这个值已经永远不可能为空了,它只能是拿原来第一个线程初始化的部分,这是没问题的

public class Mgr04 {
    private static Mgr04 INSTANCE;

    private Mgr04() {
    }

    public synchronized static Mgr04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(Mgr04.getInstance().hashCode())).start();
        }
    }
}

开始进一步的吹毛求疵synchronized一下加在方法上这个代码太长了,说不定里面还有其他的业务逻
辑,对于加锁这个事情,代码能锁的少的就要尽量锁的少。那么通过进一步的吹毛求疵又有了新的写法。

如下代码:线程判断,先别加锁,判断是否为空,如果为空在加锁初始化,更细粒度的一个锁,这叫做
锁细化,也是锁优化的一步。很不幸的是这个写法是不对的,我们分析一下,第一个线程判断它为空,
还没有执行下面的过程第二个线程来了,也判断它为空。第一个线程对它进行了加锁,synchronized完
了之后呢把锁释放了,而第二个线程也是判断为空拿到这把锁也初始化了一遍,所以这种写法是有问题
的。

public class Mgr05 {
    private static Mgr05 INSTANCE;

    private Mgr05() {
    }

    public static Mgr05 getInstance() {
        if (INSTANCE == null) { //妄图通过减小同步代码块的方式提高效率,然后不可行
            synchronized (Mgr05.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(Mgr05.getInstance().hashCode())).start();
        }
    }
}

看运行结果发现并不是单例,
所以这就产生了,我们今天要讲的volatile这个问题,这个问题是这样来产生的,看下面代码,叫做双重
检查锁或者叫双重检查的单例,在这种双重检查判断的情况下刚才上面的说的线程问题就不会再有了,分析一下:第一个线程来了判断ok,你确实是空值,然后进行下面的初始化过程,假设第一个线程把这个INSTANCE已经初始化了,第二个线程,第一个线程检查等于空的时候第二个线程检查也等于空,所以第二个线程在 if(INSTANCE == null) 这句话的时候停住了,暂停之后呢第一个线程已经把它初始化完了释放锁,第二个线程继续往下运行,往下运行的时候它会尝试拿这把锁,第一个线程已经释放了,它是可以拿到这把锁的,注意,拿到这把锁之后他还会进行一次检查,由于第一个线程已经把INSTANCE初始化了所以这个检查通过了,它不会在重新new一遍。因次,双重检查这个事儿是能够保证线程安全的。

public class Mgr06 {
    private static /*volatile*/ Mgr06 INSTANCE;

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) { 
            synchronized (Mgr06.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //双重检查
                if(INSTANCE==null){
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> System.out.println(Mgr06.getInstance().hashCode())).start();
        }
    }
}

就这个程序无论你运行多少遍,就算你在高并发的情况下运行,拿一百台机器同时访问这一台机子上的
getInstance(),每个机器上跑个一万个线程,使劲儿跑,ok,这个程序运行的结果也会是正确的。

好,那么会有人会说要不要加volatile?
这是一道面试题:**你听说过单例模式吗,单例模式里面有一种叫双重检查的你了解吗,这个单例要不要加volatile?**答案是要加的,我们这个实验很难做出来让它出错的情况,所以以前很多人就不加这个volatile他也不会出问题,不加volatile问题就会出现在指令重排序上,第一个线程 INSTANCE = new Mgr06()经过我们的编译器编译之后呢的指令呢是分成三步 1.给指令申请内存 2.给成员变量初始化 3.是把这块内存的内容赋值给INSTANCE。既然有这个值了你在另外一个线程里头上来先去检查,你会发现这个值已经有了,你根本就不会进入锁那部分的代码。加了volatile会怎么样呢,加了volatile指令重排序就不允许存在了。对这个对象上的指令重排序不允许存在,所以在这个时候一定是保证你初始化完了之后才会赋值给你这个变量,ok 这是volatile的含义。

以上就是通过单例模式中衍生出的关于volatile的特性的使用。

1.3 volatile与synchronized的区分

下面这个程序,如果不加volatile是一定会有问题的, 我们通过debugger方式运行一下,总会出现number的值不是100的情况,原因很简单,number值被改后只是被其他线程看到,但是number++的操作不是一个原子性的操作,所以会出现俩个线程同时修改的问题,即没有保证原子性。所以synchronized是不可缺少的,也就是说volatile并不能替代synchronized。

public class VolatileDemo {
    //volatile并不能保证多个线程共同修改number变量时所带来的的不一致问题,也就是说volatile不能 替代synchronized
    private volatile int number = 0;

    public int getNumber() {
        return this.number;
    }

    public void increase() {
        this.number++;
    }

    public static void main(String[] args) {
        final VolatileDemo volDemo = new VolatileDemo();
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    volDemo.increase();
                }
            }).start();
        }

        //如果还有子线程在运行,主线程就让出CPU资源
        //直到所有的子线程都运行完了,主线程再继续往下执行
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }

        System.out.println("number:" + volDemo.getNumber());
    }
}

OK,我们再试试加上synchronized后的效果是啥样的,可以加上断点模拟延时,但值永远都是100,每次的操作都会先获取锁,若其他线程不释放锁,当前线程就不能进行操作,即保证了原子性。

锁优化其中有一个叫做把锁粒度变细 ,还有一个叫把锁粒度变粗,其实说的是一回事儿,什么意思呢,作为synchronized来说你这个锁呢征用不是很剧烈的前提下,你这个锁呢,粒度最好还是小一些。

下面程序是什么意思,如果是说m1方法他前面有一堆业务逻辑,后面有一堆业务逻辑,这个业务逻辑我用sleep来模拟了它,那么中间是你需要加锁的代码,那这个时候你不应该把锁加在整个方法上,只应该加在count++上(参见m2),这很简单就叫做锁的细化。那什么时候需要将锁粗化呢,在征用特别频繁,由于你锁的粒度越变越细,好多小的细锁跑在你这个上面,这个方法,或者某一段业务逻辑里头,好,那你干脆不如弄成一把大锁,他的征用反而就没有那么频繁了,程序写的好,不会发生死锁。

public class FineCoarseLock {
    int count = 0;

    synchronized void m1() { //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        count++;
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    void m2() {
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        //采用细粒度的锁,可以使线程争用时间变短,从而提高效率
        synchronized (this) {
            count++;
        }
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

下面有一个小概念,你在某一种特定的不小心的情况下你把o变成了别的对象了,这个时候线程的并发就会出问题。锁是在对象的头上标记的,你这线程本来大家都去访问,结果突然把这把锁变成别的对象,去访问别的对象了,这俩之间就没有任何关系了。因此,以对象作为锁的时候不让它发生改变,加final。

/**
 * 锁定某对象o,如果o的属性发生改变,不影响锁的使用
 * 但是如果o变成另外一个对象,则锁定的对象发生改变
 * 应该是避免将锁定对象的引用变成另外的对象
 */
public class T {
    final   Object o = new Object();

    void m() {
        synchronized (o) {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //创建第二个线程
        Thread t2 =new Thread(t::m,"t2");
       // t.o=new Object();//锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永 远得不到执行机会
        t2.start();
    }

}

好,那么倒现在为止,我们volatile和synchronized都已经基本讲完了,稍微简单的回顾一下。

  1. synchronized锁的是对象而不得代码,锁方法锁的是this,锁static方法锁的是class,锁定方法和
    非锁定方法是可以同时执行的,锁升级从偏向锁到自旋锁到重量级锁
  2. volatile 保证线程的可见性,同时防止指令重排序。线程可见性在CPU的级别是用缓存一直性来保
    证的;禁止指令重排序CPU级别是你禁止不了的,那是人家内部运行的过程,提高效率的。但是在
    虚拟机级别你家volatile之后呢,这个指令重排序就可以禁止。严格来讲,还要去深究它的内部的
    话,它是加了读屏障和写屏障,这个是CPU的一个原语。

2.CAS

cas号称是无锁优化,或者叫自旋。这个名字无所谓,理解它是干什么的就行,概念这个东西是人为了
描述问题解决问题而定义出来的,所以怎么定义不是很重要,重点是在解决问题上。

我们通过Atomic类(原子的)。由于某一些特别常见的操作,老是来回的加锁,加锁的情况特别多,所
以干脆java就提供了这些常见的操作这么一些个类,这些类的内部就自动带了锁,当然这些锁的实现并不是synchronized重量级锁,而是CAS的操作来实现的(号称无锁)。

我们来举例几个简单的例子,凡是以Atomic开头的都是用CAS这种操作来保证线程安全的这么一些个类。AtomicInteger的意思就是里面包了一个Int类型,这个int类型的自增 count++ 是线程安全的,还有拿值等等是线程安全的,由于我们在工作开发中经常性的有那种需求,一个值所有的线程共同访问它往上递增 ,所以jdk专门提供了这样的一些类。使用方法AtomicInteger如下代码

public class T01_AtomicInteger {
    AtomicInteger count = new AtomicInteger(0);

    void m() {
        for (int i = 0; i < 10000; i++) {
            count.incrementAndGet();
        }
    }

    public static void main(String[] args) {
        T01_AtomicInteger t = new T01_AtomicInteger();
        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }
        threads.forEach((o) -> o.start());
        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

好,我们来分许分析,它的内部实现的原理,主要是聊原理,它的用法看看API都会用。这个原理叫
CAS操作,incrementAndGet() 调用了getAndAddInt

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

当然这个也是一个CompareAndSetInt操作

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;
}

它的内部调调调,就会跑到Unsafe类去(不安全的)。也就是说AtomicInteger它的内部是调用了Unsafe这个类里面的方法CompareAndSetI(CAS),说一下字面意思,比较并且设定。这个比较并且设定的意思是什么呢,我原来想改变某一个值0 ,我想把它变成1,但是其中我想做到线程安全,就只能加锁synchronized ,不然线程就不安全。我现在可以用另外一种操作来替代这把锁,就是cas操作,你可以把它想象成一个方法,这个方法有三个参数,cas(V,Expected,NewValue)。
V第一个参数是要改的那个值;Expected第二个参数是期望当前的这个值会是几;NewValue要设定的新值。当前这个线程想改这个值的时候我期望你这值就是0,你不能是个1,如果是1就说明我这值不对,然后想把你变成1。这句话说的是什么意思呢,比如原来这个值变成3了,我这个线程想改这个值的时候我一定期望你现在是3 ,是3我才改,如果你在我改的过程中变成4了,那你跟我的期望值就对不上了,说明有另外一个线程改了这个值了,那我这个cas就重新在试一下,再试的时候我希望你这个值是4,在修改的时候期望值是4,没有其他的线程修改这个值,那好,我给你改成5,这就是cas操作,在本质上就是这么一个意思。

Expected如果对的上期望值,NewValue才会去对其修改,进行新的值设定的时候,这个过程之中来了一个线程把你的值改变了怎么办,我就可以再试一遍,或者失败,这个是cas操作。

当你判断的时候,发现是我期望的值,还没有进行新值设定的时候值发生了改变怎么办,cas是cpu的原语支持,也就是说cas操作是cpu指令级别上的支持,中间不能被打断。

ABA问题

一般的面试会问一下,了解这个ABA问题吗?
这个ABA问题是这样的,假如说你有一个值,我拿到这个值是1,想把它变成2,我拿到1用cas操作,期
望值是1,准备变成2,这个对象Object,在这个过程中,没有一个线程改过我肯定是可以更改的,但是
如果有一个线程先把这个1变成了2后来又变回1,中间值更改过,它不会影响我这个cas下面操作,这就
是ABA问题。
这种问题怎么解决。如果是int类型的,最终值是你期望的,也没有关系,这种没关系可以不去管这个问
题。如果你确实想管这个问题可以加版本号,做任何一个值的修改,修改完之后加一,后面检查的时候
连带版本号一起检查。
如果是基础类型:无所谓。不影响结果值;
如果是引用类型:就像是你的女朋友和你分手之后又复合,中间经历了别的男人。

ReentrantLock

第一种锁比较新鲜可重入锁ReentranLlock,synchronized本身就是可重入锁的一种,什么叫可重入,
意思就是我锁了一下还可以对同样这把锁再锁一下,synchronized必须是可重入的,不然的话子类调用
父类是没法实现的,看下面这个小程序是这样写的,m1方法里面做了一个循环每次睡1秒钟,每隔一秒
种打印一个。接下来调m2,是一个synchronized方法也是需要加锁的,我们来看主程序启动线程m1,
一秒钟后再启动线程m2。分析下这个执行过程在第一个线程执行到一秒钟的时候第二个线程就会起
来,假如我们这个锁是不可重入的会是什么情况,第一个线程申请这把锁,锁的这个对象,然后这里如
果是第二个线程来进行申请的话,他start不了,必须要等到第一个线程结束了,因为这两个是不同的线
程。两个线程之间肯定会有争用,可以在m1里面调用m2就可以,synchronized方法是可以调用
synchronized方法的。锁是可重入的。

public class T01_ReentrantLock1 {
    synchronized void m1() {
        for (int i = 0; i < 10; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
            if (i == 2) m2();
        }
    }

    synchronized void m2() {
        System.out.println("m2 ...");
    }

    public static void main(String[] args) {
        T01_ReentrantLock1 rl = new T01_ReentrantLock1();
        new Thread(rl::m1).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //new Thread(rl::m2).start();
    }
}

子类和父类如果是synchronized(this)就是同一把锁,同一个this当然是同一把锁。

ReentrantLock是可以替代synchronized的,怎么替代呢,看如下代码,原来写synchronized的地方换写lock.lock(),加完锁之后需要注意的是记得lock.unlock()解锁,由于synchronized是自动解锁的,大括号执行完就结束了。lock就不行,lock必须得手动解锁,手动解锁一定要写在try…fifinally里面保证最好一定要解锁,不然的话上锁之后中间执行的过程有问题了,死在那了,别人就永远也拿不到这把锁了。

public class T02_ReentrantLock2 {
    Lock lock = new ReentrantLock();

    void m1() {
        try {
            lock.lock(); //synchronized(this)
            for (int i = 0; i < 10; i++) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    void m2() {
        try {
            lock.lock();
            System.out.println("m2 ...");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        T02_ReentrantLock2 rl = new T02_ReentrantLock2();
        new Thread(rl::m1).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(rl::m2).start();
    }
}

可能有同学会说reentrantlock既然和synchronized差不多的话,那我们要它有什么用呢,ReentrantLock有一些功能还是要比synchronized强大的,强大的地方,你可以使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行,synchronized如果搞不定的话他肯定就阻塞了,但是用ReentrantLock你自己就可以决定你到底要不要wait。

下面程序 就是说比如5秒钟你把程序执行完就可能得到这把锁,如果得不到就不行。由于我的第一个线
程跑了10秒钟,所以你在第二个线程里申请5秒肯定是那不到的,把循环次数减少就可以能拿到了。

public class T03_ReentrantLock3 {
    Lock lock = new ReentrantLock();

    void m1() {
        try {
            lock.lock();
            for (int i = 0; i < 3; i++) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行
     * 可以根据tryLock的返回值来判断是否锁定
     * 也可以指定tryLock的时间
     */
    void m2() {
        /*boolean locked = lock.tryLock(); System.out.println("m2 ..." + locked); if(locked) lock.unlock(); */
        boolean locked = false;
        try {
            locked = lock.tryLock(5, TimeUnit.SECONDS);
            System.out.println("m2 ..." + locked);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (locked) lock.unlock();
        }
    }

    public static void main(String[] args) {
        T03_ReentrantLock3 rl = new T03_ReentrantLock3();
        new Thread(rl::m1).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(rl::m2).start();
    }
}

当然除了这个之外呢,ReentrantLock还可以用lock.lockInterruptibly() 这个类,对interrupt()方法做出相应,可以被打断的加锁,如果以这种方式加锁的话我们可以调用一个 t2.interrupt(); 打断线程2的等待。 线程1 上来之后加锁,加锁之后开始睡,睡的没完没了的,被线程1拿到这把锁的话,线程2如果说在想拿到这把锁不太可能,拿不到锁他就会在哪儿等着,如果我们使用原来的这种lock.lock() 是打断不了它的,那么我们就可以用另外一种方式lock.lockInterruptibly() 这个类可以被打断的,当你要想停止线程2就可以用interrupt() ,这也是ReentrantLock比synchronized好用的一个地方。

public class T04_ReentrantLock4 {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            try {
                lock.lock();
                System.out.println("t1 start");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                System.out.println("t1 end");
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
            } finally {
                lock.unlock();
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            try {
                //lock.lock();
                lock.lockInterruptibly();//可以对interrupt()方法做出相应
                System.out.println("t2 start");
                TimeUnit.SECONDS.sleep(5);
                System.out.println("t2 end");
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
            } finally {
                lock.unlock();
            }
        });
        t2.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.interrupt();//打断线程2的等待
    }
}

ReentrantLock还可以指定为公平锁,公平锁的意思是当我们new一个ReentrantLock你可以传一个参数为true,这个true表示公平锁,公平锁的意思是谁等在前面就先让谁执行,而不是说谁后来了之后就马上让谁执行。如果说这个锁不公平,来了一个线程上来就抢,它是有可能抢到的,如果说这个锁是个公平锁,这个线程上来会先检查队列里有没有原来等着的,如果有的话他就先进队列里等着别人先运行,这是公平锁的概念。
ReentrantLock默认是非公平锁。

我们稍微回顾一下: Reentrantlock vs synchronized
ReentrantLock可以替代synchronized这是没问题的,他也可以重入,可以锁定的。本身的底层是cas。
trylock:自己来控制,我锁不住该怎么办。
lockInterruptibly:这个类,中间你还可以被打断。
它还可以公平和非公平的切换。
现在除了synchronized之外,多数内部都是用的cas。当我们聊这个AQS的时候实际上它内部用的是
park和unpark,也不是全都用的cas,他还是做了一个锁升级的概念,只不过这个锁升级做的比较隐秘,
在你等待这个队列的时候如果你拿不到还是进入一个阻塞的状态,前面至少有一个cas的状态,他不像
原先就直接进入阻塞状态了。

CountDownLatch

CountDown叫倒数,Latch是门栓的意思(倒数的一个门栓,5、4、3、2、1数到了,我这个门栓就开
了)
看下面的小程序,这小程序叫usingCountDownLatch,new了100个线程,接下来,又来了100个数量
的CountDownLatch,什么意思,就是,这是一个门栓,门栓上记了个数threads.length是100,每一
个线程结束的时候我让 latch.countDown(),然后所有线程start(),再latch.await(),最后结束。那
CountDown是干嘛使得呢,看latch.await(),它的意思是说给我看住门,给我插住不要动。每个线程执
行到latch.await()的时候这个门栓就在这里等着,并且记了个数是100,每一个线程结束的时候都会往
下CountDown,CountDown是在原来的基础上减1,一直到这个数字变成0的时候门栓就会被打开,这
就是它的概念,它是用来等着线程结束的。

用join实际上不太好控制,必须要你线程结束了才能控制,但是如果是一个门栓的话我在线程里不停的
CountDown,在一个线程里就可以控制这个门栓什么时候往前走,用join我只能是当前线程结束了你才
能自动往前走,当然用join可以,但是CountDown比它要灵活。

我们考虑一个场景:用户购买一个商品下单成功后,我们会给用户发送各种消息提示用户『购买成功』,比如发送邮件、微信消息、短信等。所有的消息都发送成功后,我们在后台记录一条消息表示成功。
单线程的话效率太低,多线程实现就要考虑要等到所有消息发送成功后才可以提示。那多线程可以这么设计订单操作是主线程,发送消息是多个子线程,所有子线程完成后,订单才可成功。这样CountDown就可以用了。

public class OrderServiceDemo {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("main thread: Success to place an order");

        int count = 3;
        CountDownLatch countDownLatch = new CountDownLatch(count);

        Executor executor = Executors.newFixedThreadPool(count);
        executor.execute(new MessageTask("email", countDownLatch));
        executor.execute(new MessageTask("wechat", countDownLatch));
        executor.execute(new MessageTask("sms", countDownLatch));

        // 主线程阻塞,等待所有子线程发完消息
        countDownLatch.await();
        // 所有子线程已经发完消息,计数器为0,主线程恢复
        System.out.println("main thread: all message has been sent");
    }

    static class MessageTask implements Runnable {
        private String messageName;
        private CountDownLatch countDownLatch;

        public MessageTask(String messageName, CountDownLatch countDownLatch) {
            this.messageName = messageName;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                // 线程发送消息
                System.out.println("Send " + messageName);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                // 发完消息计数器减 1
                countDownLatch.countDown();
            }
        }
    }
}

对比注释countDownLatch.await();前后的结果,可以看到作为计时器的效果。

CyclicBarrier

来讲这个同步工具叫CyclicBarrier,意思是循环栅栏,这有一个栅栏,什么时候人满了就把栅栏推倒,哗啦哗啦的都放出去,出去之后扎栅栏又重新起来,再来人,满了,推倒之后又继续。
下面程序,两个参数,第二个参数不传也是可以的,就是满了之后不做任何事情。第一个参数是20,满了之后帮我调用第二个参数指定的动作,我们这个指定的动作就是一个Runnable对象,打印满人,发车。什么barrier.await()会被放倒,就是等够20个人了,后面也可以写你要做的操作 s。什么时候满了20人了就发车。下面第一种写法是满了之后我什么也不做,第二种写法是用Labda表达式的写法。这个意思就是线程堆满了,我们才能往下继续执行。
举例:CyclicBarrier的概念呢比如说一个复杂的操作,需要访问 数据库,需要访问网络,需要访问文件,有一种方式是顺序执行,挨个的都执行完,效率非常低,这是一种方式,还有一种可能性就是并发执行,原来是1、2、3顺序执行,并发执行是不同的线程去执行不同的操作,有的线程去数据库找,有的线程去网络访问,有的线程去读文件,必须是这三个线程全部到位了我才能去进行,这个时候我们就可以用CyclicBarrier。

public class T07_TestCyclicBarrier {
    public static void main(String[] args) {
        //CyclicBarrier barrier = new CyclicBarrier(20);
        CyclicBarrier barrier = new CyclicBarrier(20, () -> System.out.println("满人"));
    /*    CyclicBarrier barrier = new CyclicBarrier(20, new Runnable() {
            @Override
            public void run() {
                System.out.println("满人,发车");
            }
        });*/

        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Phaser

Phaser是按照不同的阶段来对线程进行执行,就是它本身是维护着一个阶段这样的一个成员变量,当前
我是执行到那个阶段,是第0个,还是第1个阶段啊等等,每个阶段不同的时候这个线程都可以往前走,有的线程走到某个阶段就停了,有的线程一直会走到结束。你的程序中如果说用到分好几个阶段执行 ,而且有的人必须得几个人共同参与的一种情形的情况下可能会用到这个Phaser。
有种情形很可能用到,如果你写的是遗传算法,遗传算法是计算机来模拟达尔文的进化策略所发明的一
种算法,当你去解决这个问题的时候这个Phaser是有可能用的上的。这个东西更像是CyclicBarrier,栅栏这个东西是一个一个的栅栏,他原来是一个循环的栅栏,循环使用,但是这个栅栏是一个栅栏一个栅栏的。

好,来看我们自己模拟的一个小例子。模拟了一个结婚的场景,结婚是有好多人要参加的,因此,我们
写了一个类Person是一个Runnable可以new出来,扔给Thread去执行,模拟我们每个人要做一些操作,有这么几种方法,arrive()到达、eat()吃、leave()离开、hug()拥抱这么几个。作为一个婚礼来说它会分成好几个阶段,第一阶段大家好都得到齐了,第二个阶段大家开始吃饭,三阶段大家离开,第四个阶段新郎新娘入洞房,那好,每个人都有这几个方法,在方法的实现里头我就简单的睡了1000个毫秒,我自己写了一个方法,把异常处理写到了方法里了。

public class T09_TestPhaser2 {
    static Random r = new Random();
    static MarriagePhaser phaser = new MarriagePhaser();

    static void milliSleep(int milli) {
        try {
            TimeUnit.MILLISECONDS.sleep(milli);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        phaser.bulkRegister(7);
        for (int i = 0; i < 5; i++) {
            new Thread(new Person("p" + i)).start();
        }
        new Thread(new Person("新郎")).start();
        new Thread(new Person("新娘")).start();
    }


    static class MarriagePhaser extends Phaser {
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {
            switch (phase) {
                case 0:
                    System.out.println("所有人都到齐了!" + registeredParties);
                    System.out.println();
                    return false;
                case 1:
                    System.out.println("所有人都吃完了!" + registeredParties);
                    System.out.println();
                    return false;
                case 2:
                    System.out.println("所有人都离开了!" + registeredParties);
                    System.out.println();
                    return false;
                case 3:
                    System.out.println("婚礼结束!新郎新娘抱抱!" + registeredParties);
                    return true;
                default:
                    return true;
            }
        }
    }

    static class Person implements Runnable {
        String name;

        public Person(String name) {
            this.name = name;
        }

        public void arrive() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 到达现场!\n", name);
            int i = phaser.arriveAndAwaitAdvance();
            System.out.println("到达现场后,阶段为"+i);
        }

        public void eat() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 吃完!\n", name);
            int i = phaser.arriveAndAwaitAdvance();
            System.out.println("吃完后,阶段为"+i);
        }

        public void leave() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 离开!\n", name);
            int i = phaser.arriveAndAwaitAdvance();
            System.out.println("离开后,阶段为"+i);
        }

        private void hug() {
            if (name.equals("新郎") || name.equals("新娘")) {
                milliSleep(r.nextInt(1000));
                System.out.printf("%s 洞房!\n", name);
                int i = phaser.arriveAndAwaitAdvance();
                System.out.println(i);
            } else {
                int i = phaser.arriveAndDeregister();
                System.out.println("此时为"+i);
                //phaser.register()
            }
        }

        @Override
        public void run() {
            arrive();
            eat();
            leave();
            hug();
        }
    }
}

在看主程序,一共有五个人参加婚礼了,接下来新郎,新娘参加婚礼,一共七个人。它一start就好调用
我们的run()方法,它会挨着牌的调用每一个阶段的方法。那好,我们在每一个阶段是不是得控制人
数,第一个阶段得要人到期了才能开始,二阶段所有人都吃饭,三阶段所有人都离开,但是,到了第四
阶段进入洞房的时候就不能所有人都干这个事儿了。所以,要模拟一个程序就要把整个过程分好几个阶
段,而且每个阶段必须要等这些线程给我干完事儿了你才能进入下一个阶段。
那怎么来模拟过程呢,我定义了一个phaser,我这个phaser是从Phaser这个类继承,重写onAdvance
方法,前进,线程抵达这个栅栏的时候,所有的线程都满足了这个第一个栅栏的条件了onAdvance会被自动调用,目前我们有好几个阶段,这个阶段是被写死的,必须是数字0开始,onAdvance会传来两个参数phase是第几个阶段,registeredParties是目前这个阶段有几个人参加,每一个阶段都有一个打印,返回值false,一直到最后一个阶段返回true,所有线程结束,整个栅栏组,Phaser栅栏组就结束了。
我怎么才能让我的线程在一个栅栏面前给停住呢,就是调用phaser.arriveAndAwaitAdvance()这个方法,这个方法的意思是到达等待继续往前走,直到新郎新娘如洞房,其他人不在参与,调用phaser.arriveAndDeregister() 这个方法。还有可以调用方法phaser.register()往上加,不仅可以控制栅栏上的个数还可以控制栅栏上的等待数量,这个就叫做phaser

ReadWriteLock

这个ReadWriteLock 是读写锁。读写锁的概念其实就是共享锁和排他锁读锁就是共享锁写锁就是排他锁。那这个是什么意思,我们先要来理解这件事儿,读写有很多种情况,比如说你数据库里的某条儿数据你放在内存里读的时候特别多,而改的时候并不多。
举一个简单的例子,我们公司的组织结构,我们要想显示这组织结构下有哪些人在网页上访问,所以这个组织结构被访问到会读,但是很少更改,读的时候多写的时候就并不多,这个时候好多线程来共同访问这个结构的话,有的是读线程有的是写线程,要求他不产生这种数据不一致的情况下我们采用最简单的方式就是加锁,我读的时候只能自己读,写的时候只能自己写,但是这种情况下效率会非常的底,尤其是读线程非常多的时候,那我们就可以做成这种锁,当读线程上来的时候加一把锁是允许其他读线程可以读,写线程来了我不给它,你先别写,等我读完你在写。读线程进来的时候我们大家一块读,因为你不改原来的内容,写线程上来把整个线程全锁定,你先不要读,等我写完你在读。

我们看这个读写锁怎么用,我们这有两个方法,read()读一个数据,write()写一个数据。read这个数据的时候我需要你往里头传一把锁,这个传那把锁你自己定,我们可以传自己定义的全都是排他锁,也可以传读写锁里面的读锁或写锁。write的时候也需要往里面传把锁,同时需要你传一个新值,在这里值里面传一个内容。我们模拟这个操作,读的是一个int类型的值,读的时候先上锁,设置一秒钟,完了之后read over,最后解锁unlock。再下面写锁,锁定后睡1000毫秒,然后把新值给value,write over后解锁,非常简单。
我们现在的问题是往里传这个lock有两种传法,第一种直接new ReentrantLock()传进去,分析下这种方法,主程序定义了一个Runnable对象,第一个是调用read() 方法,第二个是调用write() 方法同时往里头扔一个随机的值,然后起了18个读线程,起了两个写线程,这个两个我要想执行完的话,我现在传的是一个ReentrantLock,这把锁上了之后没有其他任何人可以拿到这把锁,而这里面每一个线程执行都需要1秒钟,在这种情况下你必须得等20秒才能干完这事儿;
第二种,我们换了锁 new ReentrantReadWriteLock() 是ReadWriteLock的一种实现,在这种实现里头我又分出两把锁来,一把叫readLock,一把叫writeLock,通过他的方法readWriteLock.readLock()来拿到readLock对象,读锁我就拿到了。通过readWriteLock.writeLock()拿到writeLock对象。这两把锁在我读的时候扔进去,因此,读线程是可以一起读的,也就是说这18个线程可以一秒钟完成工作结束。所以使用读写锁效率会大大的提升。

public class T10_TestReadWriteLock {
    static Lock lock = new ReentrantLock();
    private static int value;
    static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static Lock readLock = readWriteLock.readLock();
    static Lock writeLock = readWriteLock.writeLock();

    public static void read(Lock lock) {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"read中");
            Thread.sleep(1000);
            System.out.println("read over!"); //模拟读取操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void write(Lock lock, int v) {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"write中");
            Thread.sleep(1000);
            value = v;
            System.out.println("write over!"); //模拟写操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        //Runnable readR = ()-> read(lock);
        Runnable readR = () -> read(readLock);
        //Runnable writeR = ()->write(lock, new Random().nextInt());
        Runnable writeR = () -> write(writeLock, new Random().nextInt());
        for (int i = 0; i < 18; i++) new Thread(readR,"read"+i).start();
        for (int i = 0; i < 2; i++) new Thread(writeR,"write"+i).start();
    }
}

一句话总结就是 允许多个线程同时读,但写锁独占。写锁和读锁互斥。

Semaphore

我们来聊这个Semaphore,信号灯。可以往里面传一个数,permits是允许的数量,你可以想着有几盏信号灯,一个灯里面闪着数字表示到底允许几个来参考我这个信号灯。
s.acquire()这个方法叫阻塞方法,阻塞方法的意思是说我大概acquire不到的话我就停在这,acquire的意思就是得到。如果我 Semaphore s = new Semaphore(1) 写的是1,我取一下,acquire一下他就变成0,当变成0之后别人是acquire不到的,然后继续执行,线程结束之后注意要s.release(),执行完该执行的就把他release掉,release又把0变回去1,还原化。
Semaphore的含义就是限流,比如说你在买票,Semaphore写5就是只能有5个人可以同时买票,acquire的意思叫获得这把锁,线程如果想继续往下执行,必须得从Semaphore里面获得一个许可,他一共有5个许可用到0了你就得给我等着。
例如,有一个八条车道的机动车道,这里只有两个收费站,到这儿,谁acquire得到其中某一个谁执行。
默认Semaphore是非公平的,new Semaphore(2, true)第二个值传true才是设置公平。公平这个事儿是有一堆队列在哪儿等,大家伙过来排队。用这个车道和收费站来举例子,就是我们有四辆车都在等着进一个车道,当后面在来一辆新的时候,它不会超到前面去,要在后面排着这叫公平。所以说内部是有队列的,不仅内部是有队列的,这里面用到的东西,我今天将的所有的从头到尾reentrantlock、CountDownLatch、CyclicBarrier、Phaser、ReadWriteLock、Semaphore还有后面要讲的Exchanger都是用同一个队列,同一个类来实现的,这个类叫AQS。

public class T11_TestSemaphore {
    public static void main(String[] args) {
        Semaphore s = new Semaphore(2, true);
        new Thread(() -> {
            try {
                s.acquire();
                System.out.println("T1 running...");
                Thread.sleep(200);
                System.out.println("T1 end...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                s.release();
            }
        }).start();
        new Thread(() -> {
            try {
                s.acquire();
                System.out.println("T2 running...");
                Thread.sleep(200);
                System.out.println("T2 end...");
                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Exchanger

这个Exchanger是给大家扩宽知识面用的,看下面这个小程序,这里我们定义了一个Exchanger,
Exchanger叫做交换器,俩人之间互相交换个数据用的。怎么交换呢,看这里,我第一个线程有一个成
员变量叫s,然后exchanger.exchange(s),第二个也是这样,t1线程名字叫T1,第二个线程名字叫T2。
到最后,打印出来你会发现他们俩交换了一下。线程间通信的方式非常多,这只是其中一种,就是线程
之间交换数据用的。
exchanger你可以把它想象成一个容器,这个容器有两个值,两个线程,有两个格的位置,第一个线程
执行到exchanger.exchange的时候,阻塞,但是要注意我这个exchange方法的时候是往里面扔了一个
值,你可以认为吧T1扔到第一个格子了,然后第二个线程开始执行,也执行到这句话了,exchange,
他把自己的这个值T2扔到第二个格子里。接下来这两个哥们儿交换一下,T1扔给T2,T2扔给T1,两个
线程继续往前跑。exchange只能是两个线程之间,交换这个东西只能两两进行。
exchange的使用场景,比如在游戏中两个人装备交换。

public class T12_TestExchanger {
    static Exchanger<String> exchanger = new Exchanger<>();

    public static void main(String[] args) {
        new Thread(() -> {
            String s = "T1";
            try {
                s = exchanger.exchange(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + s);
        }, "t1").start();
        new Thread(() -> {
            String s = "T2";
            try {
                s = exchanger.exchange(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + s);
        }, "t2").start();
    }
}

总结

  • 我们首先讲了ReentrantLock和synchronized一个区别,ReentrantLock更灵活,更方便;
  • 讲了CountDownLatch的用法,就是倒计时,什么时候计数完了,门栓打开,程序继续往下执
    行;
  • CycliBarrier一个栅栏,循环使用,什么时候人满了,栅栏放倒大家冲过去;
  • Phaser分阶段的栅栏;
  • ReadWriteLock读写锁,重点掌握;
  • Semaphore限流用的;
  • Exchanger两个线程之间互相交换数据;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值