zoukankan      html  css  js  c++  java
  • Java 关于线程的面试题及答案

    一、职场可能碰到的关于线程的面试题:

    1、 什么是线程?

      线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

    2、 线程和进程有什么区别

    简介:

    进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)

    线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

    线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

    区别:

    进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

    1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

    2) 线程的划分尺度小于进程,使得多线程程序的并发性高。

    3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

    4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行必须依存在应用程序中,由应用程序提供多个线程执行控制。

    5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

    3、 如何在 Java 中使用新线程

       第一种(继承Thread):

      

    package com.test;
    
    public class ThreadTest {
    
        public static void main(String[] args) {
            System.out.println("主线程ID:"+Thread.currentThread().getId());
            //实例化对象
            MyCustomThread thread1 = new MyCustomThread();
            //启用线程
            thread1.start();
            MyCustomThread thread2 = new MyCustomThread();
            thread2.start();
    
    
        }
    
    }
    
    //创建一个类,继承Thread,重写run()
    class MyCustomThread extends Thread{
        @Override
        public void run() {
            //任务执行代码
            System.out.println("当前线程ID:"+Thread.currentThread().getId());
        }
    }

      

      第二种(实现Runnable):

      

    package com.test;
    
    public class ThreadTest {
    
        public static void main(String[] args) {
            System.out.println("主线程ID:"+Thread.currentThread().getId());
            Thread t1= new Thread(new MyCustomThread());
            t1.start();
            Thread t2 = new Thread(new MyCustomThread());
            t2.start();
    
        }
    
    }
    
    //创建一个类,实现Runnable接口,实现run()方法
    class MyCustomThread implements Runnable{
        @Override
        public void run() {
            //任务执行代码
            System.out.println("当前线程ID:"+Thread.currentThread().getId());
        }
    }

    4、 start() 和 run() 有什么区别

       start:
      用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,

      并没有运行,一旦得到CPU时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。


      run:
      run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代

      码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,

      start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void.。

    5、 Thread.join 方法有什么用

     Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。

            MyCustomThread t1 = new MyCustomThread();
            MyCustomThread t2 = new MyCustomThread();
    
            /**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
             程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕
             所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
             */
            
             t1.start();
             t1.join();
             t2.start();
             t2.join();
             
     
    
    }

      注意:join方法必须在线程start方法调用之后调用才有意义。这个也很容易理解:如果一个线程都没有start,那它也就无法同步了。

    6、 假如新建T1、T2、T3三个线程,如何保证它们按顺序执行?

      第一种(join方法):

    package com.test;
    
    public class ThreadTest {
    
        public static void main(String[] args) throws InterruptedException {
            MyCustomThread t1 = new MyCustomThread("线程A");
            MyCustomThread t2 = new MyCustomThread("线程B");
            MyCustomThread t3 = new MyCustomThread("线程C");
             t1.start();
             t1.join();
             t2.start();
             t2.join();
             t3.start();
             t3.join();
    
        }
    
    }
    
    
    class MyCustomThread extends Thread{
        private String name;
        public MyCustomThread(String name){
            this.name=name;
        }
        @Override
        public void run() {
            System.out.println("线程:"+name);
        }
    }

    结果:

      第二种(synchronized修饰方法,该方法不会同时被执行 ):

        synchronized关键字修饰的方法。

        由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,

        内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

    package com.test;
    
    public class ThreadTest {
    
        public static void main(String[] args) throws InterruptedException {
            //注意,下面是多线程,单实例
            Box obj = new Box();
            MyCustomThread myCustomThread = new MyCustomThread(obj);
            Thread t1 = new Thread(myCustomThread);
            Thread t2 = new Thread(myCustomThread);
            Thread t3 = new Thread(myCustomThread);
             t1.start();
             t2.start();
             t3.start();
    
        }
    
    }
    
    class Box{
        public int number;
    }
    
    class MyCustomThread implements Runnable{
        Box obj;
        public MyCustomThread(Box obj){
            this.obj = obj;
        }
        @Override
        public void run() {
            counter();
        }
        /**
         * 同步方法,锁的是调用该方法的对象,
         * 所有当不是单实例的时候就失效了
          */
        public synchronized void counter(){
            //可能会发生资源征用导致得到结果错误的地方
            for (int i = 0; i < 10000; i++) {
                obj.number = obj.number+1;
             }
            System.out.println(obj.number);
        }
    
    }

    结果:

        第三种(synchronized修饰关键字):

      synchronized关键字修饰的语句块。 
      被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

    package com.test;
    
    public class ThreadTest {
    
        public static void main(String[] args) throws InterruptedException {
    
            Box obj = new Box();
            MyCustomThread myCustomThread = new MyCustomThread(obj);
            Thread t1 = new Thread(myCustomThread);
            Thread t2 = new Thread(myCustomThread);
            Thread t3 = new Thread(myCustomThread);
             t1.start();
             t2.start();
             t3.start();
    
        }
    
    }
    
    class Box{
        public int number;
    }
    
    class MyCustomThread implements Runnable{
        Box obj;
        public MyCustomThread(Box obj){
            this.obj = obj;
        }
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                /*
                     锁住要进行修改的对象,此时对象obj对象在执行
                     obj.number = obj.number + 1时,只允许一个线程进行
                 */
                synchronized (obj) {
                    obj.number = obj.number + 1;
                }
            }
            System.out.println(obj.number);
        }
    
    
    }

       第四种(关键字volatile修饰字段):

        使用特殊域变量(volatile)实现线程同步

        volatile:不稳定的;反复无常的;易挥发的;

        1、volatile关键字为域变量的访问提供了一种免锁机制,

        2、使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,

        3、因此每次使用该域就要重新计算,而不是使用寄存器中的值

        4、volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

       注意:在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,

          如n=n+1、n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。

          所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。

       错误事例:

    package com.test;
    
    public class ThreadTest {
    
        public static void main(String[] args) throws InterruptedException {
    
            MyCustomThread myCustomThread = new MyCustomThread();
            Thread t1 = new Thread(myCustomThread);
            Thread t2 = new Thread(myCustomThread);
            Thread t3 = new Thread(myCustomThread);
             t1.start();
             t2.start();
             t3.start();
    
        }
    
    }
    
    
    class MyCustomThread implements Runnable{
        private volatile int number=0;
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                /*
                    这个是错误实例,this.number = this.number+1
                    变量的值由自身的上一个值决定,所以volatile失效
                */
                this.number = this.number + 1;
            }
            System.out.println(this.number);
        }
    
    
    }

    结果:

       

      第五种(通过Lock锁):

     使用重入锁实现线程同步

      在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。

      ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 
      它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力

      ReenreantLock类的常用方法有:

        ReentrantLock() : 创建一个ReentrantLock实例

        lock() : 获得锁

        unlock() : 释放锁

    注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用 

       代码实现:

    package com.test;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ThreadTest {
    
        public static void main(String[] args) throws InterruptedException {
    
            MyCustomThread myCustomThread = new MyCustomThread();
            Thread t1 = new Thread(myCustomThread);
            Thread t2 = new Thread(myCustomThread);
            Thread t3 = new Thread(myCustomThread);
             t1.start();
             t2.start();
             t3.start();
    
        }
    
    }
    
    
    class MyCustomThread implements Runnable{
        private int number=0;
        //创建对象
        private Lock lock = new ReentrantLock();
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                //锁住
                lock.lock();
                try {
                    this.number = this.number + 1;
                }finally{
                    //解锁
                    lock.unlock();
                }
            }
            System.out.println(this.number);
        }
    
    
    }

       结果:

    7、 volatile 关键字是做什么的

      Volatile字段是用于线程间通讯的特殊字段。每次读volatile字段都会看到其它线程写入该字段的最新值;实际上,程序员之所以要定义volatile字段是因为在某些情况下由于缓存和重排序所看到的陈旧的变量值是不可接受的。编译器和运行时禁止在寄存器里面分配它们。它们还必须保证,在它们写好之后,它们被从缓冲区刷新到主存中,因此,它们立即能够对其他线程可见。相同地,在读取一个volatile字段之前,缓冲区必须失效,因为值是存在于主存中而不是本地处理器缓冲区。在重排序访问volatile变量的时候还有其他的限制。

    在旧的内存模型下,访问volatile变量不能被重排序,但是,它们可能和访问非volatile变量一起被重排序。这破坏了volatile字段从一个线程到另外一个线程作为一个信号条件的手段。

    在新的内存模型下,volatile变量仍然不能彼此重排序。和旧模型不同的时候,volatile周围的普通字段的也不再能够随便的重排序了。写入一个volatile字段和释放监视器有相同的内存影响,而且读取volatile字段和获取监视器也有相同的内存影响。事实上,因为新的内存模型在重排序volatile字段访问上面和其他字段(volatile或者非volatile)访问上面有了更严格的约束。当线程A写入一个volatile字段f的时候,如果线程B读取f的话 ,那么对线程A可见的任何东西都变得对线程B可见了。

    8、 如果一个线程中发生了异常会怎么样

      Java中Throwable分为Exception和Error: 
        出现Error的情况下,程序会停止运行。 
        Exception分为RuntimeException和非运行时异常。 
        非运行时异常必须处理,比如thread中sleep()时,必须处理InterruptedException异常,才能通过编译。 
        而RuntimeException可以处理也可以不处理,因为编译并不能检测该类异常,比如NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException等。

        由此题目所诉情形下发生的应该是RuntimeException,属于未检测异常,编译器不会检查该异常,可以处理,也可不处理。 
     所以这里存在两种情形: 
        ① 如果该异常被捕获或抛出,则程序继续运行。 
        ② 如果异常没有被捕获该线程将会停止执行。 
        Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用

        Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler,并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。

    9、 什么是 ThreadLocal 变量

      ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

    从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。

    通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

    10、 什么是线程池,为什么要使用

      1、什么是线程池:  java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池

        多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。    
             假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。

             如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

         一个线程池包括以下四个基本组成部分
                    1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
                    2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
                    3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
                    4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

       线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
        线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
        假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。

      代码实现:

    package com.test;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ThreadExectorService {
    
        public static void main(String[] args) {
            //线程池创建
            ExecutorService service = Executors.newFixedThreadPool(4);
            int coutn = 100;
            for (int i = 0; i < coutn; i++) {
                //线程池使用
                service.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread());
                    }
                });
            }
        }
    }

    11、 Thread.sleep 和 wait 方法有什么不同

      1、sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复,调用sleep 不会释放对象锁。由于没有释放对象锁,所以不能调用里面的同步方法。

    sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
    sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
    在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

    wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);可以调用里面的同步方法,其他线程可以访问;
    wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
    wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

      2、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

      sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。

    注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,通过t.sleep()让t对象进入sleep,这样的做法是错误的,它只会是使当前线程被sleep 而不是t线程

    wait属于Object的成员方法,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也同样会在wait的过程中有可能被其他对象调用interrupt()方法而产生  

    12、 synchronized 关键词是做什么的

      synchronized 关键字,它包括两种用法:synchronized 方法 synchronized 块
        1. synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。

        2. synchronized 块:通过 synchronized关键字来声明synchronized 块。

    13、什么是Servlet?

      Servlet(Servlet Applet),全称 Java Servlet,是用Java编写的服务器端程序。其主要功能在与交互的浏览器和修改数据时,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet的类,Servlet只用于来扩展基于HTTP协议的Web服务器。

    14、什么是JSP?

      JSP全称Java Server Pages,是一种动态网页开发技术,它使用JSP标签在HTML网页中插入Java代码。

  • 相关阅读:
    C语言 va_start 宏
    C语言 strcat_s 函数
    C语言 strcat 函数
    C语言 memcpy_s 函数
    C语言 memcpy 函数
    C语言 strcpy_s 函数
    C语言 strcpy 函数
    C语言 sizeof 函数
    c++实现扫雷游戏 初学
    .Net vs .Net Core,我该如何选择?看这一篇文章就够了
  • 原文地址:https://www.cnblogs.com/ldl326308/p/9469091.html
Copyright © 2011-2022 走看看