首页 > 其他 > 详细

狂神说_JUC并发编程

时间:2021-06-08 20:33:55      阅读:22      评论:0      收藏:0      [点我收藏+]

0.学习方法
源码+官方文档:

其实官方文档就是源码编译出来的,其本质还是看源码,不过文档会比较方便学习

  • 只有多看源码,多研究文档才会进步
  • Java英文文档可以通过点击查看源码获取
  • Java1.8中文文档(中文 – 谷歌版)
    •       在线版: https://blog.fondme.cn/apidoc/jdk-1.8-google/
    •       下载链接: https://pan.baidu.com/s/10wTC1F_4EUPsHzrn-_sPTw 密码:k7rm

1.什么是JUC
JUC其实就是Java.Util.concurrent包的缩写

java.util.concurrent
java.util.concurrent.atomi
java.util.concurrentlocks


是 java.util 工具包、包、分类

  • 回顾开启线程的三种方式:

Thread

Runnable

Callable

 技术分享图片

 

 

 

2.线程与进程
线程、进程,如果不能使用一句话说出来的技术,不扎实!

打开(Win10)任务管理器可以清楚看到执行的线程与进程:

技术分享图片

 

 

 


参考博客:什么是线程?什么是进程

进程:

  • 官方定义:

  进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础

  • 简单理解:

  进行(运行)中的程序,如打开任务管理器后中各种.exe程序

线程:

  • 官方定义:

  线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

  • 简单理解:

  线程是真正执行资源调度(使程序跑起来)的主体,一个进程往往可以包含多个线程,但至少包含一个线程。

  如:开一个idea进程,其中至少有—> 线程1:输入代码,线程2:自动保存

??老是强调多线程,那么 Java真的可以开启线程吗?

答案是 : 不能。查看Thread.start()源码可得知:

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}
// 本地方法,底层的C++ ,Java 无法直接操作硬件
private native void start0();

并发编程

并发编程:并发、并行

  • 并发(多线程操作同一个资源)
    • CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替
  • 并行(多个人一起行走)
    • CPU 多核 ,多个线程可以同时执行; 线程池
public class Test1 {
    public static void main(String[] args) {
        // 获取cpu的核数
        // CPU 密集型,IO密集型
        System.out.println(Runtime.getRuntime().availableProcessors());
       //输出为8
        //说明笔者电脑为八核处理器
    }
}

线程的几个状态:

从源码回答,有理有据

public enum State {
    // 新生
    NEW,
    // 运行
    RUNNABLE,
    // 阻塞
    BLOCKED,
    // 等待,死死地等
    WAITING,
    // 超时等待
    TIMED_WAITING,
    // 终止
    TERMINATED;
}

wait与sleep的区别

看源码说话嗷??

//Object.wait()
public final void wait() throws InterruptedException {
    wait(0);
}

//Thread.sleep()
public static native void sleep(long millis) throws InterruptedException;

来自不同的类

  • wait() 来自 Object类
  • sleep() 来自 Thread类

关于锁的释放

  • wait() 会释放锁:wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。
  • sleep() 不出让系统资源;(简单来说,就是抱着??睡觉)

是否需要捕获异常?

  需要。源码都在上面写的死死的,throws InterruptedException ??都不知网上随手一搜的博客说wait() 不用捕获异常怎么搞得。

使用范围:

  wait() 需要在同步代码块中使用

// wait、notify/notifyAll必须在同步控制块、同步方法里面使用。而sleep的使用在任意地方。
synchronized(x){
    x.notify()
   //或者wait()
}
    1. sleep()可以在任何地方睡

  1. 作用对象:

    1. wait()定义在Object类中,作用于对象本身
    2. sleep()定义在Thread 类中,作用当前线程。
  2. 方法属性:

    1. wait()是实例方法
    2. sleep()是静态方法 有static

技术分享图片

 

 

 并发类中进程睡眠使用的函数TimeUnit.Days.sleep(1)

同时只要线程都有中断异常,所以wait也有中断异常。

技术分享图片

 

 

 

3.Lock锁(重点)

  • 回顾用传统的 synchronized 实现 线程安全的卖票例子

真正的多线程开发,公司中的开发,降低耦合性 线程就是一个单独的资源类,没有任何附属的操作! 对于资源类只有: 属性、方法

开启线程错误方式:

class Ticket implements Runnable{}

耦合度高,违背了oop(面向对象)思想

public class SaleTicket_WithSynchronized {
    public static void main(String[] args) {
        // 并发:多线程操作同一个资源类, 把资源类丢入线程
        Ticket ticket = new Ticket();
        // @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "C").start();

    }
}

//资源类 OOP:
class Ticket {
    //属性、方法
    private int number = 30;
    //卖票方法
    //用synchronized 上锁
    public synchronized void saleTicket() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票,剩余" + number + "张");
        }
    }
}

lambda表达式简化了runnable函数式接口的使用,使得程序更加简洁,保证了资源类的独立性和程序的解耦合

 

