一文搞定Java多线程及高并发

1. 谈谈volatile的理解

volatile是java虚拟机提供的轻量级同步机制,主要特点有三个:

  1. 保证线程之间的可见性
  2. 禁止指令重排
  3. 不保证原子性

谈到volatile关键字,就不得不首先提到java的内存模型(JMM)

1.1 什么是JMM

JMM是java提供的一种抽象模型,主要是通过一组规范来定义程序中的变量的访问方式。(包括实例字段、静态字段等),JMM主要用于线程间的同步,主要做了如下规定:

  1. 线程解锁前,必须把共享变量刷新到主内存
  2. 线程加锁之前,必须读取主内存的最新值到工作内存
  3. 加锁、解锁都是操作的同一把锁。

上面提到了主内存和工作内存又是什么东西呢?
JVM运行程序的实体都是线程,每次创建线程的时候,JVM都会给线程创建属于自己的工作内存,注意工作内存是该线程独有的,也就说别的线程无法访问工作内存中的信息。而Java内存模型中规定所有的变量都存储在主内存中,主内存是多个线程共享的区域,线程对变量的操作(读写)必须在工作内存中进行。

比如:如果要操作一个变量,首先需要将变量从主内存中拷贝到自己的工作内存,然后对变量操作,操作完成之后再写会主内存,而不能直接操作主内存中的变量。各个线程中的工作内存存储的都是主内存变量的副本。

上面这种更新或者说读取变量的方式在单线程下不存在问题,但是在多线程下呢?多个线程工作去读取或修改一个变量,如何保证每个线程都能够读取到正确的值呢?

设想一下:存在两个线程A、B,同时从主线程中获取一个对象(i = 25),某一刻,A、B的工作线程中i都是25,A效率比较高,片刻,改完之后,马上将i更新到了主内存,但是此时B是完全没有办法i发生了变化,仍然用i做一些操作。问题就发生了,B线程没有办法马上感知到变量的变化!!

1.1.1 可见性

Volatile就是解决该问题的,保证A线程修改值之后,B线程能够知道参数已经修改了,这就是线程间的可见性。 A修改共享变量i之后,B马上可以感知到该变量的修改。

可见性demo:
第一次number没有加上volatile关键字

import lombok.Data;

import java.util.concurrent.TimeUnit;

/**
 * @author lanzhou
 * @time 2020/3/15 16:38
 * @Description volatile的可见性
 */
public class Juc001VolatileSee {
    public static void main(String[] args) {
        Resource resource = new Resource();

        // 创建一个线程
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+ " start");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 修改资源中的数据
            resource.addNum();

            System.out.println(Thread.currentThread().getName()+ " update number to " + resource.getNumber());
        },"Thread one").start();


        // 判断number是否等于0,如果等于零,则主线程一直循环等待中
        while (resource.getNumber() == 0){
            
        }

        // 观察主线程是否能够感知到number的变化,如果感知到了(可见性),则会到达该步。
        System.out.println(Thread.currentThread().getName()+ " finish");
    }
}

/**
 * 定义线程操作的资源
 */
@Data
class Resource{
    private int number = 0;

    public void addNum(){
        this.number = 60;
    }
}

结果如下:
在这里插入图片描述

加上volatile关键字之后的执行结果:
在这里插入图片描述

1.1.2 原子性

JMM的目的是解决原子性,但volatile不保证原子性。如何保证原子性,下文再继续讲解。
为什么volatile无法保证原子性呢?
因为上述的Java的内存模型的存在,修改一个i的值并不是一步操作,过程可以分为三步:

  1. 从主内存中读取值,加载到工作内存
  2. 工作内存中对i进行自增
  3. 自增完成之后再写回主内存。

每个线程获取主内存中的值修改,然后再写回主内存,多个线程执行的时候,存在很多情况的写值的覆盖。

用下面的例子测试volatile是否保证原子性。

import lombok.Data;

/**
 * @author lanzhou
 * @time 2020/3/15 17:14
 * @Description JMM原子性模拟
 */
