Android点2

一、编译时与运行时

编译时与运行时

二、java几种锁的区别

问题简答
这里我们就需要详细解析一下AQS与公平锁、非公平锁的概念了
公平锁与非公平锁
公平锁:
多个线程按照申请锁的顺序去获得锁。线程会直接进入到队列去排队,永远都是队列第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多。队列里面除了第一个线程,其它的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
简单来说,缺点比优点大,所以ReentrantLock默认实现是非公平锁。
非公平锁:
多个线程去获取所的时候,会直接尝试获取。获取不到再进入等待队列,如果能获取到就直接获取锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐率会比非公平锁高。CPU也不用唤醒所有线程,会减少唤起线程的数量。
缺点:可能导致队列中间的线程一直获取不到所或者长时间获取不到锁导致饿死。
我们从ReentrantLock入手:
当我们new一个ReentrantLock的时候,可以看到:

image.png

image.png

image.png

可以看到在ReentrantLock的构造方方法里会创建Sync对象,fair默认为false,sync实现为NonfairSync(),fair为true的时候,sync实现为FairSync()。
NonfairSync与FairSync都继承自Sync对象。

image.png

Sync是ReentrantLock中的一个抽象静态内部类,其继承自AbstractQueueSynchronizer.这个AbstractQueueSynchronizer实际上就是我们常说的AQS
我们先看一下NonfairSync和FairSync里公平锁与非公平锁如何实现的:
①NonfairSync
image.png

image.png

image.png

可以看到NonfairSync中并不会判断是否有阻塞队列的存在,而是直接调用UnSafe里的compareAndSwap(native层实现)也就是CAS去获取锁。
②FairSync

image.png

image.png

而FairSync会判断当前线程是否位于同步队列的首位,是返回true,如果通过CAS也能获取到锁,则当前线程拿到当前锁

公平锁与非公平锁 中锁的获取过程

我们再往里看看公平锁和非公平锁是如何给当前线程加锁的,先看一下非公平锁的加锁过程:
非公平锁获取锁过程

image.png

A线程准备获取锁,首先判断一下state状态,如果是0,CAS成功。将自己修改为持有锁的那个线程。
这个时候B线程也过来了,判断一下state状态,发现是1,那么CAS就失败了,只能去等待队列里等待唤醒。
A持有锁结束,准备释放掉锁。会修改state状态,抹掉持有锁线程的痕迹,准备去叫醒B。
也就是A线程调用了unLock方法:
image.png

image.png

image.png

这时候线程C进来了,发现state是0,果断CAS后修改state为1,将持有锁的线程修改为自己。
B线程被A唤醒准备去获取锁,结果发现state是1,CAS失败,直接继续去等待队列。
以上就是一个非公平锁中线程获取锁的过程。
公平锁获取锁过程
ReentrantLock构造参数中传true,改成公平锁,默认非公平锁。
①线程A想要获取锁,先判断一下state,发现是0;看了一下队列,自己是第一位,果断将锁持有线程改为自己。
②线程B过来了,判断一下state,是1,CAS失败,只能去排队。
③线程A释放锁之后,唤醒B,这时候线程C进来了,先判断一下state是0,以为有戏,但是发现自己不是等待队列中的第一位,作为良好市民,果断去排队了。
④线程B被A欢喜你后,去判断state,发现是0,且自己现在是队列中的第一位,那么获得了当前锁。
//TODO 需要将源码串联起来,根据代码梳理这些流程。

AQS

AQS详解
AQS详解二
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch...。
AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。
AbstractQueuedSynchronizer中对state的操作是原子的,且不能被继承。所有的同步机制的实现均依赖于对改变量的原子操作。为了实现不同的同步机制,我们需要创建一个非共有的(non-public internal)扩展了AQS类的内部辅助类来实现相应的同步逻辑。

AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。

三、线程池相关

Java线程池入门
线程池面试问题总结--很nice,必须看看
美团技术博客--线程池
系统中有四个自带的线程池,java建议使用这个几个来建议线程池,但是阿里巴巴禁止用四个Executors创建的线程池!(为什么禁止我们后面说,先简单说一下这个四个线程池)
1.newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2.newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程的最大容量。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3.newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60s不执行任务)的线程,当任务数增加时,此线程池又可以智能的增加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖操作系统(或者说JVM,哈哈一个字不差,全是照着敲的)能够创建的最大线程大小
4.newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。(定时任务,类似Timer和TimerTask)

