【一篇吃透面试八股】——Java并发编程:JUC篇

目录

JUC——java高级并发

思考题:交替执行的思路

  • ①wait与notify,一个执行完了就notify,同时自己wait【Plus版就是多个Condition精准通知】
  • ②纯标志控制【不推荐,难以检查】
  • ③synchronousQueue:同步队列,生产了,必须消费完才能再生产另一个【虽然确实是ABAB,但是打印这个动作可能延后,如果严格要求还是使用方案①】

零、解决线程同步的三大方法

  • 互斥量法——锁机制【即Lock、Syn等】
  • 事件控制法——阻塞与唤醒【wait、notify】
  • 信号量法——生产者消费者模型【此方法归根到底还是前两种方法,或者看成前两种方法的一种应用】

一、JUC——java-util-concurrent包

1
是一个并发处理的工具包

Runnable:没有返回值,效率比Callable低

二、线程和进程

2.1 程序、进程、线程

程序: 用户编写的代码

进程: 程序的一次执行过程【一个可运行jar包的一次执行过程就是一个进程】

线程: 一个进程往往包含多个线程

java默认几个线程——2个

  • main线程

  • gc线程

线程举例:Typora软件,一个线程负责程序输入,一个线程负责自动保存,一个线程负责统计字数等。

2.2 线程管理

要明白,线程的创建不是java所能实现的,其本质是通过C++的本地方法来开启。

对于java而言:三种开启线程的方式Thread、Runnable、Callable、还可以通过Lambda表达式

2.3 并发VS并行

并发:在一核CPU中模拟多个线程,通过快速交替造成同时执行的假象。(宏观多个,微观1个)

并行:多个线程同时执行(始终多个)

8核心16线程:实际上只有8个核心,但是通过超线程技术,让cpu最多可以运行16个线程

并发编程的本质:充分利用CPU的资源

public static void main(String[] args) {
   
    // 得出cpu逻辑核心数,即最大能运行的线程数
    System.out.println(Runtime.getRuntime().availableProcessors());
}
2.4 线程状态(java实现的)

获取状态

Thread thread = new Thread();
thread.getState();
// 结果一定是以下枚举之一

状态枚举

public enum State {
   
    // 新生
    NEW,
    // 运行
    RUNNABLE,
    // 阻塞
    BLOCKED,
    // 等待
    WAITING,
    // 超时等待
    TIMED_WAITING,
    // 终止
    TERMINATED;
}
2.5 wait/sleep区别

①来自不同的类

  • wait——Object类

  • sleep——Thread类【很少用】

我们很少直接使用sleep方法,而是使用TimeUnit方法

TimeUnit.SECONDS.sleep(100);

②锁的释放

  • wait——释放锁
  • sleep——抱着锁睡觉【不会释放锁】
  • TimeUnit睡觉不会释放锁

③使用的范围不同

  • wait:在同步代码块中使用【你得先使用】,否则报IlleaglMonitorStateException

  • sleep:任何都可使用

三、Lock锁

3.0 原理

Lock锁其实锁的是这个锁本身,就像syn锁对象一样,只不过它锁的是自己。

当多个线程尝试去拿到锁时,发现已经被拿了,就会阻塞!

而且Lock是可重入的,可以多次lock

3.1 锁的体系

在这里插入图片描述

3.2 ReentrantLock构造方法

默认是非公平锁。如果传true——公平锁,传false——非公平锁

在这里插入图片描述

公平锁:公平——必须先来后到【容易造成效率问题,如3s钟进程等3h进程,低效】

非公平锁:不公平——可以插队(默认)

3.3 Lock的使用
public class Test{
   
    // 1、创建锁
	private Lock lock = new ReentrantLock();    
	public void test(){
   
        // 2、加锁 
        lock.lock();
        try{
   
            // 业务代码    
        }catch(Exception e){
   
            e.printStackTrace();
        }finally{
   
            // 3、解锁
            lock.unlock();
        }
    }
}
// 
public static void main(){
   
        Test ticket2 = new Test();
        new Thread(()->{
   for (int i = 0; i < 50; i++) ticket2.sell();},"A").start();
        new Thread(()->{
   for (int i = 0; i < 50; i++) ticket2.sell();},"B").start();
        new Thread(()->{
   for (int i = 0; i < 50; i++) ticket2.sell();},"C").start();
}
3.4 Syn和Lock的区别

1、syn是内置的java关键字、Lock是一个类!

2、syn无法是隐式锁,不利于判断,Lock是显式锁,可以直观判断是否获取到锁

3、syn是全自动的,执行完后会自动释放锁。Lock必须手动释放锁【如果不释放锁,可能会造成死锁问题】