public class Juc002VolatileAtomic {
    public static void main(String[] args) {
        AtomicResource resource = new AtomicResource();

        // 利用for循环创建20个线程,每个线程自增100次
        for(int i = 0; i < 20; i++){
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    resource.addNum();
                }
            },String.valueOf(i)).start();
        }

        // 用该方法判断上述20线程是否计算完毕,如果小于2,则说明计算线程没有计算完,则主线程暂时让出执行时间
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        // 查看number是否可以保证原子性,如果可以保证则输出的值则为2000
        System.out.println("Result = "+resource.getNumber());
    }

}

@Data
class AtomicResource{

    volatile int number = 0;

    public void addNum(){
        number++;
    }
}

结果如下:

Result = 1906

Process finished with exit code 0

接回上面的问题,如何保证原子性呢?
Juc下面提供了多种方式,比较轻量级的有Atomic类的变量,更重量级的有Synchronized关键字修饰,下面用Atomic来保证原子性的测试:

import lombok.Data;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author lanzhou
 * @time 2020/3/15 17:43
 * @Description 利用Atomic来保证原子性
 */
public class Juc003VolatileAtomic {
    public static void main(String[] args) {
        AtomicResource resource = new AtomicResource();

        // 利用for循环创建20个线程,每个线程自增100次
        for(int i = 0; i < 20; i++){
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    resource.addNum();
                }
            },String.valueOf(i)).start();
        }

        // 用该方法判断上述20线程是否计算完毕,如果小于2,则说明计算线程没有计算完,则主线程暂时让出执行时间
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        // 查看number是否可以保证原子性,如果可以保证则输出的值则为2000
        System.out.println("Result = "+resource.getNumber());
    }
}

@Data
class AtomicResource{

    AtomicInteger number = new AtomicInteger();

    public void addNum(){
        number.getAndIncrement();
    }
}

输出结果如下:

Result = 2000
Process finished with exit code 0

1.1.3 禁止指令重排

计算机在底层执行程序的时候,为了提高效率,经常会对指令做重排序,一般重排序分为三种

  1. 编译器优化的重排序
  2. 指令并行的重排
  3. 内存系统的重排

单线程下,无论怎么样重排序,最后执行的结果都一致的,并且指令重排遵循基本的数据依赖原则,数据需要先声明再计算;多线程下,线程交替执行,由于编译器存在优化重排,两个线程中使用的变量能够保证一致性是无法确定的,结果无法预测。

volatile可以防止指令重排情况。

禁止指令重排底层原理:
利用内存屏障来实现,通过插入内存屏障禁止在内存屏障前后的指令执行重排序的优化。内存屏障主要有两个功能:

  1. 保证特定操作的执行顺序
  2. 保证某些变量的内存可见性。

Volatile与内存屏障又是如何起着作用的呢?

对于Volatile变量进行写操作时,会在写操作后面加上一个store屏障指令,将工作内存中的共享变量值即可刷新到主内存;
对于Volatile变量进行读操作时,会在读操作前面加入一个load屏障指令,读取之前马上读取主内存中的数据。

1.2 工作哪用到指令重排

单例模式在多线程的安全性保证。
在查看多线程之前,首先来回归一下单线程下的单例模式:

// 懒汉模式
public class Juc004SingletonDemo {

    /**
     * 私有化构造方法、只会构造一次
     */
    private Juc004SingletonDemo(){
        System.out.println("构造方法");
    }

    private static Juc004SingletonDemo instance = null;

    public static Juc004SingletonDemo getInstance(){
        if(instance == null){
            instance = new Juc004SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
    	// 如果是两次都构建同一个,则会返回true
        System.out.println(Juc004SingletonDemo.getInstance() == Juc004SingletonDemo.getInstance());
    }
}

输出结果如下:

构造方法
true

懒汉模式的单例:

public class Juc004SingletonDemoHunger {

    /**
     * 私有化构造方法
     */
    private Juc004SingletonDemoHunger(){

    }

    private static Juc004SingletonDemoHunger instance = new Juc004SingletonDemoHunger();;

    public static Juc004SingletonDemoHunger getInstance(){
        return instance;
    }

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

输出结果:

true

但是如果在多线程下呢?

public class Juc004SingletonMultiThread {
    /**
     * 私有化构造方法、只会构造一次
     */
    private Juc004SingletonMultiThread(){
        System.out.println("构造方法");
    }

    private static Juc004SingletonMultiThread instance = null;

