菜鸟的JUC并发编程总结

说明

更新时间:2020/12/08 14:04,更新完基本内容

本文主要记录java的JUC编程总结,本文会持续更新,不断地扩充

注意:本文仅为记录学习轨迹,如有侵权,联系删除

一、基本知识

进程与线程

进程计算机中运行中的程序,如QQ.exe等。
线程单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。
注意在java里面,一个进程可以包含多个线程,一个进程至少有一个线程。Java程序至少有两个线程:GC线程和Main线程。

并发与并行

并发多个线程操作同一个资源并且交替执行的过程,并不是真正意义上的同时运行
并行同一个时刻多个线程同时执行,只有在多核CPU下才能完成。

同步与异步

同步同步调用必须等方法调用返回以后,才能继续调用。
异步异步调用更像是一个消息传递,一旦开始方法便立即返回,调用者可以继续完成后面相关的调用。此时异步方法就在另一个线程中真实的执行。

临界区

临界区表示一种公共资源或者共享数据,可以被多个线程使用。但每一次只能有一个线程使用它。一旦临界区被占用,其他线程要想使用这个资源就必须等待。在并行程序中,临界区是保护的对象,比如一个会议室不能同时提供给两个开发团队开会一样,只能轮流开会,这就是临界资源的概念。

阻塞与非阻塞

阻塞一个线程占用了临界区资源,那么其他需要这个资源的线程就必须在临界区等待。等待会导致线程挂起,这种情况就是阻塞。
非阻塞非阻塞意思恰恰相反,它强调的是没有一个线程能够阻碍其他线程的执行,所以线程都会尝试不断的向前执行。
注意阻塞与非阻塞通常形容的是多线程之间的相互影响。

java线程状态

新建(NEW)新创建了一个线程对象。
可运行(RUNNABLE)线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
运行(RUNNING)可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
阻塞(BLOCKED)阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。
死亡(DEAD)线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

注意,阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

JUC

JUCJUC,即java.util.concurrent包的缩写,是java原生的并发包和一些常用的工具类。

JUC编程主要学习下图的三个接口
在这里插入图片描述

二、Lock

Lock是java.util.concurrent.locks包下的接口,Lock下面还有几个字类,通过Lock就可以实现跟synchronized同样的功能,以最经典的卖票的为例,下面是卖票的两种实现方式

package com.zsc.lock;

/**
 * @ClassName : Lock01
 * @Description : lock锁
 * @Author : CJH
 * @Date: 2020-12-08 22:39
 */


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

/**
 * 卖票
 */
class Ticket{
    /**
     * 票数
     */
    private int num = 100;

    /**
     * 可重复锁
     */
    Lock lock = new ReentrantLock();


