对线程的理解?

一、对线程和进程的理解?(面试过程被问到过)

         一个进程是一个程序一次执行的过程,或者是正在运行的一个程序,是一个动态的过程。进程可以分为多个线程,是一个程序内部的执行路径。如果一个进程可以同时进行多个线程,那么这么进程就是支持多线程的;线程是调度和执行的基本单位,每个线程都拥有自己独立的程序计数器与运行栈;一个进程中的多个线程可以共享相同的资源,这就会带来一些安全隐患(比如:产生死锁、读脏数据等等)。

二、创建执行线程的方式有四种(面试过程被问到过):

  1、继承Thread类:

        1、定义子类继承Thread类;

        2、子类中重写run()方法;

        3、创建Thread子类对象,即创建了线程对象;

        4、调用线程对象的start方法:启动线程,调用run()方法。

      注意:如果手动调用的run()方法并不算是多线程;run()方法由JVM调用,何时调用由CPU说了算;想要启动多线程必须调用start()方法;

         一个线程对象只能调用一次start方法启动,如果重复调用则会抛出lllegalThreadStateException。

      1.1 Thread中相关方法:static voi yeild()//线程让步,暂停当前线程把机会让给优先级更高或者相同的线程

                      static void sleep(long millis)// 暂停当前线程并且暂停的时长为你设置的时间(单位:毫秒),结束后重新进行排队。

                      区别:线程调用sleep()方法后进入堵塞状态,醒来后因为没有释放锁直接进入就绪状态,运行yeild后也没有释放锁于是进入了就绪状态。sleep()方法使用时需要处理InterruptException异常,而yield没有。sleep()执行后进入堵塞状态(记时等待)醒来后进入就绪状态也可能是堵塞状态,而yeild()是直接进入就绪状态。由于两个方法在当前正在运行的线程上工作,所以其他处于等待状态的线程调用它们是没有意义的。

      1.2 run()方法与start()方法的区别联系:调用start()方法的本质就是调用run()方法,为什么要这样呢?当我们new 一个新的线程对象后,线程进入新建状态,start()方法作用就是使线程进入就绪的状态,当分配到时间片后就可以运行。 start方法会执行线程前的相应的准备工作,然后执行run()方法运行线程体,这才是一个真正的多线程。 如果直接执行run(),就相当于在一个主线程中调用一个普通的run()方法,在主线程里执行,并没有体现多线程的作用。

·      1.3 sleep()与wait()的区别:sleep()是Thread下的静态方法,wait()是Object类下的方法;

                    sleep()不释放锁,wait()释放锁;

                    sleep()常用于暂停执行,wait()常用于线程间的通信;

                    wait()用完后线程不会自动执行,必须调用notify()或notifyAll()方法才能执行,sleep()方法调用后线程经过一段时间会自动苏醒,wai()虽然也可以传递参数使其苏醒,可是wait会释放锁,所以苏醒后没有获得锁就进入堵塞状态,而sleep()苏醒后进入就绪状态,但是如果碰到cpu不空闲,就会堵塞。

      1.4 为什么wait(),notify(),notifyAll()要被定义到Object类中:看到一种很有意思的说法:对于多线程的竞争同一个对象资源,相当于多个男孩同时追一个女孩,女孩则是那个共享资源对象,而男孩则是线程。当某个男孩要约那个女孩吃饭看电影,这个女孩就需要告诉其他男孩那个时间段我没有空,而不是约她的那个男孩告诉其他男孩这个消息。就相当于共享资源对象实现了多个线程之间的通信。在同步代码块中,使用wait,notify来控制当前占用资源的线程进入阻塞与唤醒进入就绪队列,也就是说上述两个方法实现了线程之间的通信,用来通知线程的阻塞与唤醒。线程为了进入临界区(也就是同步代码块内)需要获得锁并等待锁可用,他们并不知道也不需要知道哪些线程持有锁,他们只需要知道当前资源是否被占用是否可以获得锁,所以锁的持有状态应该由同步监视器来获取而不是线程本身,因此wait(),notify()定义在Object类中而不是Thread中。

         1.5 如何停止一个正在运行的线程?使用stop方法终止(已经过期);使用interrupt方法终止线程;run()方法正常退出

  2、实现Runnable接口:

        1、定义子类实现Runnable接口;

        2、子类中重写Runnable中的run()方法;

        3、通过Thread类含参构造器创建线程对象;

        4、将Runnable接口的子类对象作为实际参数传递给Thread含参构造器中;

        5、调用Thread类的start方法:开启线程,调用Runnable子类接口的run()方法。

    2.1 上述两种方式的区别:Thread类实现了Runnable接口

  3、实现Callable接口:可实现子线程结果传回主线程这一功能(面试过程被问到过)

    3.1 两种接口方式的区别:Runnable接口run()方法没有返回值,Callable接口call()方法有返回值,是个泛型,和Future和Future Task配合使用获取异步执行结果。调用FutureTask.get()方法获取返回值,但是这样会造成主线程堵塞,不调用就不会堵塞。Runnable接口run()方法只能抛出运行时异常,且无法捕获;Callable接口中call方法允许抛出异常且可以获取异常信息。

    3.2 什么是FutureTask、Future、Callable?:Future可以拿到Callable被线程异步执行后的返回值,表示异步任务,是一个可能没有完成的异步任务结果。Callable用于产生结果,Future用于接收结果.FutureTask是Future的实现类,也是Runnable接口的实现类。

  4、Excecutors工具类创建线程池

     4.1为何创建线程池:当程序要处理大量并短小的任务时候,如果进行创建、销毁线程就会使得时间以及系统资源开销过大,性能效率很低。因此我们创建一个线程池,来避免短时间内多次创建、销毁线程池提高效率,且也可以避免CPU内存溢出。没创建一个线程,栈就会给其分配一个内存空间,假设有1024个线程,一个线程内存为1M,这样的话就会占用系统1G资源,很容易导致系统内存崩溃。

    4.2线程池的工作机理:一个线程池有以下几个重要参数:

      corePoolSize:核心线程大小。线程池只要存在,核心线程就不会被销毁。

      maxinumPoolSize:线程池创建的最大线程数。如果workQueue满了,就开始创建非核心线程,但是非核心线程+核心线程<=maxinumPoolSize

      keepAliveTime:非核心线程的心跳时间。如果非核心线程在keepAliveTime时间内没有工作,就会销毁。

      workQueue:阻塞队列。当核心线程都在工作,新的线程任务就会进入workQueue进行排队。如果使用无界队列,即等待线程个数是无限的,可以一直加入,这样会导致OUT OF Memory(OOM)问题。

      defaultHandler:饱和策略。当线程池线程数已经达到了maxinumPoolSize,且workQueue已经满了,所以就会拒绝其他线程再加入任务,就会抛出RejectedExecutionException 异常,让其他线程自行处理。

       4.3线程池的类型

      newCachedThreadPool:创建可缓存线程池。工作线程数量无限制。

      newFixedThreadPool:创建一个指定工作线程的线程池。

      newSingleThreadExecutor:创建一个单线程化的Executor。

      newScheduleThreadPool:创建一个定长的线程池,且支持定时的以及周期性的任务执行。