    public static Juc004SingletonMultiThread getInstance(){
        if(instance == null){
            instance = new Juc004SingletonMultiThread();
        }
        return instance;
    }

    public static void main(String[] args) {

        // new 30个线程,观察构造方法会创建几次
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                Juc004SingletonMultiThread.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

构造方法
构造方法
构造方法
构造方法

奇怪的是,构造方法竟然执行了四次,说明并不是真正的new一个对象,并不是真的线程安全的*(当然每个人测试的结果是不一样的)*

如何解决上述问题呢?

  1. getInstance()方法中加上Synchronized关键字来保证线程安全。
  2. 线程安全的双重检查来保证。(在对象的加锁前后都加上非空检查)

双重检查的单例模式:

public class Juc004SingletonMultiThread {
    /**
     * 私有化构造方法、只会构造一次
     */
    private Juc004SingletonMultiThread(){
        System.out.println("构造方法");
    }

    private static Juc004SingletonMultiThread instance = null;

    public  static Juc004SingletonMultiThread getInstance(){
        if(instance == null){
            synchronized (Juc004SingletonMultiThread.class){
                if(instance == null){
                    instance = new Juc004SingletonMultiThread();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {

        // new 30个线程,观察构造方法会创建几次
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                Juc004SingletonMultiThread.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

单例这种双重检查的近线程安全的单例模式也有可能出现问题,因为底层存在指令重排,检查的顺序可能发生了变化,可能会发生读取到的instance !=null,但是instance的引用对象可能没有完成初始化。,导致另一个线程读取到了还没有初始化的结果。

为什么会发生以上的情况呢?得来分析一个对象的初始化过程。

public  static Juc004SingletonMultiThread getInstance(){				// step 1
        if(instance == null){											// step 2
            synchronized (Juc004SingletonMultiThread.class){			// step 3
                if(instance == null){									// step 4
                    instance = new Juc004SingletonMultiThread();		// step 5
                }
            }
        }
        return instance;
    }

第五步初始化过程会分为三步完成:

  1. 分配对象内存空间 memory = allocate()
  2. 初始化对象 instance(memory)
  3. 设置instance指向刚分配的内存地址,此时 instance = memory

再使用该初始化完成的对象,似乎一起看起来是那么美好,但是计算机底层编译器想着让你加速,则可能会自作聪明的将第三步和第二步调整顺序(重排序),优化成了

1.  memory = allocate()         分配对象内存空间  
3.  instance = memory          设置instance指向刚分配的内存地址,此时对象还没有哦
2.  instance(memory)           初始化对象					

这种优化在单线程下还不要紧,因为第一次访问该对象一定是在这三步完成之后,但是多线程之间存在如此多的的竞争,如果有另一个线程在重排序之后的3后面访问了该对象则有问题了,因为该对象根本就完全初始化的。具体可以看看下图:
在这里插入图片描述
但是上述问题在单线程下不存在该问题,只有涉及到多线程下才会发生。
为了解决该问题可以从两个角度解决问题,
1. 不允许2和3进行重排序
2. 允许2和3重排序,但是不允许其他线程看到这个重排序。

因此可以加上Volatile关键字防止指令重排。

public class Juc004SingletonMultiThread {
    /**
     * 私有化构造方法、只会构造一次
     */
    private Juc004SingletonMultiThread(){
        System.out.println("构造方法");
    }

    private  static volatile Juc004SingletonMultiThread instance = null;

    public  static Juc004SingletonMultiThread getInstance(){
        if(instance == null){
            synchronized (Juc004SingletonMultiThread.class){
                if(instance == null){
                    instance = new Juc004SingletonMultiThread();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {

        // new 30个线程,观察构造方法会创建几次
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                Juc004SingletonMultiThread.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

2. 如何理解CAS

2.1 是什么

书面上的话:比较并交换(Compare And Swap),主要是为了解决原子性问题,同时又不想利用重量级的锁。
通俗的话说:如果想更新一个值,想看看该值是否等于某个值,如果等于则更新该值。
下面看一个简单的例子:
其中Atomic类是使用CAS思想最广泛的类,具体的思想和上面一样,源码此处先不分析。

public class Juc005Cas {
    public static void main(String[] args) {
        AtomicInteger integer = new AtomicInteger(10);
        // compareAndSet如果更新成功,则返回true,如果更新失败,则返回false
        System.out.println(integer.compareAndSet(10,11));
        System.out.println(integer.get());
        System.out.println(integer.compareAndSet(10,15));   // 这一次数值已经变成了11,先比较发现,诶,库里面不是10,直接跳过
        System.out.println(integer.get());
    }
}

输出结果如下:

true
11
false
11

2.2 为什么可以保证原子性

底层源码:

/**
 * 该方法本身是调用unsafe中的compareAndSwapInt()方法
 * this 表示当前对象 
 * valueOffset 内存地址
 * expect 内存中的值
 * update 更新值
 * 
 **/
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

// unsafe.class
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

CAS本身通过本地(native)方法来调用,该类可以直接操作底层内存的数据,Unsafe中所有的方法都是用native修饰,可以直接调用操作系统底层资源执行任务。

其中变量valueOffset表示该对象值在内存中的偏移地址,用来寻找数据的地址。

在底层判断内存中某个位置的值是否为预期值、如果是则更新为新的值,该比较和赋值动作是原子的。是完全依赖于硬件的功能,通过底层硬件实现原子的功能,该过程的执行是不允许中断的,不会造成所谓的数据不一致问题。

2.3 CAS缺点

  1. 循环开销大:如果比较的时候一直不等于预期值,则会一直循环等待,直到比较成功为止,该过程会给CPU带来较大开销。
  2. 只能保证一个共享变量的原子操作。对于多个共享变量无法保证原子性,因为每次比较的都是一个元素。
  3. ABA问题。

2.4 ABA问题与底层原子引用及如何解决

1 ABA问题是什么?

比如数值i = 5,A线程本来想改成10,在更改之前,B线程先将i先由5变成了6,再更新成5,但是A线程并不知道i发生过改变,仍然将i改成10。尽管最后的结果没有问题,但是整个过程还是不对的。

public class Juc005CASImprove {
    static AtomicReference<Integer> reference = new AtomicReference <>(100);

    public static void main(String[] args) {

        new Thread(()->{
            reference.compareAndSet(100,111);
            reference.compareAndSet(111,100);
        },"Thread One").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 还是可以更新成功
            System.out.println(reference.compareAndSet(100,222)+" "+ reference.get());
        },"Thread Two").start();
    }
}

2 原子引用

将需要修改的一系列值保证成一个类,每次不是比较值,而是比较一个引用的整体,然后每次保证引用的原子性。

public class Juc005CASAbaProblem {
    public static void main(String[] args) {
        User a = new User("a",12);
        User b = new User("b",12);

        AtomicReference<User> userReference = new AtomicReference <>();
        userReference.set(a);

        System.out.println(userReference.compareAndSet(a,b) +" "+userReference.get().toString());
    }
}

3. 如何解决ABA问题

一般都是利用时间戳作为版本,保证每次修改的值都是独一无二的。
可以将元素与版本号构成一个对象,每次保证引用的原子性。

public class Juc005CASImprove {
    static AtomicReference<Integer> reference = new AtomicReference <>(100);

    /**
     * 初始化时间值及初始化时间戳
     */
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference <>(100,1);

    public static void main(String[] args) {

        /**
         * 利用第一个线程模拟ABA问题
         */
        new Thread(()->{
            int initStamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 第0次版本号:"+initStamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 模拟ABA问题100->101->100
            System.out.println(Thread.currentThread().getName()+" 第一次版本号:"+stampedReference.getStamp());
            stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+" 第二次版本号:"+stampedReference.getStamp());
            stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+" 第三次版本号:"+stampedReference.getStamp());

        },"Thread Three").start();


        /**
         * 利用另一个线程尝试修改
         */
        new Thread(()->{
            int initStamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 第0次版本号:"+initStamp);
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Boolean result = stampedReference.compareAndSet(100,2019,initStamp,stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+" 是否更新成功,"+result);
            System.out.println(stampedReference.getReference());
        },"Thread Four").start();
    }
}

3. ArrayList是线程不安全的,编码写出不安全案例并给出解决方法

HashSet及HashMap同理
案例
现象如下:

public class Juc006ArrayListUnsafe {
    public static void main(String[] args) {
        List<String> lists = new ArrayList <>();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                lists.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(lists);
            },String.valueOf(i)).start();
        }
    }
}

结果会报异常:

java.util.ConcurrentModificationException 并发修改的异常

为什么会产生这种问题呢?
因为在多线程下,每次往容器中写数据时,不保证顺序,谁抢占到了容器谁开始写入数据,因此可能存在覆盖情况,导致每次执行的结果都不一致。

如何解决这种问题呢?

  1. Vector集合类。但是该类在方法上加上Synchronize关键字,保证线程安全。不推荐,重量级,效率低。
  2. Collections.synchronizedList保证线程安全。
// 保证线程安全
 List<String> lists = Collections.synchronizedList(new ArrayList <>());
  1. 利用写时复制的集合类。CopyOnWriteArrayList
List<String> lists = new CopyOnWriteArrayList <>();

写时复制类似于将读数据和写数据过程分离开来。
A线程和B线程都开始写数据,A、B每次写数据之前,都需要拿到一个许可证(类似于锁),主内存中数据复制到工作内存中,然后再进行修改,修改完毕之后将容器的引用指向新的数据集,然后再允许别的线程修改。

public boolean add(E e) {
		// 先获取锁,也就是前文说的许可证
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // 将原数组复制一份
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 添加新值
            newElements[len] = e;
            // 将数组索引指向新数组
            setArray(newElements);
            return true;
        } finally {
        	// 最后释放锁
            lock.unlock();
        }
    }

4. 锁的理解

公平锁、非公平锁、可重入锁(递归锁)、自旋锁,并手写一个自旋锁。

1 公平锁、非公平锁

抢占资源的顺序按照先后顺序依次获取,保证公平性,但是损失了效率。
非公平锁:抢占式的获取资源,哪个线程抢到资源哪个开始执行,也有可能存在一个线程一直获得资源,提高了效率。Synchronized是非公平锁。

上述只是简单的说公平锁的含义,具体的底层内容详见:JUC—ReentrantLock核心知识讲解

2 可重入锁

又称为递归锁:同一线程外层函数获取锁之后,内部函数仍然可以获取锁对象,线程可以进入任何一个已经拥有的锁所同步的代码块。

ReentrantLock和Synchronized两个都是典型的可重入锁。
可重入锁最大的作用可以避免死锁

3 自旋锁

尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁,可以减少线程上下文切换的消耗,但是循环也比较消耗CPU。

// CAS是最常用的自旋锁原理实践
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;
}

自旋锁实现:

public class Juc007LockSpinLock {

    AtomicReference<Thread> reference = new AtomicReference <>();

    /**
     * 获取锁
     */
    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() +" come in ....");

        // 如果更新成功,则跳出while循环;如果失败,则一直循环等待
        while(!reference.compareAndSet(null,thread)){
            System.out.println(thread.getName() +" I am waiting....");
        }
    }

    /**
     * 释放锁
     */
    public void unLock(){
        Thread thread = Thread.currentThread();
        //如果是当前线程获取锁,则尝试释放锁
        reference.compareAndSet(thread,null);
        System.out.println(thread.getName()+" unlock finished....");
    }

    /**
     * new两个线程去尝试获取锁及释放锁
     * @param args
     */
    public static void main(String[] args) {

        Juc007LockSpinLock spinLock = new Juc007LockSpinLock();

        new Thread(()->{
            spinLock.lock();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.unLock();
        },"Thread One").start();

        // 主线程等待1s
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            spinLock.lock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.unLock();
        },"Thread Two").start();
    }
}

4 独占锁(写锁)/共享锁(读锁)

独占锁:该锁对象一次只能被一个线程持有。上面说的ReentrantLockSynchronized都是独占锁。
共享锁:一个锁对象可以被多个线程持有。
ReentrantReadWriteLock该锁读的时候是共享锁,写的时候是独占锁。

5 synchronized和lock有什么区别?用lock有什么好处?

