part 01:Java线程
1、进程和线程的区别:
-
进程是操作系统进行资源分配的最小单位,线程是操作系统进行任务分配的最小单位,一个进程可以有多个线程。
2、Java开启线程的方式:
-
继承Tread类,重写run方法;
-
实现Runnable接口,实现run方法;
-
实现Callable接口,实现call方法;(FutureTask创建线程,获取线程返回值);
-
通过线程池开启线程(优点:提升线程池中线程的使用率,减少线程的创建、销毁所花的时间以及系统资源的开销;线程池可以控制线程数,有效提升服务器的使用资源)
![](https://i-blog.csdnimg.cn/blog_migrate/39dfa946971238383034e6c7c536ac60.png)
3、四种常用的线程池:
-
newCachedThreadPool:可缓存线程池,灵活回收空闲线程,若无可回收,则新建线程。没有核心线程,线程数量没有上线,默认消亡时间为60秒,阻塞队列是SynchronousQueue,没有存储性质的阻塞队列,取值操作和放入操作必须是互斥的,可以理解为每当有任务放入时立即有线程将它取出执行。
-
优点:提高了线程的复用率,当第二个任务开始,第一个任务已经结束,那么第二个任务会服用第一个任务创建的线程,并不会重新创建新的线程。
-
缺点:无法控制最多需要多少个线程同时处理,它会自动扩展线程数
![](https://i-blog.csdnimg.cn/blog_migrate/18a10b31951515c2d698b7a2173c50f3.png)
-
newFixedThreadPool:固定核心线程数线程池,可以控制线程的最大并发数量,使服务器达到最大的使用率,同时又可以保证流量突然增大也不会占用服务器过多的资源。
-
缺点:核心线程数量就是最大线程数量,所以线程池内的线程永远不会销毁,采用无参数的链表阻塞队列,存在任务积压导致内存溢出的风险。
![](https://i-blog.csdnimg.cn/blog_migrate/f83e50a984de3e3b98dec384c500c2a5.png)
-
newScheduledThreadPool:固定调度线程池。有固定的核心线程,线程的数量没有限制,支持定时以及周期性任务执行,可以延迟任务的执行时间,也可以设置一个周期性的时间让任务重复执行。两种延迟方法:
-
scheduleAtFixedRate:当前任务时间小于间隔时间,每次到点即执行;当前任务执行时间大于等于间隔时间,任务执行后立即执行下一次任务,相当于连续执行。
-
scheduledWithFixedDelay:每当上次任务执行完毕后,每隔一段时间执行。(可以做每天几点执行任务)
![](https://i-blog.csdnimg.cn/blog_migrate/6948c2dd2fe0b0153403620cbb718d85.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/c66baf052b9e1915ce7d7ba37404cb41.png)
-
newSingleThreadExecutor:单线程线程池,由始至终都由一个线程来执行。所有任务按照FIFO指定顺序执行
![](https://i-blog.csdnimg.cn/blog_migrate/c4ecdf39d4fcb6bdc8dde99dc458dc85.png)
4、线程池参数:
![](https://i-blog.csdnimg.cn/blog_migrate/732482420846d2961f0a4ccb59d411bf.jpeg)
-
corePoolSize:核心线程数;
在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;一般需要根据任务的类型来配置线程池大小:如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1如果是IO密集型任务,参考值可以设置为2*NCPU
-
maximumPoolSize:最大线程数,核心线程+线程队列;最大线程数 =(最大任务数-队列容量)/每个线程每秒处理能力
-
keepAliveTime:最大空闲时间,超过空闲时间不工作的线程会被销毁,默认用于非核心线程,通过设置 allowCoreThreadTimeOut(true)后,也会用于核心线程
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);
-
unit:最大空闲时间时间单位
TimeUnit.DAYS; //天TimeUnit.HOURS; //小时TimeUnit.MINUTES; //分钟TimeUnit.SECONDS; //秒TimeUnit.MILLISECONDS; //毫秒TimeUnit.MICROSECONDS; //微妙TimeUnit.NANOSECONDS; //纳秒
-
workQueue:等待执行的任务队列,如果核心线程没有空闲的,新来的任务会被放到这个等待队列中
-
SynchronousQueue:将任务直接交给线程而不保持它们。如果不存在可用于立即运行任务的线程,则试图将任务加入队列失败,会构造一个新的线程。要求maximumPoolSize,以避免拒绝新提交的任务。
-
无界队列:LinkedBlockingQueue,所有corePoolSize线程都在忙时新任务在队列中等待。适用场景:所有任务互不影响,用于处理瞬态突发请求。
-
有界队列:ArrayBlockingQueue,有助于防止资源耗尽,但是可能较难调整和控制。当ArrayBlockingQueue满时,则又会开启新的线程去执行,直到线程数量达到maximumPoolSize。
-
延时队列:DelayedWorkQueue,保证添加到队列中的任务,会按照任务的延时排序,延时时间少的任务首先被获取
-
-
threadFactory:用于实现生成线程的方式,定义线程名格式,是否后台执行等等
-
Spring框架提供的:CustomizableThreadFactory
-
ThreadFactory springThreadFactory = new CustomizableThreadFactory("springThread-pool-");
-
Google guava工具类提供的:ThreadFactoryBuilder
-
Apache commons-lang3 提供的:BasicThreadFactory
ThreadFactory basicThreadFactory = new BasicThreadFactory.Builder().namingPattern("basicThreadFactory-").build();
-
handler:当线程数达到最大线程数maximumPoolSize,再有新的任务到达时启动的策略。
-
ThreadPoolExecutor.AbortPolicy:直接抛出异常(默认)
-
ThreadPoolExecutor.DiscardPolicy:直接丢弃任务,但不抛出异常
-
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后将新加入的任务加入等待队列
-
ThreadPoolExecutor.CallerRunsPolicy:由线程池所在的线程处理该任务
-
5、线程工具类:
-
CountDownLatch:栅栏,一般用于某个线程等待若干个其他线程执行完任务之后,他才执行,不可重用
-
CylicBarrier: 回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行,一组线程互相等待至某个状态,然后这一组线程再同时执行,可重用
-
Semaphore:信号量
part 02:Java锁
1、synchronized和reentrantLock的区别:
-
synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。
-
Lock等待可中断,而synchronized只会让等待的线程一直等待下去。如果线程A正在执行锁中代码,线程B正在等待获取该锁。时间太长,线程B不想等了,可以让它中断自己。
-
synchronized是非公平锁,reentrantLock可以设置是否为公平锁,通过构造方法new ReentrantLock时传入值进行选择,true为公平锁,false为非公平锁
-
一个ReentrantLock可以绑定多个Condition对象,结合await()/singal()方法实现线程的精确唤醒,而synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机幻想一个线程要么唤醒全部线程
-
synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
-
通过Lock可以知道有没有成功获取到锁
大量线程同时竞争,ReentrantLock要远胜于synchronized。JDK5中,synchronized是性能低效的,因为这是一个重量级操作,对性能的最大影响是阻塞的实现,挂起线程和恢复线程的操作,都需要转入内核态中完成,给并发带来了很大压力。JDK6中synchronized加入了自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等一系列优化,官方也支持synchronized,提倡在synchronized能实现需求的前提下,优先考虑synchronized来进行同步。
2、锁升级过程:
-
偏向锁:一个线程多次获得同一个锁,不存在锁竞争。
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。(先看对象头中的threadID是否一致,不一致看持有锁的对象是否存活,存活看是否还持有锁,还持有锁就升级为轻量级锁)
-
轻量级锁:竞争锁对象的线程不多,而且线程持有锁的时间不长的情景,其他线程自旋等待锁。
线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。 自旋锁简单来说就是让线程2在循环中不断CAS但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
-
重量级锁:很多线程竞争锁,且锁持有时间长。
3、threadlocal
-
threadlocal主要作用是做数据隔离,填充的数据只属于当前线程,防止当前线程的变量被其他线程篡改
-
Spring采用threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,实现事务隔离级别(threadlocal+AOP实现)
![](https://i-blog.csdnimg.cn/blog_migrate/3551c56ba6841655279c88ba209bf94a.png)
![](https://i-blog.csdnimg.cn/blog_migrate/5cf4e6258962bc5a2c1ea8696174cb2a.png)
-
每个Thread都维护了自己的threadLocals变量
-
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值
-
ThreadLocal的key被设计成weakReference弱引用,在没有被外部强引用时,发生GC会被回收, 如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。怎么解决:在使用的最后remove把值清空