4、tryLock方法

  • 使用syn,如果一个线程获得了锁,另一个线程就一直等待。如果拿着锁的线程阻塞了,另一个线程就持续等待。

  • lock锁有一个trylock方法,可以避免等待情况【使用场景:先尝试,尝试成功就执行同步方法,否则先去执行其他的】

5、公平性

  • syn一定是非公平的;
  • lock可以自己设置公平与非公平

6、syn 适合锁少量的代码同步问题,lock适合大量的同步代码

总结:Lock非常灵活!Syn就像手动挡,而Lock就像自动挡

四、生产者和消费者问题

4.0 问题描述

生产者和消费者在同一时间段内共用同一个存储空间,如下图所示,生产者向空间里存放数据,而消费者取用数据。我们需要通过代码保证该模型的正确执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uDY7d52S-1649568717503)(java并发编程——JUC/2011091018554595.gif)]

注意以下几点:

  • 消费者消费完了应该等待生产者生产,产品数量不能为负数
  • 生产者生产的产品数量应当小于空间容量,当空间容量满时停止生产,将空间使用权交给消费者
4.1 Syn实现
package juc.pc;

/**
 * 生产者-消费者问题
 * 问题描述:
 * 两个线程交替执行操作num,即缓冲区容量为1的情况
 * A num+1
 * B num-1
 * 如果没有线程通信机制,是无法实现的,可以通过《等待-唤醒》来操作
 */

