JUC java并发面试题

1、synchronized的底层原理
java虚拟机里面的同步是基于进入和退出monitor对象实现的,无论是显式同步(同步代码块)还是隐式同步都是如此,当同步方法的时候并不是由monitorenter和monitorexit指令来实现同步的,而是由方法调用指令读取运行时常量池中的表结构的ACC_SYNCHRONIZED标志来隐式实现的;
同步代码块:monitorenter插入到同步代码块开始位置,monitorexit指令插入到同步代码块结束的位置,任何对象都有一个monitor与之关联,当且一个monitor被持有了之后,他将处于锁定的状态,当线程执行到monitorenter的地方时,他将会尝试获取对象锁持有的monitor所有权,即获取对象的锁;
monitorenter:每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter时尝试获取monitor的所有权,过程如下:
1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数置为1,该线程即为monitor的持有者;
2.如果线程已经占有该锁,只是重新进入,则将monitor的进入数加1;
3.如果其他线程已经占有了该锁,那么该线程就处于阻塞状态,直到monitor的进入数为0,则将再次尝试获取该锁的所有权;
monitorexit:执行monitorexit的线程必须是对象所对应的monitor的持有者,指令执行时,monitor的进入数减1,直到monitor的进入数为0时,他将不再是该对象的monitor的持有者,其他被这个monitor阻塞的线程将尝试获取该monitor的所有权;

对于方法来说,方法的同步并没有通过monitorenter和monitorexit来实现(理论上也是通过这两个指令来实现的),相比与普通方法,其常量池中多了ACC_SYNCHRONIZED标志符;
方法调用时,调用指令会先检测方法的ACC_SYNCHRONIZED访问标志有没有被设置,如果设置了,执行线程将会获取monitor,获取成功之后才可以执行方法体,方法执行完之后会释放monitor,在方法执行期间,任何其他线程将不能获取到同一个monitor对象;

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

2、volatile关键字,为什么不能保证原子性
被volicate关键字修饰的变量会禁止线程内部缓存(不同的cpu会缓存数据到CPU cache中,操作的时候会直接读缓存的数据)和重排序的,即直接修改内存,对其他线程是可见的,
volatile原理:volatile修饰变量后,对该变量进行写操作时,汇编指令中会执行一个LOCK 前缀指令,该LOCK指令可以保证1.将当前处理器缓存行的数据写入到主内存中去;2.这个写回内存的操作会使得其他处理器缓存了该内存的地址无效;
不能保证原子性:首先,java中只有对变量的赋值和读取是原子性的,其他的操作都不是原子性的,比如a++,它分为三步,先读取a的值,然后将a+1,最后将a的值刷新的主内存上;这里面包括多个原子操作,多个原子操作加起来就不能保证原子性的了,举个例子:比如2个线程同时对a=100做了a++操作,线程A读取了a的值,此时还没来得及修改就阻塞了,B线程也去读取了a的值,此时A线程还没有来得及修改,那么B线程读取的值还是100,然后B线程对a+1,再写到内存里面,之后继续执行A线程,A线程已经读取到了a的值然后对a+1,a的值还是101,在将值写入到缓存中,并刷新到主存中;所以即使volatile即使能保证被修饰的变量具有可见性,但是不能保证原子性;
屏蔽指令重排序:编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序,达到更好的效果,指令重排序在单线程先不会有问题,但是在多线程下,可能会出现问题;

三、lock的原理
lock是由java的jdk实现的,而synchronized是jvm自带的,以lock的可重入锁ReentrantLock为例,它把所有继承自Lock的操作委派给了Sync来实现,该类继承了AbstractQueuedSynchronizer,Sync有两个实现NonfairSync,FailSync,是为了非公平锁和公平锁定义的,默认情况下为非公平锁,
加锁是通过将所有的线程加入到CLH队列里面,当一个线程执行完成了之后会激活自己的后继节点;
nonfairTryAcquire是lock间接调用的第一个方法,每次请求锁时都会率先调用该方法,首先会先判断c==0,如果等于0则表示当前没有线程正在竞争该锁,如果c!=0说明已经有线程正在占用该锁。如果c等于0则通过CAS设置该状态值为acquires,acquires的初始调用值为1,每次线程重入该锁都会+1,每次unlock的时候该值都会-1,当该值等于0的时候则释放锁,如果CAS设置成功,则可以预计其他任何线程调用CAS都不会在成功,也就人为该线程获取了该锁,很显然,该线程并未进入等待队列,如果c !=0 但发现自己已经拥有锁,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能;
公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值l
非公平锁指在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的。

四:ConCurrentHashMap
ConcurrentHashMap是java并发包下的一个高性能的且线程安全的HashMap实现,它采用了分段锁机制,将hashmap的结构分成了多个segment,以ConcurrentLevel为16而言,ConcurrentHashmap最多支持16个线程同时指向写操作,相比较于hashtab的全局锁,性能会很高;
每个segment相当于一个哈希表,他的参数和hashmap的结构类似,比如loadFactory,threshold等等,
我们查看类他的源码之后就可以得知,当指向put方法时,一共指向两步操作,1、先找到对应的segment,然后判断segment是否初始化,若初始化则调用segment的put方法;
get方法无需加锁,其中共享变量都通过volatile修饰,通过volatile修饰过的变量保证了可见性,不会读到过期的数据;

在1.8的优化:
链表改成了红黑树,当链表中的节点超过可阈值时,会将链表转成红黑树;
segment的分段锁ReentrantLock改成了cas+synchronized

五、HashTable的原理
HashTable是一个线程安全的哈希表;其key和value不能为空;其数据结构是和HashMap相同的;
HashTable内部通过synchronized来实现线程安全的,由于synchronized锁住了整张表,所以效率很低于ConcurrentHashMap的;

