线程面试题

1,并发,并行,串行的区别。

串行是指是时间上不可能发生重叠,上一个任务没结束,下一个任务就不会执行。

并行是指在时间上是重叠的,两个任务互不干扰。

并发是指两个任务互相干扰,一个时间点只有一个任务的执行,交替执行。

2,谈谈你对线程安全的理解。

准确的说应该是内存安全,堆是内存共享的,是可以被所有线程访问到的,当多个线程访问同一个对象时,如果不用进行额外的同步控制其他的协调工作,调用这个对象的行为都可以获得正确的结论,那么可以认为这是线程安全的。我认为所谓的正确的结论是指在单机操作下得到的结论。

3,什么是强引用,弱引用,软引用,虚引用。

强引用(StrongReference):通过关键字new创建的对象所关联的引用就是强引用。垃圾回收机制是绝对不会回收它的,就算是内存空间不足时,jvm宁愿抛出OutOfMemoryError,使程序异常终止也不愿意将强引用回收。

Object obj = new Object();

软引用(SoftReference):如果一个对象只具有软引用,则内存空间充足时,垃圾回收器不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

    // 软引用
    String str = new String("abc");
    SoftReference<String> softReference = new SoftReference<String>(str);

弱引用(WeakReference):弱引用软引用的区别在于:只具有弱引用的对象拥有更短暂生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定很快发现那些只具有弱引用的对象。

    String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);

虚引用(PhantomReference): 虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

    String str = new String("abc");
    ReferenceQueue queue = new ReferenceQueue();
    // 创建虚引用,要求必须与一个引用队列关联
    PhantomReference pr = new PhantomReference(str, queue);

4,说说你对ThreadLocal 的理解?

ThreadLocal即线程变量,它将需要并发访问的资源复制多份,让每个线程拥有一份资源,由于每个资源都有自己的资源副本,所以没有必要再对该变量进行同步了。每一个Thread中有一个threadLocals的变量,它的类型是ThreadLocalMap 这个ThreadLocalMap是维护在ThreadLocal类中的类部类,ThreadLocal中有一个set方法,以下是set方法的源码

  public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

set方法需要传进来一个值,这个值以后回设置到ThreadLocalMap中的value中,调用set方法时会先拿到这个线程,然后调用ThreadMap中的getMap方法拿到这个线程中的ThreadLocalMap,判断是否为空,如果不为空就以threadlocal这个对象为键,value为值传入到threadlocalmap中,为空则调用createMap方法创建这个map。

 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

get方法不需要传入任何值,首先也是拿到当前线程,再调用getMap方法拿到此线程的ThreadLocalMap,因为每个ThreadLocalMap中都维护了一个Entry(以ThreadLocal为键,这里需要注意的是引用的ThreadLocal为弱引用,以我们传入的值为value),当这个map不为空时会以当前ThreadLocal为键拿到value值。如果这个map为空的话会调用setInitialValue方法,创建这个map并赋初始值,以当前ThreadLocal为键,以null为值。

 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();
    }
 private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
 protected T initialValue() {
        return null;
    }

内存泄漏问题:

首先看看Entry的源码:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocalMap中的Entry弱引用了一个ThreadLocal,上面也提到了弱引用机制,随时都有可能会被gc,THreadLocal的根源就在于ThreadLocalMap的生命周期是和ThreadLocal一样长的,但是它里面的Entry中以threadLocal为键是弱引用,随时都有可能会被回收,如果没有手动去删除对应的key就会导致内存泄漏,解决办法就是每次在调用完ThreadLocal之后都调用它的remove()方法。

5,ThreadLocal和Synchronized的区别。

 6,多线程有哪几种创建方式。

继承自Thread:重写run方法。在run方法中写具体任务,调用start方法启动线程。

实现Runnable:调用这样子的线程时还是需要new一下Thread类,将实现runnable的类放入其中,再通过thread调用start方法。注意,如果直接使用run方法是达不到多线程的目的,他还是一样的会等上一个线程结束之后再开始执行。

