Java面试--多线程

Java 多线程

1.什么是线程?什么是多线程?

  • 线程是程序最小的调度单位,进程包含多个线程。如打开电脑任务管理器会看到多个进程在运行,进程可能含有很多功能,这些功能又由很多线程去执行。
  • 多线程是多个线程并发执行的技术。Jvm中栈都是独立不共享的,一个线程一个栈,启动五个线程就会开辟出五个栈内存,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发,出现多线程就是为了提高效率。

2.多线程的生命周期?

在这里插入图片描述

  • 新建:创建一个新的线程;
  • 就绪:处于可运行状态,是争夺cpu使用权;
  • 运行:cpu处于运行状态,开始执行;
  • 阻塞:线程处于等待状态,会释放之前抢夺了cpu的时间片,结束后进入就绪状态继续抢夺cpu使用权;
  • 死亡:线程正常结束和异常结束。

3.线程常出现的基本方法?

  • wait():线程等待,会释放锁,用在同步代码块和同步方法中;
  • sleep():线程睡眠,不会释放锁,进入等待状态;
  • yield():线程让步,会使线程让出cpu使用权,进入就绪状态
  • join():指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
  • notify():随机唤醒一个在等待中的线程,进入就绪状态。
  • notifyAll():唤醒全部在等待中的线程,进入就绪状态。

4.wait()和sleep()的区别?

①wait()会释放锁,sleep()不会释放锁;
②wait()属于Object,sleep()属于Thread;
③wait()只能在同步代码块或者同步方法中,sleep()可以用在任何地方;
④wait()不需要捕获异常,sleep()需要捕获异常。

5.实现多线程的方式?

①继承Thread类:继承thread类,重写run()的方法,再用线程对象调用start()方法启动线程。

②实现Runnable接口:实现runnable接口,重写run()方法,调用start()方法启动线程。

③实现Callable接口:前两种执行结果没有返回对象,Callable接口可以实现。

  • 实现Callable接口,重写call()方法,用FutureTask把Callable对象封装成线程任务对象
  • 创建Thread线程调用start()方法启动线程。
  • 线程执行完毕后、通过FutureTask的get()方法去获取任务执行的结果

④创建线程池方式:

  • ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象;
  • Executors(线程池的工具类)调用方法返回不同特点的线程池对象;

注意:
在这里插入图片描述

6.start()和run()的区别?

  • start(): 是启动线程,调用了之后线程会进入就绪状态,一旦拿到cpu使用权就开始执行run()方法,不能重复调用start()否则会报异常。
  • run(): 一个普通的方法,用于重写具体执行代码。直接调用run()方法就还只有一个主线程,还是会顺序执行,也可以重复调用run()方法。

7.如何正确停止线程?

start()启动线程,stop()终止线程,但现在不建议使用stop()方法,使用isInterrupted()和interrupt()方法配合来停止线程。

8.使用线程池的好处?

  • 使用线程池可以减少线程的频繁的创建和销毁,减少资源消耗,每个线程都可重复使用;
  • 可根据实际场景调整线程数,防止cpu飙高服务器崩溃。

9.线程池主要参数?

  • corePoolSize: 核心线程数,创建不能被回收,可以设置被回收。
  • maximumPoolSize: 最大线程数。
  • keepAliveTime: 空闲线程存活时间。
  • unit: 单位。
  • workQueue: 等待队列。
  • threadFactory: 线程工程,用于创建线程。
  • handler: 拒绝策略。

10.线程池的执行过程?

①描述:一个线程开始时会先去判断线程数和核心线程数的大小,如果小于的话就创建一个线程,并将这个线程作为执行的第一个任务;如果大于的话会将任务加到队列(workQueue),加入时会判断队列是否满了,没满加入队列中等待执行,满了的话判断线程和最大线程数,如果小于最大线程数创建线程执行任务,如果不小于执行拒绝任务(相当于执行execute()方法抛出异常)
②源码分析:以execute()方法为例,代码中英文解释三步骤很清晰。workerCount()工作的线程和corePoolSize比较,大于的话判断队列和入队操作,如果队列满了走下个判断执行addWorker()方法判断当前线程大于等于最大线程数就会返回false。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

11.四大拒绝策略?

①new ThreadPoolExecutor.AbortPolicy(): 添加线程池被拒绝,会抛出异常(默认策略)。
②new ThreadPoolExecutor.CallerRunsPolicy(): 添加线程池被拒绝,不会放弃任务,也不会抛出异常,会让调用线程池的线程去执行这个任务。
③new ThreadPoolExecutor.DiscardPolicy(): 添加线程池被拒绝,丢掉任务,不抛异常。
④new ThreadPoolExecutor.DiscardOldestPolicy(): 添加线程池被拒绝,会把线程池队列中等待最久的任务放弃,把拒绝任务放进去。

12.线程池的各个状态?

①RUNNING:会接收任务并且处理队列中的任务;
②SHUTDOWN:不会接受新任务并且会处理队列中的任务,任务处理完会中断所有线程;
③STOP:不会接受新任务并不会处理队列中的任务,并直接终中断所有线程;
④TIDYING:所有线程终止后线程池状态会改为TIDYING,一旦达到此状态就会调用线程池terminated();
⑤TERMINATED:执行terminated()方法后状态就会转变TERMINATED;

注意:
shutDown()方法源码改为shutdown状态 ,shutDownNow()直接改为stop状态 先改状态再终止线程(防止新的任务进来先改状态)

