多线程

1.介绍下几种典型的锁?
答:读写锁:允许多个线程同时进行读;当多个线程进行写操作的时候必须互斥;当有读有写的时候优先线程写,写完再读。
互斥锁:一个线程拥有锁的时候,其他线程只能等待。一般其他线程没有锁的时候会进入阻塞态,等待CPU唤醒。
自旋锁:基于互斥锁,但是一个线程争夺锁失败后会自旋一段时间,尝试争夺锁,如果过时间了就会进入阻塞状态,这种锁适合线程占用锁时间短的情况。
2.一个进程可以创建多少个线程?和什么有关?
答:创建多少线程由虚拟内存和系统最大限制有关。创建一个线程需要给线程分配栈空间,如果虚拟内存有限,创建线程的个数也有限。
3、常见的线程池?说说线程池的七大参数?
答:下面这些都是ExecutorService的实例。

  • FixedThreadPool:固定线程数的线程池,任何时间最多只有n个线程处于活跃状态。
    • 使用无界阻塞队列,不会拒绝任务,核心线程也不会回收。
    • 适合CPU密集型任务,用尽可能少的线程完成任务,适合长时间任务。
  • SingleThreadExecutor:只有一个线程的线程池
    • 使用无界阻塞队列
    • 适合串行执行任务的场景
  • CachedThreadPool:根据需要创建新线程的线程池
    • 适合大量但短小的任务。
  • ScheduledThreadPool

4、说说线程池的七大参数?

  • corePoolSize核心线程数:线程池会维护的最小线程数,即使线程处于空闲状态也不会销毁。
  • maximumPoolSize最大线程数:当线程池中的线程数达到了最大连接数会执行拒绝策略。
  • keepLiveTime空闲线程存活时间:一个线程处于空闲状态,并且当线程数大于核心线程数,小于最大线程数时,当这个空闲线程超过这个时间会被销毁。
  • unit存活时间单位
  • workQueue工作队列:新任务提交后,会先放到工作队列中,等待执行,又分为了四种工作队列
    • ArrayBlockingQueue,基于数组的有界阻塞队列:当前线程池线程数量达到核心线程数后且没有空闲线程,会放到队列中,队列放满之后会创建新线程去处理线程,直到创建的线程等于最大线程数。
    • LinkedBlockingQueue:基于链表的无界阻塞队列,当线程池中线程数量达到核心线程数后且没有空闲线程,会一直会加到队列后面,而不会去创建线程。
    • SynchronousQuene,不缓存任务的阻塞队列:新任务来的时候要么被空闲线程执行,要么创建新线程去执行。
    • PriorityBlockingQueue:基于优先级的无界阻塞队列,和基于链表的无界阻塞队列机制一样,但是按线程的优先级来取出执行的。
  • threadFactory线程工厂:创建新线程时用的工厂,可以用来设置线程名,线程属性(是否是deamon线程)等
  • handler拒绝策略:当工作队列满了之后且线程池中线程达到了最大线程数量会执行拒绝策略,有四种
    • CallerRunsPolicy:会转去执行拒绝策略的run方法
    • AbortPolicy:丢弃任务,抛出异常
    • DiscardPolicy:直接丢弃任务,什么也不做
    • DiscardOldestPolicy:对丢弃进入任务队列中最早的那个任务,然后把这个任务放到队列尾中。

4、线程池的execute和submit的区别?
答:execute只能接受Runnable类型任务,submit可以接受Runnable和Callable类型任务;
submit有返回值,而execute没有。
5、线程间的通信方式?**
答:1)volatile,但只能保证变量对线程的可见性,不能保证原子性。
2)用synchronized实现线程同步,用锁锁住共享变量,变量一次只能被一个线程访问