前面说了阿里强制线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
①FixedThreadPool和SingleThreadPool(newSingleThreadExecutor):
允许的请求队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM.
②CachedThreadPool和ScheduledThreadPool:
运行创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

Demo:

image.png

image.png

创建一个死循环塞进线程池线程,最终会报OOM异常。
Java中的 BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。

ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。
这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。

newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

抛出异常的点为LinkedBlockingQueue.offer方法.我们先看一下这四个线程池都是怎么创建的:


image.png

image.png

image.png

image.png

可以看到这四个线程池底层都是ThreadPoolExecutors创建的。
重点就是ThreadPoolExecutors

ThreadPoolExecutors

ThreadPoolExecutors构造参数,有七个。如下:


image.png

image.png

①corePoolSize:线程池中核心线程的数量(看源码注释:一直在线程池中的线程个数)
②maximumPoolSize:线程池允许的最大的线程数
③keepAliveTime:非核心空闲线程最大存活时间,一个线程如果处于空闲状态,且当前线程超过核心线程数,那么在这个指定时间后,线程会被销毁。
④TimeUnit:时间单位
⑤workQueue:也就是我们常说的阻塞队列:BlockingQueue。jdk提供了四种工作队列:
1>ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经满了,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
2>LinkedBlockingQueue
基于链表的无界阻塞队列。(默认最大容量为Integer.MAX_VALUE,可指定size),按照FIFO排序。

双锁分离机制(核心亮点)
LinkedBlockingQueue 最关键的设计是读写分离锁,这是它与 ArrayBlockingQueue(单锁)的核心区别,也是面试高频考点:
两把独立锁:
takeLock:ReentrantLock,控制 “取元素” 操作(take/poll 等);
putLock:ReentrantLock,控制 “存元素” 操作(put/offer 等);

读写操作互斥性降低,生产和消费可同时进行(如一个线程 put,另一个线程 take),并发效率远高于单锁的 ArrayBlockingQueue。

由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存在该队列,而不会去创建新线程。直到maxPoolSize(Integer.MAX_VALUE)因此使用该工作队列时,参数maxPoolSize其实是不起作用的(而SingleThreadPool和FixedThreadPool默认用的就是LinkedBlockingQueue,所以可能会导致OOM!!)。

线程池为什么默认用 LinkedBlockingQueue?
标准答案:ThreadPoolExecutor 默认使用 LinkedBlockingQueue(无界)的核心原因:
避免任务拒绝:无界队列(默认容量极大)不会满,线程池达到核心线程数后,新任务会全部入队,不会触发拒绝策略;
并发效率高:双锁分离设计,线程池从队列取任务、向队列放任务的操作可并行,提升吞吐量;
适配核心线程池模型:线程池核心线程数固定后,无界队列可缓存大量任务,适合任务提交速率高于执行速率的场景。

3>SynchronousQueue
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务。如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略.
4>PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。(优先级!!这个好使!!)
⑥threadFactory线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等。
⑦handler拒绝策略
当工作队列中的任务已达到最大限制,并且线程池中的线程数量也达到最大限制。这时如果有新任务提交进来时,该如何处理呢。这里的拒绝策略,这时解决这个问题的,jdk中提供了4种拒绝策略:
1>CallerRunsPolicy
该策略下,当任务被拒绝时,由提交任务的线程(而非线程池线程)直接执行该任务。
2>AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutioinException异常。
3>DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
4>DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。

创建线程池的正确姿势

避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造参数来自己创建线程池,给BlockingQueu指定容量就可以了。比如我们创建一个核心线程池,也就是newFixedThreadPool:

image.png

这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.untl.concurrent.RejectdExecutionException.异常(Exeception)总比错误(Error)好。
说到错误和异常,我们下面简单说一下程序中捕获异常和错误的问题。

java exception和 error

java 中error可不可以捕获?答案是可以的。

image.png

image.png

可以看到error和exception都继承自Throwable,我们完全可以在catch语句中捕获Throwable,也就是error可以捕获
image.png

那为什么不该捕获Error呢?因为出现Error的情况会造成程序直接无法运行,所以捕获了也没有任何意义。
Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做!
如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。
所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误

Runnable/Callable/Future/FutureTask

