java多线程-面试题

java多线程

  1. 多线程的几种实现方式,什么是线程安全。
  • 继承Thread类,重写run方法
  • 实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
  • 通过Callable和FutrueTask创建线程
  • 线程安全:当多个线程访问某一个类、对象、方法时,对象对应的公共数据区始终都能表现正确,那么就是线程安全的。
  1. volatile的原理,作用,能代替锁么。
  • 作用:保持内存可见性防止指定重排序
  • 原理:
    • 每次变量读取前必须先从主内存刷新最新值,每次写入后必须同步回主内存中。
    • 编译器在生成字节码时,会在指令排序中插入内存屏障来禁止特定类型的处理器重排序。
  • volatile不能代替锁,其不具有 原子性操作。例如:变量自增操作。
  1. 线程的生命周期状态图。

    线程生命周期图

  2. sleep和wait的区别。

  • sleep是线程方法,wait是Object 方法
  • sleep不释放lock, wait会释放
  • sleep不依赖同步方法,wait需要
  • sleep不需要被唤醒,wait需要
  1. sleep和sleep(0)的区别。
  • 在线程没退出之前,线程有三个状态,就绪态,运行态,等待态。当线程调用sleep(n)的时候,线程是由运行态转入等待态,线程被放入等待队列中,等待定时器n秒后的中断事件,当到达n秒计时后,线程才重新由等待态转入就绪态,被放入就绪队列中,等待队列中的线程是不参与cpu竞争的,只有就绪队列中的线程才会参与cpu竞争
  • 而sleep(0)之所以马上回去参与cpu竞争,是因为调用sleep(0)后,因为0的原因,线程直接回到就绪队列,而非进入等待队列,只要进入就绪队列,那么它就参与cpu竞争
  1. Lock与Synchronized的区别 。
  • synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断
  • sysnchronized会自动释放锁,Lock需要在finally中手工释放锁
  • sysnchronized 的锁可重入、不可中断、非公平,Lock锁可重入、可中断、可公平(两者皆可)
  1. synchronized的原理是什么,一般用在什么地方(比如加在静态方法和非静态方法的区别,静

    态方法和非静态方法同时执行的时候会有影响吗),解释以下名词:重排序,自旋锁,偏向锁,轻

    量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁。

  • 在java中任何一个对象都有一个监视器(Monitor)与之对应,当一个Monitor被持有后,该对象处于锁定状态。synchronized在JVM里的实现都是基于进入和退出monitor对象来实现方法的同步和代码块同步

    • MonitorEnter指令:插入在同步代码块开始位置,当代码执行到该指令时,将会尝试获取该对象monitor的所有权,即尝试获得该对象的锁。
    • MonitorExit指令:插入在方法结束处和异常处,jvm保证每个MonitorEnter必须有对应的MonitorExit.
  • synchronized 加在静态方法上是锁住整个类的,针对的是类。synchronized加在非静态方法上是锁住类的对象上,是针对对象的,对象之间没有影响。静

    态方法和非静态方法同时执行的时候会产生互斥,是有影响的。

  • 名词解释:

    • 重排序:不改变单线程程序语义前提下,重新安排执行顺序
    • 自旋锁:当一个线程在获取锁时,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,知道获取到锁才推出循环。
    • 轻量级锁:线程在执行同步块之前,JVM会先在当前线程的栈帧中穿件用于存储锁记录的空间,并将锁对象的Mark Word复制到锁记录中。然后线程尝试用CAS将对象头中的Mark Work替换为指向锁记录的指针。如果成功,当先线程获得锁,如果失败自旋获取锁。再失败锁升级。
    • 偏向锁:在轻量级锁的基础上继续优化无资源竞争的情况。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。偏向锁只有在初始化时需要一次CAS,将Mark Word中记录ower,如果记录成功,则偏向锁获取成功,记录状态为偏向锁,以后当前线程等于owner就可以直接获取锁。否做,索命有其他线程竞争,升级为轻量级锁。
    • 可重入锁:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
    • 公平锁:加锁前先查看是否有排队等待的线程,有有限处理排在前边的线程
    • 非公平锁:线程加锁时直接尝试获取锁,获取不到自动到队尾等待
    • 乐观锁:假设不会发生并发冲突,直接不加锁去操作,如果冲突直接返回(CAS)
    • 悲观锁:假设一定会发生并发冲突,通过阻塞其他线程来保证数据完整性
  1. 用过哪些原子类,他们的原理是什么。
  • java.util.concurrent 包下提供了一些原子类:AtomicInteger、AtomicLong
  • 原子类中通过引入CAS机制,结束属性偏移量计算预期值。并配合volatile,保证属性原子性。
  1. JUC下研究过哪些并发工具,讲讲原理。
  • CountDownLatch :通过基数阻塞线程等待,内部通过lock实现。
  • CyclicBarrier: 循环栅栏,一组线程到达临界点后恢复执行
  • Exchange: 线程交换器。用于两个线程的数据交换,当Exchange只有一个线程时,该线程会阻塞直到有别的线程调用exchange缓冲区,当前线程与新线交换数据后同时恢复运行。
  • Semaphore:信号量。类似生产消费模式。
  1. 用过线程池吗,如果用过,请说明原理,并说说newCache和newFixed有什么区别,构造函

    数的各个参数的含义是什么,比如coreSize,maxsize等。

  • ThreadPoolExecutor 类是线程池最核心的类,其构造参数:
    • corePoolSize: 核心池大小。在创建线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoreSize后,就会把到达的任务放到缓存队列当中。
    • maximumPoolSize:线程池最大线程数
    • keepAliveTime: 表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池的线程数大于corePoolSize时才会起作用。
    • unit:参数keepAliveTime的时间单位
    • workQueue: 一个阻塞队列,用来存储等待执行的任务
    • threadFactory:线程工厂,用来创建线程
    • handler: 表示当拒绝处理时用的策略
  • Executors 提供四种线池的默认实现:
    • newCachedThreadPool:缓存线程池,如果线程池长度超过处理需要,可回收空闲线程,若无可回收线程,则创建线程
    • newFixedThreadPool: 定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
    • newScheduledThreadPool:计划线程池,支持定时及周期性任务执行。
    • newSingleThreadExecutor:单线程线程池,用唯一线程来执行任务。
  1. 线程池的关闭方式有几种,各自的区别是什么。
  • shutdown
    • 调用后不允许线程池继续添加线程,线程池的状态为shutdown状态,池中已有任务会继续执行,待所有任务执行完毕后线程池关闭。
  • shutdownNow
    • 该方法返回尚未执行的task的列表,线程池状态变为stop。阻止所有正在等待启动的任务,并且停滞正在执行的任务
  1. 假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有10个线程同

    时调用它,如何做到。

  • 借助Executors的计划线程池ScheduledThreadPoolExecutor

    public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { 	  
          super(corePoolSize, Integer.MAX_VALUE, 0,TimeUnit.NANOSECONDS, new DelayedWorkQueue(),threadFactory);
     }
    
  • 这个问题有时间要搞一下---------mark

  1. spring的controller是单例还是多例,怎么保证并发的安全
  • springmvc 默认是单例的,是线程非安全的。
  • 进来避免在controller中定义实例变量,如果非要定义实例变量并且有多线程修改情况下可以尝试一下方式处理:
    • 单例改多例 scope=“prototype”
    • 在controller中使用ThreadLocal变量。ThreadLocal会为每一个线程提供独立的变量副本,从而隔离多个线程对数据的访问冲突。因为每个线程都有自己的变量副本,从而也就没有必要对该变量进行同步。
  1. 用三个线程按顺序循环打印abc三个字母,比如abcabcabc。
  • synchronized 搭配wait/notify实现

    public class ThreadPrint implements Runnable{
        private String name;
        private Object prve;
        private Object value;
    
        public ThreadPrint(String name, Object prve, Object value) {
            this.name = name;
            this.prve = prve;
            this.value = value;
        }
    
        public void run() {
          int count = 3;
          while (count>0){
              synchronized (prve){
                  synchronized (value){
                      System.out.print(name);
                      count --;
                      value.notifyAll();
                  }
                  try {
                      if (count == 0){
                          prve.notifyAll();
                      }else {
                          prve.wait();
                      }
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
        }
    
        public static void main(String[] args) throws Exception{
            Object a = new Object();
            Object b = new Object();
            Object c = new Object();
    
            ThreadPrint p1 = new ThreadPrint("a",c,a);
            ThreadPrint p2 = new ThreadPrint("b",a,b);
            ThreadPrint p3 = new ThreadPrint("c",b,c);
            new Thread(p1).start();
            Thread.sleep(10);
            new Thread(p2).start();
            Thread.sleep(10);
            new Thread(p3).start();
        }
    
  • lock锁方式:

    public class ThreadPrintType2 {
        static Lock lock = new ReentrantLock();
        static int state = 0;
        static class ThreadA extends Thread{
            @Override
            public void run() {
                for (int i= 0;i<3;){
                    try {
                        lock.lock();
                        while (state%3 == 0){
                            System.out.print("a");
                            state++;
                            i++;
                        }
                    }finally {
                        lock.unlock();
                    }
                }
            }
        }
        static class ThreadB extends Thread{
            @Override
            public void run() {
                for (int i= 0;i<3;){
                    try {
                        lock.lock();
                        while (state%3 == 1){
                            System.out.print("b");
                            state++;
                            i++;
                        }
                    }finally {
                        lock.unlock();
                    }
                }
            }
        }
        static class ThreadC extends Thread{
            @Override
            public void run() {
                for (int i= 0;i<3;){
                    try {
                        lock.lock();
                        while (state%3 == 2){
                            System.out.print("c");
                            state++;
                            i++;
                        }
                    }finally {
                        lock.unlock();
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            new ThreadA().start();
            new ThreadB().start();
            new ThreadC().start();
        }
    
  1. ThreadLocal用过么,用途是什么,原理是什么,用的时候要注意什么。
  • 实例变量如果涉及到多线程操作导致线程安全问题,可以借助ThreadLocal,其提供线程变量副本,隔离线程之间的影响。其常用来解决数据库连接、session管理的问题。
  • Thread为每个线程维护了一个map:ThreadLocalMap.该map的key是LocalThread,value则是要存储的对象。ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value
  • ThreadLocalMap使用的是ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部引用,待系统gc时,该弱引用被回收,map中key值为null,而与之对应的value会一致维持一个强引用链无法回收,造成内存泄漏。使用时应该注意调用remove函数,手动清除。
  1. 如果让你实现一个并发安全的链表,你会怎么做。
  • 在链表实现的基础上加锁,所有设计到链表结构变化的地点加锁,例如,新增,删除等操作。例如:借助ReentrantLock()可重入锁对新增结点加锁,避免冲突。

    public class ConcurrentSingleLinedList{
        final static Lock lock = new ReentrantLock();
        Node head = null;
        static class Node {
            Node next = null;
            int data;
            public Node(int data){
                this.data = data;
            }
        }
        public void add(int data) {
            try {
                lock.lock();
                Node newNode = new Node(data);
                if(head == null){
                    head = newNode;
                    return;
                }
                Node temp = head;
                while(temp.next != null){
                    temp = temp.next;
                }
                temp.next = newNode;
            }finally {
                lock.unlock();
            }
        }
    }
    
  1. 有哪些无锁数据结构,他们实现的原理是什么。
  • java.util.concurrent.atomic 提供了AtomicInteger、AtomicLong、AtomicIntegerArray 等一系列的类,提供了一系列的原子操作。其实现借助于CAS:对比变量的值和expect是否相等,如果相等则将变量的值更新为update。
  1. 讲讲java同步机制的wait和notify。
  • java中 wait()和notify()方法都是Object方法。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。知道有其他线程调用对象notify()唤醒该线程,才能继续获取对象所,并继续执行。相应的notify()就是对象锁的唤醒操作。wait()和notify()需要在同步锁中。
  1. CAS机制是什么,如何解决ABA问题。
  • CAS:当需要更新一个变量的值得时候,只有当变量的预期值和内存地址中实际值相同的时候,才会把内存地址对应的值替换。
  • ABA:CAS比较的是两个时间节点的变量值变化,而在这两个时间节点内的变化并不关心,由此引发了目标值时A,但是过程有可能是 A-B-A。结果一样,过程不一定一致。解决该问题可以给操作添加版本号,jdk的atomic包里提供了一个类AtomicStampedReference来接榫ABA问题。
  1. 多线程如果线程挂住了怎么办。
  • 如果是wait()方法使线程过期,可以使用notify()和notifyAll()唤醒
  • 如果是suspend挂起,可以通过resume方法唤醒。
  1. countdowlatch和cyclicbarrier的内部原理和用法,以及相互之间的差别(比如

    countdownlatch的await方法和是怎么实现的)。

  • CountDownLatch: 一个线程等待其他多个线程完成一件事,才可以继续执行。
  • CyclicBarrier:线程之间相互等待,当所有线程都await之后,这些线程才继续执行。
  • CountDownLatch减计数,CyclicBarrier加计数。CountDownLatch是一次性的,CyclicBarrier可以重用。
  • await() 方法,初始化一个队列,将需要等待的线程加入队列中,并用waitStatus标记后继节点的线程状态。
  1. 对AbstractQueuedSynchronizer了解多少,讲讲加锁和解锁的流程,独占锁和公平所

    加锁有什么不同。

  • 独占锁和共享锁:独占锁模式下,每次只能有一个线程能持有锁,如:ReetrantLock。共享锁:允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock.
  • 公平锁和非公平锁:在公平锁上,线程将按他们发出请求的顺序来获得锁;而非公平锁则允许在线程发出请求后立即尝试获取锁,如果可用则直接获取锁,尝试失败才进行排队等待。
  • AQS是一个双向链表。其每个节点中都包含了一个线程和一个类型变量,表示当前节点是独占节点还是共享节点,头节点中的线程为正在占有锁的线程,而后的所有节点的线程表示为正在等待获取锁的线程。
  1. 使用synchronized修饰静态方法和非静态方法有什么区别。
  • 对于类锁synchronized static,是通过该类直接调用加类锁的方法,而对象锁是创建对象调用加对象锁的方法,两者访问是不冲突的
  1. 简述ConcurrentLinkedQueue和LinkedBlockingQueue的用处和不同之处
  • LinkedBlockingQueue 是阻塞队列。实现是线程安全的,实现了先进先出等特性,是作为成缠着消费者的首选
  • ConcurrentLinkedQueue 是非阻塞队列。当许多线程共享访问一个公共集合时时一个好的选择,其采用CAS操作保证元素一致性。
  1. 导致线程死锁的原因?怎么解除线程死锁。
  • 死锁条件:
    • 互斥使用:资源只被一个线程使用
    • 不可抢占: 不能抢夺资源
    • 请求和保持:保持已有资源占用并请求占有新资源
    • 循环等待: 线程间循环等待资源
  • 打破上述条件便可让死锁消失。
  • 避免死锁:注意加锁顺序,线程间对资源的访问时同一顺序。加锁设定时限,不可无限占有。
  1. 非常多个线程(可能是不同机器),相互之间需要等待协调,才能完成某种工作,问怎么设计这种协调方案。
  • 个人认为可以参考countdownlatch中await实现,借助队列协调线程之间状态。
  1. 用过读写锁吗,原理是什么,一般在什么场景下用。
  • 对于多个线程共享同一个资源的时候,多个线程同时对共享资源做读操作是不会发生线程安全性问题的,但是一旦有一个线程对共享数据做写操作其他的线程再来读写共享资源的话,就会发生数据安全性问题,所以出现了读写锁ReentrantReadWriteLock。
  • 原理:如果有线程想申请读锁的话,首先会判断写锁是否被持有,如果写锁被持有且当前线程并不是持有写锁的线程,那么就会返回-1,获取锁失败,进入到等待队列等待。如果写锁未被线程所持有或者当前线程和持有写锁的线程是同一线程的话就会开始获取读锁。借助CAS来实现。
  • 场景:大量读操作下,可以使用读写锁来并发处理。
  1. 开启多个线程,如果保证顺序执行,有哪几种实现方式,或者如何保证多个线程都执行完

    再拿到结果。

  • 保证顺序执行可以参考14题的两种实现
  • 保证多个线程都执行完再拿结果可以使用countdownlatch.
  1. 延迟队列的实现方式,delayQueue和时间轮算法的异同。
  • DelayQueue:位于java.util.concurrent包下,DelayQueue本质是封装了一个PriorityQueue,使之线程安全,加上Delay功能,也就是说,消费者线程只能在队列中的消息“过期”之后才能返回数据获取到消息,不然只能获取到null。其数据结构采用最小堆,插入和获取时,时间复杂度都为O(logN)
  • 时间轮:是一种数据结构,其主体是一个循环列表(circular buffer),每个列表中包含一个称之为槽(slot)的结构。这个数据结构最重要的是两个指针,一个是触发任务的函数指针,另外一个是触发的总第几圈数。时间轮可以用简单的数组或者是环形链表来实现。相比DelayQueue的数据结构,时间轮在算法复杂度上有一定优势。DelayQueue由于涉及到排序,需要调堆,插入和移除的复杂度是O(lgn),而时间轮在插入和移除的复杂度都是O(1)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章