8.多线程
1.多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么?
多线程有两种实现方法:
1.通过继承Thread类,重写Thread的run方法,将线程运行的逻辑放在其中(Thread类也实现了runnable接口)
2.通过实现runnable接口
3.多线程实现的第四种方式是实现 Callable 接口,通常需要对需要获取线程处理结果的时候使用
同步的实现方面有两种,分别是synchronized,wait与notify
2.请说出你所知道的线程同步的方法。
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
3.sleep() 和 wait() 有什么区别?
sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
4.同步和异步有何异同,在什么情况下分别使用他们?举例说明。
如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
5.线程的基本概念、线程的基本状态以及状态之间的关系
线程指在程序执行过程中,能够执行程序代码的一个执行单位,每个程序至少都有一个线程,也就是程序本身。
Java中的线程有五种状态分别是:创建、运行、就绪、挂起、结束
6.简述synchronized和java.util.concurrent.locks.Lock的异同 ?
主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。
7.线程池的了解
1、线程池的作用:在没用使用线程池的情况下,开发人员通常自己来开发管理线程,很容易将线程开启过多或者过少;过多将造成系统拥堵,过少又浪费系统资源。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
2、由于手动维护线程成本很高(大家都知道多线程的开发难度大,对程序员要求高,而且代码不易调试维护),所以我们应尽可能用 JDK 提供的比较成熟的线程池技术,它减少了创建和销毁线程的次数,每个工作线程都可以被重复利用, 可执行多个任务。可以根据系统的承受能力,调整线程池中工作线线程的数目, 以达到系统性能最优。
3、线程池的父接口是 Executor,它只定义了一个 Execute 方法,而通过使用的是它的扩展接口 ExecutorService,它有几个常用的实现类 AbstractExecutorService, ScheduledThreadPoolExecutor, ThreadPoolExecutor(详见 API)。而在实际开发中,通过使用的是一个 java.util.concurrent 中的一个工具类 Executors,里面提供了一些静态工厂,生成常用的线程池(下面这些作为了解,说出一两个就可以了)。
newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
4.线程池实现原理:要说清楚线程池的工作原理,就要先了解一下线程池的一些参数设置;
corePoolSize:核心线程数,就是线程池在空闲的时候,也会一直保持的线程数。
maximumPoolSize:最大线程数,但任务队列满的时候,线程数大于 corePoolSize 而小于maximumPoolSize,会创建线程,直到线程数等于 maximumPoolSize。
keepAliveTime:闲置时,线程存活的时间。
workQueue: 任 务 队 列 。
work: 工 作 线 程 。
workers: 工 作 线 程 集 合 。
RejectedExecutionHandler:运行策略(默认是抛异常)。
线程池的原理:
1、在线池程启动时,线程中并没有线程,当进行调用 execute 方法时, 会进行线程的初始化。
2、会首先判断当前线程数是不是小于 corePoolSize,如果是小于, 则会直接创建线程,并且执行任务。
3、如果当前执行线程数等 corePoolSize,此时新增加的任务会被放到工作队列 workQueue 中去。
4、当 workQueue 队列任务满时,会判断当 前线程是否小于 maximumPoolSize,如果小于就会继续增加线程到 maximumPoolSize 数。
5、如果当前线程数等于 maximumPoolSize,而且任务队列也满时,将会调用一些策略处理,比如抛异常 RejectExecutionException。当工作队列中的任务结束后,而当前线程数量大于 corePoolSize 数时,若闲置时间超过 keepAliveTime 时间,线程将会关闭至 corePoolSize。
8.死锁的原因
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他 的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:T1 和 T2 互相等释放,这样就形成了一个等待环。
9.进程和线程的区别
定义:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
特点:
1、一个进程可以拥有很多个线程,但每个线程只属于一个进程。
2、线程相对进程而言,划分尺度更小,并发性能更高。
3、进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高 了程序的运行效率。
4、线程必须依赖应用,在应用中调度,每个线程必须执行的入口、出口、执行序列, 线程是不能够独立存在运行的。
5、进程是资源分配的基本单位,线程是处理机调度的基本单位,所有的线程共享其 所属进程的所有资源与代码。
6、多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统 并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。
10.线程常用的并发类及关键字
ReentrantLock: 类位于 JDK 的并发包下,JDK5 后引入的(jdk5 之前的sysnchronized 性能很差的,在版本 5 后做了很大的优化),用法语义和 sysnchronized 都很像,需要手动的在 finally 进行释放锁,这块详细可以参考版本一中的 4.8 节。
Volatile:并发类中的一个关键字,主要用来保证各个线程间的可见性(并不保证原
子性);当定义一个全局变量,它是在主存中的,各个线程读取的只是它的副本(可以认为是缓存),当某个线程修改副本时,其它线程不可见,就会导致数据的不一致性;Volatile 修饰变量后,当各个线程试图修改副本中的数据时,会先确定和主存中的数据是一致的, 同时修改后会刷新到主存中,这样其它线程就可见了。
Atomics:这是一个包 java.util.concurrent.atomic,该包下主要提供了一下无锁的原子
类操作,有点像 volatile 的作用。包里一共有 12 个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic 包里的类基本都是使用 Unsafe 实现的包装类。
CountDownLatch:是一个同步工具类,它允许一个或多个线程一直等待,直到其他
线程的操作执行完后再执行。是一个倒计数的锁存器,当计数减至 0 时触发特定的事件, 构造时传入 int 参数,该参数就是计数器的初始值,每调用一次 countDown()方法,计数器减 1,计数器大于 0 时,await()方法会阻塞程序继续执行。
CyclicBarrier:是一个同步工具类,允许一组线程互相等待,直到到达某个公共屏障
点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。有点像 CountDownLatch,可以用于一个主任务多个子任务的应用场景,主任务等所有的子任务完成后再执行。
Semaphore : 用来控制同时访问特定资源的线程数量,它通过 acquire() 和
release()来维护一个许可证集,它会阻塞 acquire()获取许可证之前的线程,只有
release()释放后,才会允许新的线程进入(。举个例子,排队买火车票,保安Semaphore 每次只会放进去(给通行症)十个人排队,每买好票走一个人(释放通行症),保安就再放进去一个;现在大家都网上买票了,不知道能否理解。)
FutureTask:它实现了 Runnable, Future, Runnable 一个是线程的标志,说明
FutureTask 可以作为一个线程实现当作线程执行;Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果,可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果,所以又可以作为 Future 得到
Callable 的返回值。
11.ThreadLocal的基本原理
ThreadLocal 并非是线程的本地实现,而是线程的本地变量,它归附于具体的线程,为每个使用该变量的线程提供一个副本,每个线程都可以独立的操作这个副 本,在外面看来,貌似每个线程都有一份变量。
线程的变量存在哪里,这里可以结果 ThreadLocal 的源码说明,这里看一下 get 实现
public T get(){
Thread t=Thread.currentThread();
ThreadLocalMap map=getMap(t);
if(map!=null){
ThreadLocalMap.Entry e=map.getEntry(this);
if(e!=null)
return (T) e.value;
}
return setInitialValue();
}
在 get 方法中,会先获得当前线程对象,然后传到 getMap()中获取 ThreadLocalMap
对象,我们要的变量副本是从 ThreadLocalMap 对象中取出来的,可见每个线程的变量副本是保存在 ThreadLocalMap 对象里,而跟一下代码可以看到 ThreadLocalMap 是在 Thread
中声明实现的,所以每个线程的变量副本就保存在相应线程的 ThreadLocalMap 对象中。
第二个问题,可以理解 ThreadLocal 如何把变量的副本复制并且初始化的(声明和初始化),这里看一下源码中的 set 方法实现
public void set(T value){
Thread t=Thread.currentThread();
ThreadLocalMap map=getMap(t);
if(map!=null)
map.set(this,value);
else
createMap(t,value);
}
当第一次调用 set 方法时,获取的 ThreadLocalMap 对象为空,这里会调用 createMap 方法创建一个 ThreadLocalMap 对象,并且完成相应的初始,将 value 值存放进去。后面再次调用将会直接从线程中获取 ThreadLocalMap 对象,然后将副本保存进去。
12.Lock 锁的实现
Lock 是一种轻量级锁,它相比 synchronized 定义更细致的锁操作,它定义了lock,lockInterruptibly,trylock,tryLock(long,timeUnit) 四 个 获 取 锁 的 操 作 , 提供更 轻量的,tryLock(long,timeUnit)定时锁,lockInterruptibly()阻断锁,在调用 interrupt 方法时,可以阻断当前线程的等待,当使用锁时必须调用 unlock()方法进行释放。它有一个直接实现类ReentrantLock (可 重 入 锁 ), 和 一 个 间 接 实 现 类 ReentrantReadWriteLock , ReentrantReadWriteLock 中 的内 部类 读锁和 写锁 是实 现了 Lock 的。 这 里说一 下ReentrantLock 的实现原理,它有一个内部类 sync,这个类实现了 AQS 接口 acquire、tryAcquire 获取锁,release,tryRelease 释放锁;而 AQS 内部则是通过维护一个线程队列和一个 int 型 volatile 原子变量 state 来实现线程调试对锁的获取和释放。