JUC面试题

1. 什么是JUC

  • java.util.concurrent包名的简写,是关于并发编程的API
  • 与JUC相关的有三个包:java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks。
  • java.util表示工具包,包最开始是为了屏蔽同名,但是更多的是考虑分类。

重点讲解这四个包

image-20210425155652778

  • 解决一些业务的时候,会用普通的线程代码:Thread实现,但是这样效率并不高。
  • Runnable:没有返回值,效率先比于Callable相对较低,功能也没有Callable强大。企业中用Runnable比较少。

image-20210425161937935

  • 我们还需了解一下Lock

image-20210425162006617

2. 线程和进程

进程线程是什么?

进程:就是一个应用程序,如QQ.exe ,music.exe程序
线程:进程的一个执行单位,负责当前进程中程序的执行,一个进程至少有一个线程。

  1. 一个进程中可能包含多个线程,但是至少包含一个线程。

​ 开了一个进程 :在Typora中,写字,自动保存等操作是线程负责的。

  1. 对于Java而言:通过Thread、Runnable、Callable、线程池实现。

在java中一个应用程序至少有几个线程?

2个, main线程(用户线程)、GC线程(守护线程)。

Java中Native关键字的作用

​ native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

​ Java是跨平台的,既然是跨平台就,所以付出的代价是牺牲了对底层的控制,而Java要是先对底层的控制就要一些其他语言的帮助。

​ JNI是Java本机接口(Java Native Interface),是一个本机编程接口,它是Java软件开发工具箱(java Software Development Kit,SDK)的一部分。JNI允许Java代码使用以其他语言编写的代码和代码库。

Invocation API(JNI的一部分)可以用来将Java虚拟机(JVM)嵌入到本机应用程序中,从而允许程序员从本机代码内部调用Java代码。

Java真的可以开启线程吗?

答案是开不了的!

关键在最后一句代码native修饰的方法,调用了底层的C++,因为Java是运行在JVM之上的无法直接操作硬件。

public synchronized void start() {
    

/**
* This method is not invoked for the main method thread or "system" 
* group threads created/set up by the VM. Any new functionality added 
* to this method in the future may have to also be added to the VM. 
*
* A zero status value corresponds to state "NEW". 
*/ 

if (threadStatus != 0) 
throw new IllegalThreadStateException(); 

/* Notify the group that this thread is about to be started 
* so that it can be added to the group's list of threads 
* and the group's unstarted count can be decremented. 
*/
group.add(this); 
boolean started = false; 
try {
   
start0(); 
started = true; 

} finally {
    

try {
   

if (!started) {
    

group.threadStartFailed(this); 

} 

} catch (Throwable ignore) {
    

/* do nothing. If start0 threw a Throwable then 
   it will be passed up the call stack 
*/ 

} 

} 

}
// 本地方法,底层的C++ ,因为Java 无法直接操作硬件 
private native void start0();

线程有6个状态:

public enum State {
   
NEW,		     //尚未启动的线程,处于此状态。
    
RUNNABLE,       // 在Java虚拟机中执行的线程,处于此状态。
    
BLOCKED,        //阻塞:被阻塞等待监视器锁定的线程,处于此状态。
    
WAITING, 	    // 等待: 正在等待 执行特定动作的线程,处于此状态。
    
TIMED_WAITING,  // 延时等待:正在等待另一个线程执行动作达到指定等待时间的线程,处于此状态。
    
TERMINATED;    //终止,结束:已退出的线程,处于此状态。
}

并发、并行

并发:

同一个CPU执行多个任务,按细分的时间片交替执行;多个线程操作一个资源类,快速交替的过程。

并发编程的主要目的,充分利用CPU的资源,提高性能

为什么这样做能提高效率?
这相当于2个小孩一共吃两碗菜,但只有一双筷子,必须两个孩子都吃完才能去睡觉,他们就轮流着用那双筷子吃,但是不论是轮流着吃还是孩子吃完后,另一个孩子再吃,同时吃完的时间都相同

并行:

在多个CPU上同时处理多个任务,也就是CPU多核,多个线程可以同时运行。例如:线程池。

总结:交替叫并发,同时叫并行。

在这里插入图片描述

举个例子:

你吃饭,吃到一半,电话来了,3种情况:

1.先吃完饭,再接电话(单线程)
2.先接电话再吃 (交替、并发)
3.边吃边接电话 (并行)

通过代码获取本机的核数

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

// 获取cpu的核数 
// CPU 密集型,IO密集型 
System.out.println(Runtime.getRuntime().availableProcessors()); 
} 

}

