多线程高并发编程基础(代码示例+详解)

以下代码均通过使线程睡眠模拟实际业务场景来解释原理

Case1(synchronized 介绍):

public class T {

    private int count = 10;
    private Object o = new Object();

    public void m() {
        synchronized (o){  //这里的加锁是对堆内存中的对象进行加锁,而不是对栈中的引用
            count--;
            System.out.println(Thread.currentThread().getName() + "count:" + count);
        }
    }

}

知识点:

  • synchronized锁定的是一个对象,而不是代码块
  • 对对象的加锁是对堆内存中的对象进行加锁,而不是对栈中的引用

上面的代码是new出来一个Object的对象,用来充当一把锁,也可以使用下面的方式对对象进行加锁:

public class T1 {

    private int count = 10;
    private Object o = new Object();

    public void m() {
        synchronized (this){  //这里的加锁是对堆内存中的对象进行加锁,而不是对栈中的引用
            count--;
            System.out.println(Thread.currentThread().getName() + "count:" + count);
        }
    }
}

知识点:

  • 上面的代码写成synchronized (this),每次执行m函数时,就会锁定当前对象(new 出来的T1对象,而不是Object对象)

如果在函数开始的时候就要锁定,函数结束时才能释放的话,可以直接把锁加到函数上,如下:

public synchronized m(){
       count--;
       System.out.println(Thread.currentThread().getName() + "count:" + count);
    }

这种方式也不是锁定的代码,而是锁定的当前对象(函数所在的对象)。

Case2(对静态加锁):

package com.gmail.fxding2019;

public class T2 {

    //当要锁定对象的是静态的时候
    private static  int count = 10;

    //对m方法加锁,实际上是锁定的com.gmail.fxding2019.T2.class。
    public synchronized static void m() {

            count--;
            System.out.println(Thread.currentThread().getName() + "count:" + count);
    }
}

知识点:

  • 对静态的加锁,锁定的不是对象,所以上面的代码并不等同于synchronized (this),静态方法没有this
  • 对静态方法m加锁,实际上是锁定的com.gmail.fxding2019.T2.class

其实锁定的是T类型的class对象。等同于下面的代码:

public static void m(){
        synchronized (T2.class){
            count--;
            System.out.println(Thread.currentThread().getName() + "count:" + count);
        }
    }

Case3(解决线程重入问题):

package com.gmail.fxding2019;

public class T3 implements Runnable {

    private int count = 10;

    public /*synchronized*/ void run(){
        count--;
        System.out.println(Thread.currentThread().getName() + "count:" + count);
    }

    public static void main(String[] args) {
        T3 t = new T3();
        for (int i = 0; i < 5; i++){
            new Thread(t, "Thread" + i + ":").start();
        }
    }
}


/*====================output========================
Thread0:count:8
Thread1:count:8
Thread4:count:7
Thread2:count:6
Thread3:count:5
*/

上面的代码是通过实现Runnable接口,重写run方法,new了一个对象t。然后创建了5个线程,其操作的count--都是堆中的同一个对象。上面的代码会出现线程的重入问题。

出现的原因就是:当Thread0运行完count--时(这时count=9),还没执行打印语句,Thread1就已经执行完了count--(这时count=8),然后Thread0和Thread1再执行打印语句。就得到了两个8。

解决的方法就是对run方法前面加一个synchronized关键字(对堆中的T对象进行加锁)。

Case4(线程启动的三种写法):

package com.gmail.fxding2019;

public class T4{

