JUC并发编程

java并发编程

我们在用java并发编程时会用到java.util.concurrent(简称JUC)包,该包下包含了并发编程的类。

什么是线程

线程(thread)是操作系统能够进行运算调度的最小单位。
并发编程顾名思义就是让多个线程并行执行(多核CPU下),提高系统的并发能力。

线程状态

==NEW== 初始状态
==RUNNABLE== 运行状态
==BLOCKED== 阻塞状态
==WAITING== 等待状态
==TIME_WAITING== 超时等待状态
==TERMINATED== 终止状态

lock和synchronized 的区别

1、synchronized 是java关键字,Lock是juc下的api
2、synchronized 为非公平锁,lock可以是公平锁、非公平锁
3、synchronized 会自动释放锁,无需我们在程序中手动释放;lock必须调用unlock进行释放。
4、使用synchronized 时,如果线程阻塞,另外一个线程会一直等待;但是lock可以使用trylock尝试获取,超过一定时间获取不到便放弃。
5、synchronized 可用于方法、代码块等适合代码量较小的场景,lock可以精确锁定,具体到某行代码。

线程的精确通知

线程直接通知的三大步骤:
==条件判断==
==执行动作==
==通知线程==
Object类有wait和notify/notifyAll通知,但是不能做到精确的通知。那么在juc下有没有可以精确通知的实现方式呢?答案:==有==
先看api中的Condition接口


在这里插入图片描述

来段示例,让三个线程交替执行创建、获取、提交的动作

package com.example.springboot.test.juc.notice;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * condition实现精准通知
 * 通知三部曲:
 * 1、条件判断
 * 2、逻辑执行
 * 3、通知
 */
public class Notice {
    public static void main(String[] args) {
        Flow flow = new Flow();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                flow.create();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                flow.get();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                flow.submit();
            }
        },"C").start();

    }


}