wait/sleep()的区别

    1. wait属于Object类
    2. sleep属于Thread类
  1. 是否会释放锁

    1. wait释放

      调用wait()方法,会使当前线程放弃锁,线程暂停执行,该线程进入对象的等待池。

      只有调用对象的notify()方法或者notifyall()方法才能唤醒等待池中的线程进入等锁池,而只有获取到了锁,才能进到就绪状态。

    2. sleep不释放(可以想象成抱着睡觉哈哈哈哈哈哈嗝~)

      将执行的机会让给其他线程,即把使用CPU的机会给其他线程,但会对该对象的锁仍然保持,sleep时间结束后该线程自动恢复,即进入就绪状态。

  2. 使用的范围

    1. wait、notify、notifyAll只能使用在同步方法或者同步代码中
    2. sleep可以使用到任意地方(可以在任何地方睡觉)
  3. 异常

    1. wait不需要捕获异常
    2. sleep必须捕获异常

扩展

  1. sleep()、 suspend() 和 resume() 、yield() 阻塞时都不会释放占用的锁(如果占用了的话)。

    wait(),notify()方法阻塞时要释放占用的锁。这是最核心的区别,这一核心区别导致了一系列细节上的区别(如下几条区别)。

  2. wait()、notify()方法属于Object。也就是锁所有对象都拥有这一对方法,锁任何对象都有的。调用对象的wait()方法导致阻塞,当然也要有解除阻塞的方法,也就是notify()方法。notify()方法只是唤醒因调用该对象的 wait() 方法而阻塞的线程,且在这之中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

  3. wait(),notify()方法必须在synchronized方法或块中调用。

    原因(有锁才能释放锁):

    ​ 只有在synchronized 方法或块中,当前线程才占有锁,才有锁可以释放。同样的道理,因为使对象调用方法,所以调用这方法的对象,该对象上的锁必须为当前线程所拥有,这样才有锁释放。对象的锁=当前线程的锁

    ​ 所以,这一对方法调用必须放置在这样的 synchronized 方法或块中,这样才能使该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现illegalMonitorStateException异常。

  4. 为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用?

    这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁。

  5. wait() 和notify()与操作系统进程间的通信机制结合

注意:

关于wait()和notify():

  1. notify方法只能唤醒由调用wait方法进入阻塞的线程,且唤醒哪个线程我们是无法预期的,所以要避免虚假唤醒的情况

  2. notifyAll()方法也可以起到和notify()一样的作用,调用notifyAll()方法可以把因该对象调用wait()方法而进入阻塞的所有线程,一次性全部解除阻塞。当然只有获得锁才可以进如可运行状态。

    说到阻塞,就不得不谈一谈suspend()方法和wait()方法的调用都可能产生死锁。遗憾的是,Java并不在语言级别上支持死锁的避免。

3. Lock

传统的Synchronized

public class SaleTicketTest {
   
    public static void main(String[] args) {
   
        //资源类
        // 并发:多线程操作同一个资源类, 把资源类丢入线程
        SaleTicket saleTicket = new SaleTicket();
        //线程A
        new Thread(() -> {
   
            for (int i = 0; i < 40; i++) {
   
                saleTicket.saleTicket();
            }
        }, "A").start();


        new Thread(() -> {
   
            for (int i = 0; i < 40; i++) {
   
                saleTicket.saleTicket();
            }
        }, "B").start();


        new Thread(() -> {
   
            for (int i = 0; i < 40; i++) {
   
                saleTicket.saleTicket();
            }
        }, "C").start();
    }
}
//售票对象
class SaleTicket {
   
    private int number = 50;
    // synchronized 本质: 队列,锁
    public synchronized void saleTicket() {
   
        if (number > 0) {
   
            System.out.println(Thread.currentThread().getName() + "卖出第" + (number--) + "还剩" + number + "张票");
        }
    }
}

使用 juc.locks 包下的类操作 Lock 锁 + Lambda 表达式

Lock是一个接口,其实现类ReentranLock(可重入锁,后面章节会详细讲解)。

在这里插入图片描述

image-20210425201428277

image-20210425201449231

由上可知:

公平锁:十分公平,可以先来后到!

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

Lock的编程模型:

1、创建锁;
2、加锁:lock.lock();
3、在try/catch里面写业务代码;
4、在try/catch的finally里面解锁lock.unlock();

注意:

Lock的加锁解锁要配对出现,即有一个lock.lock()加锁,就必须在finally里面有一个lock.unlock(),出现两个lock.lock()加锁,在finally里面也必须有两个lock.unlock()。

完整的代码如下:

public class SaleTicketTest {
   
    //此处代码省略
}

//售票对象
class SaleTicket {
   
    private int number = 50;
    //锁lock
    private Lock lock=new ReentrantLock();

    public void saleTicket() {
   
        //加锁
        lock.lock();
        try{
   
            if (number > 0) {
   
                System.out.println(Thread.currentThread().getName() + "卖出第" + (number--) + "还剩" + number + "张票");
            }
        }catch (Exception e){
   
            e.printStackTrace();
        }finally {
   
            //解锁
            lock.unlock();
        }
    }
}

