2020-2021实习校招java面试题JUC大厂高频面试考点(下)(持续更新) - Go语言中文社区

2020-2021实习校招java面试题JUC大厂高频面试考点(下)(持续更新)


JUC面试题(难)

2020java基础面试题->传送门

一、volatile

1、了解volatile吗

volatile是Java虚拟机提供的轻量级的同步机制,有3个特性,分别是:保证可见性、不保证原子性、禁止指令重排

2、什么是指令重排

计算机在执行程序时,为了提高性能,编译器在编译java代码和处理器jvm字节码的时候常常会做指令重排多线程中使用线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测,所以需要使用轻量级的同步机制volatile。

3、在哪些地方用到过volatile?

多线程单例模式,通过引入DCL (Double Check Lock) 双端检锁机制

就是在进来和出去的时候,进行检测

public class SingletonDemo {
/**
instance = new SingletonDemo();可以分为以下三步进行完成:
memory = allocate(); // 1、分配对象内存空间
instance(memory); // 2、初始化对象
instance = memory; // 3、设置instance指向刚刚分配的内存地址
可能出现指令重排,故要加上volatile
*/
    private static volatile SingletonDemo instance = null;

    private SingletonDemo () {
        System.out.println(Thread.currentThread().getName() + "t 我是构造方法SingletonDemo");
    }

