Java Util Concurrent并发编程(一) 基础知识回顾

前言:现在是十一月一号。今天给接下来的三个半月做了下规划,初步计划是三大必学模块:juc并发,netty通讯框架,spring boot源码。如果时间还有富余我是打算再去看下es的。
其实说实话,上面这些知识我都使用过,spring boot更是现在工作的基石。但是仅仅是使用功能,一些源码解读,实现原理什么的直白点说是很难过得去面试的。
同样juc我更是学烂了,不管是实体书,还是视频教程我都看过学过,但是并没有什么实际的用处。知识有一点特性:单纯的肯书本而不使用最容易被遗忘。我记得今年四月份才从头又看了一遍java并发编程这本书,但是现在也就那些出现率比较高的名词术语记住了。所以说只能像是回锅肉一样来回滚了。
而netty以前也仅仅是使用,也会遇到什么理解不了的去百度一下,当时恍然大悟,事后忘得一干二净。所以这三个月打算从新整理一下这些知识。

如果时间富裕的话,余下来的时间我大概会去看看es(es可以称我是接口调用工程师,crud是会的。没有实践应用)。刷题也会保持每周刷5+题目。大概接下来三个半月的计划就是这样。下面开始进入正式的笔记。

线程和进程

说到并发,这两个概念是一切的基础。当然了一般稍微会点java基础的也都知道这两个概念,但是能不能说清和知不知道不一样。
首先附上一个最适合背书的一句话:

进程是资源分配的最小单位,线程是CPU调度的最小单位

当然了要是就这么一句话你就能明白了,那我敬你是个天才。反正我是做不到。至于具体说怎么理解,我附上一个知乎上赞同数比较高,我也觉得很形象的一个例子:
做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢ps:这里要注意,不是绝对的,有时候线程崩了也不会影响进程
  • 进程可以拓展到多机,线程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

上面就是知乎大佬的回答,我觉得真的挺好理解的,比较形象。当然了百分之百正确肯定不可能,比如之前说线程崩了的那个,还有很多说的可能不是那么准确,但是起码对于我们理解来说是够了的。起码当有人问你一些线程和进程之间的问题,我们带入到火车和车厢,这样方便记忆的多。
除了进程和线程的理解,结合java中的实际,还有几个常识要知道:

  • java中,默认是最少2个线程。一个是main方法。还有一个是GC。
  • 对于java而言,Thread,Runnable.Callable都可以创建线程。但是!!java真的可以开启线程么?
    答案是开不了!。看下面的图片
    java中start线程

    java中start线程

    看到没有,最终线程的start调用的是一个本地方法。因为java是无法直接操作硬件的。

并发/并行/串行

这几个概念也比较基础,但是这里就是要把基础重新理一遍。所以一一说说。

  • 并发:多线程操作同一个资源。
  • 并行:多个操作同时执行(一定是在多核cpu中才会出现)
  • 串行:简单理解就是一个不能同时,要一个一个排成串过。
    网上嫖的一张图片

    其实这三个概念如果明白了就会很清楚。并发和并行的共同点是都表面上看在同时发生。但是注意我说了是表面上。实际上并行是一定同时发生的,但是并发可能是假象哦。比如一个人喂两个孩子吃饭。和两个人个喂一个孩子吃饭。看上去都是两个孩子在吃饭。但是前者是并发,后者是并行。
    下面安利一个java中获取cpu核数的方法:
//获取cpu核数
Runtime.getRuntime().availableProcessors();

本地运行结果

我的电脑性能

并发编程的本质就是充分利用cpu的资源!(ps:这个可以想想请阿姨给孩子喂饭。是不是一个人喂的孩子越多,需要的人越少,就越省钱啊。)

线程

线程有几个状态?正确答案是java中是6个。操作系统层是5个。下面一一介绍。
在java的Thread源码中,一个线程有六种状态,如下截图:

线程6种状态

其实源码中注解写的挺全的,但是我这个英语渣要一个个贴到百度翻译里,哈哈。下面是具体的状态和翻译:

  • NEW 尚未启动的线程的线程状态。
  • RUNNABLE 可运行线程的线程状态。可运行线程状态正在Java虚拟机中执行,但它可能正在等待操作系统中的其他资源例如处理器。
  • BLOCKED等待监视器锁定时阻塞的线程的线程状态。处于阻塞状态的线程正在等待监视器锁定输入同步块/方法或调用后重新输入同步块/方法{@link Object\wait()对象。等等}.
  • WAITING 等待线程的线程状态。
  • TIMED_WAITING具有指定等待时间的等待线程的线程状态。
  • TERMINATED终止线程的线程状态。线程已完成执行。
    百度翻译