六、CAS
CAS 的全称是COMPARE AND SWAP,指的是,当设置的旧值与预想的值一致则修改,否则不修改;
CAS可能引发的问题:ABA
ABA是指当线程A进行CAS逻辑时,在从内存获取值val到执行CAS逻辑之间,会有一个时间差,而恰巧在这个时间差里面,另一个线程B修改了val值并做了一些其他操作之后又将val值改成了原来的值,虽然线程A仍然能CAS执行成功,但是线程B在对线程A中间执行的逻辑可能会导致出现意料之外的结果;
解决ABA方式:如果出现ABA问题会对我们的业务逻辑产生影响,那么我们就必须处理ABA问题,如果不会对我们的业务的逻辑产生影响,我们可以处理也可以不处理,比如AtomicInteger,它没有其他属性可以改变的,我们只在意它实际的值,所以就算出现ABA问题,我们也无需处理;
可以通过加version的方式来处理ABA问题。类似于我们在Mysql中用到的乐观锁,当我们进行CAS时,对要修改的值预加一个版本号,在对比内存中的值时,不仅比对值是否相等,我们还要比对版本号是否一致,并且对值修改时,版本号做自增,这样就避免了值相同,当时值代表的意义不同的后果了;

七、了解CountDownLatch吗?
CountDownLatch称之为闭锁;
闭锁可以用来确保某些活动直到其他活动全部结束之后才进行;
主要包含两个方法,一个是countDown(),一个是await();
countDown方法用来给计数器减一;
await方法是用来阻塞当前线程,直到计数器为0的时候在唤醒线程继续执行;

final CountDownLatch cdl = new CountDownLatch(6);
for(int i=0; i<6; i++){
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"同学离开了");
            cdl.countDown();
        }
    }).start();
}

cdl.await();
System.out.println(Thread.currentThread().getName()+"要关门了,教室没人了");

八、CyclicBarrier?
CyclicBarrier称之为循环屏障;
CyclicBarrier构造方法有两个参数,一个是parties,一个是barrierAction
parties是线程的个数;
barrierAction是最后一个线程要做的事;
使用场景:可以用来多线程统计数据,最后合并计算结果的场景;
和CountDownLatch的区别?
CyclicBarrier可以使用多次,用完之后可以用reset方法重置,CountDownLatch只能使用一次;

final CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"要关门了,教室没人了");
            }
        });

        for(int i=0 ; i<10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"离开教室了");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

十、Semaphore?
信号量,他有一下几种用途?
1、用于多个共享资源的互斥使用;
2、控制线程并发的数量;

final Semaphore semaphore = new Semaphore(10);
        System.out.println("机房有10台电脑,有20个学院");
        for(int i=0; i<20; i++){
           new Thread(new Runnable() {
               @Override
               public void run() {
                   try {
                       semaphore.acquire();
                       System.out.println(Thread.currentThread().getName()+"上机了");
                       Thread.sleep(5000);
                       System.out.println(Thread.currentThread().getName()+"下机了");
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }finally {
                       semaphore.release();
                   }
               }
           }).start();
        }

十一、ThreadLocal
ThreadLocal是一个线程局部变量,同一个ThreadLocal所包含的对象在不同的线程中都维护了一个副本;
一个线程内部可以存在多个ThreadLocal变量,ThreadLocal变量内部维护了一个ThreadLocalMap,这么map的key是每个线程本身,这么map的value是每个线程内部维护的值,当我们使用set()、get()方法的时候实际上调用的是ThreadLocalMap对应的set()、get()方法;

  /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

内存泄露问题:
ThreadLocal可能会导致内存泄露的问题,每个线程内部维护了一个ThreadLocalMap的映射表,map的key是ThreadLocal变量,value是正在维护的值,因为ThreadLocalMap中的key是ThreadLocal的弱引用,弱引用的特点就是:如果这个对象值存在弱引用,那么下次垃圾回收的时候就一定会回收的;所以如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候就会回收掉。但是value是强引用,不会被回收掉,就会导致出现key为null的value值,只有等到这个线程结束了才会释放掉,如果使用的是线程池就会出现严重的内存泄露的问题了;
但是ThreadLocal考虑到了这一点,当调用ThreadLocal的set()、get()、remove()方法的时候就会清理掉key为null的记录,这样一来的话。会先内存泄露的情况只有可能出现在:key为null,没有手动调用remove()方法,而且之后也不会调用set()、get()、remove()方法;
在这里插入图片描述

使用场景:
一、每个线程需要有自己单独的实例
二、实例需要在多个方法中共享,但不希望被多线程共享

十二、ArrayList、LinkedList、Vector、HashSet的区别?

ArrayList是由数组实现的,LinkedList是由双向链表实现的;
数组可以通过下标来检索数据,所以ArrayList的查找快,但是插入数据涉及到数组元素移动等内存操作,所以插入和删除慢;
链表只能通过遍历来查询数据,所以查找慢,但是插入和删除的话不需要对数据进行移动,所以插入和删除快;

Vector和ArrayList都是动态数组实现的,但是Vercor内部通过Synchronized修饰的,是线程安全的,而ArrayList是非线程安全的;Vertor扩容时,每次扩容为原来的2倍,ArrayList扩容是每次扩容为原来的1.5倍;

HashSet底层是HashMap,因为HashMap是非线程安全的,所以HashSet也是非线程安全的;HashSet调用add方法的时候,会往内部的HashMap中插入一个key为添加的值 value为预设的值的数值;如果之前key存在则覆盖;所以HashSet是不允许重复的;

  • 4
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值