实现Callable:Callable规定的方法是call(),任务执行后有返回值,可以抛出异常。

通过线程池创建线程:线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

7,java中线程的生命周期?

新建状态:就是线程刚被new出来的时候,还没有被start。

就绪状态:线程创建之后,其他线程调用了它的start方法,此时线程变成可运行状态,等待cpu的调度。

运行状态:线程被cpu调度,执行里面的方法。

阻塞状态:阻塞是指线程由于某些原因,放弃了cpu的使用权,暂时停止了运行,直到线程进入就绪状态才有机会转为运行状态。阻塞状态又分为以下三种:

  • 等待阻塞:运行中的线程执行wait方法之后,会被放入到等待池当中,进入这个状态是不能主动唤醒的,必须依靠其他线程调用notify方法或notifyAll才能被唤醒,wait是Object中的方法。
  • 同步阻塞:运行中的线程获取某个对象的同步锁时,这个锁已被别的线程占用,这时此线程就会被jvm放入到锁池当中。
  • 其他阻塞:运行中的线程执行sleep或者join方法,或者发出了I/O请求,jvm就会把这个线程设置为阻塞状态,只有当sleep过时,join等待结束,I/O处理完毕,才会重新进入到就绪状态。

死亡状态:线程执行完毕,或者因为异常退出了run方法,该线程生命周期结束。

 8,线程池的底层工作原理

线程池的内部是通过队列+线程实现的,当我们利用线程池执行:

当有一个线程需要执行时:

如果线程池中的线程数小于corePoolSize(核心线程数),即时此时线程池中的线程都是空闲状态,也会创建线程来执行此任务。

如果线程池中的线程数大于或等于corePoolSize,但是workQueue(缓冲队列)未满,那么就会将此线程添加到workQueue中等待执行。

如果线程池中的线程数大于corepoolSize,且workQueue已满,但是线程池中的线程总数小于maxmumPoolSize,那么会创建临时线程来代为执行。

如果线程池中的线程数大于corePoolSize,且线程池总数等于maxmumPoolSize,那么会通过handler所指定的策略来处理此任务。

如果线程池中的线程数大于corePoolSize,且线程等待时间超过了keepAliveTime,线程将被终止,这样,线程池可以动态的调整线程池中的线程数。

9,线程池中的线程复用原理

线程池将线程进行解耦,线程是线程,任务是任务,同一个线程可以从阻塞队列中不断的获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都调用start方法来创建新线程,而是让每个线程去执行一个循环任务,在这个循环任务中,不断的检索是否有新任务需要被执行,如果有则执行,即调用任务中的run方法。将run方法当做一个普通的方法执行。

10,为什么要用线程池,解释一下线程池中的参数?

因为线程是我们程序中的稀缺资源。通过线程池可以做到:

1,降低资源的消耗,提高线程的利用率,降低创建和销毁线程的消耗。

2,提高响应速度,任务一旦来了,马上就有线程响应执行。

3,提高线程线程的可管理性,线程是稀缺资源,使用线程可以统一分配调用监控。

  • corePoolSize:代表核心线程数,也就是正常情况下能够创建创建个工作线程,这些线程创建之后不会销毁,常驻线程。
  • maxnumPoolSize: 代表的是最大线程数,与核心线程数相对应,表示允许线程池中所有能够工作的线程总数。
  • keepAliveTime:表示超出核心线程那部分线程能够存活的时间,意思是当我们核心线程数满了的时候,创建的一些临时线程,这些线程都有一个空闲时间,一旦超过了这个时间,就会被销毁,可以通过setkeepAliveTime来进行设置过期时间。
  • workQueue:用来存放任务的队列,当核心线程满了的时候,任务就会被加入到这个队列中进行排队。
  • ThreadFactory:实际上是一个线程工厂,用来生产线程执行任务,我们可以使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。我们也可以自定义工厂。
  • Handler:任务拒绝策略,有两种情况,第一种是当我们手动调用shutdown方法关闭线程池时,这时候哪怕是线程池还有没有执行完的任务,我们再想向线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理任务时,也是一种拒绝策略。

