面试-多线程

synchronized [ˈsɪŋkrənaɪzd] v.同步
35. 并行和并发有什么区别?
36. 进程和线程的区别?
37. 守护线程是什么?
38. 创建线程有哪几种方式?
39. 说一下 runnable 和 callable 有什么区别?
40. 线程有哪些状态?
41. sleep() 和 wait() 有什么区别?
42. notify()和 notifyAll()有什么区别?
43. 线程的 run()和 start()有什么区别?
44. 创建线程池有哪几种方式?
45. 线程池都有哪些状态?
46. 线程池中 submit()和 execute()方法有什么区别?
47. 在 java 程序中怎么保证多线程的运行安全?(synchronized,happens-before原则)
48. 多线程锁的升级原理是什么?
49. 什么是死锁?
50. 怎么防止死锁?(死锁的四个必要条件)
51. ThreadLocal 是什么?有哪些使用场景?
52.说一下 synchronized 底层实现原理?
53. synchronized 和 volatile 的区别是什么?
54. synchronized 和 Lock 有什么区别?
55. synchronized 和 ReentrantLock 区别是什么?
56. 说一下 atomic 的原理?

多线程

35. 并行和并发有什么区别?

  • 并行是指多个事件在同一时刻发生;而并发是指多个事件在同一时间间隔发生
  • 并发:指应用能够交替执行不同的任务,其实并发有点类似于多线程的原理,多线程并非是同时执行多个任务而是快速切换着执行。
  • 并行:指应用能够同时执行不同的任务,例如,边吃饭边听音乐
  • 并发是指一个处理器同时处理多个任务。并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。并发是逻辑上的同时发生,而并行是物理上的同时发生。如hadoop分布式集群。

所以并发编程的目标是充分的利用处理器的每一个核,以达到醉高的处理性能。

36. 进程和线程的区别?

简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个或多个线程。进程在执行过程中拥有独li的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,同一进程中的多个线程之间可以并发执行,是cpu调度和分派的基本单位,是比程序更小的能独li运行的基本单位。

37. 守护线程是什么?

守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,
1、守护线程,比如垃圾回收线程,就是最典型的守护线程。
2、用户线程,就是应用程序里的自定义线程。
当所有非守护线程结束时,没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了,程序也就终止了,同时会傻死所有守护线程。 也就是说,只要有任何非守护线程还在运行,程序就不会终止。

38. 创建线程有哪几种方式?

①. 继承Thread类创建线程类

  • 继承Thread类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。
public class FirstThreadTest extends Thread { 
    int i=0
    //重写run方法,run方法的方法体就是线程执行体
    public void run() { 
        for (; i < 100; i++) {
            System.out.println(getName() + ":" + i);
        }
    } 
    public static void main(String[] args) { 
        for (int i = 0; i < 100; i++) {
            //Thread.currentThread()方法返回当前正在执行的线程对象。
            //GetName()方法返回调用该方法的线程的名字。
            System.out.println(Thread.currentThread().getName() + ":" + i); 
             if (i == 50) { 
                //创建Thread子类的实例,即创建了线程对象
                new FirstThreadTest().start(); 
                new FirstThreadTest().start();
            }
        }
    }

②. 通过Runnable接口创建线程类

  • 实现runnable接口,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建 Runnable实现类的实例,并依此实例来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动该线程。
public class RunnableThreadTest implements Runnable{ 
    private int i; 
    public void run(){ 
        for(i = 0;i <100;i++) {
           System.out.println(Thread.currentThread().getName()+":"+i);
        }
     } 
     public static void main(String[] args) { 
        for(int i = 0;i < 100;i++) {
           System.out.println(Thread.currentThread().getName()+":"+i); 
           if(i==20){
                 //创建 Runnable接口实现类的实例
                 RunnableThreadTest rtt = new RunnableThreadTest(); 
                 new Thread(rtt,"新线程1").start(); 
                 new Thread(rtt,"新线程2").start();
                }
            }

        }
}

③. 通过Callable和Future创建线程

  • 实现Callable接口,并重写call()方法,该call()方法将作为线程执行体,并且有返回值。
  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  • 使用FutureTask对象作为Thread对象的target(n.目标,对象)创建并启动新线程。
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
package com.nf147.Constroller;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableThreadTest implements Callable {


    public static void main(String[] args) {
        CallableThreadTest ctt = new CallableThreadTest();
        FutureTask 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();
        }

    }
    //重写call()方法,该call()方法将作为线程执行体,并且有返回值
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        return i;
    }


}

创建线程的三种方式的对比
采用实现Runnable、Callable接口的方式创建多线程时,优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想

39. 说一下 runnable 和 callable 有什么区别?

有点深的问题了,也看出一个Java程序员学习知识的广度。

  • Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
  • Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

40. 线程有哪些状态?