package com.bupt;

public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(()->{
            for (int i = 0; i < 4; i++) {
                ticket.saleTicket();
            }
        },"A").start();
        //上下两个线程写法是等价的,Runable匿名内部类使用lambda表达式进行了简化

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    ticket.saleTicket();
                }
            }
        }, "B").start();
    }
}

class Ticket{   //只是一个资源类,保证只遵循oop原则
    private int number = 30;

    public synchronized void saleTicket(){
        if(number > 0){
            System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "张票,剩余"+ number);
        }
    }
}

 

用Lock接口

Lock源码定义:
技术分享图片

常用上锁语句:

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

ReentrantLock 可重入锁

技术分享图片

 

 

 

  • 公平锁与非公平锁(简单认识,后面详解)

    • 公平锁: 非常公平, 不能够插队,必须先来后到!
    • 非公平锁:非常不公平,可以插队 (默认都是非公平)
  • 上案例

 

// Lock 三部曲
// 1、 new ReentrantLock();
// 2、 lock.lock(); // 加锁
// 3、 finally=> lock.unlock(); // 解锁
public class SaleTicket_WithLock {
    public static void main(String[] args) {
        // 并发:多线程操作同一个资源类, 把资源类丢入线程
        Ticket ticket = new Ticket();
        // @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "C").start();

    }

}

class Ticket2 {
    // 属性、方法
    private int number = 30;
    Lock lock = new ReentrantLock();

    public void sale() {
        lock.lock(); // 加锁
        try {
            // 业务代码
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了第" +
                        (number--) + "张票,剩余:" + number + "张");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

Synchronized 和 Lock 区别

技术分享图片

技术分享图片

 

 

 技术分享图片

 

 

 


Java中的锁——Lock和synchronized

4、相比于synchronized,Lock接口所具备的其他特性

①尝试非阻塞的获取锁tryLock():当前线程尝试获取锁,如果该时刻锁没有被其他线程获取到,就能成功获取并持有锁

②能被中断的获取锁lockInterruptibly():获取到锁的线程能够响应中断,当获取到锁的线程被中断的时候,会抛出中断异常同时释放持有的锁

③超时的获取锁tryLock(long time, TimeUnit unit):在指定的截止时间获取锁,如果没有获取到锁返回false

 

 

 

 

 ReentrantLock中lock(),tryLock(),lockInterruptibly()的区别

技术分享图片

 

Java线程的6种状态及切换(透彻讲解)

 

 join方法:原来主线程和子线程是并行的关系,但是一旦使用了join()方法,就会变成串行的关系;当主线程调用子线程的join()方法时,意味着必须等子线程执行完毕之后,主线程才会开始执行。

 技术分享图片

 

 

 yield是线程失去CPU资源,从运行状态变成就绪状态

wait是释放当前锁,让其他线程获取锁,当被唤醒后就参与获取锁的竞争

非公平锁按照一定的策略比如耗时等选择线程执行,但是公平锁是按照先来后到,一般选择非公平锁

4.生产者消费者问题

从pc(producer<–>consumer)问题看锁的本质

1.synchronized版本

  • 生产者消费者synchronized版本

案例:对一个数字不停进行 +1 -1操作,加完了减,减完了加

 

public class PC_WithSynchronized {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"MINUS").start();
    }
}
//判断等待-->业务代码-->通知
//数字:资源类
class Data{
    //属性
    private int number = 0;
    //+1方法
    public synchronized  void increment() throws InterruptedException {
        if (number != 0){
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"-->"+number);
        //加完了通知其他线程
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if (number == 0){
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"-->"+number);
        //减完了通知其他线程
        this.notifyAll();
    }
}

此时输出:

加减交替执行,一片祥和~

 

..
ADD-->1
MINUS-->0
ADD-->1
MINUS-->0
ADD-->1
MINUS-->0
ADD-->1
MINUS-->0
ADD-->1
MINUS-->0
...

问题来了,现在只有"ADD"和"MINUS"两个线程执行操作,如果增加多两个线程会怎样呢?

于是在main方法中增加了"ADD2" 和 "MINUS2"两条线程:

new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        try {
            data.increment();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
},"ADD2").start();
new Thread(()->{
    for (int i = 0; i < 10; i++) {
        try {
            data.decrement();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
},"MINUS2").start();

出大问题了:出现了数据错误甚至死锁问题

技术分享图片

 

 

 

原因如下:

  • 用if判断只执行了一次判断,而wait()方法会导致??的释放
  • 具体说明:以两个加法线程ADD、ADD2举例:
    •   比如ADD先执行,执行时调用了wait方法,那它会等待,此时会释放锁。
    •   那么线程ADD2 获得锁并且也会执行wait()方法,且释放锁,同时两个加线程一起进入等待状态,等待被唤醒。
    •   此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程(notifyAll),那么这俩加线程不会一起执行,其中ADD获取了锁并且加1,执行完毕之后ADD2再执行。
    •   如果是if的话,那么ADD修改完num后,ADD2不会再去判断num的值,直接会给num+1,如果是while的话,ADD执行完之后,ADD2还会去判断num的值,因此就不会执行。
  • 上述情况称为:虚假唤醒

技术分享图片

 技术分享图片

 

 

 

 

 

  • 此时解决方法:将 if 改为 while

package com.kuangstudy.JUC;

/**
 * @BelongsProject: itstack-demo-design-1-00
 * @BelongsPackage: com.kuangstudy.JUC
 * @Author: lgwayne
 * @CreateTime: 2021-02-20 17:32
 * @Description:
 */
public class PC_WithSynchronized {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"MINUS").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD2").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"MINUS2").start();
    }
}
//判断等待-->业务代码-->通知
//数字:资源类
class Data{
  //data类相同,不再赘述
}

 虚假唤醒的一个形象的例子