wait和sleep的区别
  1. 来自不同的类。Wait是Object的 ,sleep是Thread的
  2. 关于锁的释放:wait会释放锁,sleep不会。
  3. 使用的范围是不同的。wait必须在同步代码块使用。sleep可以在任何地方用。

Lock锁(重点)

线程就是一个单独的资源类,没有任何附属操作。
在Lock之前是重量级锁Sychronized。属于悲观锁。每次都要一个一个进入锁住的代码块。虽然比较安全,但是性能却是大问题,所以尽量不用。
相比于sychronized,lock算是一个轻量级的锁。(可以去java8的api文档中查看,https://www.matools.com/api/java8)

java8的中文api文档

在官方文档上也简单说了lock怎么用:

随着这种增加的灵活性,额外的责任。 没有块结构化锁定会删除使用synchronized方法和语句发生的锁的自动释放。 在大多数情况下,应使用以下惯用语:

   Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); } 

当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由try-finally或try-catch保护,以确保在必要时释放锁定。
其实看着就能发现这个lock的用法很简单的。加锁,然后执行代码。最后记得解锁。为了解锁必然执行,放在finally中比较稳妥。

Lock使用三部曲:
  • new 个锁
  • lock.lock();加锁
  • lock.unlock();解锁
    下面是个使用demo来展示:


    不用锁的时候数据出问题了

    用锁以后没问题了
    public static void main(String[] args) {
        Test test = new Test();
        for(int j = 0;j<100;j++) {
            new Thread(()->{for(int i = 0;i<20000;i++) test.incr();}).start();
        }
    }
class Test{
    int i = 0;
    Lock lock = new ReentrantLock();
    public void incr() {
        lock.lock();
        try {
            i++;
            System.out.println("当前i的值是:"+i);
            System.out.println("<<<<<<<<"+i);
        } finally {
            lock.unlock();
        }
    }
}

另外说个小知识点:公平锁和非公平锁
公平锁是排队锁。非公平锁是每次每个锁获得几率差不多的(公平锁理解成排队买票。非公平锁是一窝蜂一样堵在卖票口买票。)
java默认是非公平锁。其实我们使用非公平锁是为了公平。这句话可能比较绕。因为有的线程时间长,有的时间短。如果公平锁的话,有些线程两s就能完成,硬生生等两个小时,这个就不太好。其实这里面涉及好多东西。比如cpu调度,短作业优先执行(这一块我就简单的查了下资料,也不怎么了解)
而synchronized和lock有什么区别?

  1. synchronized是一个关键字。Lock是java类。
  2. synchronized无法判断锁的状态,Lock可以判断是否获取到了锁。
  3. synchronized会自动释放锁。Lock必须手动释放锁(不释放就死锁了)。
  4. synchronized是会一直等待锁。而Lock可以加一些等待条件(比如tryLock)
  5. synchronized默认是可重入锁。而且非公平的。(这些是不能改的。)Lock是可重入,可以自己设置公不公平(默认非公平,可以在参数中传true即为公平)。
  6. synchronized适合锁少量的代码块,Lock稍微锁大量的代码也ok。

Lock

先说一下synchronized和Lock使用的区别。
synchronized常常要和Object的wait和notify/notifyall方法联合使用,才能起到监视器的作用(这里有一个小知识点:虚假唤醒)。
虚假唤醒:在wait/notifyall中,记得判断不要用if而用while。

虚假唤醒出现的原因及解决办法

而Lock则自带监视器了。这里可以从api文档中学习。
先简单的看下Lock的方法:
Lock方法

这里其实方法名称和形容让我们很容易就知道了每个方法是干什么的。但是注意到这里有个Condition了么?之说返回一个绑定的实例。但是具体是做什么用的我们可以继续往下看。
Condition的作用

注意看我框起来这句话:在拥有锁的时候Condition.await()可以释放锁。感觉熟不熟悉?有没有联想到synchronized中的ojb.wait()?如果你没想到可能是需要一些想象力了。因为着两个除了方法名很想,其实功能也差不多。都是释放锁。只不过一个释放的是synchronized一个是Lock。
下面我们点进Condition这个类瞅瞅:
Conditon的作用

看这句话,官方手册上也是这么说的,Condition之于Lock就是监视器于synchronized。
而Condition相比于传统的wait/notify更加完善的是他有个指定唤醒某资源的功能。比如传统的notify只能随机唤醒一个等待线程。但是Condition其实是可以根据不同的Condition表面上看是唤醒指定的线程的(其实绝对不是唤醒指定线程,这个说法是大大的错误!)。注意看下面的截图:
Condition的唤醒

如果单独这句话很难理解,下面附上一个demo代码:

public class Demo {