class Flow{
    String flag = "A";
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    public void create(){
        lock.lock();
        try {
            while (!"A".equals(flag)) {
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "创建");
            flag = "B";
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void get(){
        lock.lock();
        try {
            while(!"B".equals(flag)){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "获取");
            flag = "C";
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void submit(){
        lock.lock();
        try {
            while (!"C".equals(flag)) {
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "提交");
            flag = "A";
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

8锁问题知识点

1、被 synchronized 修饰的方式,锁的对象是方法的调用者。
2、被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一!

//锁的是唯一的class模板
synchronized(Hello.class){
}
//锁的是new出来的当前示例对象
synchronized(this){
}

不安全的集合类

list

我们经常用到的ArrayList非线程安全的,在并发场景下进行add操作会出现并发修改异常(ConcurrentModificationException),怎么避免异常?
1、使用安全集合vector

List list = new Vector<>();

2、使用工具类中的转线程安全的方法

List list = Collections.synchronizedList(new ArrayList<>())

3、使用juc下的copyonwrite

List list = new CopyOnWriteArrayList<>()

这里的copyonwrite是一种思想,写数据的时候利用拷贝的副本来执行,然后移动指针指向新的数据

set

同样的经常使用到的HashSet也是非线程安全的,HashSet本质是HashMap,存放的为HashMap中的key,所以是不重复的。
那么怎么实现线程安全呢?
1、使用工具类的转线程安全的方法

Set set = Collections.synchronizedSet(new HashSet<>())

2、copyonwrite思想

Set set = new CopyOnWriteArraySet()

map

HashMap也是非线程安全的,如果使用线程安全的map可以使用concurrentHashMap,如下:

Map map = new ConcurrentHashMap<>();

探索HashMap

HashMap本质是数组 + 链表、红黑树的数据结构
初始化容量为2的4次方,即16,加载因子为0.75f。
put数据时,先根据key的hash值判断数据存放哪个数组,如果数组中有值,则插入数组的链表中,当链表长度大于等于8时,转化为红黑树。

读写锁

并发包下的读写锁ReadWriteLock,有两个方法readLock()和writeLock(),返回类型为Lock,实现类有ReentrantReadWriteLock。
使用方法如下,lock和unlock同样需要成对出现,防止死锁。

ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
//do something
lock.readLock().unlock();
lock.writeLock().lock();
//do something
lock.writeLock().unlock();

阻塞队列

阻塞队列顾名思义,使用时会阻塞,典型的应用为生产者消费者模式,juc下接口类为BlockingQueue,一下为实现类


在这里插入图片描述

其中有一个特殊的队列SynchronousQueue,此队列只存一个元素,现存现取。

阻塞队列在什么情况下会阻塞呢?

1、当队列满了,继续往队列中添加时。
2、当队列为空时,要从队列取值时。

使用方法

方法 第一组会抛出异常 返回一个布尔值,不会抛出异常 延时等待 一直等待
插入 add() offer(e) offer(e,time) put()
取出 remove() poll() poll(time) take()
检查 element() peek() - -

函数式接口

juc下定义了四个函数式接口,分别为:
==Function== : 有一个输入参数有一个输出参数
==Consumer==:有一个输入参数,没有输出参数
==Supplier==:没有输入参数,只有输出参数
==Predicate==:有一个输入参数,判断是否正确!

异步回退

CompletableFuture类可以实现异步回退的功能,该类实现了Future接口。
实现方式如下:

package com.example.springboot.test.juc.async;

import java.util.concurrent.CompletableFuture;

public class ComplateFutureTest {
    public static void main(String[] args) throws Exception {
        CompletableFuture completableFuture1 = CompletableFuture.runAsync(()->{
            System.out.println(Thread.currentThread().getName() + "异步执行完成");
        });
        System.out.println("无返回值的" + completableFuture1.get());
        CompletableFuture completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName() + "异步执行返回结果");
//            int i = 5/0;
            return 1024;
        });
        System.out.println(completableFuture.whenComplete((t, r) -> {
            System.out.println("t--->" + t + "\r\n" + "r--->" + r);
        }).exceptionally((s) -> {
            return 404;
        }).get());
    }
}

线程池

线程池的默认创建方式不支持使用,看代码可以发现他们最终都使用了ThreadPoolExecutor自定义创建

package com.example.springboot.test.juc;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolTest {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,//常驻核心线程数
                6,//线程池能够容纳的同时执行的最大线程数
                3L,//线程空闲时间,达到指定值时会销毁线程,直到剩下core个
                TimeUnit.SECONDS,//时间单位,配合keepAliveTime使用
                new LinkedBlockingQueue(3),//缓存队列,当线程大于max时会进入阻塞队列
                Executors.defaultThreadFactory(),//线程工厂,用于生产一组相同任务的线程
                new ThreadPoolExecutor.CallerRunsPolicy());//拒绝策略,当线程超过core+max+queue数量时会用到
        for (int i = 0; i < 20; i++) {
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName() + "正在执行任务");
            });
        }

        threadPoolExecutor.shutdown();
    }
}

其中拒绝策略有四种,分别为:
•==AbortPolicy(默认)==:丢弃任务并抛出RejectedExecutionException异常。
• ==DiscardPolicy== :丢弃任务,但是不抛出异常,这是不推荐的做法。
• ==DiscardOldestPolicy== :抛弃队列中等待最久的任务,然后把当前任务加入队列中。
• ==CallerRunsPolicy== :调用任务的run()方法绕过线程池直接执行,谁调用的谁来处理。

juc的辅助类

juc下有三个辅助类,分别是CountDownLatch、CyclicBarrier、Semaphore。
下面分别介绍三个辅助类的作用。

CountDownLatch

CountDownLatch是一个减法计数器,有两个常用方法countDown()和await()。countDown()每调用一次计数器会减一,await方法会阻塞等待所有的线程执行完成,它不要求调用countDown线程等待计数到达零之前继续,它只是阻止任何线程通过await ,直到所有线程可以通过。

package com.example.springboot.test.juc.assist;

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

public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "执行完成");
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await(5, TimeUnit.SECONDS);
        System.out.println("全部执行完成");
    }
}

CyclicBarrier

CyclicBarrier译为篱栅,围栏。它允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。有两个await()方法,如果当前线程不是最后一个线程,那么它被禁用以进行线程调度,并且处于休眠状态,直到所有的线程都调用或者调用reset()方法,用法如下:

package com.example.springboot.test.juc.assist;

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, ()->{
            System.out.println("队伍集合成功");
        });

        for (int i = 0; i < 5; i++) {
            final int num = i;
            new Thread(()->{
                System.out.println(num + "对集合完毕");
                try {
                    cyclicBarrier.await(2, TimeUnit.SECONDS);
                } catch (Exception e){

                }
            }).start();
        }
    }
}