当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功
1.比如说买货,如果商品本来没有货物,突然进了一件商品,这是所有的线程都被唤醒了,但是只能一个人买,所以其他人都是假唤醒,获取不到对象的锁

相关博文:技术分享图片https://blog.csdn.net/qq_39455116/article/details/87101633

结果一片祥和~

...
ADD2-->1
MINUS2-->0
ADD2-->1
MINUS2-->0
ADD2-->1
MINUS2-->0
ADD2-->1
MINUS2-->0
ADD2-->1
MINUS2-->0
...    

技术分享图片

 

 

 

通过Lock找到condition来配合控制对线程的唤醒

技术分享图片

 

 

public class PC_WithLock {
    public static void main(String[] args) {
        //主线程测试方法与上述一致,不再赘述
    }
}

//判断等待-->业务代码-->通知
//数字:资源类
class Data_withLock {
    //属性
    private int number = 0;

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

    //+1方法
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                //等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "-->" + number);
            //加完了通知其他线程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //-1
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) {
                //等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "-->" + number);
            //减完了通知其他线程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

技术分享图片

 

 

4.使用condition实现精准唤醒线程

用condition搭配状态位置试试

public class PC_WithAwakeByCondition {
    public static void main(String[] args) {
        Data_AwakeInOrder data = new Data_AwakeInOrder();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printA();
            }
        },"线程A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printB();
            }
        },"线程B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printC();
            }
        },"线程C").start();
    }
}

//资源类
class Data_AwakeInOrder{
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int number = 1;

    public void printA(){
        lock.lock();
        try {
        // 业务,判断-> 执行-> 通知
            while (number != 1) {
            // 等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>AAAAAA"+"-----number为->"+number);
            // 唤醒,唤醒指定的人,B
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number != 2) {
                // 等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>BBBBBB"+"-----number为->"+number);
            // 唤醒,唤醒指定的人C
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            // 业务,判断-> 执行-> 通知
            while (number != 3) {
                // 等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>CCCCCC"+"-----number为->"+number);
            // 唤醒,唤醒指定的人A
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

运行结果:??一片祥和~依序执行

...
线程A=>AAAAAA-----number为->1
线程B=>BBBBBB-----number为->2
线程C=>CCCCCC-----number为->3
线程A=>AAAAAA-----number为->1
线程B=>BBBBBB-----number为->2
线程C=>CCCCCC-----number为->3
...

看视频的时候看到有人用wait和notify 也实现了精准唤醒,接下来稍作尝试

5.使用wait()与notify()实现精准唤醒

public class PC_AwakeWithWait {
    public static void main(String[] args) {
        Data_AwakeInOrderByWait data = new Data_AwakeInOrderByWait();

        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                data.printA();
            }
        },"T_A").start();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                data.printB();
            }
        },"T_B").start();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                data.printC();
            }
        },"T_C").start();
    }
}

//资源类
class Data_AwakeInOrderByWait{

    private int number = 1;

    public synchronized void printA() {
        // 业务,判断-> 执行-> 通知
        while (number != 1) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "=>AAAAAA->"+number);
        // 唤醒,唤醒指定的人,B
        number = 2;
        this.notifyAll();
    }

    public synchronized void printB(){
        // 业务,判断-> 执行-> 通知
        while (number != 2) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "=>BBBBBB->"+number);
        // 唤醒,唤醒指定的人,B
        number = 3;
        this.notifyAll();

    }
    public synchronized void printC(){
        // 业务,判断-> 执行-> 通知
        while (number != 3) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "=>CCCCCC->"+number);
        // 唤醒,唤醒指定的人,B
        number = 1;
        this.notifyAll();
    }
}

结果也确实可以实现精准唤醒:

...
T_A=>AAAAAA->1
T_B=>BBBBBB->2
T_C=>CCCCCC->3
T_A=>AAAAAA->1
T_B=>BBBBBB->2
T_C=>CCCCCC->3
...

后来的技术是迭代出的更优秀的版本。等学成归来对比下这两种唤醒的效率

 技术分享图片

 

狂神说_JUC并发编程

原文:https://www.cnblogs.com/henuliulei/p/14862335.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!