线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

  • 创建状态。在生成线程对象并没有调用该对象的start方法,这是线程处于创建状态。
  • 就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,等待cpu的调用,此时线程调度程序还没有把该线程设置为当前线程。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
  • 阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
  • 死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪   

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

sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。

wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程

42. notify()和 notifyAll()有什么区别?

  • 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
  • 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

43. 线程的 run()和 start()有什么区别?

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。

start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

44. 创建线程池有哪几种方式?

①. newFixedThreadPool(int nThreads)

创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最达数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。

②. newCachedThreadPool()

创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限zhi。

③. newSingleThreadExecutor()

这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。

④. newScheduledThreadPool(int corePoolSize)

创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

45. 线程池都有哪些状态?

线程池有5种状态:Running、ShutDown(停止)、Stop、Tidying(整理)、Terminated(终止)

  1. Running:线程池一旦被创建,就处于 Running 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。
  2. ShutDown:不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 Running 转变为 ShutDown 状态。
  3. Stop:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(Running 或 ShutDown ) 转变为 Stop 状态。
  4. Tidying:
    ShutDown 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 Tidying 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。

    线程池在 ShutDown 状态任务队列为空且执行中任务为空,线程池就会由 ShutDown 转变为 TIDYING 状态。

    线程池在 Stop 状态,线程池中执行中任务为空时,就会由 Stop 转变为 Tidying 状态。

  5. Terminated:线程池彻底终止。线程池在 Tidying 状态执行完 terminated() 方法就会由 Tidying 转变为 Terminated 状态

image

46. 线程池中 submit()和 execute()方法有什么区别?

  • 接收的参数不一样。
    execute() 参数 Runnable ;
    submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable)
    exucute只能执行实现Runnable接口的线程,submit可以执行实现Runnable接口或Callable接口的线程
  • submit有返回值,而execute没有
  • submit方便Exception处理

47. 在 java 程序中怎么保证多线程的运行安全?

线程安全在三个方面体现:

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
  • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到(synchronized,volatile);
  • 有序性:程序执行的顺序按照代码的先后顺序执行(happens-before原则)。
    导致原因:
  • 缓存导致的可见性问题
  • 线程切换带来的原子性问题
  • 编译优化带来的有序性问题

解决办法:

  • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
  • synchronized、volatile、LOCK,可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题

Happens-Before 规则如下:

  • 程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

48. 多线程锁的升级原理是什么?

在Java中,锁共有4种状态,级别从低到高依次为:无锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级

无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取偏向锁,指的就是偏向第1个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;
如果线程处于活动状态,升级为轻量级锁的状态。

轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第2个线程 B 所访问,此时偏向锁就会升级为轻量级锁.
线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。当前只有一个等待线程,则该线程将通过自旋进行等待。
但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。

重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高

锁升级的图示过程: 

49. 什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。

50. 怎么防止死锁?

死锁的四个必要条件:

  • 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
  • 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此次请求阻塞,但又对自己获得的资源保持不放
  • 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
  • 环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系

面试-多线程_第1张图片

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以尽可能地避免、预防和 解除死锁。

所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永jiu占据系统资源

此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

51. ThreadLocal 是什么?有哪些使用场景?

Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据
大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独li的。通过get和set方法就可以得到当前线程对应的值。

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

52.说一下 synchronized 底层实现原理?

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
jvm基于进入和退出Monitor对象来实现方法同步和代码块同步

  • 普通同步方法(修饰实例方法),对当前实例对象加锁
  • 静态同步方法(修饰静态方法),对当前类的Class对象加锁
  • 同步方法块(修饰代码块),对synchronized括号内的对象加锁

这里要注意:

  • synchronized是可重入的,所以不会自己把自己锁死
  • synchronized锁一旦被一个线程持有,其他试图获取该锁的线程将被阻塞。
    synchronized的作用主要有以下三个:
  • 原子性:线程互斥的访问同步代码块,可以将小原子合成大原子。
  • 可见性:synchronized解锁之前,必须将工作内存中的数据同步到主内存,其它线程操作该变量时每次都可以看到被修改后的值。
  • 有序性:一个线程的加锁,必须等到其它线程将锁释放;一个线程要释放锁,首先要加锁。
public class SynchronizedDemo {
    public synchronized void f(){    //为方法加锁
        System.out.println("Hello world");
    }
    public void g(){
        synchronized (this){        //为代码块加锁
            System.out.println("Hello world");
        }
    }
    public static void main(String[] args) {

    }
}

53. synchronized 和 volatile 的区别是什么?
volatile([ˈvɑːlətl] adj.不稳定的)

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

54. synchronized 和 Lock 有什么区别?

  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

55. synchronized 和 ReentrantLock 区别是什么?

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上: 

  • ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁 
  • ReentrantLock可以获取各种锁的信息
  • ReentrantLock可以灵活地实现多路通知 

另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。

56. 说一下 atomic 的原理?

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。

你可能感兴趣的