三、如何保证多线程安全

  使用安全类,比如java.util.concurrent下的类,使用原子类Atomiclnteger;使用自动锁,synchronized锁,可以使用同步代码块或同步方法;使用手动锁,Lock lock=new ReentrantLock(),lock.lock(),lock.unlock();

四、什么是线程安全?Servlet是线程安全的吗?

  线程安全是指某个方法在多线程的环境下被调用时,能够正确处理多线程之间的共享变量,使得程序能够正常执行。Servlet不是线程安全的,它是单实例多线程,当多个线程同时访问一个方法时,不能保证共享变量是安全的。SpringMVC的controller和Servlet一样是是单实例多线程。如果既想提升性能又可以不用管理多个对象的话建议使用ThreadLocal来处理多线程。

五、对单实例对象线程安全的理解

  结合JVM内存机制,在一个线程中,如果创建一个对象,就会现在堆内存中开辟一个存储空间,该对象在创建的时候就会在栈内存中开辟空间,调用其构造方法、静态代码块等,当方法结束会在栈内存中清除。一个线程在调用这个方法的时候会在栈内存中创建一个空间存储对象方法。同理,对于一个线程加载单例模式对象的时候,他会在静态共享域获取单例对象,然后再调用对象方法(非静态)的时候会在自己栈内存中开辟一个空间,说明如果多个线程处理同一个对象的时候如果不涉及到对象共有的属性值,就不会存在线程安全问题。我们通常将数据连接层(dao)层设计成单例模式,因此当多个线程都想使用这个数据库连接时会对性能有很大影响。如果多个县茨城同时访问一个对象的共有属性时,需要对该资源加锁。

六、为什么ssh中的struts中action层必须创建多利?而ssm中的springmvc的controller层不需要创建多例?

  struts2中因为将前端获取的值全部存储在对象属性中,所以肯定需要设置为多例;而springMVC中前端获取的值直接进入方法中,所以设置为单例模式不会存在线程安全问题。

七、同步锁机制

  Java中每个对象都有一个内置锁,当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例有关的锁,获得一个对象的锁称为获取锁、锁定对象、在对象上锁定或在对象上同步。当程序运行到synchronized同步方法或代码块时该对象锁才会起作用。一个对象只有一个锁,所以一个线程获得该锁,就没有其他线程可以获得锁,知道第一个线程释放锁,这也意味着在这期间任何其他线程都不能对该同步锁内的资源进行操作。

只能同步方法而不能同步变量和类;要同步静态方法则需一个用于整个类对象的锁,这个对象就是这个类。

posted @ 2022-03-22 10:38  ganrui~~~  阅读(85)  评论(0)    收藏  举报