本文很长很长,对原理深入至内存层面,以顺序结构讲述集合框架的设计故事,请耐心阅读顺序阅读 或 挑选疑惑点阅读。
目录结构太大,导致点击后索引到内容末尾,需要查看上滑或下滑即可。
目录
一切的缘起是昂贵的CPU
什么是并发?
什么是多线程?
Java中的多线程情况是怎么样的?
Java并发机制中的原子性、有序性、可见性
万能的synchronized关键字?
Monitor对象
Java对象头
synchronized+monitor+对象头 的配合
Synchronized JDK1.6后的优化
备受争议的volatile关键字?
缓存一致性协议
volatile为什么不能取代synchronized?
synchronized与Lock的区别
Sleep()与Wait()的区别
如何实现多线程
创建线程的3种方式
线程池的原理
ThreadPoolExecutor 线程池执行器
ScheduledThreadPoolExecutor 计划性线程池执行器
如何选择合适的线程池?
强大的 ConcurrentHashMap 其实并不难
多样的锁与应用场景
数据读写层面的锁
悲观锁
乐观锁
对象层面的锁
无锁
偏向锁
轻量级锁
重量级锁
多线程与并发优化
我们都十分清楚,计算机的核心是计算,而负责这个功能的组件就是CPU。
CPU有一个特性,在一个时刻只能处理一个程序。
开发人员编写代码,代码被编译为机器语言,CPU收到机器语言(指令集),开始处理程序,而这个正在被CPU处理的程序就是进程(正在进行的程序)。
当CPU正在处理一个程序时,由于其特性,其他程序就只能等待。
你可能会想,一个接一个处理,不是很合理的设计吗?
这仅仅对于CPU执行指令而言,的确如此。可是,数据在存储媒介上的I/O速度与CPU的速度相比,是十分缓慢的,这就导致了CPU经常处于被一个进程占用,但很闲的情况。
针对这种浪费造价昂贵的CPU的情况,就出现了中断处理机制。
中断处理机制 简而言之就是CPU将自己的资源分划为一个一个时间片,根据一定策略分发给待处理的程序集合,[time1,time2]执行程序1(执行了n%,停顿等待下一个时间片)、(time2,time3]一会执行程序2(执行了n%,停顿等待下一个时间片).....因为CPU超快的执行速度,人类观察到的程序似乎没有停顿。放佛在同时运行一般。而CPU则一刻不停地运算着。
如果我们以1帧(1/60秒)为单位去观察程序,发现程序1、程序2、程序3都同时启动,同时被执行完毕,一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行的现象,我们称之为程序的并发运行。
为了提高CPU的资源利用率,我们对CPU的资源抽象为时间,利用分配时间片段实现程序间的并发行为。然而,CPU对于程序之间的切换(上下文切换)的代价还是比较高的。为了缩小提高CPU资源利用率的代价,有一个很简单的做法就是:不进行程序之间上下文切换了。
显然这种做法又回到了原点,CPU又处于I/O过程中的漫长等待。既然单单划分CPU的时间不足以,为什么不把进程(正在进行的程序)也划分成一个一个单位呢?这样CPU在处理进程时,优先在进程中的单位片段间切换,最小可能的避免程序的上下文切换,既提高了CPU的资源利用率,还最小化了程序间上下文切换的代价。于是,我们把进程划分成的单位称之为:线程。
线程以并发的机制被CPU处理,使得一个进程整体看起来执行的特别快,这样的机制称之为:多线程。当然,CPU一个时刻也只能将1个时间片分给1个线程,所以,当CPU处理当前线程时,其他线程要么等待、要么睡觉(sleep),调度这些工作的就是操作系统。
我们都知道Java的启动类方法psvm(public static void main),实际上这就是一段简单的程序,启动Java项目,会先执行这个程序,psvm便作为进程被CPU处理。由于Java引入了多线程机制,进程会默认分割为1个单位的线程(其实就是没开其他线程的意思),这个线程称之为 主线程。
CPU执行时,载入psvm的上下文后,便执行进程中的线程。
基于以上设计,Java中的并发机制有3个特性:原子性、有序性、可见性。
原子性
简言之,原子性就是指令集合(一行或多行程序)被CPU执行的整个阶段,不会被中断的特性。
举个例子(场景默认为多线程),
count++;
这行程序,在操作系统层面是一个指令集,有3个指令 :
指令 1:首先,需要把变量 count 从内存加载到 CPU的寄存器
指令 2:之后,在寄存器中执行 +1 操作;
指令 3:最后,将结果写入内存
操作系统的任务切换(线程切换)是以指令为单位的,因此此程序在指令2存在着被中断的风险,因此不是原子性的。
再看一个原子性的例子:
count = 0;
这行程序,在操作系统层面是一个指令集,有1个指令:
指令 1:将0写入索引指向的内存地址;
此程序在指令集执行期间不存在被中断的风险,因此是原子性的。
实际上,原子性是一个相对性质。
上述的第一个非原子性案例,当场景为单线程时,实际上也属于原子性操作。
原子性也与观察者的观察粒度有关。
{
count = 0;
count++;
}
以整个程序看,在操作系统层面是一个指令集,有4个指令,存在被中断的风险,因此不少原子性的,即使单独的".count = 0 "是原子性的。
有序性
有序性指的是程序按照代码的先后顺序执行(即指令集先后顺序执行)。
为了性能优化,编译器和处理器会进行指令重排序,有时候会改变程序中语句的先后顺序,如果读者有好奇看过.class文件,就会发现,很多时候操作都变化了,这是JDK提供的性能优化编译。(如果想提高代码的编译速度,不妨多尝试编译后的写法)
当然,这种优化并不会预见多线程情况下的原子性问题。
//Main.java
public static void main(String[] args) {
int a,b;
a=b=1;
b=2;
a=a+1;
}
//Main.class
public static void main(String[] args) {
int b = true;
int a = 1;
b = true;
int a = a + 1;
}
此时可以看到编译优化后的顺序已经发生变化。
重排序可以提高程序执行的性能,但是代码的执行顺序改变,可能会导致多线程程序出现可见性问题和有序性问题。
存在数据依赖关系的两个操作,不可以重排序。
保证有序性的方法
代码中的关键字(如:synchronized、volatile),为内存加一层屏障,使得线程被执行时,保证不会被中断。
可见性
可见性指的是当一个线程修改了共享变量后,其他线程能够立即得知这个修改。
首先我们来看一下Java内存模型设计(JMM Java Memory Model):
程序中被定义的变量都缓存在主内存中。每个线程都有自己的“工作区域”相互并不干扰,保证线程使用到的变量都是拷贝自主内存的数据。因此,线程是无法直接读取主内存的数据。
那么线程如何确保不会“脏读”?
一种解决方法就是,当前被执行的线程为进程加排他锁,其他线程处于等待或睡眠状态,等当前线程执行完毕,将副本变量刷新回主内存,这样可以确保下一个线程读取的内存拷贝是正确的。可以使用synchronized、volatile关键字确保以上的实行。
综上所述,为了确保线程的并发运行,需要同时确保程序的原子性、有序性和可见性。往往都可以通过关键字 synchronized与volatile来实现,那么synchronized、volatile到底是什么呢?
通过上述的描述,我们可以总结出,并发会带来的问题:程序间对临界资源的争夺。
而基于并发的多线程模式,同样存在着临界资源争夺问题:线程间的共享变量
对待临界资源问题,有一个很直接且有效的方案:在某一时刻,CPU一定是只处理一个线程,那当然可以让线程完完整整的执行下去,线程完整执行下去的时间段之类观察CPU的处理模式,这不就看起来和单线程一摸一样,当然就不会有并发问题。
上述的这种方案称作 排他锁。这种“锁”会使得其他线程等待当前线程完整执行完毕后去申请CPU的时间片。
Java中提供了synchronized关键字,其作用与排他锁十分相似,我们下面就探究其相似的原因。
Synchronized底层原理
Java 虚拟机中的同步(Synchronization)行为基于进入和退出管程(Monitor)对象(iow.监视器类)实现。
那就好好研究一下Monitor对象。
在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,ObjectMonitor中有两个队列,_WaitSet (等待集合)和 _EntryList(待入场序列)。
当多个线程同时访问一段同步代码时,会先都进入 EntryList(待入场序列):
此后会有两种情况:1.线程完整的被执行完毕
2.在执行完毕前,被下达等待wait()指令
以上就是ObjectMonitor类对线程进行管理的方式,该类定义了wait(),notify(),notifyAll() 方法用于线程控制,并由Object与其关联。这也解释了为什么Object中有线程控制的方法,以及为什么所有的Java类都可以进行同步管理(所有类继承Object类,因此继承并获得自己的monitor)。
那么对象的Monitor管理着的Java对象头到底是什么样子的?
对象的实例缓存在堆内存中被管理,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。Java对象头结构如下:
可以看到锁信息就保存在MarkWord中,锁信息包括:
其中偏向锁、轻量级锁是JDK1.6引入的概念。我们重点关注重量级锁。
其中重量级锁就是synchronized关键字添加后给对象附加的对象锁。即,我们为对象中的代码段添加synchronized关键字后,在类初始化后,将对象头中的锁状态更新为:10 重量级锁:
这样我们便得到了一个synchronized+monitor+对象头的关系。
当我们编码一个类,并将其一个方法标为synchronized。
启动JVM,类加载完成,进程中实例化该类,在JVM堆内存中生成一个该类的实例,识别出synchronized符号,将对象头中的锁状态更新为重量级锁(10)。
此时Thread1,Thread2都访问了该对象实例,对象的Monitor监视Thread1、Thread2,Thread1、Thread2进入待入场序列(EntryList),由于Thread1在前,Monitor先让Thread1获取对象的锁(将Thread1的线程id写入对象头中),Thread1被执行。Thread1执行过程中,Monitor由于重量级锁协定,始终控制EntryList中的线程等待入场,并不打断Thread1。直到Thread1被执行完毕,Monitor将对象的锁移交给Thread2(将Thread2的线程id写入对象头中)。
小结:synchronized关键字通过以上过程,主动构造了“临时单线程”场景,确保了原子性与有序性。通过Monitor的EntryList与WaitSet有序等待机制,确保了当前线程的修改副本可以在其他线程访问前刷新回主内存,由此确保了可见性。
在上述“synchronized+monitor+对象头 的配合”过程中,默认使用了重量级锁,实际上JDK1.6后对synchronized进行了性能优化:
至此,结束了对Synchronized的介绍,有Synchronized的保驾护航,我们便可以安心的开始多线程的解析了。
在 Java 5 之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在 Java 5之后,volatile 关键字才得以重获生机。
volatile(易变)是Java中的关键字。其作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
换言之,volatile修饰的代码具有两个特征:
可见性与有序性上文有详细的解释,注意没有保证原子性。
没有保证原子性会导致什么问题?
Java中long和double类型变量占8字节,如果变量非volatile熟悉,JMM在读写这两种变量时,时分两次进行的,每次读写4字节。在多线程环境下,如果多个线程同时操作一个long或double类型变量时,如果该变量有可能在32位系统多线程环境下使用,且没有其他同步机制,应增加volatile属性。否则会导致:线程A读取变量的前4个字节来自线程B的写入,后4个字节来自线程C的写入。
在了解了JMM模型的基础上,我们不难怀疑,如果一个变量在多个CPU中都存在缓存,那么就可能存在缓存不一致的问题。
上述问题synchronized给出的解决方案是总线(主内存)加锁。
volatile提供了另一种解决方案:缓存一致性协议。
起因就是因为总是给主内存加锁,在加锁的过程中如果频繁的发生I/O(数据读写),就又会出现CPU等待的问题,显得效率低下,于是缓存一致性协议应运而生,试图解决这个问题。
提到缓存一致性协议,最出名的就是Intel 的 MESI 协议。
MESI 协议保证了每个缓存中使用的共享变量的副本是一致的。
它的原理并不难,就是经典的观察者模式(当对象出现修改时,会自动通知依赖它的所有对象)。
当线程对变量进行修改后,主存上的缓存一致性协议机制会判断是否为共享变量,如果是共享变量,则会向其他共享着这个变量的线程的副本CPU发出信号,通知其这个变量已失效,如果使用到需要重新从主存获取刷新。
如果说synchronized是防止多个线程并发的访问一块主存资源,浪费CPU资源。
那volatile就是允许多个线程并发的访问一块主存资源(非直接),提高了CPU资源利用率,但是牺牲了并发的原子性保证,同时MESI协议需要一定的资源消耗,只能保证读操作的线程安全性。
因此结论就是volatile不等同于synchronized,也就无法取代。
如何选取策略在于 MESI协议需要一定的资源消耗 vs. CPU等待的资源消耗
一般认为,指令集中的指令确保原子性的前提下,volatile的效率更高。
如果说synchronized是对象锁,那么volatile就是分段锁,从解耦而言,volatile更灵敏。
volatile的应用在ConcurrentHashMap类中大显神通,本文下文将会详细介绍ConcurrentHashMap。
首先,synchronized是Java的关键字,在jvm层面上的。
而Lock是Java提供的一个接口类,提供了代码层面的同步方法和锁操作,更加灵活。
synchronized以获取锁的线程执行完同步代码,释放锁,如果发生异常,jvm会让线程释放锁。
Lock必须主动在finally中必须释放锁,不然容易造成线程死锁。
sleep()是Thread类的方法,Wait是JVM层面Monitor为Object提供的方法。
最核心的点在于:sleep()是线程等待,时间过后会尝试获取对象锁。
wait()是线程挂起,在notify()之前都不会尝试申请CPU资源去获取对象锁。
进入Thread类的resource,注释里有标准的介绍:
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
//The following code would then create a thread and start it running:
PrimeThread p = new PrimeThread(143);
p.start();
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
//The following code would then create a thread and start it running:
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
还有一种Thread注释没有介绍的:
public class PrimeCall implements Callable {
@Override
public Object call() throws Exception {
//...
return null;
}
}
//启动方式
PrimeCall t = new PrimeCall();
FutureTask futureTask = new FutureTask(t);
new Thread(futureTask).start();
启动线程:
PrimeThread t1 = new PrimeThread();
PrimeRun t2 = new PrimeRun();
PrimeCall t3 = new PrimeCall();
//注意,线程启动都是通过start()启动,run()方法会委托给内部启动
t1.start();
t2.start();
//FutureTask可包装Callable或Runnable对象,FutureTask+Thread获得线程的计算结果
FutureTask futureTask = new FutureTask(t3);
new Thread(futureTask).start();
上面三种方式更推荐通过实现 Runnable接口和实现 Callable接口,因为面向接口编程拓展性更好,而且可以防止 java 单继承的限制。Runnable接口是没有返回值的,如果需要返回值使用Callable接口。
以上是手动创建线程的过程,可以发现启动并不是直接调用run()方法,而是start(),这其中的原理是这样的:
是可以执行到run()方法中的程序,但是,线程并没有被激活,也就是说,创建了n个线程,如果没有委托激活,那么整个进程还是单线程的,调用run()方法的还是main主线程。
我们都知道,创建线程后,调用start()方法,放佛就可以执行到run()中的程序,显然是存在着某种代理模式。
这是因为线程被激活后开始一系列的自动的工作:当前线程执行start()方法,将待激活线程对象加入group中,并执行start0内部方法,这个方法由C++编写,大致为:Java虚拟机调用这个线程的run方法使该线程开始执行。
综上所述,我们可以得到1个线程的生命周期:
其中,除了运行由this线程负责。其他:创建与回收由JVM线程负责,激活由当前线程负责。如果1个用户的访问就运行一遍完整的线程生命周期,那么n个用户就会产生n倍的除运行外的代价,显然,一个系统的并发访问是有浮动上限的,那么,便有了一种提高性能的办法,用计算机的存储特性替代计算特性:预处理线程的创建,等需求来临时直接引用激活运行,但不回收,继续等待下一个需求。这样预处理、异步响应的机制就是典型的生产者-消费者模型,产品为线程时,我们称作该模式为线程池机制:
使用线程池主要有以下两个好处:
减少在创建和销毁线程上所花的时间以及系统资源的开销
如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存 。
那么,JDK为我们提供的线程池是什么样的?ThreadPoolExecutor类与ScheduledThreadPoolExecutor类提供了一个可扩展的线程池实现,属于大名鼎鼎的Concurrent框架中的一员:
我们通过解读Executor接口(执行协议),了解到Concurrent框架中将每个线程要执行的指令集合封装为RunnableTask(可运行状态任务),实现Executor接口的实体将提供了一种将任务提交与每个任务如何运行的机制(包括线程使用、调度等细节),像这样:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
...
隐式地为创建的线程激活,进而由JVM代理执行任务。
ExecutorService,这是一个更广泛的接口,对Executor协议进行了一层扩展服务包装,执行器服务协议将执行器描述为一个终端模型,即对线程的“生产者-消费者”模型进行了行为规定。并对RunnableTask进行了扩展:RunnableTask被Executor执行后的结果封装为Future。即,实现执行器服务协议的终端,数据流输入输出流被规定。
在此进行一个小结,Concurrent框架将线程封装为一个个可运行状态的任务(RunnableTask),并委托Executor(执行器)在终端去执行线程程序,并返回一个Future(未来结果)。
AbstractExecutorService抽象类实现了上述协议的逻辑过程,子类ThreadPoolExecutor实体化逻辑过程,并在此基础上完善了整个线程池模型,主要实现过程为:
核心线程就是线程池的固定员工,我们可以通过:
private volatile int corePoolSize;
public void setCorePoolSize(int corePoolSize){...}
来维护,它是线程池高并发的同时低开销的核心。
我们上面提到了,线程的创建-激活与回收都是与运行程序无关,但需要系统开销的系统,既然如此,我们就委托线程池安排几名线程作为“体制内员工(内部Worker类)”,找个个数就由corePoolSize来规定。当然,为了开销最小,线程池都是按需创建线程的。
比如:
开发者第一个“安装”了一个线程池,并且封装了一个RunnableTask,丢给线程池。
->线程池作为服务终端接到任务后,指派Executor去完成这个任务
->于是Executor.execute(task):看了一下corePoolSize规定,还没满(workers.size() 开发者又有几个任务了,干脆全封装好,丢给了线程池。 ->线程池作为服务终端接到任务后,指派Executor去完成这个任务 ->于是Executor.execute(task),就这么几次过后···达到corePoolSize规定,不能再新找员工了。Executor只能把这个task放到工作队列中(workQueue),因为Executor清楚,线程池里只是暂时没有可用的员工了,并不是没有员工了,等某个员工处理好手头的事情后,便会来看看workQueue里有无工作可接。(这个队列为LinkedBlockingDeque) ->当然也有workQueue无工作可做的时候,让员工被激活干巴巴地站着等无疑于资源浪费(因为激活他们需要系统开销),于是线程池将工作队列设计成阻塞队列:阻塞即当员工申请工作队列中的工作时,发现没有工作可做,便进入等待状态wait()。线程处于wait()时,是不占用任何CPU资源的。 开发者突然有很多任务,干脆全封装好,丢给了线程池。 ->线程池作为服务终端接到任务后,指派Executor去完成这个任务 -> 于是Executor.execute(task),就这么几次过后···达到corePoolSize规定(核心线程的最大数量),不能再新找员工了。Executor只能试图把这个task放到工作队列中(workQueue)。但是发现此时的workQueue是满的(workQueue达到了容量极限),此时就只能检查maximumPoolSize规定(这个规定是线程池最大的线程数),如果workers.size() 开发者竟穷追不舍在饱和的状态下又丢给了线程池一些任务。 ->线程池作为服务终端接到任务后,指派Executor去完成这个任务 -> 于是Executor.execute(task),就这么几次过后···达到corePoolSize规定(核心线程的最大数量),不能再新找员工了。Executor只能试图把这个task放到工作队列中(workQueue)。但是发现此时的workQueue是满的(workQueue达到了容量极限),此时就只能检查maximumPoolSize规定(这个规定是线程池最大的线程数),发现workers.size() == maximumPoolSize,这次是彻底饱和了,Executor只能拒绝本次任务(reject(task)),并响应开发者:RejectedExecutionException(本次执行被拒绝)。 开发者看到线程池返回的饱和消息后,减缓了任务的委托。 ->workers努力与CPU合作将workQueue的任务清理的差不多了出现了有worker空闲的情况; ->临时工的加入导致了线程池作为服务终端便启动了“摸鱼淘汰制”(keepAliveTime):这个制度会记录员工(现场)没有工作的时长,率先达到keepAliveTime时长的会被请离(workers.remove()),直到workers.size() == corePoolSize,线程池就会停止这个制度。由于体制内员工与临时员工实际上没有明显的区别,因此“大逃杀”模式下,谁都可能被线程池干掉。 自此结束了线程池的介绍,其中员工制度是为了通俗地描述线程池工作流程而讲,读者请熟悉学术词汇,设计的操作在ThreadPoolExecutor均有出现。 总结: ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,并在此基础上扩展了额外安排命令在给定的延迟后运行,或者定期执行。当需要多个工作线程时,或者需要ThreadPoolExecutor(该类扩展)的额外灵活性或功能时,这个类比Timer更可取。 ScheduledThreadPoolExecutor与ThreadPoolExecutor的区别在于WorkQueue: DelayQueue是一个无界队列,所以ThreadPoolExecutor的maximumPoolSize在ScheduledThreadPoolExecutor中无意义,实际上实例化时便不提供无关字段的入参。 ScheduledThreadPoolExecutor中有两个方法: 它们的作用同方法名,延迟后运行,或者定期执行。 其原理为:当调用计划方法时,会向Pool的DelayQueue中offer一个ScheduleFutureTask 未来计划任务。员工(线程)从DelayQueue上获取到任务后,会根据ScheduleFutureTask 未来计划任务执行任务,以此达到计划的目的。 Executors类为这个包中提供的执行器服务提供工厂方法。这个包中定义了Executor、ExecutorService、ScheduledExecutorService、ThreadFactory和Callable类的工厂和实用方法。 下面介绍3种常用无计划线程池的(实现原理实际上就是帮开发者提供了ThreadPoolExecutor()中的参数) 和2种常用的计划线程池(实现原理实际上就是帮开发者提供了ScheduledThreadPoolExecutor()中的参数) 根据实际业务需求,选择JDK提供的便捷线程池执行器。 在Java设计思想深究----集合框架数学原理(图文)_kevinmeanscool的博客-CSDN博客 一文中,详细的解析了HashMap的工作原理,并提到了HashMap不是线程安全的。当时提到了线程安全的HashMap:ConcurrentHashMap,这里将详细介绍。 ConcurrentHashMap是支持检索的完全并发性和更新的高期望并发性的哈希表。该类遵循与Hashtable相同的功能规范,并包含与Hashtable的每个方法对应的方法版本,因此该类与Hashtable完全可互操作。 其中2次幂实力话容量、扩容、增删改查、哈希冲突、预防哈希冲突攻击等与HashMap原理相同,并在此基础上将共享变量做了如下处理: 即通过volatile关键字,实现共享变量的缓存一致性协议,即MESI协议(上文有详细提到)。使得变量的有序性、可见性得到了保障,同时保障了ConcurrentHashMap读操作(get)的线程安全。但写操作(put)不具有原子性,还是需要加锁(加Synchronized关键字)实现线程安全。 值得一提的是ConcurrentHashMap还是存在一定的线程阻塞行为:迭代器被设计为一次只能被一个线程使用。这与集合框架中的大部分集合相同,这也是为什么通过iterator迭代集合是线程安全的。 由于volatile对CPU的利用特性,在并发读的情况下,ConcurrentHashMap要比代理HashMap(Collections.synchronizedMap)更高效。 这里吐槽一下,锁的种类命名是真的···复杂,只不过根据应用场景不同命名不同。这里介绍几个常用的锁设计: 悲观与乐观的区别在于是否信任会话的来临是否均匀。 悲观认为本会话访问数据的时候会话的来临十分频繁,如果我不强烈占有这部分数据,将无法保证数据的安全,因此悲观锁将在会话访问数据时占有数据,保证数据的排他性。简而言之:对主内存数据加排他锁。 优点:数据十分安全 缺点:高并发场景CPU资源利用率低 应用:数据库访问时SQL添加 for update 乐观认为本会话访问数据的时候会话的来临很均匀,无需占有数据,只需要在访问数据时验证一下数据是否被更新即可。简而言之:一致性协议的应用。 优点:高并发场景CPU资源利用率高 缺点:无法阻止外源系统也参与事务队列中 应用:字段增加version,增加版本校验 synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。 状态其实就是上面讲的乐观锁。 Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。 当线程竞争变得比较激烈时,偏向锁就会升级为 就是互斥锁。当前线程运行时其他线程全部阻塞(老实在entryList等待)。 其实优化就是对于矛盾的取舍。 CPU资源利用率 vs 数据安全 ->策略的选择:synchronized与volatile的选择 ->线程池的应用:核心线程数与最大线程数的选择、KeepAliveTime的宽容度、WorkQueue的弹性等 这些都需要做严谨的数学模型分析,比如线性规划:最小的代价最大的回报等。此类算法问题将在算法博文展开描述。 自此结束了多线程与并发的原理探究,作者水平有限,如有疑论,清不吝评论。
ScheduledThreadPoolExecutor 计划性线程池执行器
如何选择合适的线程池?
newFixedThreadPool
固定数量线程池,数量通过传入的参数决定。(corePoolSize==MaximunPoolSize=fixedNumber)newSingleThreadExecutor
创建一个线程容量的线程池,所有的线程依次执行,相当于创建固定数量为 1 的线程池。(corePoolSize==MaximunPoolSize=1)newCachedThreadPool
可缓存线程池,。如果用空闲线程等待时间超过一分钟,就关闭该线程。(corePoolSize=0;MaximunPoolSize=Integer.MAX_VALUE;KeepAliveTime=60s)
newScheduledThreadPool 计划线程池 (其实就是
ScheduledThreadPoolExecutor)
newSingleThreadScheduledExecutor
单线程池延迟任务( ScheduledThreadPoolExecutor(corePoolSize= 1) )强大的 ConcurrentHashMap 其实并不难
/* ---------------- Fields -------------- */
transient volatile Node
多样的锁与应用场景
数据读写层面的锁
悲观锁
乐观锁
对象层面的锁
无锁
偏向锁
轻量级锁
轻量级锁
,使用CAS操作(请求对象给锁,成功就停,不成功就循环这个过程retry)避免了使用互斥量的开销。重量级锁
多线程与并发优化