    public static SingletonDemo getInstance() {
        if(instance == null) {
            // a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
            synchronized (SingletonDemo.class) //b
            { 
           //c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
                if(instance == null) { 
                	// d 此时才开始初始化
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
//模拟多线程环境
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

二、CAS

1、CAS是什么?与synchronized有什么区别?cas有什么缺点?并如何解决

cas——Compare and Swap(比较并交换)是一种系统原语;它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁,所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

缺点:循环时间长,开销大,只能保证一个共享变量的原子操作,会产生ABA问题;

{ABA例子:假设有一个遵循CAS原理的提款机,小慧有100元存款,要使用这个提款机来提款50,由于提款机硬件出了点小问题,小灰的提款操作被同时提交两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。理想情况下,应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。线程1首先执行成功,把余额从100改成50。线程2因为某种原因阻塞了。这时候,小灰的妈妈刚好给小灰汇款50元。线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100。线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把变量值100更新成了50}

解决办法:1、使用AtomicReference原子引用(保证修改对象引用时的线程安全性),2、使用AtomicStampedReference时间戳原子引用修改版本号(每次修改原子值,版本号加1)

2、int变量在多线程下如何保证其原子性

使用AtomicInteger,调用其api进行增删改查操作,例如:

atomicInteger.compareAndSet(1, 2)

二、集合

1、ArrayList是否线程安全?会报出现什么异常?导致原因?怎么解决?

不安全;java.util.ConcurrentModificationException

1、使用new Vectore<>(),

2、Collections.synchronizedList(new ArrayList<>())

3、使用CopyOnWriteArrayList()

2、hashSet底层是什么实现的?

hashmap

3、hashmap使用put方法添加key-value键值对,但是hashset的add方法就添加了一个数,这怎么解释

hashset的add方法实际调用了hashmap的put方法,只不过添加的值维key,而value是一个常量PRESENTpivate static final Object PRESENT = null;)

三、锁

1、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

乐观锁,每次操作时不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
悲观锁是会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
乐观锁可以使用volatile+CAS原语实现,带参数版本来避免ABA问题,在读取和替换的时候进行判定版本是否一致
悲观锁可以使用synchronize的以及Lock

2、死锁产生的四个条件

互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请的资源。

3、理解可重入锁

可重入锁就是递归锁

指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取到该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁

也就是说:线程可以进入任何一个它已经拥有的锁所同步的代码块

ReentrantLock / Synchronized 就是一个典型的可重入锁

4、当我们在getLock方法加两把锁会是什么情况呢?

最后得到的结果也是一样的,因为里面不管有几把锁,其它他们都是同一把锁,也就是说用同一个钥匙都能够打开

当我们在getLock方法加两把锁,但是只解一把锁会出现程序直接卡死,线程不能出来,也就说明我们申请几把锁,最后需要解除几把锁

当我们只加一把锁,但是用两把锁来解锁的时候,运行程序会直接报错

5、什么是自旋锁、有什么优缺点?请手写一个自旋锁;

自旋锁:spinlock,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU,原来提到的cas,底层使用的就是自旋,自旋就是多次尝试,多次访问,不会阻塞的状态就是自旋。

优点:循环比较获取直到成功为止,没有类似于wait的阻塞

缺点:当不断自旋的线程越来越多的时候,会因为执行while循环不断的消耗CPU资源

/**
 * 手写一个自旋锁
 *
 * 循环比较获取直到成功为止,没有类似于wait的阻塞
 *
 * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
 * @author: 陌溪
 * @create: 2020-03-15-15:46
 */
public class SpinLockDemo {

    // 现在的泛型装的是Thread,原子引用线程
    AtomicReference<Thread>  atomicReference = new AtomicReference<>();

    public void myLock() {
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "t come in ");

        // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
        while(!atomicReference.compareAndSet(null, thread)) {

        }
    }

    /**
     * 解锁
     */
    public void myUnLock() {

        // 获取当前进来的线程
        Thread thread = Thread.currentThread();

        // 自己用完了后,把atomicReference变成null
        atomicReference.compareAndSet(thread, null);

        System.out.println(Thread.currentThread().getName() + "t invoked myUnlock()");
    }

    public static void main(String[] args) {

        SpinLockDemo spinLockDemo = new SpinLockDemo();

        // 启动t1线程,开始操作
        new Thread(() -> {

            // 开始占有锁
            spinLockDemo.myLock();


            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t1").start();


        // 让main线程暂停1秒,使得t1线程,先执行
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 1秒后,启动t2线程,开始占用这个锁
        new Thread(() -> {

            // 开始占有锁
            spinLockDemo.myLock();
            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t2").start();

    }
}
6、为什么需要读写锁?如何实现?

使用ReentrantLock创建锁的时候,是独占锁,也就是说一次只能一个线程访问,但是有一个读写分离场景,读的时候想同时进行,因此原来独占锁的并发性就没这么好了,因为读锁并不会造成数据不一致的问题,因此可以多个人共享读

/**
 * 读写锁
 * 多个线程 同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行
 * 但是,如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写
 *
 * @author: 陌溪
 * @create: 2020-03-15-16:59
 */

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 资源类
 */
class MyCache {

    /**
     * 缓存中的东西,必须保持可见性,因此使用volatile修饰
     */
    private volatile Map<String, Object> map = new HashMap<>();

    /**
     * 创建一个读写锁
     * 它是一个读写融为一体的锁,在使用的时候,需要转换
     */
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    /**
     * 定义写操作
     * 满足:原子 + 独占
     * @param key
     * @param value
     */
    public void put(String key, Object value) {

        // 创建一个写锁
        rwLock.writeLock().lock();

        try {

            System.out.println(Thread.currentThread().getName() + "t 正在写入:" + key);

            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            map.put(key, value);

            System.out.println(Thread.currentThread().getName() + "t 写入完成");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 写锁 释放
            rwLock.writeLock().unlock();
        }
    }

    /**
     * 获取
     * @param key
     */
    public void get(String key) {

        // 读锁
        rwLock.readLock().lock();
        try {

            System.out.println(Thread.currentThread().getName() + "t 正在读取:");

            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Object value = map.get(key);

            System.out.println(Thread.currentThread().getName() + "t 读取完成:" + value);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 读锁释放
            rwLock.readLock().unlock();
        }
    }

    /**
     * 清空缓存
     */
    public void clean() {
        map.clear();
    }


}
public class ReadWriteLockDemo {

    public static void main(String[] args) {

        MyCache myCache = new MyCache();

        // 线程操作资源类,5个线程写
        for (int i = 1; i <= 5; i++) {
            // lambda表达式内部必须是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt +  "");
            }, String.valueOf(i)).start();
        }

        // 线程操作资源类, 5个线程读
        for (int i = 1; i <= 5; i++) {
            // lambda表达式内部必须是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }
}

四、你认识哪些高并发下的计数器

1、CountDownLatch:减法,减到0

/**
现在有这样一个场景,假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的
*/
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {

        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "t 上完自习,离开教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }

        countDownLatch.await();

        System.out.println(Thread.currentThread().getName() + "t 班长最后关门");
    }
}

2、CyclicBarrier:和CountDownLatch相反,需要集齐七颗龙珠,召唤神龙。也就是做加法,开始是0

public class CyclicBarrierDemo {


    public static void main(String[] args) {
        /**
         * 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
         */
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙");
        });

        for (int i = 0; i < 7; i++) {
            final Integer tempInt = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "t 收集到 第" + tempInt + "颗龙珠");

                try {
                    // 先到的被阻塞,等全部线程完成后,才能执行方法
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

3、Semaphore:信号量

信号量主要用于两个目的

  • 一个是用于共享资源的互斥使用

  • 另一个用于并发线程数的控制

    /**
    模拟一个抢车位的场景,假设一共有6个车,3个停车位
    那么我们首先需要定义信号量为3,也就是3个停车位
     */
    public class SemaphoreDemo {
    
        public static void main(String[] args) {
    
            /**
             * 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
             */
            Semaphore semaphore = new Semaphore(3, false);
    
            // 模拟6部车
            for (int i = 0; i < 6; i++) {
                new Thread(() -> {
                    try {
                        // 代表一辆车,已经占用了该车位
                        semaphore.acquire(); // 抢占
    
                        System.out.println(Thread.currentThread().getName() + "t 抢到车位");
    
                        // 每个车停3秒
                        try {
                            TimeUnit.SECONDS.sleep(3);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        System.out.println(Thread.currentThread().getName() + "t 离开车位");
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 释放停车位
                        semaphore.release();
                    }
                }, String.valueOf(i)).start();
            }
        }
    }
    

五、jvm

1、画出JVM基本机构图

96698196-0f6b-4ef6-9a20-85dd35d75f27

  • java栈:存放局部变量,栈由一系列帧组成
  • java堆:存放所有new出来的东西
  • 方法区:被虚拟机加载的类信息、常量、静态常量等。
  • 程序计数器(和系统相关):每个线程拥有一个PC寄存器,在线程创建时创建,指向下一条指令地址
  • 本地方法栈:
2、谈谈你对GC Root的理解

用于标记回收算法:从GC root进行遍历,把可达对象都标记,剩下那些不可达的进行回收,这种方式需要中断其他线程,并且可能产生内存碎片。

java中可以作为GC Roots的对象有:虚拟机栈中引用的对象、方法区中的类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(natice方法)引用的对象。

3、 如何排查死锁

当我们出现死锁的时候,首先需要使用jps命令查看运行的程序jps -l

再使用jstack查看堆栈信息

jstack  xxxx   # 后面参数是 jps输出的该类的pid

通过查看最后一行,我们看到 Found 1 deadlock,即存在一个死锁

4、常见的GC算法

1)引用计数法

每个对象有一个计数器,当对象被引用一次则计数器加1,但对象引用失效一次减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。

缺点:每次对象复制时均要维护引用计数器,且计数器本身也有一定的消耗;较难处理循环引用。

在双端循环,互相引用的时候,容易报错,目前很少使用这种方式了

2)复制算法

复制算法在年轻代的时候,进行使用,复制时候有交换

image-20200318184759295

分对象会在From和To区域来回复制,如此交换15次(由jvm参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活就存入老年代中。

算法优点:没有产生内存碎片

3)标记清除

先标记后清除,缺点是会产生内存碎片,用于老年代多一些。

4)标记整理

标记清除整理

image-20200318185100936

5、你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值
使用jps和jinfo进行查看
1、jps:查看java的后台进程
2、jinfo:查看正在运行的java程序

生活常用调优参数

img

  • -Xms:初始化堆内存,默认为物理内存的1/64,等价于 -XX:initialHeapSize
  • -Xmx:最大堆内存,默认为物理内存的1/4,等价于-XX:MaxHeapSize
  • -Xss:设计单个线程栈的大小,一般默认为512K~1024K,等价于 -XX:ThreadStackSize
    • 使用 jinfo -flag ThreadStackSize 会发现 -XX:ThreadStackSize = 0
    • 这个值的大小是取决于平台的
    • Linux/x64:1024KB
    • OS X:1024KB
    • Oracle Solaris:1024KB
    • Windows:取决于虚拟内存的大小
  • -Xmn:设置年轻代大小
  • -XX:MetaspaceSize:设置元空间大小
    • 元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/yuec1998/article/details/108663813
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