Java多线程、反射与克隆

1.并行和并发的区别

  • 并行是指多个任务在同一时刻被执行;而并发表示多个任务在同一时间段内被执行。这些任务并不是顺序执行的,而往往是以交替的方式被执行。
  • 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
  • 并行是在多台处理器上同时处理多个任务,如hadoop分布式集群;并发是在一台处理器上“同时”处理多个任务。

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

2.线程和进程的区别

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

  1. 单位:进程是程序运行和资源分配的基本单位;线程是进程的一个实体,是cpu调度和分派的基本单位

  2. 关系:一个程序至少有一个进程,一个进程至少有一个线程

  3. 资源:线程共享了进程的地址空间、文件句柄、全局变量。线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)

  4. 效率:进程在执行过程中拥有独立的内存单元,进程切换时,耗费资源较大,效率要差一些;而多个线程共享内存,从而极大地提高了程序的运行效率

  5. 执行:每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

  6. 逻辑:从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

进程和线程的区别

3.守护进程

  守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他线程的线程。在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

  Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器)。

  User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。

4.创建线程的方式

① 继承Thread类创建线程类

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

② 通过Runnable接口创建线程类

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

③ 通过Callable和Future创建线程类

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

5.runnable 和 callable 有什么区别

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

6.线程状态

线程通常都有五种状态:新建运行阻塞等待超时等待终止

  • 新建(New):新创建了一个线程对象,但还没有调用start()方法。
  • 运行(Runable):Java线程中将就绪(Ready)和运行中(Running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(Ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(Running)。

  • 阻塞(Blocked):表示线程阻塞于锁。线程正在运行的时候,被暂停,通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。

  • 等待(Waiting):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • 超时等待(Timed Waiting):该状态不同于WAITING,它可以在指定的时间后自行返回。
  • 终止(Terminated):如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪

Java线程的6种状态及切换(透彻讲解)

7.sleep() 和 wait()的区别

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

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

8.notify()和 notifyAll()的区别

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

9.线程的 run()和 start()的区别

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

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

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

10.创建线程池的几种方式

在java doc中,并不提倡直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:

①newSingleThreadExecutor() 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会创建一个新的来替代它

②newFixedThreadPool(int nThreads) 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

③newCachedThreadPool() 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池的规模不存在任何限制。

④newScheduledThreadPool(int corePoolSize) 创建一个定长线程池,支持定时及周期性任务执行。

Java并发编程:线程池的使用

如何优雅的使用和理解线程池

Java 四种线程池的用法分析

11.线程池状态

线程池有5种状态:Running、ShutDown、Stop、Tidying(收拾、整理)、Terminated。

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

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

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(synchronized)
  • 可见性:一个线程修改了共享变量的值,其他线程能够立即得知这个修改(synchronized,final)
  • 有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的(happens-before原则,synchronized,volatile)。

13.多线程锁的升级原理

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

锁升级的图示过程:

14.死锁

  死锁,是进程死锁的简称,是指两个或两个以上的进程在执行过程中,由于资源竞争或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。是操作系统层面的一个错误。

  最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。

15.怎么防止死锁

死锁的四个必要条件:

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

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。

所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

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

  线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式,实现了线程的数据隔离

  但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

17.Synchronized 底层实现原理

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

  Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

深入理解Java并发之synchronized实现原理

18.Synchronized 和 volatile 的区别

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。

  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。

  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性

  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

19.Synchronized 和 Lock 的区别

  1. synchronized是java内置关键字;在jvm层面,Lock是个java类

  2. 锁的状态:synchronized无法判断是否获取锁的状态;Lock可以判断是否获取到锁

  3. 释放锁:synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁);Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁

  4. 等待与阻塞:用synchronized关键字的两个线程1和线程2,如果线程1获得锁,则线程2等待;如果线程1阻塞,线程2则会一直等待下去;而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了

  5. 重入、中断、公平:synchronized的锁可重入、但不可中断、非公平;而Lock锁可重入、可中断、可公平(两者皆可)

  6. synchronized锁适合少量代码的同步问题;Lock锁适合大量代码的同步问题

20.Synchronized 和 ReentrantLock的区别

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

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

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

21.Atomic的原理

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

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

22.反射

反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。

在Java运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法?

Java反射机制主要提供了以下功能:

  • 在运行时判断任意一个对象所属的类。
  • 在运行时构造任意一个类的对象。
  • 在运行时判断任意一个类所具有的成员变量和方法。
  • 在运行时调用任意一个对象的方法。

23.Java序列化以及什么情况下需要序列化?

  序列化简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。虽然你可以用你自己的各种各样的方法来保存object states,但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化。

什么情况下需要序列化:

  1. 想把内存中的对象状态保存到一个文件中或者数据库中时候
  2. 想用套接字在网络上传送对象的时候
  3. 想通过RMI传输对象的时候

24.动态代理及应用

  动态代理:当想要给实现了某个接口的类中的方法,加一些额外的处理,比如说加日志,加事务等,可以给这个类创建一个代理。顾名思义就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新类。这个代理类并不是定义好的,是动态生成的。具有解耦意义,灵活、扩展性强。

动态代理的应用:

  • Spring的AOP
  • 加事务
  • 加权限
  • 加日志

25.如何实现动态代理

  1. 必须定义一个接口
  2. 有一个InvocationHandler(将实现接口的类的对象传递给它)处理类
  3. 一个工具类Proxy(习惯性将其称为代理类,因为调用他的newInstance()可以产生代理对象,其实他只是一个产生代理对象的工具类)
  4. 利用InvocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加载,并将其实例化产生代理对象,最后返回

26.为什么要克隆

  想对一个对象进行处理,又想保留原有的数据进行接下来的操作,就需要克隆了,Java语言中克隆针对的是类的实例

27.如何实现对象克隆

有两种方式:

1)实现Cloneable接口并重写Object类中的clone()方法;  

2)实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆

注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常。这种是方案明显优于使用Object类的clone方法克隆对象:让问题在编译的时候暴露出来总是好过把问题留到运行时。

28.深拷贝和浅拷贝的区别

  • 浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址。所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝(例:assign())
  • 深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse()和JSON.stringify(),但是此方法无法复制函数类型)

29. Java中Class.class、Class.forName()、getClass()

  • Class.class 的形式会使 JVM 将使用类装载器将类装入内存(前提是类还没有装入内存),不做类的初始化工作,返回 Class 对象。
  • Class.forName() 的形式会装入类并做类的静态初始化,返回 Class 对象。
  • getClass() 的形式会对类进行静态初始化、非静态初始化,返回引用运行时真正所指的对象(因为子对象的引用可能会赋给父对象的引用变量中)所属的类的 Class 对象。

静态属性初始化是在加载类的时候初始化,而非静态属性初始化是 new 类实例对象的时候初始化。它们三种情况在生成 Class 对象的时候都会先判断内存中是否已经加载此类。

谢谢你请我吃糖果!
0%