Semaphore

Semaphore译为信号量,信号量维护一组许可证,调用acquire()会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。用法如下:

package com.example.springboot.test.juc.assist;

import java.util.concurrent.Semaphore;

public class SemaphoreTest {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "来了");
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "走了");
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

JMM

JMM意思是Java memory model,Java内存模型,它是一种理论,和线程安全相关。
所有的线程是如何工作的:

八大操作:

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

  • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态

  • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

  • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用

  • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中

  • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令

  • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中

  • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用

  • write  (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

    在这里插入图片描述

    JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存 (可见)

  • 不允许一个线程将没有assign的数据从工作内存同步回主内存

  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作

  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

volatile

volatile的特性:

  • 保证可见性,即不同线程对这个变量进行操作时的可见性,一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。
  • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
    可见性验证:
package com.example.springboot.test.juc.jmm;

import java.util.concurrent.TimeUnit;

public class VolateTest {
    volatile static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println("让线程开始运行");
            while (flag){

            }
        },"A").start();
        TimeUnit.SECONDS.sleep(1);
        flag = false;
        System.out.println("此时flag为:" + flag);
    }
}

以上代码在flag不加volatile时,程序会一直在while中循环,因为主线程修改了flag的值,对于线程A是不可见的,所以线程A会一直循环,加上volatile之后,主线程对flag变量的操作对于线程A就可见了,会停止循环。
不保证原子性验证:

package com.example.springboot.test.juc.jmm;

import java.util.concurrent.CountDownLatch;

public class VolatileTest2 {
    volatile static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int m = 0; m < 5; m++) {
            new Thread(()->{
                for (int i = 0; i < 20; i++) {
                    add();
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println("当前num的值为:" + num);
    }

    private /**synchronized*/ static void add(){
        num++;
    }
}

num加与不加volatile,最终num的值都不是期望的100,如果在add方法上
加上synchronized修饰,则一定是100。
那么有没有办法在不使用synchronized时也能保证其正确性?
juc提供了一系列原子类,比较常用的有AtomicInteger。
如下:

package com.example.springboot.test.juc.jmm;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileTest2 {
//    volatile static int num = 0;
    static AtomicInteger num = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int m = 0; m < 5; m++) {
            new Thread(()->{
                for (int i = 0; i < 20; i++) {
                    add();
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println("当前num的值为:" + num);
    }

    private static void add(){
        num.getAndIncrement();
    }
}

此时add方法不使用synchronized修饰也能得到期望的100值。
禁止指令重排
这个无法用代码验证,属于理论知识。
指令重排是指代码在实际运行时不一定是按照你写的顺序执行的。
源代码->编译器(优化重排)->指令并行重排-> 内存系统的重排-> 最终执行的!
代码经过编译器,再经过重排,最后是最终的执行结果。单线程也不能避免指令重排。
如程序:

int a = 0;//第一步
int b = 1;//第二步
a = a + 1;//第三步
int c = a + b;//第四步

经过重排后,程序不一定按照1234步执行,可能是1234、1324、2134、2314、3124、3214。
volatile使用内存屏障(Memory Barrier)来禁止指令重排,内存屏障有两个作用:

  • 保证特定的执行顺序!
  • 保证某些变量的内存可见性 (votatile就是用它这个特性来实现的)

单例模式

饿汉式模式

package com.example.springboot.test.juc.single;

/**
 * 饿汉式模式
 */
public class HungryMan {
    //构造器私有
    private HungryMan(){

    }
    private static final HungryMan hungryMan = new HungryMan();

    public static HungryMan getSingleInstance(){
        return hungryMan;
    }
}
class HungryTest{
    public static void main(String[] args) {
        HungryMan hungryMan = HungryMan.getSingleInstance();
        HungryMan hungryMan2 = HungryMan.getSingleInstance();
        System.out.println(hungryMan.hashCode());
        System.out.println(hungryMan2.hashCode());
    }
}

懒汉式

package com.example.springboot.test.juc.single;

/**
 * 懒汉式模式
 */
public class LazyMan {
    private LazyMan(){}
    private volatile static LazyMan lazyMan = null;
    public static LazyMan getSingleInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }

        return lazyMan;
    }
}
class LazyTest{
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(LazyMan.getSingleInstance().hashCode());
            }).start();
        }
    }
}