3)wait/notify 等待/通知实现线程同步
6、线程安全了解吗?
答:当多个线程同时访问共享数据的时候能够正确的访问就是线程安全。线程不安全呢就是一个线程在操作数据的时候被其他线程篡改。
7、实现线程安全的方式?
答:加锁、修改数据的时候用cas机制、ThreadLocal、修改为常量
8、线程同步的方式?
答:同步是指一定时间只允许某一个线程访问资源。
1)使用同步方法和同步代码块,用synchronized修饰。
2)阻塞队列LinkedBlockingQueue实现;
3)信号量,在生产者消费者模型中,当信号量的值小于等于0线程就阻塞,等待生产者生产,让信号量大于0。
9、ThreadLocal的原理?
答:ThreadLocal就是把线程共享的数据都复制一份在线程中保存,每个线程都有自己的公共数据的副本,互不干扰。
Thread类有一个ThreadLocalMap,而Map中元素的键为ThreadLocal,值为对应线程的变量的副本。调用threadLocal的set,实际上先获得当前线程的ThreadLocalMap,然后获取调用map.set(this,value),设置新值。调用threadLocal的get,也是先获得前线程的ThreadLocalMap,然后调用get得到副本值。
10、ThreadLocal内存泄露的原因?
答:ThreadLocalMap中的key被定义为若应用,在下次垃圾回收的时候就会被回收,而value是强引用,如果key被回收,ThreadLocalMap中就出现key为null的键值对,在线程池中一些线程可能一直不会被销毁,所以value永远不会被回收,就有可能出现内存泄露。用完ThreadLocal方法后用remove可以解决这个问题。
11、什么是进程?什么是线程?
答:进程是程序的一次执行过程,线程是程序中的一个任务,一个进程最少包含一个线程。
线程是由CPU来进行调度的,先后顺序是不能干预的。
12、怎么预防死锁?**
答:破坏死锁形成条件。

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已有资源保持不放。
  • 不剥夺条件:进程已获得资源,在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种首尾相接的循环等待资源关系。

1)破坏请求与保持条件:一次把进程需要的所有资源拿走。
2)破坏不剥夺条件:当某个线程申请不到资源的时候把已有资源释放。
3)破坏循环等待条件:按某一顺序来申请资源。
13、死锁怎么检测?
答:主要用一个有向图连接线程和资源,但当一个线程请求资源等待,看这个资源是否被其他线程持有,如果被持有,又看该线程是否在等待资源,然后看该资源是否被谁持有,如果最终寻到最初那个线程,形成一个环就说明有死锁的存在。
14、说说sleep()和wait()的区别?
答:

  • sleep和wait都是让线程暂停,sleep不会释放锁,wait会释放锁。
  • sleep需要指定一个时间,时间到了重新获得cpu执行权,wait需要等到其他线程唤醒。**

