面试:多线程

作者: 奇点一氪 | 来源:发表于2018-07-26 22:08 被阅读65次

    进程和线程的区别是什么?

    线程:是进程的一个执行单元.
    一个程序至少一个进程,一个进程至少一个线程。

    创建线程有几种不同的方式?

    ①继承Thread类(真正意义上的线程类),是Runnable接口的实现。
    ②实现Runnable接口,并重写里面的run方法。
    ③使用Executor框架创建线程池。Executor框架是juc里提供的线程池的实现。调用线程的start():启动此线程;调用相应的run()方法

    继承Thread类创建线程类

    (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
    (2)创建Thread子类的实例,即创建了线程对象。
    (3)调用线程对象的start()方法来启动该线程。
    实现Runnable接口

    package com.thread;
    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++){
                System.out.println(Thread.currentThread().getName()+"  : "+i);
                if(i==20){
                    new FirstThreadTest().start();
                    new FirstThreadTest().start();
                }
            }
        }
    }
    

    通过Runnable接口创建线程类

    (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    (2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    (3)调用线程对象的start()方法来启动该线程。
    示例代码为:

    package com.thread;
    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){
                    RunnableThreadTest rtt = new RunnableThreadTest();
                    new Thread(rtt,"新线程1").start();
                    new Thread(rtt,"新线程2").start();
                }
            }
        }
    }
    

    通过Callable和Future创建线程

    (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
    (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
    (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
    实例代码:

    package com.thread; 
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    public class CallableThreadTest implements Callable<Integer>{
     
        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();
            }
        }
     
        @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接口,还可以继承其他类。
      在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
    • 劣势是:
      编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
      使用继承Thread类的方式创建多线程时优势是:
      编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
    • 劣势是:
      线程类已经继承了Thread类,所以不能再继承其他父类。

    概括的解释下线程的几种可用状态。

    线程在执行过程中,可以处于下面几种状态:
    就绪(Runnable):线程准备运行,不一定立马就能开始执行。
    运行中(Running):进程正在执行线程的代码。
    等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。
    睡眠中(Sleeping):线程被强制睡眠。
    I/O阻塞(Blocked on I/O):等待I/O操作完成。
    同步阻塞(Blocked on Synchronization):等待获取锁。
    死亡(Dead):线程完成了执行。

    线程执行流程

    https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B/103101?fr=aladdin
    多线程

    image.png
    image.png
    image.png

    同步方法和同步代码块的区别是什么?

    在Java语言中,每一个对象有一把锁。线程可以使用synchronized关键字来获取对象上的锁。synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。

    什么是死锁(deadlock)?

    打个比方,假设有P1和P2两个进程,都需要A和B两个资源,现在P1持有A等待B资源,而P2持有B等待A资源,两个都等待另一个资源而不肯释放资源,就这样无限等待中,这就形成死锁,这也是死锁的一种情况。结果就是两个进程都陷入了无限的等待中。

    产生死锁的必要条件

    虽然进程在运行过程中可能会发生死锁,但产生死锁是必须具备一定条件的。产生死锁必须同时具备下面四个必要条件,只要其中任意一个条件不成立,死锁就不会产生:
    (1)互斥条件。进程对所分配到的资源进行排他性使用,即在一段时间内,某资源只能被一个进程占用。如果此时还有其他进程请求该资源,则请求进程只能等待,直至占有该资源的进程用毕释放。
    (2)请求和保持条件。进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己以获得的资源保持不放。
    (3)不可抢占条件。进程已获得的资源在未使用完之前不能被抢占,只能在进程使用完时由自己释放。
    (4)循环等待条件。在发生死锁时,必然存在一个进程—资源的循环链,即进程集合{P0,P1,P2,P3,...,Pn}中的P0正在等待P1占用的资源,P1正在等待P2占用的资源,... ... ,Pn正在等待已被P0占用的资源。

    处理死锁的方法

    (1)预防死锁。该方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个来预防产生死锁。
    (2)避免死锁。在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而可以避免产生死锁。
    (3)检测死锁。通过检测机构及时地检测出死锁的发生,然后采取适当的措施,把进程从思索中解脱出来。
    (4)解除死锁。当检测到系统中已发生死锁时,就采取相应的措施,将进程从死锁状态中解脱出来。常用方法是---撤销一些进程,回收他们的资源,将他们分配给已处于阻塞状态的进程,使其能继续运行。

    如何确保N个线程可以访问N个资源同时又不导致死锁?

    使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

    线程池的参数有哪些,在线程池创建一个线程的过程。

    volitile关键字的作用,原理。

    volatile保持内存可见性
    Java变量的读写
    Java通过几种原子操作完成工作内存和主内存的交互:
    lock:作用于主内存,把变量标识为线程独占状态。
    unlock:作用于主内存,解除独占状态。
    read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
    load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
    use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
    assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
    store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
    write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

    volatile的特殊规则就是:

    • read:把一个变量的值从主内存传输到线程的工作内存、
    • load:把read操作传过来的变量值放入工作内存的变量副本中、
    • use:把工作内存当中的一个变量值传给执行引擎
      动作必须连续出现。
    • assign:把一个从执行引擎接收到的值赋值给工作内存的变量、
    • store:把工作内存的一个变量的值传送到主内存中、
    • write:把store操作传来的变量的值放入主内存的变量中
      动作必须连续出现。
      所以,使用volatile变量能够保证:

    每次读取前必须先从主内存刷新最新的值。
    每次写入后必须立即同步回主内存当中。

    也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。线程1中对变量v的最新修改,对线程2是可见的。
    内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态。
    当一个被volitile声明的变量进行回写操作时,CPU会立即将这个变量所在的缓存行回写到内存中。但是,如果仅仅时这样的话,其他的CPU中缓存了该数据的缓存行仍然是旧值,如果其他CPU仍然对旧值进行操作,那么就会出现一致性的问题。因此,在这个基础上,还要增加一个缓存一致性协议:每个CPU通过嗅探总线上传播的数据来检查自己缓存行的数据是否过期,如果发现自己缓存行对应的内存地址发生了改变,就会将该缓存行中的数据置为无效并且从内存中重新获取这个新数据,从而保证对数据操作的一致性。

    (这里用到的机制是缓存锁机制,也就是说,当CPU把数据回写到内存时,不会回写到之前的存储这个数据的内存地址,而是换个内存地址存,当其他CPU发现自己缓存行中数据的内存地址发生改变时,就会使该缓存失效)

    synchronized关键字的用法,优缺点。

    关键字 synchronized 的作用是实现线程间的同步对同步的代码加锁,使得每一次,只有一个线程进入同步块,从而保证线程间的安全性。
    synchronized 可以有多种用法,下面是常用的三种方式。
    指定加锁对象:对给定对象加锁,进入同步代码前要活的给定对象的锁。
    直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
    直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

    java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

    java的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的

    Lock接口有哪些实现类,使用场景是什么。

    可重入锁的用处及实现原理,写时复制的过程,读写锁,分段锁(ConcurrentHashMap中的segment)。

    悲观锁,乐观锁,优缺点,

    悲观锁:

    总认为每次拿的数据都会被别人修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

    乐观锁:

    顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会使用版本号等机制判断一下这个数据有没有被更新.乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

    CAS有什么缺陷,该如何解决。

    CAS:Compare and Swap, 翻译成比较并交换。
    CAS应用有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    ABC三个线程如何保证顺序执行。

    使用newSingleThreadExecutor 这个线程池,保证线程里面的任务依次执行,
    t.join(); //调用join方法,等待线程t执行完毕
    t.join(1000); //等待 t 线程,等待时间是1000毫秒。

    public class TestJoin {
        public static void main(String[] args) throws InterruptedException {
            final Thread t1 = new Thread(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " run 1");
                }
            }, "T1");
            final Thread t2 = new Thread(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " run 2");
                    try {
                        t1.join(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "T2");
            final Thread t3 = new Thread(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " run 3");
                    try {
                        t2.join(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "T3");
            // method1
            //t1.start();
            //t2.start();
            //t3.start();
    
    //        method 2 使用 单个任务的线程池来实现。保证线程的依次执行
            ExecutorService executor = Executors.newSingleThreadExecutor();
            executor.submit(t1);
            executor.submit(t2);
            executor.submit(t3);
            executor.shutdown();
        }
    }
    

    发现结果每次都是 t1执行,t2执行,t3执行,

    sleep和wait的区别。

    1、这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。
    sleep是Thread的静态类方法,谁调用的谁去睡觉。
    2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
    sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
    Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
    3、使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
    synchronized(x){
    x.notify()
    //或者wait()
    }
    4、sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

    notify和notifyall的区别。

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

    ThreadLocal的了解,实现原理。

    • ThreadLocal 是线程的局部变量, 是每一个线程特有的,其他线程不能访问,使用ThreadLocal来解决对某一个变量的访问冲突问题。
    • 使用ThreadLocal维护变量的时候,为每一个使用该变量的线程提供一个独立的变量副本,会从内存中拷贝出来变量的副本, 这样就不存在线程安全问题。但是由于在每个线程中都创建了副本,内存的占用会比不使用ThreadLocal要大。

    相关文章

      网友评论

        本文标题:面试:多线程

        本文链接:https://www.haomeiwen.com/subject/isapmftx.html