这种懒汉式会被反射式破坏,不安全

package com.example.springboot.test.juc.single;

import java.lang.reflect.Constructor;

/**
 * 懒汉式模式
 */
public class LazyMan {
    private LazyMan(){
        System.out.println("构造器");
    }
    private volatile static LazyMan lazyMan = null;
    public static LazyMan getSingleInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                }
            }
        }

        return lazyMan;
    }
}
class LazyTest{
    public static void main(String[] args) throws Exception {
//        for (int i = 0; i < 10; i++) {
//            new Thread(()->{
//                System.out.println(LazyMan.getSingleInstance().hashCode());
//            }).start();
//        }
        Constructor constructor = LazyMan.class.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        LazyMan lazyMan = (LazyMan) constructor.newInstance();
        LazyMan lazyMan2 = (LazyMan) constructor.newInstance();
        LazyMan lazyMan1 = LazyMan.getSingleInstance();
        System.out.println(lazyMan.hashCode());
        System.out.println(lazyMan1.hashCode());
        System.out.println(lazyMan2.hashCode());
    }
}

使用枚举类:

package com.example.springboot.test.juc.single;

import java.lang.reflect.Constructor;

public enum  EnumSingle {
    SINGLEINSTANCE;
    public EnumSingle getSingleInstance(){
        return SINGLEINSTANCE;
    }
}

class EnumTest {
    public static void main(String[] args) throws Exception {
        Constructor constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        EnumSingle enumSingle = constructor.newInstance();
        System.out.println(enumSingle.hashCode());
    }
}

在jdk没被修改的情况下,枚举类是安全的单例模式。

CAS

package com.zeroun.test.cas;

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

public class CasTest {
    static AtomicInteger atomicInteger = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println(atomicInteger.compareAndSet(0, 5));
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicInteger.compareAndSet(6, 10));
        }).start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(Thread.currentThread().getName() + "----" + atomicInteger.compareAndSet(5, 6));
    }
}

CAS缺点:
缺点:
1、循环(自旋)开销很大!
2、内存操作,每次只能保证一个共享变量的原子性!
3、出现ABA 问题。
那么怎么解决ABA问题呢?加上版本号或者时间戳,原子引用。

package com.example.springboot.test.juc.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtoReferTest {
    public static void main(String[] args) {
        AtomicStampedReference reference = new AtomicStampedReference(1,1);
        new Thread(()->{
            int stamp = reference.getStamp();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "当前stamp的值:" + stamp);
            System.out.println(reference.compareAndSet(1, 5, reference.getStamp(), reference.getStamp() + 1));
            System.out.println(reference.compareAndSet(5, 10, reference.getStamp(), reference.getStamp() + 1));
        },"A").start();
        new Thread(()->{
            int stamp = reference.getStamp();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "当前stamp的值:" + stamp);
//            System.out.println(reference.compareAndSet(10, 15, reference.getStamp(), reference.getStamp() + 1));
            System.out.println(reference.compareAndSet(10, 15, stamp, stamp + 1));
        },"B").start();

    }
}

线程A和B先获取到版本号,然后A执行了更新,此时B也按照此版本更新,虽然旧值和期望值正确,但是版本号不对,所以更新失败。

死锁

死锁发生在互抢资源时,当一个线程拿到锁A,去拿锁B,而另一个线程拿到锁B,去拿锁A时,就发生互相抱着锁去抢别人的锁,就发生了死锁。

package com.example.springboot.test.juc.deadLock;

import java.util.concurrent.TimeUnit;

public class DeadLockTest {
    public static void main(String[] args) {
        String lockA = "a";
        String lockB = "b";
        new Thread(new test(lockA,lockB)).start();
        new Thread(new test(lockB,lockA)).start();
    }
}

class test implements Runnable{
    String lockA;
    String lockB;
    public test(String lockA,String lockB){
        this.lockA = lockA;
        this.lockB = lockB;
    }
    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "拿到了锁" + lockA + ",想拿到锁" + lockB);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "想拿到锁" + lockB);
            }
        }
    }
}

当死锁发生时,使用jprofiler工具进行分析,idea安装插件,并配置启动,就能分析当前程序发生的死锁。

你可能感兴趣的:(JUC并发编程)