    /**
     * 卖票
     */
    void sell(){
        lock.lock();
        try{
            if(num>0){
                System.out.println(Thread.currentThread().getName()+"卖出了第" + (num--)+ "票" +",剩下"+num+"张票");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    /**
     * 卖票
     */
    void  sell2(){
        //使用synchronized修饰代码块,也可以达到将代码上锁的目的
        synchronized (this){
            if(num>0){
                System.out.println(Thread.currentThread().getName()+"卖出了第" + (num--)+ "票" +",剩下"+num+"张票");
            }
        }
    }
}

public class Lock01 {

    public static void main(String[] args) {

        Ticket ticket = new Ticket();

        //对应lock
        new Thread(()->{ for (int i = 0; i <= 100; i++)  {ticket.sell();}},"售票员A").start();
        new Thread(()->{ for (int i = 0; i <= 100; i++)  {ticket.sell();}},"售票员B").start();
        new Thread(()->{ for (int i = 0; i <= 100; i++)  {ticket.sell();}},"售票员C").start();
//
//        //对应synchronized
//        new Thread(()->{ for (int i = 0; i <= 100; i++)  {ticket.sell2();}},"售票员A").start();
//        new Thread(()->{ for (int i = 0; i <= 100; i++)  {ticket.sell2();}},"售票员B").start();
//        new Thread(()->{ for (int i = 0; i <= 100; i++)  {ticket.sell2();}},"售票员C").start();
    }

}

先简单说一下Lock的基本使用,这个可以通过查询jdk得到

Lock lock = ...;//可以new Lock接口下的任意字类,例如上图用的可重入锁new ReentrantLock();
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

两者区别如下

1.synchronized是一个关键字,Lock是一个对象。
2.synchronized无法尝试获取锁,Lock可以尝试获取锁并判断。
3.synchronized会自动释放锁(a线程执行完毕,b如果出现异常也会释放锁),Lock锁必须手动进行释放,不释放就会变成死锁。。
使用synchronized时,如果线程a获得锁并阻塞,线程b会一直进行等待,使用Lock则可以尝试获取锁,失败了之后就放弃。可以使用tryLock方法
5.synchronized一定是非公平的,但Lock锁可以是公平的,需要通过参数进行设置。
代码量特别大时,一般使用Lock实现精准控制,synchronized适合代码量较小的同步问题。

三、集合类不安全

(1)常见集合

先来了解一下java中集合容器的构成,主要是Map接口和Cllection接口,这两个是所有集合框架的父接口。

Collection说明
Set接口Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
List接口List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
Map说明
实现类Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等

下面简单列举出常用的集合容器以及是否是线程安全的
线程安全的:Vector、HashTable

线程不安全的:ArrayList、LinkedList、HashMap、HashSet、TreeMap、TreeSet

(2)线程安全与不安全的区别

线程不安全的集合,在使用的时候不能同时被多个线程使用,否则会报ConcurrentModificationException这样的并发修改异常,所以在高并发的情况下,如果可能会出现线程安全问题,而线程安全的集合就不会有这样的问题。

(3)模拟线程不安全的情况

模拟出这一种情况也简单,只要让多个线程同时操作一个线程不安全的集合即可,下面用ArrayList模拟多个线程同时读写引发的并发修改异常。

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();

        for (int i = 0; i < 30; i++) {
            String a = "a"+i;
            new Thread(()->{
                list.add(String.valueOf(a));
                System.out.println(list);
            },String.valueOf(i)).start();
        }

    }

运行结果
在这里插入图片描述
分析:由于ArrayList是线程不安全的,所以在多个线程操作同一个ArrayList的时候会引发线程并发异常,如图所示。

(4)解决方案

这样的解决方案有很多种,学过锁的人可能会下意识的想到锁,通过加锁的方式来解决,这个可以,但是完全没必要,方案一就是用线程安全的集合替代就可以了

方案一:用线程安全的集合替代

这里用Vector来替代即可

    public static void main(String[] args) {

        List<String> list = new Vector<>();

        for (int i = 1; i <= 30; i++) {

            new Thread(()->{
                list.add(String.valueOf("hello"));
                System.out.println(list);
            },String.valueOf(i)).start();
        }

    }

运行结果,没有并发异常
在这里插入图片描述

注意,如果点进去Vector的源码,会发现它其实内部也是加的锁,而且是用的syhchronized关键字

方案二:使用集合工具类将其转为线程安全

    /**
     * 通过集合工具类Collections将线程不安全的转安全
     */
    static void SafeDemo2(){
        //通过集合工具类Collections将线程不安全的转安全
        List<String> list = Collections.synchronizedList(new ArrayList<String>());

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                list.add("abc");
                System.out.println(list);
            },String.valueOf(i)).start();
        }

    }

方案三(推荐):写时复制CopyOnWriteArrayList

    /**
     * 使用juc包下的CopyOnWriteArrayList,也叫写时复制技术
     */
    static void SafeDemo3(){
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                list.add("abc");
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }

(5)重点:JUC包下的集合