Runnable/Callable:定义异步任务(做什么);
Future:定义任务控制接口(获取结果、取消任务);
FutureTask:具体实现类,同时实现 Runnable 和 Future,是连接任务与控制的桥梁。

①Runnable是一个接口,只有一个run方法,没有返回值,不能抛出异常。

image.png

②Callable
参考资料
Callable也是一个接口,只有一个call方法。和Runnable差别在于它有返回的结果,而且可以抛出异常!一般配合ThreadPoolExecutor使用。
image.png

Callable确实主要在线程池中使用,如下图,在ThreadPoolExecutor的父类AbstractExecutorService中有引用到Callable:
image.png

③Future也是一个接口,它可以对具体的Runnable或者Callable任务进行取消、判断任务是否已取消、查询任务是否完成、获取任务结果。如果Runnable的话返回的结果是null。(下面会剖析为什么Runnable的任务,Future还能返回结果)。接口里面有一以下几个方法。注意两个get方法都会阻塞当前调用get的线程,直到返回结果或者超时才会唤醒当前的线程。
image.png

使用demo:
image.png

④FutureTask
因为Future只是一个接口,所以无法创建使用,因此有了FutureTask.
image.png

image.png

FutureTask相当于继承了Runnable和Future。
因此它可以作为Runnable被线程执行,又可以有Future的那些操作。
Demo:
image.png

as we all know,线程池执行任务有两种方法,一是execute,而是submit。
如果们需要返回任务的执行结果就得调用submit方法而不是execute。
submit也不神秘,就是将任务封装成FutureTask再execute。
image.png

image.png

所以submit三个方法其实都是把task转成FutureTask,如果task是Callable,就直接赋值。如果是Runnable就转为Callable再赋值,只不过返回值是null。
image.png

image.png

image.png

此外,sumbit有个方法:
image.png

demo:
image.png

image.png

传入runnable以及result,可以拿到修改后的result。
简单总结:
①Callable可以获得任务结果和抛出异常;②Runnable没结果也无法抛出异常。
③Future可以很容易的获取异步执行的结果,并且对任务进行一些操控。并且get等待结果时会阻塞,所以当任务之间有依赖关系的时候,一个任务依赖另一个任务的结果,可以用Future的get来等待依赖的任务完成的结果。④FutureTask就是具体的实现类,有Runnable的特性又有Future的特性,内部包的是Callable,当然也有接受Runnable的构造器,只是会偷偷把Runnable转成Callable来实现能返回结果的方法。

线程池方法源码解析

参考资源一
参考资源二
①shutdown()

image.png

当线程池调用该方法时,线程池的状态通过CAS会变成SHUTDOWN状态。此时,不能再往线程池中添加任务,否则会抛出RejectedExecutionException。

image.png

image.png

有几个相关问题由此产生:
1>shutdown()有什么功能?
阻止新来的任务提交,对已经提交了的任务不会产生任何影响。会将那些闲置的线程idleWorks进行中断。
2>如何组织新来的任务提交?
通过线程池的状态改成SHUTDOWN,当再执行execute提交任务时,如果测试到状态不为RUNNING,则抛出RejectedExecutionException。如下图:

image.png

image.png

3>为什么对运行中的任务不产生任何影响?
在调用中断任务的方法时,tryTerminate方法会检测workers中的线程,如果没有中断,并且是空闲线程,才会去中断这个线程。
image.png

image.png

这里判断空闲线程的方法也很简单,就是看是否从ReentrantLock中获取到锁,如果获取到了,说明不是在运行中的线程,为什么呢?
因为线程在运行之前会调用w.tryLock,会先拿到锁执行任务;那么这个中断的地方tryLock就会失败,也就拿不到锁,自然也就中断不了运行中的线程了。
这也就是shutdown为什么关闭不了运行中的任务的原因。
②shutDownNow
image.png

从最上层的调用可以看到,与shutdown不同的是通过CAS将状态改成STOP,shutdown是改成了SHUTDOWN.在组织新来的任务提交的同时,会中断当前正在运行的线程,及workes中的线程。另外将workQueue中的任务给移除,并将这些任务添加到列表中。
image.png

image.png

image.png

可以看到在interruptWorkers方法中,会加锁判断所有的Worker是否已经运行且没有被中断,如果满足上述条件,则调用interrupt方法中断所有运行的线程。
image.png