    public static void main(String[] args) {
        Test test = new Test();
        new Thread(()->{for(int i = 0;i<200;i++) test.A();},"A").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.B();},"B").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.C();},"C").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.A();},"AA").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.B();},"BB").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.C();},"CC").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.A();},"AAA").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.B();},"BBB").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.C();},"CCC").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.A();},"AAAA").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.B();},"BBBB").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.C();},"CCCC").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.A();},"AAAAA").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.B();},"BBBBB").start();
        new Thread(()->{for(int i = 0;i<200;i++) test.C();},"CCCCC").start();
    }

}
class Test{
    int i = 0;
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    public void A() {       
        lock.lock();
        try {
            while (i!=0) {
                condition1.await();             
            }
            System.out.println("当前操作的线程是:"+Thread.currentThread().getName());
            i = 1;
            condition2.signal();
        }catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void B() {       
        lock.lock();
        try {
            while (i!=1) {
                condition2.await();             
            }
            System.out.println("当前操作的线程是:"+Thread.currentThread().getName());
            i = 2;
            condition3.signal();
        }catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void C() {       
        lock.lock();
        try {
            while (i!=2) {
                condition3.await();             
            }
            System.out.println("当前操作的线程是:"+Thread.currentThread().getName());
            i = 0;
            condition1.signal();
        }catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

这个代码的输入结果一定是A/B/C交替的。但是具体几个A.几个B.几个C是随机的。
而如果只有A,B,C三个线程,我们就可以以为Condition唤醒了指定的线程(这块up主表述的我总觉得有问题。)其实这个指定唤醒的是资源。


官方文档

8锁现象(关于锁的八个问题)

  • 灵魂拷问1.如下图片中的代码。请问输出结果是什么?
    问题1

    这里揭晓答案,一定是先输入A,然后输出B,并且中间间隔1秒中。(中间可能出现AAB的时候,因为第一次A执行完了,第二次又是第一个线程抢到锁了)因为main线程会在启用第一个线程后睡一秒钟才执行下面的代码。也就是启动第二个线程。
  • 灵魂拷问2.如下图片中的代码。请问输出结果是什么?
    红框里是相对1新加的

    揭晓答案:一定是隔了一秒钟输出一次A,B。因为还是第一个线程先执行。进入到A方法中。睡了一秒钟(注意这里是抱着锁睡得!sleep不释放锁)。然后同时主线程启动了第二个线程。但是第二个试图去调用B,可是锁在第一条线程那里!
  • 灵魂拷问3.如下图片中的代码。请问输出结果是什么?
    框起来的地方是有改动的

    揭晓答案:这个答案真的是A,C不确定执行。因为都是睡了一秒。。。我这里for循环所以结果比较明显。因为A是睡一秒才输出。C是睡一秒才调用。这里因为是一样的时间,所以结果真的是随机的。。
    结果
  • 灵魂拷问4.如下图片中的代码。请问输出结果是什么?
    框出来的是需要注意的

    这个答案其实是不固定!!因为一个是先睡一秒再输出。一个是主线程先睡一秒再启动线程调用。不固定是因为这个单纯的看脸。是不是很奇怪为什么和第二个问题差不多。但是第二个可以确定是A,B。这个就随机了呢?重点就是框起来的。这是两个Test对象。而synchronized锁的是这个对象。而不是这个类。
  • 灵魂拷问5.如下图片中的代码。请问输出结果是什么?
    类变成静态类

    这个比较简单,不管从什么角度都肯定是A,B的输出。
  • 灵魂拷问6.如下图片中的代码。请问输出结果是什么?
    换成两个对象了

    这个答案比较有意思。应该大多数人都会觉得这个肯定是A,B,A,B的输出吧。毕竟静态类了,锁的是类。调用的不管是test还是test1本质上没啥区别。不过别忘了AAB是不可避免的,毕竟synchronized默认是非公平锁。
  • 灵魂拷问7.如下图片中的代码。请问输出结果是什么?
    注意框起来的

    这个其实弄明白锁的是谁就很好理解了(这里我把A的输出改成睡两秒为了得到想要的结果)。这个一定是先输出B的!因为A是静态方法,锁的是类。B是普通方法,锁的是实例对象。两个虽然都是synchronized,但是不是一个锁。不用互相等待的。
  • 灵魂拷问8.如下图片中的代码。请问输出结果是什么?
    变成两个对象了

    其实这个答案上面都懂了的话很容易就能明白了。因为一个是类,一个是实例对象。不是一个锁。结果是和上一个一样一样的。

小结:锁的到底是什么?如果是静态代码块,锁的是类。如果是普通代码块,锁的是实例对象。

这篇笔记就到这里,主要是讲了一些并发的基本知识,线程啊,进程啊什么的,还有锁是什么。8锁问题之类的,知识点比较杂,反正如果帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利!

你可能感兴趣的:(Java Util Concurrent并发编程(一) 基础知识回顾)