这个是JUC编程的重点,主要是学习JUC包下的接口,如下图
在这里插入图片描述
在这里插入图片描述
上面模拟了一个ArrayList线程不安全的例子,也给出了解决方案,用的就是上图的CopyOnWriterArrayList,也叫写时复制技术,在使用的时候可以点击进入源码,发现它用的也是Lock,重点是写时复制思想的体现,下面给出这个经典的源码
在这里插入图片描述
上面给出了ArrayList的不安全例子,下面直接给出map和set的不安全例子,以及用java.util.concurrent提供的集合来解决

package com.zsc.collection;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @ClassName : Main2
 * @Description :
 * @Author : CJH
 * @Date: 2020-12-09 23:07
 */
public class Main2 {

    /**
     * HashSet不安全案例
     */
    static void NotSafeSet(){
        Set<String> set = new HashSet<>();
        for (int i = 0; i < 50; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }

    /**
     * 通过juc包下的CopyOnWriteArraySet实现线程安全的Set
     */
    static void SafeSet(){
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 500; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }


    /**
     * HashMap不安全案例
     */
    static void NotSafeMap(){
        Map<String, String> map = new HashMap<>();
        for (int i = 0; i < 50; i++) {
            new Thread(()->{
                map.put(UUID.randomUUID().toString().substring(0,2),"hello");
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }


    /**
     * 通过juc包下的CopyOnWriteArraySet实现线程安全的Set
     */
    static void SafeMap(){
        Map<String, String> map = new ConcurrentHashMap<>();

        for (int i = 0; i < 50; i++) {
            new Thread(()->{
                map.put(UUID.randomUUID().toString().substring(0,2),"hello");
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }



    public static void main(String[] args) {
        SafeMap();
    }


}

四、synchronized锁机制

对于synchronized的锁机制,下面给出对应的几种种情况,基本涵盖了锁机制的全部内容

第一种情况

在这里插入图片描述

问题邮箱和信息先执行哪一个?
答案随机执行,两个线程,调度是看电脑的cpu的事,所以是不确定哪个先执行的。
解析新起线程的时候,java只是把这个线程状态改为就绪状态,new Thread后不会立刻执行,要等待cpu的调度

第二种情况

在这里插入图片描述

问题邮箱和信息先执行哪一个?
答案先执行邮箱 ,再执行信息
解析这里有两个线程,在main函数那里两个线程之前故意让它们沉睡了100毫秒,先让邮箱先执行,邮箱里面设置了延时5秒,又由于邮箱和信息都是加了synchronized锁,这样导致两个在争夺同一个资源 ,由于这个锁锁的是对象this,邮箱在执行的时候这个对象this就被锁住了,所以只能等邮箱执行完再执行信息。

第三种情况

在这里插入图片描述

问题先打印“发送邮箱”还是“发送信息”,注意,不是先执行哪个方法,而是方法里面的打印语句那一条先执行
答案先打印“发送信息” ,再打印“发送邮箱”
解析这里跟第二种情况的区别就是发信息的方法没有加锁,这样在方法先执行邮箱方法里面的沉睡5秒语句的时候,因为发送信息方法没有锁,也就是说不用跟邮箱争锁,这样就可以在邮箱沉睡的时候先执行发送信息

第四种情况

在这里插入图片描述

问题先打印“发送邮箱”还是“发送信息”,注意,不是先执行哪个方法,而是方法里面的打印语句那一条先执行
答案先打印“发送信息” ,再打印“发送邮箱”
解析由于这里synchronized会锁住本对象this,这里是new了两个Phone,所以是两个互不影响的锁,所以就不用等邮箱是锁释放后再执行信息

第五种情况

在这里插入图片描述

问题先打印“发送邮箱”还是“发送信息”,注意,不是先执行哪个方法,而是方法里面的打印语句那一条先执行
答案先打印“发送邮箱” ,再打印“发送信息”
解析这里虽然是两个实例对象,但是发送邮箱的方法是static修饰的,这叫全局锁,锁住的整个的类模板,所以,不管有多少实例对象,都得等锁释放后才可以用。

五、生产者和消费者

sleep()和wait()之间的区别

(1)所属的类不同,sleep是Thread类里面的静态方法,wait()是Object类的方法(了解即可)
(2)执行sleep让当前线程进入阻塞状态,让出CPU使用权,直到沉睡的时间到了,cpu重新调度;执行wait让线程进入等待状态,也是让出cpu使用权,此时如果没有线程来唤醒它,它会一直处于等待状态 (重点)
(3)还有一个区别就是sleep是不会释放锁的,这一般是在synchronized修饰的情况下,两个线程分别调用同一个类里面的A方法(调用sleep)和B方法,这两个方法都是synchronized修饰的,那么如果线程1执行了A方法,A方法执行了sleep,并且synchronized锁A是不会释放的,只能等A执行完,线程2才会执行B方法(重点,下面有案例一)
(4)wait是释放锁的,这个跟上面第(3)点区分,同样的(3)的例子,A方法不在调用sleep而是调用wait,这个时候线程1执行方法A的wait后,由于synchronized是被释放的,所以线程2可以马上执行B方法,而不用等线程1执行完(重点,下面有案例二)

案例一

public class Main {
    public static void main(String[] args) throws InterruptedException {

        MyTest test = new MyTest();

        new Thread(()->{
            try {
                test.test01();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"线程A").start();

        new Thread(()->{
            try {
                test.test02();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"线程B").start();


    }
}


class MyTest{
    public synchronized  void test01() throws InterruptedException {
        Thread.sleep(3000);
        System.out.println("this is test01");
    }

    public  synchronized void test02() throws InterruptedException {
        System.out.println("this is test02");
    }


}

线程A调用test01,线程B调用test02,两个方法都是synchronized修饰的,这意味这如果不释放锁synchronized的话,它们要竞争同一把锁,刚好sleep是不释放锁的,所以哪怕线程A让出了cpu,线程B也是没法执行test02的,因为它需要的锁被线程A的test01占用了,它不释放锁,所以执行等线程1执行完test01后线程B才会执行test02

运行结果
在这里插入图片描述

案例二

public class Main {
    public static void main(String[] args) throws InterruptedException {

        MyTest test = new MyTest();

        new Thread(()->{
            try {
                test.test01();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"线程A").start();

        new Thread(()->{
            try {
                test.test02();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"线程B").start();


    }
}


class MyTest{
    public synchronized void test01() throws InterruptedException {
        this.wait();
        System.out.println("this is test01");
    }

    public  synchronized void test02() throws InterruptedException {
        System.out.println("this is test02");
    }


}

注意:wait只能在synchronized修饰的情况下才能使用

线程A调用test01,线程B调用test02,两个方法都是synchronized修饰的,这意味这如果不释放锁synchronized的话,它们要竞争同一把锁,刚好wait是释放锁的,素以线程A让出了cpu,由于锁没被占用,线程B可以执行test02,但是需要注意一点,wait让出cpu后进入等待状态,只有被线程唤醒才会执行,不然会一直处于等待状态

运行结果
在这里插入图片描述

wait与notifyAll

注意,这里的两个方法是需要用synchronized修饰的

wait让正在运行的本线程让出cpu,进入等待状态,等待被唤醒,没唤醒会一直沉睡
notifyAll唤醒所有正在等待的线程
注意这个两个都是需要synchronized修饰才可以用,不然会报错

利用这两个可以实现进程间的通信与协作,下面给出一个案例,实现生产者和消费者的轮流交替生产和消费产品

生产者和消费者案例

public class ProCon01 {
    public static void main(String[] args) {
        ProConDemo01 proConDemo01 = new ProConDemo01();

        new Thread(()->{
            for (int i = 0;i<20;i++){
                try {
                    proConDemo01.product();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产者A").start();

        new Thread(()->{
            for (int i = 0;i<20;i++){
                try {
                    proConDemo01.product();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产者B").start();

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

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



class ProConDemo01{
    private int num = 0;

    public synchronized void product() throws InterruptedException {

        //信号量判断是否产品数量是否等于0,是的话进行生产,不是的话进入等待状态,等待消费
        while (num != 0){
            this.wait();
        }

        //生产产品
        num++;
        System.out.println("【"+Thread.currentThread().getName()+ "】生产产品 : " + num);

        //生产完毕,唤醒正在等待的消费者线程进行消费
        this.notifyAll();
    }

    public synchronized void consumer() throws InterruptedException {
        //信号量判断是否产品数量是否等于0,不是是的话进行消费,是的话进入等待状态,等待生产
        while (num == 0){
            this.wait();
        }

        //消费产品
        num--;
        System.out.println("【"+Thread.currentThread().getName()+"】消费产品 : " + num);

        //消费完毕,唤醒正在等待的生产者进行生产
        this.notifyAll();
    }


}

运行结果
在这里插入图片描述

await和signal

注意:这里不能用synchronized,这里主要实现多个线程之间的通信和协作,使用的是最新的Lock锁

await同样的实现让线程让出cpu,进入等待状态,这个功能跟上面的那个wait功能是类似的
signal唤醒线程,可以唤醒特定的线程
注意这里的await和signal是可以唤醒特定的线程,通过同一个lock,可以创建多把锁,每把锁都可以有awiat和signal

下面给出一个案例,用多线程的方式组装一个机器类,4个线程,各自分工组装头部、身体、脚步和命名,各个线程分工

public class Main {
    public static void main(String[] args) {

        /**
         * 通过4个线程,分工合作完成机器人类的头部、身体、脚步组装以及给机器人命名
         * 4个线程协作完成一个机器人的组装,并且步骤是先组装头部、身体、脚步,最后再命名
         */
        Root root = new Root();
        new Thread(()->{
            root.buildBody();
        },"线程B").start();

        new Thread(()->{
            root.getName();
        },"线程D").start();

        new Thread(()->{
            root.buildFoot();
        },"线程C").start();

        new Thread(()->{
            root.buildHead();
        },"线程A").start();



    }
}


class Root{
    //3个判断标记,false表示未组装,true表示组装完成
    private Boolean isHasHead = false;
    private Boolean isHasBody = false;
    private Boolean isHasFoot = false;

    //用可重入锁,
    private Lock  lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();
    private Condition c4 = lock.newCondition();

    public void buildHead(){
        lock.lock();
        try{
            //判断如果已经组装过了头部了就放弃cpu,进入等待,否则就进入组装头部
            while (isHasHead == true){
                c1.await();
            }

            //组装头部,并且修改标志
            System.out.println(Thread.currentThread().getName() + ":机器人头部组装完成");
            isHasHead = true;

            //组装完头部后,(唤醒特定线程)通知身体可以开始组装了
            c2.signal();

        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }

    public void buildBody(){
        lock.lock();
        try{
            //判断头部是否组装完成,如果头部还没组装完,则先进入等待状态,等头部组装完后再来唤醒该线程
            //如果已经组装过了则直接进入组装身体
            while (isHasHead == false){
                c2.await();
            }

            //组装身体并且修改标记
            System.out.println(Thread.currentThread().getName() + ":机器人身体组装完成");
            isHasBody = true;

            //身体组装完成后唤醒通知脚步的组装
            c3.signal();

        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }

    public void buildFoot(){
        lock.lock();
        try{
            //判断身体是否已经装完了,没有组装完则进入等待状态,等着被唤醒,否则直接进入组装脚步
            while (isHasBody == false){
                c3.await();
            }

            //组装脚步并且修改标记
            System.out.println(Thread.currentThread().getName() + ":机器人脚步组装完成");
            isHasFoot = true;

            //脚步组装完成,通知机器人命名
            c4.signal();

        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }

    public void getName(){
        lock.lock();
        try{
            //判断是否已经头部、身体、脚步都已经组装完,没有的话这里就先进入等待状态,等待被唤醒,否则就可以直接命名机器人
            while ( isHasHead == false && isHasBody == false && isHasFoot == false){
                c4.await();
            }

            //给机器人命名
            System.out.println(Thread.currentThread().getName() + ":机器人命名完成:终结者1号");

        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }

}

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值