image.png

接着调用drainQueue,将阻塞队列也就是workQueue(BlockingQueue)中的任务给移除掉,并将这些任务添加到列表中返回。
然后调用tryTerminate,将空闲线程也给中断掉,CAS判断后终止线程池。
image.png

相关问题:
①如何阻止新来的任务提交?
通过将线程池的状态改成STOP,当再将执行execute提交任务时,如果测试到状态不为RUNNING,则抛出rejectedExecution,从而达到阻止新任务提交的目的。
②如果我提交的任务代码块中,正在等待某个资源,而这个资源没到,但此时执行shutdownNow(),会出现什么情况?
当执行shutdownNow()方法时,如遇已经激活的任务,并且处于阻塞状态时,shutdownNow()会执行1次中断阻塞的操作,此时对应的线程报InterruptedException,如果后续还要等待某个资源,则按正常逻辑等待某个资源的到达。例如,一个线程正在sleep状态中,此时执行shutdownNow(),它向该线程发起interrupt()请求,而sleep()方法遇到有interrupt()请求时,会抛出InterruptedException(),并继续往下执行。在这里要提醒注意的是,在激活的任务中,如果有多个sleep(),该方法只会中断第一个sleep(),而后面的仍然按照正常的执行逻辑进行。

下面看看线程池的一些问题:
(1)线程池是什么,有什么好处?
线程池是一种基于池化思想管理线程的工具。 好处如下:
降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成损耗。
提高响应速度:任务到达时,无需等待线程创建即可立即执行
提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
提供更多更加强大的功能:线程池具备可扩展性,允许开发人员向其中增加更多的功能。比如延时暂定线程池ScheduledThreadPoolExecutor,就允许任务延迟执行或者定期执行。
(2)线程池解决的问题是什么?
线程池解决的核心问题就是资源管理问题。在并发环境下,系统不确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
1.频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大
2.对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险
3.系统无法合理管理内部的资源分布,会降低系统的稳定性
为了解决资源分配问题,线程池采用"池化"(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。
池化思想在计算机中还有其他的应用:
内存池:预先申请内存,提升申请内存速度,减少内存碎片。
②连接池:预先申请数据库连接,提升申请连接的速度,降低系统的开销
实例池:循环使用对象,减少资源在初始化和释放时的昂贵损耗。
(3)线程池的核心设计是怎样的?

image.png

①ThreadPoolExecutor顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程、如何调度线程来执行任务,用户只需提供Runnable对象,将任务的执行逻辑提交到执行器Executor中,由Executor框架完成线程的调配和任务的执行部分。
image.png

②ExecutorService接口增加了一些能力:扩充执行任务的的能力,补充可以为一个活一批任务生成Future的方法;提供了管控线程池的方法,比如停止线程池的运行(shutDown/shutDownNow/isShutDown)
image.png

③AbstractExecutorService则是上层的抽象类,将执行任务的流程串联起来,保证下层的实现只需关注一个执行任务的方法接口。实现了submit相关方法。
image.png

④最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
(4)线程池有哪些状态?
线程池有 5 种核心状态,基于原子整数 ctl(高 3 位存状态)管理,状态单向流转:
RUNNING:初始状态,接收新任务 + 处理队列任务;
SHUTDOWN:调用 shutdown() 触发,不接收新任务,但处理队列任务;
STOP:调用 shutdownNow() 触发(或 SHUTDOWN 状态下调用),不接收新任务、不处理队列任务,中断所有执行中线程;
TIDYING:过渡状态,SHUTDOWN 状态下 “队列空 + 线程数 0” 或 STOP 状态下 “线程数 0” 时进入;
TERMINATED:最终状态,TIDYING 状态执行 terminated() 钩子方法后进入。
(5)线程池里有个ctl,你知道它是如何设计的吗?
ctl是一个打包两个概念字段的AtomicInteger.
①workerCount:指示线程的有效数量
②runState:指示线程池的运行状态,有RUNNING/SHUTDOWN/STOP/TIDYING/TERMINATED状态
int类型占4字节有32位,其中ctl的低29位表示workerCount,高3位用于表示runState。
ctl为什么这么设计,有什么好处吗?
主要好处是将runState和workerCount的操作封装成一个原子操作。
runState和workerCount是线程池正常运转中的2个最重要的属性,线程池在某一时刻该做什么操作,取决于这2个属性的值。
无论是查询还是修改,我们必须保证对这2个属性的操作是属于"同一时刻"的,也就是原子操作,否则出现错乱的情况。如果使用2个变量分别存储,要保证原子性则需要额外进行加锁操作,这显然会带来额外的开销,而将这2个变量封装成1个AtomicInteger则不会带来额外的加锁开销,而且只需要使用简单的位操作就能分别得到runState和workerCount。
通过ctl得到runState,只需要通过位操作:ctl & ~CAPACITY。
通过 ctl 得到 workerCount 则更简单了,只需通过位操作:c & CAPACITY。
(6)任务调度流程
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
这些问题多摘自上面美团技术博客和知乎专栏,更多的问题参考上面链接。

五、ThreadLocal

ThreadLocal 使用与原理

线程局部变量。ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
应用场景:
当很多线程需要多次使用同一个对象,并且需要该对象具有相同初始化值的时候最适合使用ThreadLocal。

ThreadLocal和synchronized的异同
①进行同步控制synchronized 效率降低 并发变同步(串行)
②使用ThreadLocal 本地线程 每个线程一个变量副本(各不相干,提升并发效率)
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal原理解析
①每个Thread对象内部都维护了一个ThreadLocal的Map:ThreadLocalMap,这个map可以存放多个ThreadLocal。

image.png

②当我们调用get()方法时,先获取当前线程,然后获取当前线程的ThreadLocalMap对象。如果非空就取出ThreadLocal的value,否则进行初始化,初始化就是将initialValue的值set到ThreadLocal中。
image.png

③当我们调用set()方法时,就是拿到当前的线程对应的ThreadLocal,不为空,赋值。为空,创建ThreadLocalMap绑定Thread并赋值。
ThreadLocalMap是以弱引用的ThreadLocal为key的,不是Thread!
image.png

⑤总结:当我们调用get方法的时候,其实每个当前线程中都有一个ThreadLocal。每次获取或者设置都是对该ThreadLocal进行的操作,是与其他线程分开的

ThreadLocal内存泄漏
ThreaLocal内存泄漏
threadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确用法
①每次使用完ThreadLocal都调用它的remove()方法清除数据
②将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。【没get到。。。】

package com.dawn.zgstep.threads.objects;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadLocal {

    private static int count = 0;
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private void testThreadLocal() {
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 20; i++) {
            int finalI = i;
            executorService.submit(() -> {
                threadLocal.set(Thread.currentThread().getName());
                 soutLocal();
            });
        }

    }
    private void soutLocal(){
        System.out.println(threadLocal.get());
    }



    public static void main(String[] args) {
        TestThreadLocal testThreadLocal = new TestThreadLocal();
        testThreadLocal.testThreadLocal();
    }
}