public class OldVersion {
   
    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();
                }
            }
        }, "A").start();
        new Thread(() -> {
   
            for (int i = 0; i < 10; i++) {
   
                try {
   
                    data.decrement();
                } catch (InterruptedException e) {
   
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
   
            for (int i = 0; i < 10; i++) {
   
                try {
   
                    data.decrement();
                } catch (InterruptedException e) {
   
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
   
            for (int i = 0; i < 10; i++) {
   
                try {
   
                    data.increment();
                } catch (InterruptedException e) {
   
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

// 等待-业务-通知
// 判断是否等待,不需要等待就干活,干完就通知
class Data {
   
    private int number = 0;

    // +1
    public synchronized void increment() throws InterruptedException {
   
        // 这个判断是起一个保证作用,实际上顺序如果比较巧合的话,不会有任何线程被挂起!
        while (number != 0) {
   
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知
        this.notifyAll();
    }

    // -1
    public synchronized void decrement() throws InterruptedException {
   
        while (number == 0) {
   
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        // 通知
        this.notifyAll();
    }
}


// 曾经疑惑
// 用if:
// 如果生产者还没生产,CPU两次调度消费者怎么办,第二次就接着判断向下了
// 不用担心,因为调度一次后,发现需要等待,就会把这个线程挂起了,除非有别人notify,所以该线程是无法被调度的
// 但是,这个问题并没有彻底解决!
// 如果只有两个线程,确实不会有问题。但如果是多个线程,例如一个生产者,两个消费者,那么生产者A生产完之后notify,会醒来一个消费者B,消费者消费完后又notify,就会把另一个消费者C也唤醒了,此时货物为0,但是C已经经过判断,所以会继续向下执行,就会造成货物数量为-1

// 所以,一定要用while
// 用while之后,循环判断,被唤醒后会再次检查判断条件,从而避免了虚假唤醒的问题

/**
 * 虚假唤醒问题:
 * 如果用if作为判断wait的条件,那么假如此线程挂起后再次被唤醒,将会从wait之后的代码继续进行!
 * 但是用while作为判断条件,该线程再次被唤醒时又回到了判断条件,符合才能继续执行。
 *
 * 所谓虚假唤醒:就是本不该唤醒你,结果给你唤醒了。
 * --------
 * 唤醒信号丢失问题:
 * 那么,为什么要用notifyAll这种东西呢。因为当有多个线程时,可能会出现信号丢失问题,比如这个例子,有三个线程,两个线程+1,第三个线程-1,那么A线程+1后挂起,又调用B线程,B线程发现已经加过1了,也挂起,然后,C线程-1,唤醒A【先进先出】,C也被挂起。
 *
 * A被唤醒后,发现条件满足,+1后唤醒的是B【】,接着被挂起到C后面
 * B被唤醒后,发现已经+1过了,便挂起。
 * 此时三个线程都被永远挂起,再也无法醒来!!
 *
 * 所以要用notifyAll + while循环,notifyAll唤醒所有的线程,所有线程竞争,不符合运行条件的【即被虚假唤醒的】再次被挂起!从而解决了虚假唤醒的问题。也解决了notify丢失的问题!
 *
 * 总结:如果真的是随机唤醒的话,notify有概率解决问题,因为只唤醒一个,如果恰好唤醒的是对的,一点问题都没有,如果唤醒错的,则造成notify丢失。
 * 引入notifyall会导致所有线程唤醒,此时while就可以让他们回去睡觉,保证程序正常执行!
 */

总结:

1、代码设计思路:

一个Data类,里面放着生产方法和消费方法,通过创建多个线程,每个线程分别只调用生产或消费方法,来模拟生产者和消费者!

这样由于一个实例只有一把锁,所以很安全!

2、当设计多个生产者和多个消费者时,我们通常用while循环检查+notifyAll全部唤醒来解决!

  • 为什么while——防止虚假唤醒
  • 为什么notifyAll——防止唤醒信号丢失

不足:浪费cpu资源,如果线程很多,notifyall会造成大量切换上下文

4.2 Lock版的实现
①Condition
  • 旧的三板斧:syn + wait + notify

  • 新的三板斧:lock + await + signal

condition替换了原来老的对象监视器

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

深入理解:

这里的condition是由lock创建的,这一点已经很清楚的告诉我们了为什么wait、notify要配合syn使用。

syn关键字出现的时候,锁的一定是某个对象【即使锁的是Class,其本质也是锁的Class对象】

wait需要知道将当前线程放在哪个对象的等待队列中。所以需要配合监视器【Monitor】。就像这里的condition.await(),就能得知,要将当前线程放在lock对象的等待队列中。

syn锁某个对象时,相当于声明了接下来要监视的就是这个对象。此时再配合wait方法,就知道要将当前线程放在syn的对象中了!

  • 任何对象都有一个monitor,当它被持有后,将处于锁定状态。

  • syn本质是在代码段首和段位分别添加了monitorenter和monitorexit指令

根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁【可重入】,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。当另一个线程来检查该计数器,发现大于0时,就获取失败

②实现
class Data1 {
   
    private int number = 0;
    Lock lock = new ReentrantLock();
    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();
        } 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();
        } finally {
   
            lock.unlock();
        }
    }
}
  • lock与wait

如上,wait后,线程会释放锁,而唤醒只是让这些线程可以重新竞争锁,竞争成功的获取到锁,这个过程是隐式的,其他的仍然没有锁,处于等待状态。

当然,这里面的详细过程是,消费者消费了一个,唤醒全部,有可能其他消费者拿到锁了,上CPU,结果循环检查不通过,又wait,同时释放了锁

4.3 condition精准唤醒

问题:传统的notify只能做到随机唤醒,通过condition,我们可以做到精准唤醒

jvm规范种对于notify的规范是随机唤醒,但是实际上取决于每个jvm的实现。而广为使用的hotspot对于notify的实现是先进先出的唤醒!!

这里还是有一些区别的,condition是由lock创建的,但是实质上阻塞的时候是挂在condition上了,即把condition当作资源。但是一定要配合锁使用,即只有持有锁的时候,才有资格去await

class Method {
   
    private Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    State state = State.A;

    public void funcA() {
   
        lock.lock();
        try {
   
            while (state != State.A) {
   
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>AAAAA");
            condition2.signal();
            state = State.B;
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        } finally {
   
            lock.unlock();
        }
    }
}

五、syn详解

5.0 syn能锁什么

首先要明白,syn只能锁对象

  • 类的实例
  • 类对象

在此基础上,根据具体情况判断

  • 锁static方法,则锁的是类对象;否则锁的是实例
  • syn同步块,传入类对象,则锁类对象;否则锁实例
5.1 syn方法

syn方法锁的是方法的调用者,所以如果是static的,就会把类给锁了

public synchronized void sendMs1(){
   
    //锁实例
}

public static synchronized void sendMs2(){
   
    //锁类对象
}

public void sendMs3(){
   
    //普通方法不受锁影响 
}
5.2 syn块
public void send(){
   
    synchronized(Test.class){
   
	// 锁类
    }
    
    synchronized(this){
   
	// syn(this)就是锁当前实例
    }
}
5.3 说明
  • 每个实例对象都有一把锁,如果创建了两个对象,则锁一个不影响另一个执行
  • 每个类对象只有一把锁,如果类的锁被占用,则必须等待
  • 普通方法不受锁的影响

锁类对象的方法有三种

  • 直接syn静态方法

  • syn (类.class)

  • syn (this.getClass())

六、集合类不安全!

并发条件下,现有的集合类是不安全的。同时写入会报错:ConcurrentModificationException

根本原因是:modCount和expectedModCount不一致

6.1 ArrayList
public static void main(String[] args) throws InterruptedException {
   
    List<String> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
   
        new Thread(()->{
   
            list.add("a");
        }).start();
    }
    Thread.yield();
    System.out.println(list.
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值