面试之JAVA多线程

1如何创建线程

  1. 创建多线程实现类MyThread extends Thread,继承Thread后重载run()方法,在调用类中创建对象后调用start()方法执行该线程。
  2. 创建实现类MyThread implements Runnable,先new一个接口类,然后创建Thread类利用构造方法放入实现runnable接口的类 Thread thread = new Thread(myThread);然后调用thread.start()。

start() 方法会新建一个线程并让这个线程执行 run() 方法;而直接调用 run() 方法知识作为一个普通的方法调用而已,它只会在当前线程中,串行执行 run() 中的代码。

2.进程与线程的关系和区别

  1. 进程 :
    进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
    线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
  2. 线程 :
    线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
    程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。。

区别:地址空间、资源拥有

  • 线程与资源分配无关,它属于某一个进程,并与进程内的其他线程一起共享进程的资源
  • 每个进程都有自己一套独立的资源(数据),供其内的所有线程共享
  • 不论是大小,开销线程要更“轻量级”
  • 一个进程内的线程通信比进程之间的通信更快速,有效。(因为共享变量)
线程调度 :

分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间
抢占式调度:优先级高的线程先使用CPU,如果优先级相同则随机选择一个

3.并发与并行

并发 :指两个或多个事件在同一时间段内发生。(交替执行)
并行 :指两个或多个事件在同一时刻发生(同时发生),cpu多个线程执行

4.实现同步的三种方法

  1. 同步代码块 :
    格式:
 synchronized(锁对象){
      可能会出现线程安全的代码(访问了共享数据的代码)
}

通过代码块中的锁对象,可以使用任意的对象
但是必须保证多个线程使用的锁对象是同一个
把要同步访问的代码块锁住,只让一个线程在synchronized的代码块中执行,同步中的线程没有执行完毕不会释放锁,其他线程阻塞。

  1. 同步方法:
    使用synchronized修饰的方法,保证线程执行该 方法的时候,其他线程只能在外面等待。run()中调用同步方法。
    Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。锁的是this 即是这个对象
public synchronized void method(){
      可能会产生线程安全的代码
}
  1. 静态同步方法
    Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。
    因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。
public synchronized static void staticMethod1(){
  可能产生问题的代码块
}

5.Lock接口

lock实现比synchronized更广泛的锁操作

  1. 在成员位置创建一个ReentrantLock(实现类)对象
  2. 在可能会出现安全问题的代码前调用lock()方法获取锁
  3. 在可能会出现安全问题的代码后调用unlock()方法释放锁

6.wait()和sleep()方法区别

  1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
  2. sleep()方法必须传入参数,参数就是休眠时间,睡眠后如果CPU空闲就会进入运行状态。
  3. wait()方法可以传入参数也可以不传入参数,传入参数就是在参数结束的时间后开始等待,不传入参数就是直接等待。需要调用notify()方法后本线程才进入对象锁定池准备(Runnable/Blocked)
    两种状态合称冻结状态

7.多线程产生死锁的4个必要条件

互斥条件:一个资源每次只能被一个线程使用;
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺;
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

如何避免死锁?
答:指定获取锁的顺序,举例如下:
比如某个线程只有获得 A 锁和 B 锁才能对某资源进行操作,在多线程条件下,如何避免死锁?
获得锁的顺序是一定的,比如规定,只有获得 A 锁的线程才有资格获取 B 锁,按顺序获取锁就可以避免死锁!!!
银行家算法:银行家算法是一种最有代表性的避免[死锁]的算法。在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。为实现银行家算法,系统必须设置若干数据结构

如何排查死锁

8.对synchronized关键字的理解

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

9.线程池

一个装满了Thread的集合(容器),其中的线程可以重复使用,省去了频繁创建线程对象的操作,无需因反复创建线程而消耗过多的资源。
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类
三种最常见线程池

        使用线程池工厂类Executors中的静态方法newFixedThreadPool(nThreads)来创建一个线程池
        ExecutorService es = Executors.newFixedThreadPool(2);    定长nThreads 最大nThreads
        无限线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  定长0 最大Integer.MAX_VALUE
        单个线程线程池
        Executors.newSingleThreadExecutor();  定长1 最大1

如创建newCachedThreadPool,在实际上都是用ThreadPoolExecutor来返回的

 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

看看ThreadPoolExecutor的API

         public ThreadPoolExecutor(int corePoolSize,  线程池的基本大小
                                      int maximumPoolSize,  线程池的最大线程
                                      long keepAliveTime,  线程空闲后的存活时间
                                      TimeUnit unit,  线程空闲后的存活时间
                                      defaultHandler, 队列和最大线程池满了后的饱和策略
                                      BlockingQueue workQueue,
                                      ThreadFactory threadFactory) {
                this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                     threadFactory, defaultHandler);
            }

如果直接new ThreadPoolExecutor()需要自己装配这几个参数
ThreadPoolExecutor.execute(new Job()); 提交一个任务到线程池中
看看excute()的源代码

         int c = ctl.get();  获取当前线程状态
                if (workerCountOf(c) < corePoolSize) {  当前线程数量

这里借助《聊聊并发》的一张图来描述这个流程:


image.png

关闭线程池

  • shutdown() 执行后停止接受新任务,会把队列的任务执行完毕。
  • shutdownNow() 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop。

10.synchronized与lock的区别

类锁: 一个LockServer.class ,此时输出不会重复

        OrderService orderService = new OrderLockService();
        for (int i = 0; i < 10; i++) {
            executorService.submit(new OrderTask(latch,orderService));
        }

对象锁: lockServer1、lockServer2、lockServer3,此时输出会重复

        for (int i = 0; i < 10; i++) {
            OrderService orderService = new OrderLockService();
            executorService.submit(new OrderTask(latch,orderService));
        }
  1. 原始构成
    synchronized是底层关键字位于JVM层面,采用monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步代码块或同步方法中才能调用wait/notify等方法(monitorenter/monitorexit)
    lock是接口与具体的实现类,是api层面的锁
  2. 使用方法
    synchronized不需要用户手动去释放锁,当synchronized代码执行王城后线程会自动释放对锁资源的占用。
    Reentrantlock需要用户取手动释放锁如果没有主动释放锁就会出现思索地现象。
  3. 等待是否可中断
    synchronized不可终端,需要抛出异常或者正常运行退出
    Reentrantlock可中断,设置超时方法trylock(long timeout,TimeUnit unit)或者调用interrupt()方法中断
  4. 是否公平
    synchronized是非公平锁
    lock两者皆可,传入true为公平锁默认非公平
  5. 锁绑定多个条件Condition
    lock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒。而不像notify()随即唤醒或者notifyAll()

你可能感兴趣的:(面试之JAVA多线程)