线程池中封装了获取线程各个状态的方法,可根据场景调用。

13.什么是JMM?

JMM 是Java内存模型( Java Memory Model),简称JMM。它本身只是一个抽象的概念,并不真实存在,它描述的是一种规则或规范,是和多线程相关的一组规范。通过这组规范,定义了程序中对各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。需要每个JVM 的实现都要遵守这样的规范,有了JMM规范的保障,并发程序运行在不同的虚拟机上时,得到的程序结果才是安全可靠可信赖的。如果没有JMM 内存模型来规范,就可能会出现,经过不同 JVM 翻译之后,运行的结果不相同也不正确的情况。

​ 计算机在执行程序时,每条指令都是在CPU中执行的。而执行指令的过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程,跟CPU执行指令的速度比起来要慢的多(硬盘 < 内存 <缓存cache < CPU)。因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时,就可以直接从它的高速缓存中读取数据或向其写入数据了。当运算结束之后,再将高速缓存中的数据刷新到主存当中。

JMM 抽象出主存储器(Main Memory)和工作存储器(Working Memory)两种。

  • 主存储器是实例对象所在的区域,所有的实例都存在于主存储器内。比如,实例所拥有的字段即位于主存储器内,主存储器是所有的线程所共享的。
  • 工作存储器是线程所拥有的作业区,每个线程都有其专用的工作存储器。工作存储器存有主存储器中必要部分的拷贝,称之为工作拷贝(Working Copy)。
  • 所以,线程无法直接对主内存进行操作,此外,线程A想要和线程B通信,只能通过主存进行。

整个Java内存模型实际上是围绕着三个特征建立起来的。分别是:原子性,可见性,有序性。这三个特征可谓是整个Java并发的基础。

14.Java并发中的可见性,原子性,有序性?

①可见性: 一个线程对主内存的修改可以及时的被其他线程观察到。
例:当两个线程处理一个int变量时,A先读到数据为1并想修改数据为2,此时还没有写道内存中,线程B又来读了读到1的数据,就会出现可见性的问题。Java在多线程并发情况下改变的数据一般会用volatile关键字来修饰保证可见性,当然也可以用锁来控制。
②原子性:要么全部执行要么不执行。用锁机制保证原子性。
③有序性:

  • 创建对象在代码层面是:
    a.申请内存空间
    b.在内存空间初始化内容
    c.返回内存地址
  • 在编译层面可能会优化为:
    a.申请空间
    b.返回内存地址
    c.初始化内容
    这样会出现多线程获取的话获取内容是null的需要用锁机制去控制或者用volatile可见性控制。

15.volatile和synchronized的区别

① volatile仅能使用在变量级别的,synchronized可以使用在变量、方法、类级别的
② volatile不具备原子性,具备可见性,synchronized有原子性和可见性。
③ volatile不会造成线程阻塞,synchronized会造成线程阻塞。
④ volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好。

16.synchronized和lock的区别

① synchronized是关键字,lock是java类,默认是不公平锁。
② synchronized适合少量同步代码,lock适合大量同步代码。
③ synchronized会自动释放锁,lock必须放在finally中手工unlock释放锁,不然容易死锁。

17.ThreadLocal?

①threadLocal就是本地线程,每个线程都是互不影响相互隔离。底层是通过ThreadLocalMap来实现,存储多个entry,key存储每个线程对象,value就存储值,来实现隔离。
②实际使用问题:在线程池中使用threadLocal会导致内存泄漏。在线程池中创建线程是不会被清除,线程对象是通过强引用指向ThreadLocalMap,所以entry对象不会被回收就会出现内存越来越多导致泄露。解决办法,每次使用ThreadLocal对象后都手动调用remove()方法,清除Entry对象。
③threadLocal应用场景:隔离线程,存储一些线程不安全的工具对象,如simpleDateFormat。
使用具体场景:利用饿汉模式创建一个static的 ThreadLocal对象,利用ThreadLocal隔离性存储新建的simpleDateFormat对象,获取线程安全的simpleDateFormat对象。

  • 27
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
java面试题真的很多,下面我来回答一个有关多线程的问题。 在Java中实现多线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。这两种方式有何区别? 继承Thread类的方式是直接定义一个类继承Thread,并重写它的run()方法。然后创建该类的对象,并调用对象的start()方法来启动线程。这种方式简单直接,但因为Java是单继承的,所以如果某个类已经继承了其他类,就不能再直接继承Thread类实现多线程。 实现Runnable接口的方式是定义一个类实现Runnable接口,并实现其唯一的抽象方法run()。然后创建Thread类的对象,将实现了Runnable的对象作为参数传递给Thread类的构造方法。最后调用Thread对象的start()方法来启动线程。这种方式灵活性更大,因为Java允许一个类实现多个接口,所以即使某个类已经继承了其他类,仍然可以通过实现Runnable接口来实现多线程。 另一个区别在于资源共享的问题。继承Thread类的方式,不管是数据还是方法,都是线程自己拥有的,不存在共享的情况。而实现Runnable接口的方式,多个线程可以共享同一个对象的数据和方法,因为多个线程共同操作的是同一个Runnable对象。 总结来说,继承Thread类方式简单直接,但只能通过单继承来实现多线程;实现Runnable接口方式更灵活,可以解决单继承的限制,并且多个线程可以共享同一个Runnable对象的数据和方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值