Java SE面试题(4)多线程

1. 什么是进程,什么是线程?为什么需要多线程编程?

  • 进程执行着应用程序,而线程进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。

  • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位

  • 线程是进程的一个实体,是 CPU 调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存

使用多线程的编程通常能够带来更好的性能用户体验,但是多线程的程序对于其他程序是不友好的,因为它占用了更多的 CPU 资源。

2. 进程间的通信方式

  • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 有名管道 (namedpipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 信号量(semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列( messagequeue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号 (sinal) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 共享内存(shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
  • 套接字(socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

3. 线程间的通信方式

  • 锁机制:包括互斥锁、条件变量、读写锁
    • 互斥锁提供了以排他方式防止数据结构被并发修改的方法。
    • 读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
    • 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
  • 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
  • 信号机制(Signal):类似进程间的信号处理

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

4. 实现多线程的三种方法

  1. 继承Thread类,重写父类run()方法

    public class thread1 extends Thread {
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.println("我是线程"+this.getId());
            }
        }
        public static void main(String[] args) {
            thread1 th1 = new thread1();
            thread1 th2 = new thread1();
            th1.start();
            th2.start();
        }
    }
    
  2. 实现runnable接口

    public class thread2 implements Runnable {
        public String ThreadName;
        public thread2(String tName){
            ThreadName = tName;
        }
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.println(ThreadName);
            }
        }
        public static void main(String[] args) {
            // 创建一个Runnable接口实现类的对象
            thread2 th1 = new thread2("线程A:");
            thread2 th2 = new thread2("线程B:");
            // 将此对象作为形参传递给Thread类的构造器中,创建Thread类的对象,此对象即为一个线程
            Thread myth1 = new Thread(th1);
            Thread myth2 = new Thread(th2);
            // 调用start()方法,启动线程并执行run()方法
            myth1.start();
            myth2.start();
        }
    }
    
  3. 通过Callable和Future创建线程

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    public class CallableThreadTest implements Callable<Integer>
    {
        @Override
        public Integer call() throws Exception{
            int i = 0;
            for(;i<100;i++){
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
            return i;
        }
    
        public static void main(String[] args){
            CallableThreadTest ctt = new CallableThreadTest();
            FutureTask<Integer> ft = new FutureTask<>(ctt);
            for(int i = 0;i < 100;i++){
                System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
                if(i==20){
                    new Thread(ft,"有返回值的线程").start();
                }
            }
            try{
                System.out.println("子线程的返回值:"+ft.get());
            } catch (InterruptedException e){
                e.printStackTrace();
            } catch (ExecutionException e){
                e.printStackTrace();
            }
        }
    }
    

5. 三种创建多线程方法的对比

  1. 采用实现Runnable、Callable接口的方式创建多线程时,线程类只是实现了Runnable接口或Callable接口,还可以继承其他类缺点是编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
  2. 使用继承Thread类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。缺点是线程类已经继承了Thread类,所以不能再继承其他父类
  3. Runnable和Callable的区别
    1. Callable规定重写call(),Runnable重写run()
    2. Callable的任务执行后可返回值,而Runnable的任务是不能返回值的
    3. call方法可以抛出异常,run方法不可以。
    4. 运行Callable任务可以拿到一个Future对象表示异步计算的结果。它提供检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果

6. 线程状态

  • 新建状态( new ):新建线程对象,并没有调用start()方法之前
  • 就绪状态( runnable ):调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态。
  • 运行状态( running ):线程被设置为当前线程,获得CPU后,开始执行run()方法,就是线程进入运行状态。
  • 阻塞状态( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种
    1. 等待阻塞:运行( running )的线程执行 o . wait ()方法, JVM 会把该线程放 入等待队列( waitting queue )中。
    2. 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
    3. 其他阻塞: 运行( running )的线程执行 Thread . sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。 当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。
  • 死亡状态( dead ):处于运行状态的线程,当它主动或者被动结束,线程就处于死亡状态。结束的形式,通常有以下几种:
    1. 线程执行完成,线程正常结束;
    2. 线程执行过程中出现异常或者错误,被动结束
    3. 线程主动调用stop方法结束线程。

7. 线程控制

  • join():等待。让一个线程等待另一个线程完成才继续执行。如A线程线程执行体中调用B线程的join()方法,则A线程被阻塞,知道B线程执行完为止,A才能得以继续执行。

  • sleep():睡眠。让当前的正在执行的线程暂停指定的时间,并进入阻塞状态。

  • yield():线程让步。将线程从运行状态转换为就绪状态。当某个线程调用 yiled() 方法从运行状态转换到就绪状态后,CPU 会从就绪状态线程队列中只会选择与该线程优先级相同或优先级更高的线程去执行。

  • setPriority():改变线程的优先级。每个线程在执行时都具有一定的优先级,优先级高的线程具有较多的执行机会。每个线程默认的优先级都与创建它的线程的优先级相同。main线程默认具有普通优先级。参数priorityLevel范围在1-10之间,常用的有如下三个静态常量值:MAX_PRIORITY:10;MIN_PRIORITY:1;NORM_PRIORITY:5。

    具有较高线程优先级的线程对象仅表示此线程具有较多的执行机会,而非优先执行。

  • setDaemon(true):设置为后台线程。后台线程主要是为其他线程(相对可以称之为前台线程)提供服务,或“守护线程”。如JVM中的垃圾回收线程。当所有的前台线程都进入死亡状态时,后台线程会自动死亡。

8. sleep() 和 yield() 两者的区别:

  1. sleep()方法会给其他线程运行的机会,不考虑其他线程的优先级,因此会给较低优先级线程一个运行的机会。yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。
  2. 当线程执行了 sleep(long millis) 方法,将转到阻塞状态,参数millis指定睡眠时间。当线程执行了yield()方法,将转到就绪状态
  3. sleep() 方法声明抛出InterruptedException异常,而 yield() 方法没有声明抛出任何异常。

9. wait、notify、notifyAll的区别

方法注意

  • wait、notify、notifyAll是java同步机制中重要的组成部分结合synchronized关键字使用,可以建立很多优秀的同步模型。这3个方法并不是Thread类或者是Runnable接口的方法,而是Object类的3个本地方法
  • 调用一个Object的wait与notify/notifyAll的时候必须保证调用代码对该Object是同步的,也就是说必须在作用等同于synchronized(obj){……}的内部才能够去调用obj的wait与notify/notifyAll三个方法,否则就会报错:java.lang.IllegalMonitorStateException:current thread not owner

锁池和等待池

  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池

方法介绍

  • 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
  • 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争

总结

  • wait:线程自动释放其占有的对象锁,并等待notify
  • notify:唤醒一个正在wait当前对象锁的线程,并让它拿到对象锁
  • notifyAll:唤醒所有正在wait当前对象锁的线程
    notify和notifyAll的最主要的区别是:notify只是唤醒一个正在wait当前对象锁的线程,而notifyAll唤醒所有。值得注意的是:notify是本地方法,具体唤醒哪一个线程由虚拟机控制;notifyAll后并不是所有的线程都能马上往下执行,它们只是跳出了wait状态,接下来它们还会是竞争对象锁。

sleep() 和 wait() 有什么区别?

sleep()方法是线程类(Thread)的静态方法,导致此线程暂停执行指定时间,将执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复(线程回到就绪(ready)状态),因此调用 sleep 不会释放对象锁

wait()Object 类的方法,对此对象调用 wait()方法导致本线程放弃对象锁(线程暂停执行),进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入就绪状态。

10. 线程池

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
线程池的产生和数据库的连接池类似,系统启动一个线程的代价是比较高昂的,如果在程序启动的时候就初始化一定数量的线程放入线程池中,在需要是使用时从池子中取用完放回池子里,这样能大大的提高程序性能,再者,线程池的一些初始化配置,也可以有效的控制系统并发的数量,防止因为消耗过多的内存,而把服务器累趴下。

通过Executors工具类可以创建各种类型的线程池,如下为常见的四种:

  • newCachedThreadPool :大小不受限,当线程释放时,可重用该线程;
  • newFixedThreadPool :大小固定,无可用线程时,任务需等待,直到有可用线程;
  • newSingleThreadExecutor :创建一个单线程,任务会按顺序依次执行;
  • newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行

11. 使用线程池的好处

  • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
  • 对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现

12. 线程池都有哪几种工作队列

  1. ArrayBlockingQueue
    是一个基于数组结构有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

  2. LinkedBlockingQueue
    一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列

  3. SynchronousQueue
    一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

  4. PriorityBlockingQueue

    一个具有优先级的无限阻塞队列

参考

尾尾部落

Java多线程

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 7,186评论 1 15
  • 本文出自 Eddy Wiki ,转载请注明出处:http://eddy.wiki/interview-java.h...
    eddy_wiki阅读 6,618评论 0 14
  • 林炳文Evankaka原创作品。转载自http://blog.csdn.net/evankaka 本文主要讲了ja...
    ccq_inori阅读 3,865评论 0 4
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 8,065评论 1 18
  • 相关概念 面向对象的三个特征 封装,继承,多态.这个应该是人人皆知.有时候也会加上抽象. 多态的好处 允许不同类对...
    东经315度阅读 6,159评论 0 8