synchronized和lock区别(自动挡、手动挡)

  1. synchronized :Java内置的。

    Lock :是一个Java 类;

  2. synchronized:无法判断是否获取锁。

    Lock:可以判断是否获得锁(通过isLocked()方法);

  3. synchronized:锁会自动释放!

    Lock:需要手动在 finally 释放锁,如果不释放锁,就会死锁,Lock的加锁解锁要配对出现;

  4. synchronized:线程1阻塞,线程2永久等待下去。

    Lock:可以 lock.tryLock(); // 尝试获取锁,如果尝试获取不到锁,可以结束等待;

  5. synchronized:可重入,不可中断,非公平的。

    Lock:可重入、可以判断是否获得锁(通过isLocked()方法)、非公平(也可设置公平)

注意:可重入就是说某个线程已经获得某个锁,可以多次获取相同的锁而不会出现死锁。

  1. Synchronized:适合锁少量的代码同步问题,

    Lock:适合锁大量的同步代码

锁是什么,如何判断锁的是谁??!!

4. 生产者和消费者的问题

面试的:单例模式、排序算法、生产者和消费者、死锁

生产者和消费者问题 Synchronized 版

public class A {
   
    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();
    }
}
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);
        //通知其他线程 我+1完毕了
        this.notifyAll();
    }

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

问题存在,A线程B线程,现在如果我有四个线程A B C D!

问题如下:

image-20210425204934455

简单说就是if判断只会判断一次

image-20210425205239122

image-20210425205338513

虚假唤醒:

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

image-20210425210639335

if改为while就可以了!!!

生产者和消费者-新版JUC写法

await、signal 替换 wait、notify

image-20210425212709829

通过Lock 找到 Condition

img

代码实现:

通过Lock实例调用lock()和unlock()方法加锁

通过Lock实例.newConditional()创建Conditional实例

Conditional实例.await//等待

Conditional实例.signal//通知

package com.juc.study.lockdemo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* @ClassName:
* @PackageName: com.juc.study.lockdemo
* @author: youjp
* @create: 2020-04-14 15:25
* @description: TDOO 生产者消费模型:判断、干活、通知  //新版写法 
* @Version: 1.0
*/
public class B {
   
    public static void main(String[] args) {
   


        Data2 data = new Data2();
        new Thread(() -> {
   
            for (int i = 0; i < 10; i++) {
   
                data.increment();
            }
        }, "A").start();


        new Thread(() -> {
   
            for (int i = 0; i < 10; i++) {
   
                data.decrement();
            }
        }, "B").start();


        new Thread(() -> {
   
            for (int i = 0; i < 10; i++) {
   
                data.increment();
            }
        }, "C").start();
        new Thread(() -> {
   
            for (int i = 0; i < 10; i++) {
   
                data.decrement();
            }
        }, "D").start();

    }

}

//属性、方法
class Data2 {
   
    
    private int num = 0;
    //定义锁
    private Lock lock=new ReentrantLock();
    private Condition condition=lock.newCondition();


    //+1操作
    public  void increment() {
   
        //加锁
        lock.lock();

        try {
   
            //判断
            while (num > 0) {
   
                 condition.await(); //等待
             }
            //干活
            num++;
            System.out.println(Thread.currentThread().getName() + "线程加操作\t" + num);
            condition.signalAll();//通知
            
        } catch (Exception e) {
   
            e.printStackTrace();
        } finally {
   
            //解锁
            lock.unlock();
        }
    }


    //-1 操作
    public  void decrement() {
   
        //加锁
        lock.lock();
        try {
   
            //判断
            while (num == 0) {
   
                condition.await();//等待
             }
            //干活
            num--;
            System.out.println(Thread.currentThread().getName() + "线程减操作\t" + num);
            condition.signalAll();//通知
        } catch (Exception e) {
   
            e.printStackTrace();
        } finally {
   
            lock.unlock();//    解锁
        }
    }
}

任何一个新的技术,绝对不是仅仅只是覆盖了原来的技术,优势和补充!

以上4个进程,唤醒是随机的,即进程的执行时随机的,不是有序进行的,那怎么能让进程有序执行呢。接下来我们讨论进程间如何精确通知访问。

控制线程精确通知顺序访问 Condition

/**
* @ClassName:
* @PackageName: com.juc.study.lockdemo
* @author: youjp
* @create: 2020-04-15 14:19
* @description:    TODO 精确线程调用A->B->C->A
* @Version: 1.0
*/
public class C {
   
    public static void main(String[] args) {
   
        Data3 da = new Data3();
        new Thread(() -> {
   
            for (
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值