    public synchronized void m1(){
        System.out.println(Thread.currentThread().getName() + ":m1 start ... ");
        try {
            Thread.sleep(10000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":m1 end ... ");
    }

    public void m2(){
        try {
            Thread.sleep(5000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + ":m2");
    }

    public static void main(String[] args) {
        T4 t = new T4();

        //lambda表达式写法
        new Thread(()->t.m1()).start();
        new Thread(()->t.m2()).start();


        /*映射的写法
        *new Thread(t::m1,"t1").start();
        new Thread(t::m2,"t2").start();
        * */


        /*原始匿名对象的写法
         new Thread(new Runnable() {
            @Override
            public void run() {
                t.m1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m2();
            }
        }).start();
         */
    }
}

/*====================output========================
Thread-0:m1 start ... 
Thread-1:m2
Thread-0:m1 end ...
*/

上面的代码分别使用两个线程调用m1和m2方法,m1方法先启动,且m1是同步方法,m2方法后启动,m2方法是普通方法。

知识点:

  • 当执行m1时,是需要锁定当前的对象的,当执行m2时,是不需要锁定当前的对象的。那么,当执行m1的过程中,m2能否被执行?
    答案是可以的。只有加synchronized的方法才需要互斥等待。其他的方法不影响
  • 注意启动线程的三种写法:
    1. lambda表达式写法
    2. 映射的写法
    3. 原始匿名对象的写法

Case5(读脏数据):

package com.gmail.fxding2019;

import java.util.concurrent.TimeUnit;

public class T5_Account {

    String name;
    double balance;  // 默认值为0.0

    public synchronized  void  set(String name, double balance){
        this.name = name;

        try {
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        this.balance = balance;
    }

    public /*synchronized*/  double getBalance(String name){
        return this.balance;
    }

    public static void main(String[] args) {
        T5_Account a = new T5_Account();
        new Thread(()->a.set("zhangsan",100.0)).start();

        //TimeUnit是工具类,功能相当于sleep
        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        //第一次读
        System.out.println(a.getBalance("zhangsan"));

        try {
            TimeUnit.SECONDS.sleep(2);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
         //第二次读
        System.out.println(a.getBalance("zhangsan"));

    }
}
/*====================output========================
0.0
100.0
*/

上面的代码对写的方法加锁,对读的方法不加锁,然后set改变Balance的值,通过睡眠的方式模拟两次读Balance,可以看到,结果是不一样的。

上面的代码只对set(写)设置了synchronized,没有对getBalance(读)设置,所以读不受同步的影响,这可就能能会导致读脏数据。脏读(dirtyRead)的定义:读到还没写成功的数据解决的方法就是对读再进行加锁

Case6(可重入锁):

package com.gmail.fxding2019;

import java.util.concurrent.TimeUnit;

public class T6 {

    synchronized void m1(){
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //在同步方法中调用另一个同步方法
        m2();
    }


    synchronized void  m2(){
        try {
            TimeUnit.SECONDS.sleep(2);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println("m2");
    }
}

知识点:

  • 一个同步的方法是可以调用另一个同步方法的。这是因为一个线程若已经有了某个对象的锁,再次申请时仍会得到该对象的锁(在堆中的操作就是在锁上面加了个数字,从1变成2,相当于锁定了2次)
  • 以上的例子说明了:synchronized获得的锁是可重入的(可重入:获得锁之后还可以再获得一遍)

重入锁的另一种情形:子类的同步方法调用父类的同步方法,其锁定的还是TT(子类)的this对象。new TT时会先创建父类的对象:

package com.gmail.fxding2019;

import java.util.concurrent.TimeUnit;

public class T7 {

    synchronized void m(){
        System.out.println("m start");

        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        System.out.println("m end");
    }
}

class TT extends T7{
    synchronized void m(){
        System.out.println("child m start");
        super.m();
        System.out.println("child m end");
    }

    public static void main(String[] args) {
        new TT().m();
    }
}
/*====================output========================
child m start
m start
m end
child m end
*/

Case7(遇到异常释放锁):

package com.gmail.fxding2019;

import java.util.concurrent.TimeUnit;

public class T8 {

    int count = 0;

    synchronized void  m(){
        System.out.println(Thread.currentThread().getName() + "start");

        while (true){
            count++;
            System.out.println(Thread.currentThread().getName() + "count = " + count);

            try {
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e ){
                e.printStackTrace();
            }

            if(count == 5){
                int i = 1/0;  //此处会抛异常,锁会释放。要想不释放,就使用catch,使循环继续
            }
        }
    }

    public static void main(String[] args) {
        T8 t = new T8();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                t.m();
            }
        };

        new Thread(r, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        new Thread(r, "t2").start();  //如果t1不释放锁的话,t2永远执行不起来,因为while是死循环
    }
}

/*====================output========================
t1start
t1count = 1
t1count = 2
Exception in thread "t1" java.lang.ArithmeticException: / by zero
    at com.gmail.fxding2019.T8.m(T8.java:23)
    at com.gmail.fxding2019.T8$1.run(T8.java:33)
    at java.lang.Thread.run(Thread.java:745)
t2start
t2count = 3
t2count = 4
t2count = 5
*/

上面代码中,m方法使用while循环对count进行加1操作,当count = 5时,就执行1/0操作(会抛出by zero异常)。启动两个线程,先启动t1,再启动t2。如果出现异常锁不会被释放的话,那么t2永远得不到执行。而结果是t2得到了执行,所以t1出现异常后释放了锁。

知识点:

  • 程序在执行过程中,如果出现了异常,默认情况下锁是会被释放的(synchronized遇到异常锁会被释放)
  • 所以,在并发处理的过程中,有异常要多加小心,不然可能会造成不一致的情况:例如:在一个web app 处理的过程中,多个servlet线程共同访问同一个资源,这时候如果异常处理不合适,在某一个线程中出现了异常,而这个线程可能只执行可一半,这会导致其释放同步锁,其他的线程就会进入同步代码区。其他线程就有可能访问到上个线程处理了一半的数据。

Case8(volatile关键字):

package com.gmail.fxding2019;

import java.util.concurrent.TimeUnit;

public class T9_volatile {

    /*volatile*/boolean running = true;

    void m(){
        System.out.println("m start");
        while (running){

        }

        System.out.println("m end");
    }

    public static void main(String[] args) {
        T9_volatile t = new T9_volatile();

        new Thread(t::m,"t1").start();

        try{
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        t.running = false;  //主线程中把running改为false
    }
}

上面的代码就是通过设置running = true来是while出现死循环,即t1线程一直停不下来。虽然在主线程中设置了 t.running = false; 但是还是不起作用。如果在声明对running时加上volatile关键字,就可以使线程停下来。

产生这个现象的原因就是Java的JMM(java memory model)。可以这样理解:running保存在内存中,然后线程t1会把内存中的running copy到线程的缓存区中(每条线程还有自己的工作内存,可类比处理器的高速缓存类比,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成),所以while循环会一直执行。

这时候,主线程读取running的值到其线程缓冲区中,并把running的值改为false,由于running的值改变,所以主线程会把running=false再写回内存。但是t1线程并不会再从内存读数据了,所以t1会一直停不下来。

volatile关键字的作用就是:每当running在内存中的值发生改变时,会通知其他使用该变量的线程更新其缓存区。注意:是发生改变时才会从内存中读,而不是每一次都是从内存中读。volatile 并不能保证多个线程共同修改runing变量时所带来的不一致问题,也就是说volatile不能替代synchronized。volatile只能保证可见性,并不能保证原子性。

注意:多核CPU下,volatile也可以保证可见性,其他处理器通过嗅探总线上传播过来了数据监测自己缓存(每个CPU都有自己的缓存)的值是不是过期了,如果过期了,就会对应的缓存中的数据置为无效。而当处理器对这个数据进行修改时,会重新从内存中把数据读取到缓存中进行处理。

在这种情况下,不同的CPU之间就可以感知其他CPU对变量的修改,并重新从内存中加载更新后的值,因此可以解决可见性问题。

知识点总结:

  • volatile关键字,使一个变量在多个线程间可见:
    A、B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道,使用volatile关键字,会让所有线程都读到该变量的修改值
  • 在上面的代码中,running是存在于堆内存的t对象中,当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行。使用volatile,当内存中的running改变时,将会强制所有线程都去堆内存读取running的值。
  • volatile不能替代synchronized。volatile只能保证可见性,并不能保证原子性

下面通过小示例来演示volatile不能替代synchronized。volatile只能保证可见性,并不能保证原子性:

package com.gmail.fxding2019;


import java.util.ArrayList;
import java.util.List;

// volatile 并不能保证多个线程共同修改runing变量时所带来的不一致问题,也就是说volatile不能替代synchronized。volatile只能保证可见性,并不能保证原子性
public class T10 {

    volatile  int count = 0;  //保证了线程之间的可见性

    /*synchronized*/  void m(){
        for (int i = 0; i<10000; i++){
            count++;
        }
    }



    public static void main(String[] args) {
        T10 t = new T10();

        List threads = new ArrayList();

        for (int i = 0; i< 10; i++){
            threads.add(new Thread(t::m,"Thread:" + i));
        }

        threads.forEach((o)->o.start());

        threads.forEach((o)->{
            try {
                //Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行
                o.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }
}
/*====================output========================
37517
*/

从结果中可以看出,所有线程对count的操作结果应该为100000,但是最后的结果才为37517。这就说明了volatile 并不能保证多个线程共同修改runing变量时所带来的不一致问题。如果对m方法加上synchronized*关键字则可以解决这个问题。

Case9(原子类AtomXX):

package com.gmail.fxding2019;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class T11_AtomXX {

    //AtomicInteger是原子性的Integer类,用来进行简单的原子操作
    //凡是Atomic开头的,里面的方法都是原子性的,用来做简单的同步
    AtomicInteger count = new AtomicInteger(0);

    void  m(){
        for (int i = 0; i < 10000; i++){
            //incrementAndGet是原子操作,其是原子的方法,但并不是用synchronized实现的,而是用相当底层的方法,效率非常高
            //用来替代count++
            count.incrementAndGet();
        }
    }

    public static void main(String[] args) {
        T11_AtomXX t = new T11_AtomXX();

        List threads = new ArrayList();

        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);
    }
}
/*====================output========================
100000
*/

凡是Atomic开头的,里面的方法都是原子性的,用来做简单的同步,AtomicInteger是原子性的Integer类,用来进行简单的原子操作,上面的代码中,就是使用了AtomicInteger的incrementAndGet同步方法来代替count++。因为count++是原子性的,所以结果是100000,可以得到正确的结果。

知识点:

  • incrementAndGet是原子操作,其是原子的方法,但并不是用synchronized实现的,而是用相当底层的方法,效率非常高
  • 注意:Atomic的多个方法同时使用就不再具备原子性啦,比如:
if (count.get() < 1000){
              count.incrementAndGet();
        }

count.get()和count.incrementAndGet();都是原子性操作,但是在两条语句的中间还是有可能被其他线程打断

Case10(synchronized的简单优化 ):

package com.gmail.fxding2019;

import java.util.concurrent.TimeUnit;

public class T12 {

    int count=0;


    //锁住了整个方法,锁的粒度比较粗
    synchronized void m1(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑中只有下面这句需要syn,这时不应给整个方法上锁
        count++;

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end=System.currentTimeMillis();

    }

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

        //只锁住了count++,细粒度的锁
        synchronized (this){
            count++;
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end=System.currentTimeMillis();
    }
}

synchronized简单的优化原则:同步代码块中的语句越少越好,比较m1和m2,都是对count++操作进行加锁同步。m1锁住了整个方法,m2只锁住了关键的操作count++(注意:synchronized锁住的是对象,这里提到的是指,当执行当关键语句时,才对整个对象加锁,提高并发度)。我们称对m1加的锁是粗粒度锁,对m2加的锁是细粒度锁。

Case11(锁的引用发生改变):

package com.gmail.fxding2019;

import java.util.concurrent.TimeUnit;


public class T13 {

    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) {
        T13 t=new T13();

        //启动第一个线程
        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();
    }
}

/*====================output========================
t1
t1
t1
t1
t2
t1
t2
t1
t2
t1
*/

上面代码中,t1锁住对象o后开始执行死循环,这时候把引用o执行的对象改变,然后执行t2再次锁定它。如果t1和t2锁定的是同一个对象的话,t2将得不到执行。从执行结果来看,t2锁定的和t1锁定的并不是同一个对象啦。

这个例子说明:锁定的并不是栈中的引用o,而是new出来的堆内存的对象,即锁的信息是记录在堆内存里面的。锁定某个对象o,如果o的属性发生改变,不影响锁的使用,但如果o变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另外的对象。

Case12(不要以字符串常量作为锁定对象):

public class T14 {
    String s1="Hello";
    String s2="Hello";

    void m1(){
        synchronized (s1){

        }
    }

    void m2(){
        synchronized (s2){

        }
    }
}

上面代码锁定的是同一个对象。因为String类实现了常量池,hello存放在堆中的常量池中。常量池的概念见这篇文章。

在上面的例子中,m1和m2其实锁定的是同一个对象,这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串"Hello",但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁。

Case13(wait && notify):

面试题:
写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束

public class T15 {

    volatile List lists = new ArrayList();

    public void add(Object o){
        lists.add(o);
    }

    public int size(){

        return  lists.size();
    }

    public static void main(String[] args) {


        T15 t = new T15();

        new Thread(()->{

            for (int i=0; i<10; i++){
                t.add(new Object());
                System.out.println("add" + i);

                try {
                    TimeUnit.SECONDS.sleep(1);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }

        },"t1").start();

        new Thread(()->{
            while (true){
                if(t.size() == 5){
                    break;
                }
            }
            System.out.println("t2结束");
        },"t2").start();
    }
}
 
 

上面的实现中,如果对list不加volatile。那么list的size不可见。所以t2不会结束。

虽然上面的这个程序可以完成上述要求的功能,但是也存在两个问题:

  1. 通知的不精确,可能已经add8了,t2结束才打印出来(原因是没有进行同步)
  2. 浪费cpu,t2一直用while进行检测

给lists添加volatile之后,t2能够接到通知,但是t2线程的死循环很浪费cpu,如果不用死循环怎么做呢?可以尝试使用wait和notify解决这个问题,如下面的代码:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class T16 {

    //添加volatile,使t2能够得到通知
    volatile List lists=new ArrayList();

    public void add(Object o){
        lists.add(o);
    }

    public int size(){
        return lists.size();
    }

    public static void main(String[] args) {
        T16 t = new T16();

        //声明一把锁
        final Object lock=new Object();

        new Thread(()->{

            //t2锁住lock,并进入wait状态。注意:进入wait前必须加锁
            //而wait后会释放lock锁,等待别的线程notify它。
            //这里必须先写t2的等待。再写t1
            synchronized (lock){
                System.out.println("t2启动");
                if (t.size()!=5){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2结束");
            }
        },"t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t1对lock进行加锁。然后在t.size = 5时,再唤醒t2
        new Thread(()->{
            synchronized (lock){
                System.out.println("t1启动");
                for (int i = 0; i < 10; i++) {
                    t.add(new Object());
                    System.out.println("add "+i);


                    //注意:notify不用指定线程。也无法指定线程。对lock的notify cpu会自己找一个阻塞在lock上面的线程进行唤醒
                    if (t.size()==5){
                        lock.notify();
                    }

                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t1结束");
            }
        },"t1").start();
    }
}
/*====================output========================
t2启动
t1启动
add 0
add 1
add 2
add 3
add 4
add 5
add 6
add 7
add 8
add 9
t1结束
t2结束
*/

上面的代码中,先使用wait让t2线程进行等待,然后启动t1不断向里面添加对象,等待size=5时,唤醒t2线程,打印t2结束。

注意:

  • wait后会释放lock锁,等待别的线程notify它
  • 上面的代码必须先写t2的等待,再写t1
  • notify不用指定线程。也无法指定线程。对lock的notify cpu会自己找一个阻塞在lock上面的线程进行唤醒

然后上面这个程序还是存在一些问题:这个程序在t1唤醒t2时,t2并不会打印。而是等到t1运行退出时,t2才会打印。因为t2运行需要对lock进行加锁,而这把锁t1没运行完之前是不会释放的。所以上面的打印结果才出现t1结束后t2得到了打印。下面代码可以解决这个问题:

package com.gmail.fxding2019;

import java.util.ArrayList;
import java.util.List;

public class T17 {

    //保持list的size可见性
    volatile List lists = new ArrayList();

    public void add(Object o){
        lists.add(o);
    }

    public int size(){
        return lists.size();
    }

    public static void main(String[] args) {

        T17 t = new T17();

        //声明一把锁
        final  Object lock = new Object();

        new Thread(()->{

            System.out.println("t2启动");
            synchronized (lock){
                if (t.size() != 5){
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println("t2结束");

                //注意:必须在退出该锁之前唤醒其他阻塞在该锁上的线程
                //t2打印完,释放lock,唤醒t1继续运行
                lock.notify();
            }

        },"t2").start();


        new Thread(()->{

            synchronized (lock){

                System.out.println("t1启动");
                for (int i = 0; i<10; i++){
                    t.add(new Object());

                    System.out.println("add" + i);
                    if(t.size() == 5){
                        //通知t2
                        lock.notify();

                        try {
                            //阻塞t1,释放lock,等待t2拿到lock后打印
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                    }

                }
            }

        },"t1").start();
    }
}
/*====================output========================
t2启动
t1启动
add0
add1
add2
add3
add4
t2结束
add5
add6
add7
add8
add9
*/

上面的程序逻辑为:

  • t2先执行,遇到size!=5时调用wait等待,并释放对象的锁
  • t1拿到锁后再执行,不断向数组中添加,等到size=5时,使用notify唤醒t2,并且自己调用wait进行阻塞,释放对象锁(注意:一定要先唤醒t1后再进行自我阻塞)
  • 由于只有t2阻塞在该信号量上,所以notify会唤醒t2,进行打印。打印结束后,使用notify再唤醒t1
  • t1得到锁之后,继续执行,直到结束

Case14(CountDownLatch——门闩):

package com.gmail.fxding2019;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;


public class T18_interviewFinal {
    //添加volatile,使t2能够得到通知
    volatile List lists=new ArrayList();

    public void add(Object o){
        lists.add(o);
    }

    public int size(){
        return lists.size();
    }

    public static void main(String[] args) {

        T18_interviewFinal c=new T18_interviewFinal();

        //CountDownLatch就是门闩,当CountDownLatch为0时,门闩就开啦
        CountDownLatch latch=new CountDownLatch(1);

        new Thread(()->{
            System.out.println("t2启动");
            if (c.size()!=5){
                try {

                    //等着开门
                    //门闩的等待是不需要锁定任何对象的
                    latch.await();

                    //也可以指定等待时间
                    //latch.await(5000,TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2结束");

        },"t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        new Thread(()->{
            System.out.println("t1启动");
            for (int i = 0; i < 10; i++) {
                c.add(new Object());
                System.out.println("add "+i);

                if (c.size()==5){
                    //打开门闩,让t2得以执行
                    //只要调用一次CountDownLatch,其值就会减一
                    latch.countDown();
                }

                //门闩打开之后,t1是可以向下正常运行的

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1结束");

        },"t1").start();
    }
}

使用Latch(门闩)替代wait notify来进行通知,好处是通信方式简单,同时也可以指定等待时间,使用await和countdown方法替代wait和notify,CountDownLatch不涉及锁定,当count的值为零是当前线程继续运行。当不涉及同步,只是涉及线程通信的时候,用synchronized+wait/notify就显得太重了的时候,时应该考虑CountDownLacth/cyclicbarrier/semaphore。

知识点:

  • CountDownLatch latch=new CountDownLatch(1);CountDownLatch就是门闩,当CountDownLatch为0时,门闩就开啦,可以设置其初始值。
  • 门闩的等待是不需要锁定任何对象的,只要调用一次CountDownLatch,其值就会减一



参考:马士兵老师java多线程高并发编程

你可能感兴趣的:(多线程高并发编程基础(代码示例+详解))