15、谈谈你对volatile的理解?
答:volatile用来修饰变量,保证变量的可见性。在每个线程对内存中变量进行操作时都需要先把它拷贝到自己的工作内容中,修改完之外再写到内存中,当用volatile修饰变量之后,一个线程在对变量进行操作的时候其他线程都会立马知道,其他线程就会从内存中重新去获取新值,而丢掉自己工作内存中的值。
除了能够解决变量的可见性,还能禁止指令重排序,指令重排序是在不影响结果的情况打乱代码执行的顺序。
16、volatile能保证线程安全吗?为什么?
答:不能保证线程安全,因为volatile只能保证变量对线程的可见性,也就是一个线程的缓存区有该变量,其他缓存区不能从自己缓存区操作该变量,而是需要从内存中拿取。如果第一个线程将变量拿到缓存区进行了修改但还未写到内存中,第二个线程就从内存中拿到该值,也进行了修改,这时就出现了线程安全问题。
重点是volatile只能保证一个线程在操作变量时其他线程要拿该变量只能从内存中拿,实际上的操作还是在缓存区。
17、创建线程有哪几种方法,有什么区别?
答:继承Thread、或者实现Runnable或者实现Callable接口。
Thread是通过继承的方式实现的,并且用this指针就可以代替当前线程,但是受限于java单继承的机制。
Runnable接口和Callable使用实现接口方式实现,两者的区别是Runnable实现的是run方法,只能只能独立的任务,不能返回值,而Callable实现的call方法,方法可以返回值,并且可以抛出异常。
18、说一下synchronized的原理?
答:在JDK1.6之后,synchronized还涉及到一个锁升级的过程,分别是无锁状态-偏向锁-轻量级锁-重量级锁。
线程当开启偏向级锁之后,当线程进入代码块之后,对象头会记录该线程ID,之后还是该线程重复进入的话就直接让其执行,适合于只有一个线程执行的情况。当有多个线程试图获得锁的时候就会升级为轻量级锁,通过CAS机制尝试自旋一段时间来尝试获得锁,适合于线程竞争小的情况下,当自旋失败就会升级为重量级锁,重量级锁通过监控对象头里面的monitor对象,当线程进入代码块,该线程就会获得该monitor对象的所有权,其他线程获取不到锁就会进入阻塞装填。
19、重量级锁怎么实现的?
答:在synchonized关键字锁住的对象头里会关联到一个monitor对象,当一个线程进入代码块,这个线程就会获得monitor对象的所有权,monitor对象里面的进入数就为1,当monitor对象里面的进入数为0时才会释放锁,被其他线程持有。
20、CAS是什么?存在什么问题?
答:CAS是比较并交换,CAS机制使用了三个操作数,内存地址,旧值,和新值,更新一个变量的时候先从内存中取出值放到旧值中,然后进行操作后放到新值中,在将新值写回内存之前判断内存中变量是否与旧值相同,如果相同就将新值写入。
ABA问题:现在有一个变量A,三个线程,线程1对读取了变量A,此时线程2也读取了变量A,通过CAS对变量+1,然后呢线程3又对变量进行了操作,将其-1,这时候返回线程1,通过比较旧值没变会将新值写入到内存中,但此时已经有了别的线程进行了操作。
21、synchronized和ReentrantLock的区别?**
答:

  • ReentrantLock显示的获得,释放锁,而Synchronized隐式的获得释放锁。
  • ReentrantLock可响应中断,用lockInterruptibly()方法就可以响应中断 如果当前线程收到其他线程的中断消息,synchronized则不可以,除非加锁的代码中出现异常。
  • ReentrantLock可以实现公平锁,公平锁会以他们请求的获得锁的顺序来获得锁,非公平锁在新来线程尝试CAS获取锁失败的情况下才会放到队列中;公平锁来了就放队列中。synchronized则不可以。
  • ReentrantLock可以通过Condition绑定条件
  • ReentrantLock发生异常如果没有unlock很可能出现死锁,所以一定要由finally模块进行对锁的释放。
  • 锁对象:ReentrantLock是根据进入线程的state标识来判断线程是否可入,是API层面的,而synchronized的锁在对象头里,是JVM层面的。**

23、synchronized和volatile的区别?
答:volatile只能作用在变量上,而synchronized可以作用在类,变量,方法和代码块上;
volatile保证了变量修改对线程的可见性;而synchronized保证变量修改的可见性和原子性;
volatile禁用指令重排序;而synchronized不会。
24、volvatile的原理?
答:根据缓存一致性协议,就是cpu写数据时发现操作的变量是共享变量,在其他cpu中也存在该变量的副本就会让其他cpu将缓存中的变量置为无效,需要再次去内存中去取。
25、线程顺序怎么控制?
答:使用join来控制,比如三个线程,线程调用join来阻塞其他线程。
22、乐观锁和悲观锁的区别?他们的应用场景是什么?
答:悲观锁在每次访问代码的时候都会给代码加上锁,总是认为自己在操作这块代码的时候别人会来访问。当其他线程访问的时候就会阻塞。
乐观锁不会加锁,总是认为自己在操作的时候别的线程不会访问,当需要更新数据的时候通过CAS判断一下在此期间别人有没有操作过这个数据。
悲观锁的应用场景是有多个线程会同时访问方法的时候。
乐观锁应用在不会有多个线程同时的访问方法的时候。
26、锁的分类?
答:

  • 乐观锁和悲观锁
  • 共享锁和独占锁
    • 独占锁是同一时刻只能有一个线程运行,属于悲观锁;
    • 共享锁在同一时刻可以有多个线程读,但是写操作只能同一时刻只有一个线程拥有。
  • 公平锁和非公平锁
    • 公平锁按照线程访问顺序获取锁
    • 非公平锁不按顺序来获得锁,synchronized是非公平的,lock可以设置为公平的。