  1. synchronizedjava的关键字,是JVM层面控制(底层是monitor实现的),lock是具体类的api层面的锁(java.util.concurrent.locks.Lock)。
  2. synchronized不需要用户手动释放锁,当synchronized代码执行完毕系统会自动让线程释放对锁的占有。ReentrantLock则需要用户去手动释放锁,如果不手动释放锁,则可能会造成死锁现象。
  3. 等待是否中断。synchronized不可中断,除非正常执行完或者抛出异常;lock可以设置超时中断。
  4. 加锁是否可以公平。synchronized默认是非公平锁,而lock可以自己设置公平与否,默认是非公平锁。
  5. 锁是否可以绑定多个条件conditionsynchronized无法绑定多个条件,而lock可以分组唤醒线程,可以做到精确唤醒某一个线程,而不是像synchronized随机唤醒任意一个线程。

可以利用lock实现精确的唤醒,比如先执行A线程,打印5次,再执行B线程打印10次,再执行C线程打印15次

public class Juc010SynAndLockCompare {
    public static void main(String[] args){
        CompareResource resource = new CompareResource();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    resource.print5();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"Thread One").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    resource.print10();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"Thread Two").start();

        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    resource.print15();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"Thread Three").start();
    }
}

class CompareResource{

    // 利用number来表示不同的线程
    private int number = 1;

    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void print5() {
        lock.lock();

        try {
            // 循环判断
            while(1 != number){
                condition1.await();
            }

            // 干活
            for(int i = 0; i < number *5; i++){
                System.out.println(Thread.currentThread().getName() +" doing....");
            }
            // 唤醒下一个线程
            number = 2;
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void print10() {
        lock.lock();

        try {
            // 循环判断
            while(2 != number){
                condition2.await();
            }

            // 干活
            for(int i = 0; i < number *5; i++){
                System.out.println(Thread.currentThread().getName() +" doing.....");
            }
            // 唤醒下一个线程
            number = 3;
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void print15() {
        lock.lock();

        try {
            // 循环判断
            while(3 != number){
                condition3.await();
            }

            // 干活
            for(int i = 0; i < number *5; i++){
                System.out.println(Thread.currentThread().getName() +" doing....");
            }
            // 唤醒下一个线程
            number = 1;
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

6 Juc中常见的类CountDownLatch、CyclicBarrier、Semaphore的理解?

1. CountDownLatch

多个线程都执行完成时某一个线程才允许工作,否则该线程必须处于阻塞状态,其他线程工作完时,则latch-1,直到latch==0时,等待的线程才开始工作。
比如:只有其他人都走了(都完成),才可以关灯

public class Juc008CountDownLatch {

    public static void main(String[] args) throws Exception{
        CountDownLatch latch = new CountDownLatch(5);

        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+ " doing own work");
                latch.countDown();
            },String.valueOf(i)).start();
        }

        // 其他线程工作的时候,主线程处于阻塞状态,直到latch==0
        latch.await();
        System.out.println("main Thread working....");
    }
}

2. CyclicBarrier

该类与CyclicBarrier相反,只有当所有线程都达到某个状态时,才允许所有线程开始执行,否则所有的线程都必须处于等待状态。
比如:所有的人到了才可以开饭(才可以一起做某事)。

public class Juc008CyclicBarrier {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(7, ()->{
            System.out.println(Thread.currentThread().getName() +" starting.....");
        });

        for (int i = 1; i <= 7; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" waiting....");
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

3. Semaphore

之前有一篇仔细研究底层的原理文章,移步至:Semaphore

7. 谈谈阻塞队列

之前写过一篇基本的阻塞队列文章,阻塞队列
简单而言:本质上是一个队列,但是与普通队列有不同之处,阻塞队列,只有队列非空(有元素)时才可以获取元素,当队列非满的时候才可以添加元素,否则只能阻塞等待。
为什么需要阻塞队列呢?
在多线程领域,所谓阻塞,在某些情况下会挂起线程,一旦条件满足,被挂起的线程又会自动被唤醒。
通常我们需要手动处理何时阻塞线程、何时唤醒线程,但是有了阻塞队列之后,根本不需要关心该问题,一切都有BlockingQueue实现。

核心方法同样可以参考:阻塞队列

主要的实现类:
在这里插入图片描述

主要运用:生产者消费者模式

传统的生产者消费者写法:

/**
 * @Description 生产者消费者模式
 *  线程      操作 (方法)       资源类
 *  判断      干活        通知
 *  防止虚假唤醒机制
 */
public class Juc009ProducerAndConsumer {
    public static void main(String[] args) {
        Food food = new Food();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    food.produce();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"生产者").start();



        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    food.consumer();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"消费者").start();
    }
}


class Food{

    private int number = 0;

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void produce() throws Exception{
        lock.lock();
        try {
            // 1. number等于0,才开始生产,不等于0,则生产者处于等待状态
            
            // 这里必须用while判断,否则可能产生虚假唤醒
            while(number != 0){
                condition.await();
            }
            // 2. 等于0,则开始干活
            number++;
            System.out.println("生产者生产.... " + number);

            // 3. 生产者生产完了之后,需要通知唤醒消费者
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void consumer() throws Exception{
        lock.lock();
        try {
            // 1. number ==0 ,生产者等待
            while(number == 0){
                condition.await();
            }

            // 2. 如果number != 0,则消费者消费
            number--;
            System.out.println("消费者消费.... " + number);

            // 3. 消费者消费之后,通知唤醒生产者
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

利用阻塞队列实现生产者消费者模式

public class Juc011ProducerAndConsumerImprove {
    public static void main(String[] args) {
        ShareResource resource = new ShareResource(new ArrayBlockingQueue <>(10));
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+" 启动啦....");
            try {
                resource.produce();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Producer").start();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+" 启动啦....");
            try {
                resource.consumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Consumer").start();

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        resource.stop();

    }
}


class ShareResource{

    /**
     * 利用flag来控制生产消费
     */
    private volatile boolean flag = true;

    /**
     * 生产和消费的东西
     */
    private AtomicInteger value = new AtomicInteger();

    BlockingQueue<String> queue = null;

    public ShareResource(BlockingQueue<String> queue){
        this.queue = queue;
    }

    /**
     * 生产方法
     */
    public void produce() throws Exception{

        String data = null;

        Boolean result = false;

        /**
         * 如果为true,则开始生产,并且加入队列中
         */
        while (flag){
            data = value.incrementAndGet()+"";
            result = queue.offer(data, 2L,TimeUnit.SECONDS);
            if(result){
                System.out.println(Thread.currentThread().getName()+" 生产成功!");
            }else{
                System.out.println(Thread.currentThread().getName()+" 生产失败!");
            }
            TimeUnit.SECONDS.sleep(1);
        }


        System.out.println(Thread.currentThread().getName()+" 整个生产过程都被终止.....");
    }

    /**
     * 消费者方法
     */
    public void consumer() throws Exception{

        String result = null;

        while(flag){
            result = queue.poll(2L,TimeUnit.SECONDS);
            if(result == null || result.equalsIgnoreCase("")){
                flag = false;

                System.out.println(Thread.currentThread().getName()+" 超时未获取到资源...!");
            }else {
                System.out.println(Thread.currentThread().getName()+" 消费成功!");
            }
        }

    }

    public  void stop(){
        flag = false;
        System.out.println("终止了.............");
    }

}

8. Callable接口理解

是什么?
callable同样是一个线程的接口,但是可以自己定义返回值。

/**
 * 与Runnable接口区别在于可以接受返回值
 */
class OwnCall implements Callable<Integer>{

    /**
     * 返回值与Callable中泛型类型有关
     * 
     * @return
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        System.out.println(".......");
        return 1;
    }
}

下面可以获取线程的返回结果:

public class Juc012CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask <>(new OwnCall());
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

为什么?
因为Callable可以获取线程的计算结果,并且可以做到多个线程之间并行进行,比如先执行主线程,等该线程执行完了之后,在获取FutureTask的执行结果。

9. 线程池用过吗?ThreadPoolExecutor谈谈你的理解?

详细的可以参考之前的博文:JUC—线程池核心类ThreadPoolExecutor源码解析,对于线程池有更加准确的讲解。

9.1 为什么用线程池

线程的生成和销毁都是需要消耗系统的资源,因此可以提前准备好一堆可以使用的线程、供任务调度的使用。主要有以下几点好处:

  1. 降低资源的消耗。通过重复利用已经创建的线程降低线程创建和线程销毁的资源损耗。
  2. 提高响应速度。当任务达到时,不需要等待线程创建就可以执行
  3. 提供线程的可管理性。线程池本身提供了很多方法供用户监控、设置线程池。

9.2 线程池如何使用?

9.3 线程池重要参数讲解

9.4 底层工作原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值