JAVA并发编程
2021-03-23 23:30:07 35 举报
AI智能生成
Java并发编程
作者其他创作
大纲/内容
volatile
轻量级的volatile,保证了可见性,顺序性,不保证原子性,它比synchronized的使用和执行成本更低,因为它不会引起上下文的切换和调度
实现原理:如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条lock前缀的命令,该命令在多核处理器会引发两件事情
1.将当前处理器缓存行的数据写回系统内存,LOCK#信号一般会独占共享内存(会锁住总线,让其他CPU无法访问内存),最新的处理器一般改为锁缓存,即如果访问的内存区域已经缓存到处理器内部了,则不声言LOCK#信号,它会锁住这块内存区域的缓存并写回到内存,并写回内存,并使用缓存一致性机制来确保修改的原子性
2.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
缓存锁定
如果访问的内存区域已经缓存到处理器内部了,则不声言LOCK#信号,它会锁住这块内存区域的缓存并写回到内存,并写回内存,并使用缓存一致性机制来确保修改的原子性
总线锁定
总线索就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号,其他处理器的请求将被阻塞,那么该处理器就可以独占共享锁
优化
实现:LinedTransferQueue
原理:追加字节,避免头尾节点被读入到同一缓存行中,导致操作头节点的时候锁住整个缓存行,使得尾节点无法被读取
synchronized
对象锁
方法块
普通方法
类锁
静态同步方法
指定锁为类锁
实现
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者实现细节不同,代码块同步是指使用monitorenter和monterexit指令实现的,而方法同步是使用另外一种方式来实现的。
JAVA对象头
synchronized用的锁是存在Java对象头里的,数组对象,3个字宽存储对象头,非数组对象,2个字宽存储对象头。包括Mark Word(存储对象的hashcode或锁信息等),Class Metadata Address(存储到对象类型数据的指针),Array Length(数组的长度,如果当前对象是数组)
锁的级别从低到高:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,随着竞争锁状态回逐渐升级,但不会降级
偏向锁
Mark Word:线程ID,Epoch,对象分代年龄,是否是偏向锁,锁标志位
使用场景:同一线程调用同步块
锁的释放:线程不会主动释放锁,只有出现竞争时才会释放锁
当一个线程访问同步快并获取锁时,会在对象头和栈帧中的锁记录里存储所偏向的线程ID ,以后该线程在进入和退出同步块时就不需要加锁解锁了,只需要取锁对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果是,则获取锁。如果不是,如果不是,查看Mark Word中是否是偏向锁是否设置为1(表示当前锁是否为偏向锁),如果不是,则使用CAS竞争锁。如果设置,则尝试使用CAS将对象头的偏向锁指向当前线程
关闭偏向锁:java6和7中默认启用,在程序启动几秒胡才激活,可以设置参数-XX:BiasedLockingStartupDelay=0来关闭延迟。设置--XX:UseBiasedLocking=false,程序默认进入轻量级锁。
轻量级锁
加锁:线程尝试使用CAS将对对象头中的Mark Word替换为指向锁记录的指针,如果成功,当前线程获取锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果在自旋一定次数后仍为获得锁,那么轻量级锁将会升级成重量级锁。
解锁:轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark Word替换回对象头,如果的成功,则表示没有竞争发生,如果失败,表示当前锁存在竞争,锁就会膨胀为重量级锁
重量级锁
操作系统级别的锁, 是基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态和内核态之间切换,相对开销较大。
synchronized在内部基于监视器锁(monitor)实现,监视器锁基于底层的操作系统的Mutex Lock实现。获取锁失败时,线程会进入阻塞状态
synchronized在内部基于监视器锁(monitor)实现,监视器锁基于底层的操作系统的Mutex Lock实现。获取锁失败时,线程会进入阻塞状态
隐式的支持可重入
为什么要并发编程?
上下文切换
任务从保存到再加载的过程就是一次上下文切换
减少上下文切换的方法
1.无锁并发编程
2.CAS算法
3.使用最少线程
4.协程
充分利用CPU资源
更快的相应速度
更好的编程模型
名词
自旋:自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。
因为自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。
因为自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。
Displaced Mark Word:线程执行同步块时,JVM回先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word替换为指向锁记录的指针。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁
并发基础
等待/通知
等待/通知的相关方法是任意Java对象都具备,因为这些方法被定义在所有对象的超类java.lang.Object上
方法
notify():通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该前程获取到了对象锁
notyfyAll():通知所有等待在该对象上的线程
wait():调用该方法的线程进入WAITTING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁
wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
wait(long,int):对于超时时间更细粒度的控制,可以达到纳秒
使用
使用wait(),notify()和notifyAll()时需要先对调用对象加锁
调用wait()方法后,线程状态由RUNNING变为WAITTING,并将当前线程放置到等待队列中
notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁后,等待线程才有机会从wait()返回
notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED
从wait()方法返回的前提是获得了调用对象的锁
Thread.join()
如果线程A执行了thread.join语句,其含义是,当前线程A等待thread线程终止之后才从thread.join返回()
ThreadLocal
线程变量,是一个以ThradLocal对象为键,任意对象为值的存储结构,这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值
锁
Lock接口
提供synchronized关键字所不具备的主要特性
尝试非阻塞地获取锁:当前线程尝试获取锁,如果这以时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁:在指定的戒指时间之前获取锁,如果截至时间到了仍旧无法获取锁,则返回
API
void lock
获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回
void lockInterruptibly() throws InterruptedException
可中断地获取锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
boolean tryLock()
尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false
boolean tryLock(long time, TimeUint unit) throws InterryptedException
超时的获取锁,当前线程在以下3种情况下会返回
当前线程在超时时间内获取了锁
当前线程在超时时间内被中断
超时时间结束,返回false
void unlock()
释放锁
Condition newCondition()
获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的wait方法,而调用后,当前线程将释放锁
队列同步器AbstractQueuedSynchronizer(AQS,抽象类)
用来构建锁或者其他同步组件的基础框架,使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作
同步器的主要使用方法是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,同步器提供了3个方法(getState(),setState(int newState))和compareAndSetState(int expext, int update)来进行操作。子类推荐被定义为自定义同步组件的静态内部类
理解同步器和锁的关系
锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方法,屏蔽了同步状态管理,线程排队,等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现这所需关注的领域
同步器方法
不可重写
getState():获取当前同步状态
setState(int newState):设置当前同步状态
compareAndSetState(int except, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
可重写
protected boolean tryAcquire(int arg):独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后在进行CAS设置同步状态
protected boolean tryRelease(int arg):独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg):共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
protected boolean tryReleaseShared(int arg):共享式释放同步状态
protected boolean isHeldExclusively():当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
模板方法
void acquire(int arg)
独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法
void acquireInterruptibly(int arg)
与acquire(int arg)相同,但是相应中断
boolean tryAcquireNanos(int arg, long nans)
在acqureiInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false;如果获取到了返回true
void acquireShared(int arg)
共享式获取同步状态,未获取到同步状态则进入进入同步队列等待
void acquireSharedInterruptibly(int arg)
与acquireShared(int arg)相同,但是响应中断
boolean tryAcquireSharedNanos(int arg, long nanos)
在acquireSharedInterruptibly(int arg)基础上增加了超时限制
boolean release(int arg)
独步式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
boolean releaseShared(int arg)
共享式的释放同步状态
Collection<Thread> getQueuedThreads()
获取等待在同步队列上的线程集合
队列同步器的实现
同步队列
FIFO双向队列
节点:保存获取同步状态失败的线程引用,等待状态以及前缀和后继节点
compareAndSetTail(NOde expect, Node update):基于CAS的设置尾节点的方法,需要传递当前线程认为的尾节点和当前节点
独占式同步状态的获取与释放
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点
共享式同步状态的获取与释放
重入锁ReentrantLock
支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁
重进入:是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞
实现
线程再次获取锁时,锁需要去识别获取锁的线程是否为当前占据锁的线程
最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁
提供了一个构造函数,能够控制锁是否式公平的
公平锁与非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大
非公平锁
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁
排他锁
读写锁ReentrantReadWriteLock
共享锁,维护了一对锁,一个读锁和一个写锁。写线程访问时,所有的读线程和其他写线程均被阻塞。保证写锁的操作要对读锁可见
接口ReadWriteLock
readLock():获取读锁
writeLock():获取写锁
方法
int getReadLockCount()
返回当前读锁被获取的次数。该次数不等于获取读锁的线程数(读写锁时可重入锁)
int getReadHoldCount()
返回当前线程获取读锁的次数
boolean isWriteLocked()
判断当前写锁是否被获取
int getWriteHoldCount()
返回当前写锁被获取的次数
特性
支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
支持重进入
遵循获取写锁,获取读锁在释放写锁的次序,写锁能够降级成为读锁
实现
读写锁将同步状态
这一个变量切分为了两个部分,高16位表示读,低16位表示写
假设当前状态值为S
写状态:S & 0x0000FFFF
读状态:S >>> 16
读锁的读取和释放
写锁是一个支持可重入的共享锁
如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁
写锁的读取和释放
写锁是一个支持可重入的排他锁
如果存在读锁,则写锁不能被获取
写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0
时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对
后续读写线程可见
时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对
后续读写线程可见
锁降级
写锁降级为读锁,锁降级是指把持住当前拥有的写锁,再获取读锁,随后释放写锁的过程
LockSupport
当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应
工作
工作
方法
void park()
阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回
void parkNanos(long nanos)
阻塞当前线程,最长不超过nanos秒,返回条件在park()的基础上增加了超时返回
void parkUntil(long deadline)
阻塞当前线程,知道deadline时间(从1970年开始到deadline的毫秒数)
void unpark(Thread thread)
唤醒处于阻塞状态的线程thread
Condition
Condition对象是由Lock对象(调用Lock对象的newCondition()方法创建出来的)
方法
void await() throws InterruptedException
当前线程进入等待状态直到被通知(signal)或中断
void awaitUninterruptibly()
和await相同,但对中断不敏感
long awaitNanos(long nanosTimeout) throws InterruptedException
当前线程进入等待状态知道被通知,中断或者超时。返回值表示剩余的时间
boolean awaitUntil(Date dealine) throws InterruptedException
当前线程进入等待状态知道被通知,中断或者到某个时间
void signal()
唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition想关联的锁
void signalAll()
唤醒所有等待在Condition上的锁
实现
每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。在并发包中的Lock拥有一个同步队列和多个等待对了
等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态
等待
调用Condition的await()方法,会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态
通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),将该节点插入到同步队列的最后
Java并发工具类
CountDownLatch
允许一个或多个线程等待其他线程完成操作
CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N
方法
countDown
N就会减1
await
CountDownLatch的await方法会阻塞当前线程,直到N变成零
CyclicBarrier
可循环使用的屏障,让一组线程到达一个屏障时被阻塞,知道最后一个线程到达屏障
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrierAction),用于在线程到达时,优先执行barrierAction,方便处理更复杂的业务场景
方法
reset():重置
getNumberWaitting():获取CyclicBarrier阻塞的线程数量
isBroken():了解阻塞的线程是否被中断
Semaphore
用控制同时访问特定资源的线程数量
构造方法
Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量
方法
acquire():获取一个许可证
release():归还许可证
int availablePermits():返回此信号量中当前可用的许可证数
int getQueueLength():返回正在等待获取许可证的线程数
boolean hasQueuedThreads():是否有线程正在等待获取许可证
void reducePermits(int reduction):减少reduction个许可证,是个protected方法
Collection getQueuedThreads():返回所有等待获取许可证的线程集合,是个protected方法
Exchanger
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换
Executor
任务
Runnable
Callable
任务的执行
Executor
可以创建三种类型的ThreadPoolExecutor
FixedThreadPool
创建单个线程的SingleThreadExecutor的API
new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable()>);
使用无界的LinkedBlockingQueue(其实是有界,容量为Integer.MAX_VALUE)作为工作队列,存在以下问题
当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中 的线程数不会超过corePoolSize
由于1,使用无界队列时maximumPoolSize将是一个无效参数
由于1和2,使用无界队列时keepAliveTime将是一个无效参数
由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或 shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)
SingleThreadExecutor
创建固定线程数的FixedThreadPool的API
new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())
CacheThreadPool
大小无界的线程池
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUni.SECONDS, new SynchronousQueue<Runnable>())
CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但 CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于 maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下, CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源
可以创建两种类型的ScheduledThreadPool
ScheduledThreadPoolExecutor
包含若干个线程的ScheduledThreadPoolExecutor
SingThreadScheduledExecutor
包含一个线程的ScheduledThreadPoolExecutor
ExecutorService
ThreadPoolExecutor
可以通过Executor创建(阿里巴巴编程规范不推荐)
ScheduledThreadPoolExecutor
继承ThreadPoolExecutor
使用DelayQueue作为工作队列,DelayQueue是一个无界队列
异步计算的结果
Future
FutureTask
类与接口
三大性质
可见性:当一个线程修改一个共享变量时,其他线程能够读取到这个修改后的值
原子性:一个操作不可以中断,要么全部成功,要么全部失败
有序性:程序按照代码的先后顺序执行
MESI缓存一致性协议
MESI (Modified-Exclusive-Shared-Invalid)协议是一种广为使用的缓存一致性协议, x86处理器所使用的缓存一致性协议就是基于MESI协议的,MESI分别指缓存行的四种状态,可用2bit来表示。缓存一致性在CPU要写入数据时,如果发现需要操作的变量时共享变量(缓存行状态为S),则将其他缓存行中的状态置为I。读取变量时发现状态为I,则从内存中重新读取该变量、读取数据时,如果CPU缓存行修改了数据,则将修改数据写回内存。修改数据时,其他缓存行状态变成Invaild
M修改(Modified)):该Cache Line数据有效,是被修改过的,和内存的数据不一样,该数据只存在本Cache中
子主题
E独享互斥(Exclusive):该Cache Line数据有效,和内存中的数据一样,该数据只存在本Cache中
S共享(Shared):该Cache Line数据有效,和内存中的数据一样,数据存在于很多Cache中
I无效(Invaild):该Cache Line数据无效
原子操作
不可被中断的一个或者一系列操作
处理器提供总线锁定和缓存锁定两个机制来保证负载内存操作的原子性
JAVA内存模型
并发编程中两个关键问题
线程如何通信?
共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态
进行隐式通信
进行隐式通信
消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消
息来显式进行通信
息来显式进行通信
线程如何同步?
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型
里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的
里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对
程序员完全透明
程序员完全透明
Java内存模型的抽象结构
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享
变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽
象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地
内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的
一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优
化
变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽
象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地
内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的
一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优
化
重排序
编译器重排序
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句
的执行顺序
的执行顺序
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排
序(不是所有的编译器重排序都要禁止)。
序(不是所有的编译器重排序都要禁止)。
处理器重排序
指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level
Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应
机器指令的执行顺序
Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应
机器指令的执行顺序
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上
去可能是在乱序执行
去可能是在乱序执行
对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序
常见的处理器重排序规则
常见的处理器都允许Store-Load重排序,这是因为现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序;常见的处理器都不允许对存在数据依赖的操作做重排序
内存屏障分类
重排序只指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。重排序可能会导致多线程程序
出现内存可见性问题
出现内存可见性问题
happens-before
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作顺序排在第二个操作后面,
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
volatile规则:对于一个volatile域的写,happens-before于任意后续对这个volatile域的读
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
数据依赖性
如果两个操作访问一个变量,且有一个操作为写,则这两个操作之间就存在数据依赖性
写后读
写后写
读后写
as-if-serial
不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义
当代码存在控制依赖性时,会影响指令序列执行的并行度,为此,编译器和处理器会采用猜测执行来客服控制相关性对并行度的影响。
顺序一致性内存模型
顺序一致性模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证
两大特性
一个线程中的所有操作必须按照程序的顺序来执行
(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性的内存模型中,每个操作都必须原子执行且立刻对所有线程可见
JMM
未同步程序在JMM中不但整体的执行顺序时无序的,而且所有线程看到的操作执行顺序也可能不一致
临界区内的代码可以重排序
对于未同步或者未正确同步的多线程程序,JMM只提供最小安全性,JMM保证线程读操作读取到的值不会无中生有的冒出来。=
JMM不保证64位的long型和double型变量的写操作具有原子性
双重检查锁与延迟初始化
在Java多线程中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。双重检查锁定是常见的延迟初始化技术,但它是一个错误的用法
双重检查锁定的为什么错误?
Java对象的建立,1.分配对象的内存空间 2.初始化对象 3.设置instance指向刚分配的内存地址。2,3可能会重排序,导致判断对象是否为null时错误判断,导致访问到一个未初始化的对象
解决方案
基于volatile的解决方案:将要初始化的对象声明为volatile型的
基于类初始化的解决方案
JVM在类初始化阶段,会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
该方案的实质是,允许2,3的重排序,但不允许非构造线程“看到”这个重排序
Java初始化
通过在Class对象上同步(即获取Class对象的初始化锁),来控制类或接口的初始化。这个获取锁的线程会一直等待,直到当前线程能够获取到这个初始化锁
线程A执行类的初始化,同时线程B在初始化锁对应的condition上等待
线程A设置state=initalized,然后唤醒在condition中等待的所有线程
线程B结束类的初始化处理
线程
现代操作系统调度的最小的单元是线程
线程拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量
线程的状态
NEW:初始状态,线程被构建,但还没有调用start()方法
RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称作“运行中”
BLOCKED:阻塞状态,表示线程阻塞于锁
WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或终端)
TIME_WAITING:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED:终止状态,表示当前线程已经执行完毕
Java中的13个原子类
原子更新基本类型
AtomicInteger
原子更新整形
方法
int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值
int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值
void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值
AtomicLong
原子更新长整型
AtomicBoolean
原子更新布尔类型
原子更新数组
AtomicIntegerArray
原子更新整型数组里的元素
方法
int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加
boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子
方式将数组位置i的元素设置成update值
方式将数组位置i的元素设置成update值
AtomicLongArray
原子更新长整型数组里的元素
AtomicReferenceArray
原子更新引用类型数组里的元素
原子更新引用
AtomicReference
原子更新引用类类型
AtomicReferenceFieldUpdater
原子更新引用类型里的字段
AtomicMarkableReference
原子更新带有标记位的引用类型
原子更新属性
AtomicIntegerFieldUpdater
原子更新整型的字段的更新器
AtomicLongFieldUpdater
原子更新长整型字段的更新器
AtomicStampedReference
原子更新带有版本号的应用类型
AtomicReferenceFieldUpdater
Java并发容器和框架
ConcurrentHashMap
为什么使用ConcurrentHashMap
线程不安全的HashMap
HashMap在并发执行put操作时会引起死循环,因为多线程会导致HashMap的Entry链表形成环形数据结构
效率低下的HashTable
HashTable就是将HashMap的每个操作都使用了synchronized来保证线程安全
Java 8以前
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在 ConcurrentHashMap里扮演锁的角色;HashEnrty则用于存储键值对数据。
ConcurrentHashMap数据结构
初始化
默认容量为 16,负载因子为 0.75,hashEntry数组长度1,阈值0
操作
get
get过程不需要加锁,因为get方法里将要使用的共享变量都定义为volatile类型
put
需要加锁。put方法需要先定位到segment,然后在segment里进行插入操作
扩容
是否需要扩容
判断segment里的hashentry数组是否超过阈值,如果超过,则进行扩容
如何扩容
创建一个容量是原来容量两倍的数据,然后将原数组里的元素进行再散列后插入到新的数组里
size
有一个volatile修饰的count变量,ConcurrentHashMap先尝试2次通过不锁住Segment的方法来统计各个Segment大小,如果统计过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小
Java 8
Java 8为进一步提高并发性,摒弃了分段锁的方案,而是直接使用一个大的数组。同时为了提高哈希碰撞下的寻址性能,Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))
使用CAS + synchronized 控制并发操作
ConcurrentLinkedQueue
基于链接节点的非阻塞的无界线程安全队列
阻塞队列
ArrayBlockingQueue
一个数组结构组成的有界阻塞队列
LinkedBlockingQueue
一个链表结构组成的有界阻塞队列
PriorityBlockingQueue
一个支持优先级排序的无界阻塞队列
DelayQueue
一个使用优先级队列实现的无界阻塞队列
SynchronousQueue
一个不存储元素的阻塞队列
LinkedTransferQueue
一个由链表结构组成的无界阻塞队列
LinkedBlockingDeque
一个由链表结构组成的双向阻塞队列
Fork/Join
Java7提供的一个用于并行执行任务的框架,是一个把大人物切割成若干小任务,最终汇总每一个小人物结果后得到大任务结果的框架
工作窃取算法
某个线程从其他队列里窃取任务来执行
双向队列,被窃取的任务拥有从双向队列的头部拿任务执行,而窃取任务的线程永远从双向队列尾部拿任务执行
优点
充分利用线程进行并行计算,减少了线程间的竞争
缺点
在某种情况下还是存在竞争
框架设计
分割任务
合并任务
实现
ForkJoinTask
子类
RecursiveAction
没有返回结果的任务
RecursiveTask
有返回结果的任务
需要实现compute方法,在这个方法里面,首先要判断任务是否足够小,如果足够小就直接执行任务,如果不够小,则分割成两个子任务,子任务在diaoyongfork
ForkJoinPool
ForkJoinTask需要通过ForkJoinPool来执行
异常处理
isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且通过ForkJoinTask的getException方法来获取异常
线程池
优点
降低资源消耗
提高响应速度
提高线程的可管理性
线程池的主要处理流程
线程池的创建
ThreadPoolExecutor
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, handler)
corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建
maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列,可以选择
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
PriorityBlockingQueue
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略
AbortPolicy:直接抛出异常
CallerRunsPolicy:只用调用者所在的线程来运行任务
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
DiscardPolicy:不处理,丢弃
也可以实现RejectedExecutionHandler接口自定义策略
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率
TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒
提交任务
execute()
提交不需要返回值的任务
submit()
提交需要返回值的任务,返回一个future类型的对象
关闭
shutdown
shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程
shutdownNow
shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
原理
遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止
线程池配置
CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池
IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务
线程池的监控
taskCount:线程池需要执行的任务数量
completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount
largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过
getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减
getActiveCount:获取活动的线程数
taskCount:线程池需要执行的任务数量
0 条评论
下一页