//结果:
pool-1-thread-1
pool-1-thread-3
pool-1-thread-5
pool-1-thread-2
pool-1-thread-4
pool-1-thread-6
pool-1-thread-7
pool-1-thread-8
pool-1-thread-9
pool-1-thread-10
pool-1-thread-11
pool-1-thread-12
pool-1-thread-13
pool-1-thread-14
pool-1-thread-15
pool-1-thread-16
pool-1-thread-17
pool-1-thread-18
pool-1-thread-19
pool-1-thread-20

总结

①每个线程都有一个ThreadLocalMap 类型的 threadLocals 属性。
②ThreadLocalMap 类相当于一个Map,key 是 ThreadLocal 本身,value 就是我们的值。
③当我们通过 threadLocal.set(new Integer(123)); ,我们就会在这个线程中的 threadLocals 属性中放入一个键值对,key 是这个threadLocal.set(new Integer(123)) 的 threadlocal,value就是值new Integer(123)。
④当我们通过 threadlocal.get() 方法的时候,首先会根据这个线程得到这个线程的 threadLocals 属性,然后由于这个属性放的是键值对,我们就可以根据键 threadlocal 拿到值。 注意,这时候这个键 threadlocal 和 我们 set 方法的时候的那个键 threadlocal 是一样的,所以我们能够拿到相同的值。
⑤ThreadLocalMap 的get/set/remove方法跟HashMap的内部实现都基本一样,通过 "key.threadLocalHashCode & (table.length - 1)" 运算式计算得到我们想要找的索引位置,如果该索引位置的键值对不是我们要找的,则通过nextIndex方法计算下一个索引位置,直到找到目标键值对或者为空。
⑥hash冲突:在HashMap中相同索引位置的元素以链表形式保存在同一个索引位置;而在ThreadLocalMap中,没有使用链表的数据结构,而是将(当前的索引位置+1)对length取模的结果作为相同索引元素的位置:源码中的nextIndex方法,可以表达成如下公式:如果i为当前索引位置,则下一个索引位置 = (i + 1 < len) ? i + 1 : 0。

