终于在耽搁了几天之后,开始看关于线程的面试题了。之前被腾讯大佬问过,这一块得好好补补。
1.什么是线程,什么是进程,两者的区别举个例子?
我们先来举一个例子把,假设把cpu比作工厂的发电机,单核cpu只能每次为一个车间供电使其工作,多核呢,能同时为多个车间供电,我们可以假设一个车间是一个进程,车间里面有很多机器人,里面的机器人为车间工作生产产品,机器人就是线程,一个车间里面可以有多个机器人,一个进程可以有多个线程。
所以在实际生产中,一台发电机只能给一个机器人供电,下次给另一台机器人供电,这样交替完成所有的任务
在系统中也一样,单核CPU一次只能给一个进程的一个线程调度,然后不同的线程交替执行,来完成系统中所有的任务
车间的空间是这个车间的机器人们共享的,相应地,一个进程中的内存空间可以给这个进程中的所有线程共享
但不同车间的空间不能共享,所以不同进程不能共享内存空间。
什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程当中,是进程的实际运作单位
什么是进程?
进程是指在操作系统中正在运行的一个应用程序,是分配资源的基本单位,一个进程可以有多个线程,每个线程执行不同的任务,不同的进程使用不同的内存空间,而线程共享所属进程的内存空间。
2.java中实现线程的三种方式?
1)直接继承Thread类重写run方法
1、定义一个类继承Thread类,并重写Thread类的run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的执行体;
2、创建该类的实例对象,即创建了线程对象;
3、调用线程对象的start()方法来启动线程;
2)实现Runable接口,然后传给Thread 的构造函数
1、定义一个类实现Runnable接口;
2、创建该类的实例对象obj;
3、将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;
4、调用线程对象的start()方法启动该线程;
3.实现Callable接口,用FutureTask包装Callable对象
1、创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例;
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
3、使用FutureTask对象作为Thread对象的target创建并启动新线程;
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
4.Callable实现解释
前两种创建线程的方法中呢,都有一个缺陷,就是当需要线程结束时需要返回值的时候,是满足不了需求的,所以自从JAVAjdk1.5之后创建了Callable和FutureTask这种方式。我们看源码的时候知道,Callable接口定一个,返回值是泛型的Call方法
但是由于Thread 类没有接受Callable类型的方法或则构造器,所以只能通过FutureTask来进行封装
FutureTask 实现了RunableFuture接口,相当于同时实现了Runable,Future接口
从而继承了run,cancle,isCancelled,isDone,get方法,FutureTask提供了参数位Callable类型的构造器用于初始化对Callable的引用。我们通过3标题下 的代码可以看到,FutureTask封装Callable返回的一个对象,可以当作Runable对象交给Thread去实现,调用FutureTask重写的run方法
从这里我们可以看到,run方法实际调用的时callable中的call()方法,并将函数返回值给了set()函数,
最后将结果给了全局对象,outcome,最后可以通过get方法返回
这大致就是FutureTask实现的一个过程,但是值得一提的是,类中有一个用volatile,修饰的state变量,用来记录线程所达到的一个状态,至于具体如何让状态发生变化的,等我们学到了后面的只是再来研究。(再其中cacel方法中是比较明显的)
5.三种实现方式区别与各自优缺点
通过继承Thread类实现多线程:
优点:
1、实现起来简单,而且要获取当前线程,无需调用Thread.currentThread()方法,直接使用this即可获取当前线程;
缺点:
1、线程类已经继承Thread类了,就不能再继承其他类;
2、多个线程不能共享同一份资源(如前面分析的成员变量 i );
通过实现Runnable接口或者Callable接口实现多线程:
优点:
1、线程类只是实现了接口,还可以继承其他类;
2、多个线程可以使用同一个target对象,适合多个线程处理同一份资源的情况。
缺点:
1、通过这种方式实现多线程,相较于第一类方式,编程较复杂;
2、要访问当前线程,必须调用Thread.currentThread()方法。
6.在线程使用时,对Thread.currentThread()和this做解释
Runable 和callable 接口是没有this关键字的,Thread是从父类继承下来的,这也算是两则的一个区别
Thread.currentThread 指向的是调用当前方法的线程,this指向的是当前对象
其实,this指向的还是当前对象,只是他继承的父类Thread有一个public 方法叫getName(),所以this.getName()可以访问到其父类中的方法,而runable和callable的实现方式,是实现了一个接口,所以用this获取不到父类中的属性!!!
7.Thread 类中的start() 和 run() 方法有什么区别?
start方法
是用来将线程从创建状态变成就绪状态的,当new 了一个Thread对象时,线程只是创建状态,调用start方法时,线程达到就绪状态,可以被cpu调度了。但是通过网上大佬看源代码,发现,调用start方法时最后还会调用run()方法。
调用start方法的时候是会去创建一个新的子线程,但是最后还是调用了run方法;即调用start()--->start0()-->JVM_StartThread-->thread_entry-->run()
run方法:
我们看方法的介绍
If this thread was constructed using a separate <code>Runnable</code> run object, then that <code>Runnable</code> object's <code>run</code> method is called; otherwise, this method does nothing and returns. <p> Subclasses of <code>Thread</code> should override this method.
如果该线程是使用一个单独的Runnable run对象构造的,则调用该Runnable对象的run方法;否则,此方法不执行任何操作并返回。<代码>线程</代码>的>子类应该覆盖这个方法。
即如果线程单独直接调用run()方法,则相对于调一个普通方法,程序会按照顺序执行下去。
两者最后都会执行到run方法,只是执行方法的时机和上下文不一样。
8.Java中CyclicBarrier 和 CountDownLatch有什么不同?
CoutDownLatch 和 CylicBarrier 都能该线程等待一组线程执行完之后,再去执行,但是CountDownLatch的计数器不能重用,CyclicBarrier可以调用reset函数,进行重用,然后两则的底层实现不同
CountDownLatch底层用的是AQS重写了tryAcquireshare方法和tryReleaseshared方法,其中tryAcuqireshare则判断该实例化对象传入的count是否是等于0的,不等于返回-1(表示没有获取到同步状态),tryReleaseShared则每次由countDown()调用去执行,将count用CAS的方式减1,如果为0则返回true,唤醒后继节点。
CylicBarrier让一组线程达到屏障时被阻塞,直到最后一个线程达到屏障,屏障才会打开,底层使用了ReenTrantLock和 Condition来实现的,首先构造函数默认可以只传一个parties参数用来表示总共有多少个线程,第二个参数默认可以不传,如果传的话需要传一个Runable对象,并且实现了run方法,当我们调用CylicBarrier的await方法时(表示该线程已经到达了屏障),先获取锁,判断该paties参数是否为0,如果不为0则则将parites--操作,(不需要用CAS),然后将调用Condition.await方法将该线程加入到等待队列中。当最后一个线程到达屏障时,则会首先判断哟没有 Runable的方法要执行有的话优先执行,然后唤醒所有等待队列中的线程,去竞争同步资源。执行后面的代码。
9.什么是Java内存模型?
Java程序是需要运行在Java虚拟机上面的,Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
JMM八大数据原子操作,保证了变量从工作内存和主内存之间的互相操作
read:从主内存中读取数据,但是不写入,等待load操作
load:将read读到得数据,写入工作内存
use:从工作内存中读取数据来计算
assign:将use计算好的值重新赋值到工作内存中
store:将工作内存中的变量放到主内存
write:将store中的变量赋值给主内存中的变量
lock:将主内存变量加锁,标识为线程独占状态
unlock:将主内存变量解锁,解锁后其他线程可以锁定此变量
10.volatile底层原理
首先,volatile解决的问题是JMM内存中的三大特性(原子,有序,可见性)之一的,可见性,有序性,即当工作内存操作某一变量时,其他工作内存对这个变量是可见的。只能用来修饰变量。
- 可见性
当线程使用volatile变量W进行写操作时,工作内存会将W立刻写入主内存中,但这个时候,其他工作内存的W变量并没有改变,再次写入就会有问题,所以这个时候会采用像“缓存一致性协议”采用在总线上采用的嗅探机制,所有工作内存再从主存中读取改变量,并修改。
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,Lock前缀的指令在多核处理器下会引发了两件事情:
1)将当前处理器缓存行的数据写回到系统内存。
2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。在多核处理器系统中进行在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存,处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。
- 有序性
volatile解决有序性的主要是使用用(内存屏障来禁止指定的重排序)
重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则
1.重排序操作不会对存在数据依赖关系的操作进行重排序。
2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
假设线程A执行changeStatus后,线程B执行run,我们能保证在4处,b一定等于3么?
但是在多线程下答案依然是无法保证!也有可能b仍然为2。上面我们提到过,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
内存屏障即可理解为 在两个操作之间加入一条指令,是的两个操作之前是无法重排序的,必须满足上一个操作完成之后,下个操作才能继续执行
volatile禁止指令重排序也有一些规则,简单列举一下:
1.当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
2.当第一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
3.当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序
主要是为了防止这个变量的赋值被提前到其他逻辑之前,导致另外的线程提前收到了这个变量的改变
11.什么是线程安全的?
一段代码所在的进程有多个线程在同时运行,假设这些线程可能同时运行这段代码,我们认为,如果这段代码被多线程运行或者单线程运行,所有的变量和结果和所预期的值都是一样的,我们可以认为它是线程安全的。
12.那些集合是线程安全的?那些集合不是线程安全的?
线程安全和线程不安全的集合
Vector、HashTable、Properties是线程安全的;
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的。
值得注意的是:为了保证集合是线程安全的,相应的效率也比较低;线程不安全的集合效率相对会高一些。吧
13.java中如何停止(中断)一个线程?
java中断一个线程有三种方法
1.stop()
这是java自带的一个线程中断的方法,它确实可以中断一个线程,但是这个方法已经被标记为过时了,因为它是不安全的,最好不要用它。
1)stop方法会立刻中断run()中所有的操作,包括Catch,Finally中的操作,所有可能会造成一些清理性的工作没有完成,例如文件,数据库关闭等
2)stop方法会立刻释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题
存在一个对象 u 持有 ID 和 NAME 两个字段,假如写入线程在写对象的过程中,只完成了对 ID 的赋值,但没来得及为 NAME 赋值,就被 stop() 导致锁被释放,那么当读取线程得到锁之后再去读取对象 u 的 ID 和 Name 时,就会出现数据不一致的问题,如下图:
2.使用标志位去中断
我们使用线程时,在run()中经常会遇到类似于循环这样的代码,所以想让其中断一般可以用标志位去中断,代码如下
3.使用interrupt中断线程
interrupt()方法,调用interrupt方法时并不会像break那样直接中断该线程,而是通过打入一个标记位,相当于告诉该线程,有人需要你中断,可以看看如下代码
输出:
可以看到,线程并没有被直接中断,还是在继续执行,那么该如何中断该线程呢?
这就需要使用另外两个与线程中断的方法
public boolean Thread.isInterrupted()//判断该线程是否被中断
public boolean Thread.interrupted()//判断是否被中断,并清除中断标志
这两个方法都是通过检查标志位是否有中断标志
如果希望线程被中断后进行一些处理,可以进行如下处理
1.使用isInterrupted()
2.使用interrupted
可以看到我们如果使用了两个,Thread.interrupted,第一个被触发了,然后清除了标记位,接着第二个是无法触发的
14.一个线程发生运行时异常会怎样?
Java中Throwable分为Exception和Error:
出现Error的情况下,程序会停止运行。
Exception分为RuntimeException和非运行时异常。
非运行时异常必须处理,比如thread中sleep()时,必须处理InterruptedException异常,才能通过编译。
而RuntimeException可以处理也可以不处理,因为编译并不能检测该类异常,比如NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException等。
由此题目所诉情形下发生的应该是RuntimeException,属于未检测异常,编译器不会检查该异常,可以处理,也可不处理。
所以这里存在两种情形:
① 如果该异常被捕获或抛出,则程序继续运行。
② 如果异常没有被捕获该线程将会停止执行。
Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler,并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
15.多线程之间共享数据的方法
1,如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。
2,如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,例如,设计4个线程。其中两个线程每次对j增加1,另外两个线程对j每次减1,银行存取款。
这是因为调用了同一个引用对象,将对象作为传参进行工作。
简单的多线程间数据共享,每个线程执行的代码不同,用不同的Runnable对象
16.notify 和 notifyall 有什么区别?
1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
4、notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
17.为什么wait, notify 和 notifyAll这些方法不在thread类里面?
一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通 过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁 就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
18.什么是ThreadLocal变量?
ThreadLocal变量的用处呢,一般是指线程用来存储自己该线程得一些私有资源,ThreadLocal底层是由一个TheardLocalMap的的数据结构实现的,该数据结构节点是用一个Entry<key,value>的结构实现的,其中key是弱引用(这是个关键)
然后我们来将一下Thread,ThreadLocal和ThreadLocalMap之间的关系,首先Thead类自己有一个ThreadLocalMap的变量,专门用来记录属于自己的ThreadLocalMap变量的,ThreadLocalMap中的key不是对该线程的引用而是对该ThreadLocal实例对象的引用,举个例子,当调用ThreadLocal进行set操作时,会首先判断当前线程的theadLocalMap属性是否为空,如果为空则创建一个初始值长度为16的Entry[]数组,不为空则直接调用当前线程自己的theadLocalMap,然后将该threadLocal作为对像作为key传入到map中。
然后至于ThreadLocalMap是ThreadLocal的一个静态内部类,底层实现就是一个Entry[]数组,当进行set操作时会hash寻址,找到指定数组下标然后进行插入,如果该寻址的时候发现该下标被占用则会依次往后寻找,如果,发现超过阈值则进行扩容,扩容的时候会用擦除函数,将Entry数组中key为null的下标进行擦除,然后再判断需不需要扩容,如果还需要扩容,则将数组的长度变为2倍,然后将所有的Entry[]下标重新hash寻址,并同时擦除为空的Enrty<>.
ThreadLocalMap会有一个内存泄漏的问题,内存泄漏是指,已经分配好的堆内存可能会由于某种原因使得,该内存无法回收从而造成系统运行变慢或奔溃的一种现象。(主要是某个变量已经没有用,但是存在到gcRoot的路径所以无法回收)所以这里存在当key为空时,可能线程的生命周期还比较长,所以存在了一个线程-->threadLocalmap-->Enrty<null.value>-->value这样的一个引用对象,但是由于key为空,所以该Enrty已经不具备实际价值,所以产生了内存泄漏。
所以这里才使用了擦除函数,将其堆
19.什么是FutureTask类?
在Java并发程序中FutureTask表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法,FutureTask类是一个实现了,Future和Runable接口的类,所以里面有run方法以及get(获取运算结果),cancle(取消运算),isCancelled(是否已经取消运算),isDone(是否已经完成运算),这些方法,这些方法的实现主要是通过一个Volatitle修饰的state变量来做的逻辑判断,FutureTask还可以包装Runable,Callable类,并且run()方法实际调用的是包装类中的call方法,并将Call()的返回值用变量保存。以便get()获取。
run()方法执行过程,在futureTask中run方法中它实际去调的是通过构造函数传进来的callable对象的call()方法,获取返回值,并用set函数用CAS的方式修改state状态,如果修改成功,则讲返回值赋给一个全局变量outcom。
如果在run方法执行完之前,有线程进来了,则会判断该线程的状态是不是小于等于COMPLEATING,如果是的话,则会调用awaitDone()方法,awaitDone()方法会将改线程构造成一个waitNode节点,加入到链表中,然后使用lockSupport.park方法将该线程阻塞。
当线程的run方法执行完之后呢,则会调用finishCompleting()方法,longSuport.unpark()方法唤醒等待队列中的阻塞线程,并返回结果值。
为了保证并发安全性,所有的关键赋值都是采用自旋+cas的方式,并且state是用了volatile修饰来保证可见性
20.Java中interrupted 和 isInterruptedd方法的区别?
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来 检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛 出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。
21.为什么wait和notify方法要在同步块中调用?
主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常,wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。还有一个原因是为了避免wait和notify之间产生竞态条件。
22.Java多线程中调用wait() 和 sleep()方法有什么不同?
Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而 sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。
Thread.sleep()将当前线程发送到“Not Runnable”状态一段时间。线程保持它已获取的监视器 - 即,如果线程当前处于synchronized块或方法中,则其他线程无法进入此块或方法。如果另一个线程调用t.interrupt()。它会唤醒睡眠线程。
虽然sleep()是一种static方法,这意味着它总是影响当前线程(正在执行睡眠方法的线程)。一个常见的错误是调用t.sleep()这里t是一个不同的线程; 即使这样,它是当前线程将睡眠,而不是t线程。
23.Java中同步集合和并发集合有那些?
同步集合常值被synchronized所修饰的集合
Vector,Stack,HashTable等防止这个变量的赋值被提前到其他逻辑之前,导致另外的线程提前收到了这个变量的改变
1. Vector是线程安全的,源码中有很多的synchronized可以看出,而ArrayList不是。导致Vector效率无法和ArrayList相比
2. Stack是继承于Vector,基于动态数组实现的一个线程安全的栈
3. HashMap是非synchronized的,而Hashtable是synchronized的。这说明Hashtable是线程安全的,而且多个线程可以共享一个Hashtable .由于Hashtable是线程安全的,也是synchronized的,所以在单线程环境下比HashMap要慢
4.Collections:
Collections是为集合提供各种方便操作的工具类,通过它,可以实现集合排序、查找、替换、同步控制、设置不可变集合
Collections.synchronizedCollection(Collection<T>t)
Collections.synchronizedList(List<T>list)
Collections.synchronizedMap(Map<K, V>map)
Collections.synchronizedSet(Set<T> t)
Collections工具类将集合变为同步集合,从而解决集合的线程安全问题。
常见的并发集合:
ConcurrentHashMap:线程安全的HashMap的实现
CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList
CopyOnWriteArraySet:基于CopyOnWriteArrayList,不添加重复元素
ArrayBlockingQueue:基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制
LinkedBlockingQueue:基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue
24.Java中同步集合和并发集合有什么区别?
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。
在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。
不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有他们如何实现的线程安全上。
同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他们并发的实现(ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)会慢得多。造成如此慢的主要原因是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过使用先进的和成熟的技术像锁剥离。
比如ConcurrentHashMap 会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段。
同样的,CopyOnWriteArrayList 允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。
如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。
30.并行和并发有什么区别?
并发指的是在外界看来,多个程序或者多个进程或者多条指令可以同时运行的一种现象,而实际上是每个进程执行一段时间都会停下来,然后cpu调度切换到下一个进程继续执行,由于切换的块,所以在用户看来,是在同时运行。
并行指的是同一时刻,多个cpu同时执行不同的任务,是真正意义上的同时执行。
举个例子,一间教室有两个门,两个同学在同一时刻穿过了不同的门叫并行,并发指的是两个同学先后传过门之后,门外的同学看到他们某个时间端同时在教室里面出现,认为是并发。
31.说一下 Future 和 FutureTask,以及他们之间的区别
Future 是一个接口,定义了五种方法,用来获取异步计算的结果 提供三种功能(1)能够中断执行中的任务(2)判断任务是否执行完成(3)获取任务执行完成后额结果。
-V get() 用来获取任务执行后返回的结果,如果没有结果,该方法会阻塞直到异步计算完成
- V get(Long timeout , TimeUnit unit) :获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
- Bollean isDone() 判断任务是否执行结束
- Bollean isCanceller() 如果任务完成前被取消 返回true
- Boolean cancel(boolean mayInterruptRunning) mayInterruptRunning表示是否中断正在执行的任务,
如果为false 则对执行的任务不会有任何影响,如果为true则以中断执行线程的方式(interrupt())中断线程如何中断成功返回ture.如果线程还没开始调用此方法返回false
FutureTask 实现了Runable 接口和Future接口,所以除了将上面提到过的方法实现了一遍之外,还能够封装Callable 接口,重写run 方法调用的是Callable 的call 接口并将返回值 给一个全局变量 outcom ,通过get()方法可以获取。
32.ThreadLocal的实现原理及内存泄漏
ThreadLocal 是用来存储线程自身数据的,底层自己维护了一个ThreadLocalMap 集合,集合底层是用了一个Entry<>数组,我们可以看一下代码首先聊一下它的实现逻辑
1.Thread,ThreadLocal,ThreadLocalMap三者之间的关系
首先,我们可以通过以上代码可以看到,为了保证每次ThreadLocal调用的是当前线程,采用的是Thread.currentThread();获取当前线程对象,然后通过getmap(t)方法,去查看Thread线程自身的一个 ThreadlocalMap对象 threadLocals 是否为空,如果为空,我们可以看到,调用creatMap()方法初始化一个新ThreadLocalMap对象给t。而初始化的时候,去调用ThreadLocalMap的构造函数,创建一个 Entry数组,初始大小为16。
ps:这里我们注意一个细节,传递给ThreadLocalMap()构造函数时,传递对象是 “this”也就是ThreadLocal自身的这个实例对象,这样做的好处就是,可以相当于把自己的实例对象和不同的线程绑定在一起了。
现在我们单独看下,threadLocal对外的三个函数,set()、get()、remove()三个函数
2.get()
get()函数 的流程是这样的:
1.)获取当前调用线程,然后拿到当前线程的成员变量thredlocals
2.)如果该变量不为空,则用当前实例对象作为KEY去查找
3.)如果找到,则去查找该KEY对应的value
4)找到则直接返回。return;
没有的话则按照当前位置一直往下找直到找到位置,期间如果碰到key为空的脏Entry进行擦除
2.1)如果该属性为空或者该对应的value没有找到则调用setInitialValue()方法
调用createMap()方法
新建一个新ThreadLocalMap对象给线程的threadLocals字面量。
3.Set()
通过代码可以知道,如果调用set()方法同样会先和get()方法一样获取线程的thredlocal成员变量,判断是否为空。如果为空则进行初始化,不为空则去寻找对应的位置,通过int i = key.threadLocalHashCode & (len-1);去定位对应的位置,如果在在该索引上没有找到,则按照顺时针一样在像一个环一样去Entry中寻找,如果直到遇到数组的元素为空还没找到的话,则在这个将值放在这个空数组元素上。如果当前插入的当前元素大于阈值则扩容操作。
4.Hash冲突
ThreadLocal是如何解决hash冲突的呢,我们知道,当我们一个线程中使用多个ThreadLocal对象时,这个时候,多个实例对象共用一个线程的ThreadLocalMap,就需要解决Hash冲突
这里的hash值主要是由
private static final int HASH_INCREMENT = 0x61c88647; 这个魔法参数决定的,每次创建一个thredLocal对象是,都会在原来的threlocalHashCode上累加上这个参数,我们知道,数组的长度是2的N次方 且寻址是通过hash值和数组长度减1做&与操作,如果数组的长度比较小,则hash值的高16位是很容易被浪费的,所以我在想这个地方应该也和hashMap集合底层解决hash冲突的方式有点类似。
5.resize
resize之前会进行一次擦除操作遍历整个数组进行擦除操作,腾出table[i]为空的元素,便于利用
这里的扩容操作,每次将table的长度翻倍,循环遍历旧数组,如果不为空则将hash值与新的数组长度做&操作,将旧值复制到新的下标上,同时如果发现key为空的,则将value也为null,让垃圾收集器去回收。更新size,和阈值。这里的阈值是数组长度 乘以2/3
6.remove
remove操作就比较简单了,一样采用开发寻址法,找到key对应的位置,用expungeStaleEntry(i);将该Entry当作脏Entry处理掉。
7.ThreadLocal 内存泄漏问题
1.)什么是内存泄漏?
内存泄漏是指,程序中已经分配的的堆内存由于某种原因未及时释放或无法释放,造成内存浪费,导致程序运行变慢甚至系统崩溃等。
2.)什么是强引用,软引用,弱引用,虚引用?
- 强引用:强引用就是我们平时用到的最多的引用,最普遍的引用,如:Object sb =new Object();
- 软引用:常用来描述有用但不是必须的对象,用SoftReferenc类来表示,一般在内存不足的时候,常会被垃圾收集器回收
-弱引用:和软引用一样常用来描述有用但不是必须的对象,用WeakReference类来表示,一般只要发生GC操作,就会被回收
-虚引用:虚引用,顾名思义就是指,加上引用和没有被引用一样,用phantomReference表示,并不影响生命周期,任何时刻都有可能被垃圾收集器回收
3.)ThreadLocal为什么会发生内存泄漏?
通过源码可以发现,ThreadLocalMap底层使用的Entry数据结构是一个key为弱引用,value为强引用的,所以可能会出现一种情况就是,当生垃圾回收之后,ThreadLocalMap中会出现很多key为空,value存在的Entry,这种情况下,如果线程的生命周期很长,那么会出现一种Thread ->ThreadLocalMap->Entry->value的这种强链接,导致key为空的Entry不会被回收,从而导致内存泄漏。
如果使用强引用作为key会出现什么情况?
如果用强引用的话,只要是线程或则ThreadLocal两者中有任意一个存在或生命周期较长的话,都会在存在Entry对应一个强引用关系导致两者都不被回收。自己可以从三者之间的关系思考这个问题。
4.)内存泄漏处理机制
当Entry的key被垃圾回收时会出现一些key为null的Entry,这些都是需要我们手动进行删除的,我们调用get(),set(),remove()方法时调expungeSlateEntry()方法,expunge擦除,删除的意思,顾名思义这个方法会将Key为null的Entry擦除,我们看看expungeSlateEntry(int i)是如何操作的。
第一步将方法参数中的i位置下标的数组元素,的value为空,方便垃圾回收,将Tbale[i]为空便于给其他元素使用
第二步从当前i往后走如果找到key为null的元素,继续进行擦除。
第三步直到找到下一个Tab[i]为null的元素下标则退出,为什么要直到下一个Tab[i]为null的时侯退出呢,因为Entry<key,value>内存泄漏的前提是key为空,如果整个Entry都会空,前提条件都不满足,则这段下标下的元素,就自然不可能发生内存泄漏了。
5.)总结 如何避免内存泄漏呢?
每次调用完ThreadLocal时,调用Remove手动清除,引用当前ThreadLocal实例的Entry元素。
26.死锁是什么?以及如何避免
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.
死锁的发生必须满足以下四个条件:
互斥条件:两个或以上进程竞争同一个资源,但是资源每次只能被一个进程使用。
请求与保持条件:进程1占用进程2所需资源,进展2占用进程1所需资源,两者都互相请求等待对方释放
不可剥夺条件:进程获得资源,如果没有释放,不能被剥夺
循环等待条件:若干进程间形成首尾相接循环等待资源的关系
避免死锁的常见方法:
1.避免一个线程同时获得多个锁
2.避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
3.尝试使用定时锁,来替代非定时锁机制
4.对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况