11,sleep(),wait(),join(),yield()的区别

   锁池:所有需要竞争锁的线程都会被放入到锁池当中,比如a对象的锁已被其中一个线程拿到,其他竞争线程都会进入到锁池中,等待a对象释放锁,当某个对象得到锁之后会进入到就绪状态。

等待池:线程调用wait方法之后,会进入到等待池当中,等待池中的线程不会去竞争同步锁的,只有调用notify或者notifyAll之后,线程才会开始竞争锁,notify是随机将等待池中的一个线程放到锁池当中,而notifyAll是将等待池中所有的线程放入到锁池当中。

1,sleep只是将线程进行休眠,不会释放锁,而wait会释放锁,使得其他线程可以竞争锁。

2,sleep是可以在任何地方使用的,但是wait,notify,notifyAll只能在同步块中使用。

3,sleep必须捕获异常,wait不用。

yield:调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间。

join:等待某个线程执行结束。

12,谈谈对守护线程的理解?

守护线程为所有非守护线程提供服务,任何一个守护线程都是jvm中所有非守护线程的保姆。它并不能只单独服务于某个线程。可以通过thread.setDaemon(true)设置,但必须是在thread.start上面。

13,并发中的三大特性?

  • 原子性:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。
  • 可见性:当一个线程对一个共享变量发生修改后,其他线程可以马上看到。
  • 有序性:有序性是指在java并发中,执行编码的两条指令按照代码的先后顺序执行。

14,谈谈你对volatile的理解?

volatile用来修饰对象的属性,在对被volatile修饰的属性进行修改时,会直接将高级缓存区中的值刷到住内存当中,这样子就保证了可见性,同样读的时候也会是直接从主内存中读取。底层是通过操作系统的内存屏障实现的,由于是内存屏障所以也会禁止指令重排,也就保证了有序性。

15,谈谈你对synchronized的理解?

synchronized是java中的一个关键字,也叫同步锁,一般可以用在实例方法上,静态方法上,以及代码块上,主要维护的是一个状态,这个状态就是同一时刻,只能有一个线程去访问synchronized修饰的方法或代码块,用在实例方法上,锁调用该方法的对象,用在静态方法上,锁当前类的所有对象,用在代码块上如果修饰的是对象则锁的是对象,如果修饰的是类,锁的是该类的所有对象。

16,synchronized可以用来修饰构造方法吗?

不能用来修饰构造方法,但是能修饰构造方法里面的代码块。

17,sychronized的偏向锁,轻量级锁,重量级锁。

1,偏向锁:在锁对象的对象头中记录改锁的线程id,下次这个线程再来获取锁时,可以直接获取到了。

2,轻量级锁:有偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争,偏向锁会升级成轻量级锁,之所以叫轻量级锁是为了和重量级锁区分开来。底层有自旋锁实现,并不会阻塞线程。

3,如果自旋次数过多,仍然没有获取到锁则会升级成重量级锁。重量级锁会导致线程阻塞。

3,自旋锁:自旋锁是线程获取锁的过程中,不会去阻塞线程,也就是无所谓唤醒,阻塞和唤醒这两步都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到则表示获取到了锁,这个过程线程一直在运行。

18,volatile和synchonized有什么区别?Volatile能不能保证线程安全?DCL单例为什么要加Volatile?

synchonized用来加锁,而Volatile用来只是保证线程的可见性,通常使用与一个线程写,多个线程读的情况。

volatile不能保证线程安全,因为Volatile不能保证原子性。

Volatile防止指令重排,在DCL中,防止高并发情况,指令重排造成的线程安全的问题。

19,谈谈你对AQS的理解,AQS如何实现可重入锁?

  • AQS是一个java线程的同步框架,是jdk中很多锁工具的核心实现框架。
  • 在AQS中,维护了一个信号量state和一个线程组成的线程队列。这个线程队列就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行,不同场景下应用不同。
  • 在可重入锁场景下,state就用来表示锁的状态。0表示无锁,没加一次锁,state就加1,释放锁state减1。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值