六、网络相关

必看网络面试1
这个挺详细,必须得看
网络相关查漏补缺
比如状态码等。
下面一些问题是上面提的不全面的,这里做一下简单总结

1.Http1.0、Http1.1、Http2.0区别

参考资料
HTTP1.0最早在网页中使用是在1996年,那个时候只是使用一些较为简单的网页上和网络请求上,而HTTP1.1则在1999年才开始广泛应用于现在的各大浏览器网络请求中,同时HTTP1.1也是当前使用最为广泛的HTTP协议。
主要区别:
①带宽优化及网络连接的使用:
Http1.1在请求头引入了range头域,允许只请求资源的某个部分,返回码是206.这是我们断点续传的基础!
新增了24个错误状态响应码。比如409表示请求的资源与资源的当前状态发生冲突;410表示服务器上的某个资源被永久删除。
③ Host头处理;Http1.0中认为每台服务器都绑定一个唯一的ip。但随后的虚拟主机的发展,多个虚拟主机共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误
长连接。Http1.1支持长连接和请求的流水线处理。在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。在Http1.1中默认开启Connection:keep-alive选项,一定程度上弥补了Http1.0每次请求都要创建连接的缺点。

HTTP2.0和HTTP1.X相比的新特性:
新的二进制格式:HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮,不同于HTTP1.x的解析是基于文本
多路复用:连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request。
服务端推送:服务器主动向客户端推送消息

2.HTTPS与HTTP的一些区别

HTTPS协议需要到CA申请证书,一般免费证书很少,需要交费。
HTTP协议运行在TCP之上,所有传输的内容都是明文,HTTPS运行在SSL/TLS之上,SSL/TLS运行在TCP之上,所有传输的内容都经过加密的。
HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
HTTPS可以有效的防止运营商劫持,解决了防劫持的一个大问题。

3.TCP和UDP

TCP传输控制协议:面向连接;使用全双工的可靠信道;提供可靠的服务,即无差错、不丢失、不重复且按序到达;拥塞控制、流量控制、超时重发、丢弃重复数据等等可靠性检测手段;面向字节流;每条TCP连接只能是点到点的;用于传输可靠性要求高的数据
UDP用户数据报协议:无连接;使用不可靠信道;尽最大努力交付,即不保证可靠交付;无拥塞控制等;面向报文;支持一对一、一对多、多对一和多对多的交互通信;用于传输可靠性要求不高的数据

TCP的三次握手、四次挥手

参考博客
TCP提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用4次挥手来关闭一个连接。
三次握手
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_SEND 状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。

第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。

第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

问:为什么需要三次,两次不行吗?
假如现在客户端想向服务端进行握手,它发送了第一个连接的请求报文,但是由于网络信号差或者服务器负载过多,这个请求没有立即到达服务端,而是在某个网络节点中长时间的滞留了,以至于滞留到客户端连接释放以后的某个时间点才到达服务端,那么这就是一个失效的报文,但是服务端接收到这个失效的请求报文后,就误认为客户端又发了一次连接请求,服务端就会想向客户端发出确认的报文,表示同意建立连接。
假如不采用三次握手,那么只要服务端发出确认,表示新的建立就连接了。但是现在客户端并没有发出建立连接的请求,其实这个请求是失效的请求,一切都是服务端在自相情愿,因此客户端是不会理睬服务端的确认信息,也不会向服务端发送确认的请求,但是服务器却认为新的连接已经建立起来了,并一直等待客户端发来数据,这样的情况下,服务端的很多资源就没白白浪费掉了
四次握手
为什么建立连接是三次,断掉连接是四次呢?
我想可能是因为相见时难别亦难吧。
刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求:

第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。(中间多了这一步,所以是四次)
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容