多线程并发内容开发经验分享
2022-10-13 09:51:20 0 举报
AI智能生成
多线程并发内容开发经验分享
作者其他创作
大纲/内容
实现方式
1.实现Runnable接口
2.继承Thread类
关键字
synchronized
对象锁与类级别的锁
关键字synchronized取得的锁都是对象锁,而不是把一段代码或者方法当作锁
如果是两个对象,线程获得的就是两个不同的锁,互不影响
如果在静态方法上加synchronized,表示锁定.class类,那么该类不同对象获得的则是相同的锁
修饰对象
修饰代码块:作用于调用的对象
修饰方法:整个方法,作用于调用的对象
修饰静态方法:整个静态方法,作用于所有对象
修饰类:作用于所有对象
同步代码块
锁this
锁当前对象
其他线程可以访问非同步代码快内容
其他线程访问该对象其他同步代码块时阻塞
非this
相同对象监听器之间是同步的,不同对象监听器之间是异步的
eg:两个anything锁之间会阻塞,anything和this之间不会阻塞
如果将某一个对象X作为对象监听器。
syn(X)同步
syn方法同步
syn(this) 同步
静态同步synchronized方法和synchronized(class)代码块都是锁对应的Class类
当一个线程进入同步的静态方法或者class锁时,其他线程其他对象进入其他同步代码快或者class锁时会受阻塞
当一个线程进入同步的静态方法时或者class锁,其他线程对象仍然可以进如this同步代码块方法
当一个线程进入this同步代码块方法时,其他线程仍然可以进入同步惊呆方法或者class锁
String常量池
两个不一样的常量持有的是同一个锁
new 出来的则为不一样的锁
同步与异步
同步(synchronized):同步的概念就是共享,如果不是共享段资源,就没有必要同步
异步(asynchronized):异步的概念是独立,相互之间不受任何制约
如果A线程先持有object对象的LOCK锁,B线程在此时调用该对象中的同步方法则需要等待,即同步
如果A线程先持有object对象的LOCK锁,B线程在此时调用该对象中的异步方法即非synchronized方法修饰时,则无需等待
避免脏读
在对一个对象的方法加锁时,要考虑业务的一致性,比如 get/set方法同时加锁,保证数据的原子性,不然会出现业务错误
锁重入
当一个线程得到了一个对象锁后,再次请求此对象时是可以再次或者该对象的锁
一个实体里面有多个方法加锁,且一个方法调其他的方式时,锁可以重入,方法仍可以继续进行,如果子类和父类方法均有加锁那么,这也是线程安全的
synchronized是公平锁
出现异常时,锁自动释放
web应用中,异常释放锁的情况,如果不及时处理,很可能对业务逻辑产生严重错误,比如现在执行一个队列任务,很多对象都在等待第一个对象正确执行完毕再去释放锁,单第一个对象由于异常的出现,导致业务逻辑没有执行完毕,就释放了锁,那么后续对象执行的都是错误的。
注意问题
不要对String 常量进行加锁 会出现死循环
由于常量为一个引用,那么当一个线程进来的时候,该引用就被加了锁,那么其他线程就不能进来
执行时间方面:比如当A线程执行需要很长时间时,那么B线程就必须要等待很长时间才能执行。此时可以使用synchronized代码块去优化执行时间,也就是减少锁的粒度
锁对象的改变问题。当使用一个对象进行加锁,如果对象本身发生变化,那么持有的锁就不同。如果对象本身不发生变化,那么依然是同步的,即使对象的属性发生了变化
死锁
synchronized可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能
Synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或者某一个代码快。它包含两个特征:互斥性和可见性。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者代码块的每一个线程,都看到有一个锁保护之前所有的修改效果
volatile
主要作用:使变量在多个线程间可见
其他解决方法,在该变量上加锁,别的线程在修改该变量时,其他线程在此等候
保证主内存与线程内存中变量值的一致性
程序.png
程序内存图.png
修饰的值不适合参与运算
可见性
在java中每一个线程都会有一块工作内存区,存放着所有线程共享的主内存中的变量值的拷贝。当线程执行时,他在自己的工作内存中操作这些变量。为了存取一个共享的变量,一个线程通常先获取锁定并去清除他的工作内存区,当线程解锁时保证该工作内存区中变量的值写回到共享内存中
volatile 会强制线程到主内存中读取变量
执行操作
线程的执行操作有:使用(use),赋值(assign),装载(load),存储(store),锁定(lock),解锁(unlock)
住内存的执行操作有:读(read),写(write),锁定(lock),解锁(unlock),每个操作都是原子的
volatile不能保证原子性,是一个轻量级的synchronized,性能要比synchronized强很多,不会造成阻塞(在很多开源的架构里,比如netty的底层代码大量使用volatile)。需要注意的是 volitile 用于只针对多个线程可见的变量操作,并不能代替synchronized 同步功能。如果多个线程同时修改该变量,那么该变量的值任然不可控
要实现原子性建议使用atomic类的系列对象。支持原子性操作
atomic多次操作实例.png
原子性可理解为:该操作不可分割。如果该操作可以分割为多个步骤,当线程1去进行某个步骤时,线程2就可能会去进行其他的步骤,就会造成数据问题
ThreadLocal
线程局部变量,是一种多线程间并发访问变量的解决方案。与synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保证线程安全。
从性能上说,ThreadLocal 不具备绝对的优势,在并发不是很高的时候加锁的性能会更好,但在高并发或者竞争激烈的场景,使用它可以在一定程度上减少锁竞争
ConnThreadLocal.java
ReentrantLock
ReentrantLock(重入锁)
在需要进行同步的代码上加入锁,但最后一定要记得释放
Condition
await()相当与syn的wait()
同Synchronized配合的notify/wait UseReentrantLocak 则用Condition 通知等待针对condition独自对象
signal()相当于syn的notify();
signalAll()相当于syn的notifyAll();
代码
UseReentrantLock.java
UseCondition.java
UseManyCondition.java
api
new ReentrantLock(boolean)
true 公平锁--先到先得
false 非公平锁--根据抢占机制随机获得
默认 为非公平锁
signal()不释放锁 await()释放锁
tryLock() 可以尝试的去获得锁
getHoldCount() 查询当前线程保持此锁定的个数 即调用lock方法的次数
getQueueLength() 正等待获取此锁定的线程数量
getWaitQueueLength(Condition condition) 正等待获取此锁定相关的给定条件的线程数量
hasQueuedThread()
isFair() 判断是否公平锁
isLocaked()判断是否被锁
tryLock()
公平锁和非公平锁
在ReentrantLock中,对于公平和非公平的定义是通过对同步器AbstractQueuedSynchronizer的扩展加以实现的,也就是在tryAcquire的实现上做了语义的控制。
公平锁逻辑相比较非公平的获取,仅加入了当前线程(Node)之前是否有前置节点在等待的判断。也就是说当前面没有人排在该节点(Node)前面时候队且能够设置成功状态,才能够获取锁。
使用场景
https://blog.csdn.net/zhousenshan/article/details/53026785
ReentrantReadWriteLock(读写锁)
核心为 实现读写分离的锁
在高并发访问下,尤其是读多写少的情况下,性能要远高于重入锁
UseReentrantReadWriteLock.java
读读共享 写写互斥 读写互斥
方法
Thread.yield();
释放当前线程的cpu占用
thread.join();
thread线程加入到当前线程中,知道thread线程执行完,当前线程才执行
线程间的通信
线程是操作系统中独立的个体,但这些个体如果不经过特殊处理就不能成为一个整体,线程间的通信就成为整体的必用方法之一。当线程存在通信指挥,系统间的交互性就会更强大,在提高CPU利用率的同时还会使开发人员对线程在处理的过程中进行有效的把控与监督
wait/notify
使用wait/notify方法实现线程间的通信。(注意这两个方法都是object类的方法,即java为所有对象都提供了这两个方法)
wait和notify必须配合synchronized关键字使用
wait方法释放锁 notify方法不释放锁
notify()方法只唤醒一个等待线程 notifyAll()方法可以唤醒所有等待线程
WaitNotifyTest.java
使用wait/notify模拟queue
子主题 1
wait(long) 等待一段时间内是否有线程对锁唤醒,如果没有则自动唤醒
join
将当前线程阻塞,待join的所属线程执行完后在进行
join与synchronized的区别
join在内部使用wait方法进行等待
synchronized使用对象监视器原理做同步
在join的过程中,如果当前线程对象被中断,则当前线程出现异常。join与interrupt方法相遇,也会出现异常
join(long) 内部使用wait(long)实现,所以有释放锁的特点
其他知识点
线程安全
start() 方法启动后 线程是准备运行状态 具体什么时候运行由cpu决定
arrylist在添加元素的时候先扩容再添加,当多个线程一起添加时可以还未扩容成功新的已添加造成异常以及添加进去的实际数量的不确定性
单例和多线程
常见的单例模式
饥饿模式
直接实例化对象
懒汉模式
调用方法时实例化
多线程模式中的单例模式
DubbleSingleton.java
static inner class
声明一个静态内部类 在内部类中生命一个静态变量
添加一个或者该内部类静态变量的方法
InnerSingleton.java
双重判断方式
在懒汉模式的get对象方法中,先判断该一次该对象是否为空
对该对象类加锁,并在锁内再判断一次该变量是否为空,为空时再声明
容器
ConcurrentHashMap
基本原理
HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占。
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。
在JDK 1.6中,HashEntry中的next指针也定义为final,并且每次插入将新添加节点作为链的头节点(同HashMap实现),而且每次删除一个节点时,会将删除节点之前的所有节点 拷贝一份组成一个新的链,而将当前节点的上一个节点的next指向当前节点的下一个节点,从而在删除以后 有两条链存在,因而可以保证即使在同一条链中,有一个线程在删除,而另一个线程在遍历,它们都能工作良好,因为遍历的线程能继续使用原有的链。因而这种实现是一种更加细粒度的happens-before关系,即如果遍历线程在删除线程结束后开始,则它能看到删除后的变化,如果它发生在删除线程正在执行中间,则它会使用原有的链,而不会等到删除线程结束后再执行,即看不到删除线程的影响。
ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。最高分为16个段
Segment(桶)
segments.png
Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。
Segment下面包含很多个HashEntry列表数组。对于一个key,需要经过三次hash操作,才能最终定位这个元素的位置
对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。
注意事项
ConcurrentHashMap中的key和value值都不能为null,HashMap中可以为null,HashTable都不能为null。
ConcurrentHashMap是线程安全的类并不能保证使用了ConcurrentHashMap的操作都是线程安全的!
ConcurrentHashMap的get操作不需要加锁,put操作需要加锁
Copy-on-Write容器
原理:当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
常用:CopyOnWriteArrayList和CopyOnWriteArraySet
应用场景
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。
使用注意
1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。
缺点
内存占用问题
数据一致性问题
队列
ConcurrentLinkedQueue
适用于高并发场景,通过无锁的方式实现高并发下的高性能,性能通常要高于BlockingQueue
是一个基于链接节点的无界线程安全队列。该队列遵循先进先出原则。头是最先加入的,尾部是最近加入的
值不能为空
add()和offer() 增加 没有区别
poll()和peek()是取头部元素节点,区别:前者会删除元素后者不会
无阻塞(阻塞:put 和 get时 当不满足条件时是否暂停)
BlockingQueue接口
ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,
在生产者放入数据和消费者获取数据,都是共用同一个锁对象,并没有没实现读写分离,也就意味着生产者和消费者不能完全并行
长度是需要定义的,可以指定的先进先出或者先进后出,也叫有界队列。
LinkedBlockingQueue
基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成)
实现读写分离锁
无界队列
SynchronousQueue
没有缓存的队列 ,生产者产生的数据会直接被消费者获取消费
PriorityBlockQueue
基于优先级阻塞队列 优先级的判断根据构造函数传入的Compator对象来决定即传入的对象必须实现Comparable接口
内部控制线程同步锁采用的是公平锁
无界队列
排序并非发生在add时,当发生take()时,队列才排序
PriorityQueue.java
Task.java
DelayQueue
带有延迟的队列
其中的元素只有到了指定的延迟时间后,才能从队列中取得该元素
其中的元素必须实现Delayed接口
无界队列
应用场景:缓存超时的数据移除、任务超时处理、空闲连接的关闭等
线程框架
Executor
Executors--提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口
public static ExecutorService newFixedThreadPool(int nThreads) 创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
scheduler.scheduleAtFixedRate(task, 5, 10, TimeUnit.SECONDS); 初始化5s后开始执行,每隔10s执行一次
自定义线程池
public ThreadPoolExecutor(int corePoolSize,
 int maximumPoolSize,
 long keepAliveTime,
 TimeUnit unit,
 BlockingQueue<Runnable> workQueue,
 ThreadFactory threadFactory,
 RejectedExecutionHandler handler)
corePoolSize:核心线程池的大小,在线程池被创建之后,其实里面是没有线程的。(当然,调用prestartAllCoreThreads()或者prestartCoreThread()方法会预创建线程,而不用等着任务的到来)。当有任务进来的时候,才会创建线程。当线程池中的线程数量达到corePoolSize之后,就把任务放到 缓存队列当中。(就是 workQueue)---------初始化时线程数量
maximumPoolSize:最大线程数量是多少。它标志着这个线程池的最大线程数量。如果没有最大数量,当创建的线程数量达到了 某个极限值,到最后内存肯定就爆掉了。
keepAliveTime:当线程没有任务时,最多保持的时间,超过这个时间就被终止了。默认情况下,只有 线程池中线程数量 大于 corePoolSize时,keepAliveTime值才会起作用。也就说说,只有在线程池线程数量超出corePoolSize了。我们才会把超时的空闲线程给停止掉。否则就保持线程池中有 corePoolSize 个线程就可以了。
Unit:参数keepAliveTime的时间单位,就是 TimeUnit类当中的几个属性。
workQueue:用来存储待执行任务的队列,不同的线程池它的队列实现方式不同(因为这关系到排队策略的问题)比如有以下几种&#13;<br> ArrayBlockingQueue:基于数组的队列,创建时需要指定大小。&#13;<br> LinkedBlockingQueue:基于链表的队列,如果没有指定大小,则默认值是 Integer.MAX_VALUE。(newFixedThreadPool和newSingleThreadExecutor使用的就是这种队列)。&#13;<br> SynchronousQueue:这种队列比较特殊,因为不排队就直接创建新线程把任务提交了。(newCachedThreadPool使用的就是这种队列)。
threadFactory:线程工厂,用来创建线程。
Handler:拒绝执行任务时的策略,一般来讲有以下四种策略,&#13; (1) ThreadPoolExecutor.AbortPolicy 丢弃任务,并抛出 RejectedExecutionException 异常。&#13; (2) ThreadPoolExecutor.CallerRunsPolicy:该任务被线程池拒绝,由调用 execute方法的线程执行该任务。&#13; (3) ThreadPoolExecutor.DiscardOldestPolicy : 抛弃队列最前面的任务,然后重新尝试执行任务。&#13; (4) ThreadPoolExecutor.DiscardPolicy,丢弃任务,不过也不抛出异常。
线程池流程.png
队列为有界队列 ArrayBlockingQueue
在使用有界队列时,若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,&#13; 若大于corePoolSize,则会将任务加入队列,&#13; 若队列已满则在总线程数不大于maximumPoolSize的前提下,创建新的线程&#13; 若线程数大于maximumPoolSize,则执行拒绝策略。或其他自定义方式。
UseThreadPoolExecutor1.java
队列为是无界队列 LinkedBlockingQueue
与有界队列相比,除非系统资源耗尽,否则无界任务队列不存在任务入队失败的情况。当有新任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后就不会继续增加。若后续仍有新的任务加入,而没有空闲的线程,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会快速增长,直到耗尽系统内存。maximumPoolSize 在此无用
队列使用直接提交策略,SynchronousQueue
它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
Handler:拒绝执行任务时的策略
ThreadPoolExecutor.AbortPolicy 丢弃任务,并抛出 RejectedExecutionException 异常。
ThreadPoolExecutor.CallerRunsPolicy:该任务被线程池拒绝,由调用 execute方法的线程执行该任务。
ThreadPoolExecutor.DiscardOldestPolicy : 抛弃队列最前面的任务,然后重新尝试执行任务。
ThreadPoolExecutor.DiscardPolicy,丢弃任务,不过也不抛出异常。
自定义拒绝策略 实现RejectedExecutionHandler接口
MyRejected.java
生命周期
一个Executor的生命周期有三种状态,运行 ,关闭 ,终止 。
Executor创建时处于运行状态。
当调用ExecutorService.shutdown()后,处于关闭状态,isShutdown()方法返回true。
shutdown(),平滑的关闭线程池。(如果还有未执行完的任务,就等待它们执行完)。
shutdownNow()。简单粗暴的关闭线程池。(没有执行完的任务也直接关闭)。
所有已添加的任务执行完毕后,Executor处于终止状态,isTerminated()返回true。
如果Executor处于关闭状态,往Executor提交任务会抛出unchecked exception RejectedExecutionException。
Submit()和execute的区别
submit可以传入实现Callable接口和Runnable 接口的实例对象 而 execute只能传Runnable 接口对象
Submit有返回值
Disruptor
并发编程.4(四).ppt
Fork/join
并行执行任务框架,可以分隔任务汇总结果
类
RecursiveAction 用于没有返回结果的任务
RecursiveTask:用于有返回结果的任务
ForkJoinPool
进程与线程
进程
是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。每一个进程都有一个自己的地址空间,即进程空间或(虚空间)。进程空间的大小 只与处理机的位数有关,一个 16 位长处理机的进程空间大小为 216 ,而 32 位处理机的进程空间大小为 232 。
进程至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
单机系统中进程通信有 4 种形式:主从式,会话式,消息或邮箱机制,共享存储区方式。
主从式典型例子:终端控制进程和终端进程。
会话式典型例子:用户进程与磁盘管理进程之间的通信。
线程
在网络或多用户环境下,一个服务器通常需要接收大量且不确定数量用户的并发请求,为每一个请求都创建一个进程显然是行不通的,——无论是从系统资源开销方面或是响应用户请求的效率方面来看。因此,操作系统中线程的概念便被引进了。线程,是进程的一部分,一个没有线程的进程可以被看作是单线程的。线程有时又被称为轻权进程或轻量级进程,也是 CPU 调度的一个基本单位。
线程可以有效地提高系统的执行效率,但并不是在所有计算机系统中都是适用的,如某些很少做进程调度和切换的实时系统。使用线程的好处是有多个任务需要处理机处理时,减少处理机的切换时间;而且,线程的创建和结束所需要的系统开销也比进程的创建和结束要小得多。最适用使用线程的系统是多处理机系统和网络系统或分布式系统。
线程的执行特性。
线程只有 3 个基本状态:就绪,执行,阻塞。
线程存在 5 种基本操作来切换线程的状态:派生,阻塞,激活,调度,结束。
关系
进程的执行过程是线状的,尽管中间会发生中断或暂停,但该进程所拥有的资源只为该线状执行过程服务。一旦发生进程上下文切换,这些资源都是要被保护起来的。这是进程宏观上的执行过程。而进程又可有单线程进程与多线程进程两种。进程有 一个进程控制块 PCB ,相关程序段 和 该程序段对其进行操作的数据结构集 这三部分,单线程进程的执行过程在宏观上是线性的,微观上也只有单一的执行过程;而多线程进程在宏观上的执行过程同样为线性的,但微观上却可以有多个执行操作(线程),如不同代码片段以及相关的数据结构集。线程的改变只代表了 CPU 执行过程的改变,而没有发生进程所拥有的资源变化。出了 CPU 之外,计算机内的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。与进程控制表和 PCB 相似,每个线程也有自己的线程控制表 TCB ,而这个 TCB 中所保存的线程状态信息则要比 PCB 表少得多,这些信息主要是相关指针用堆栈(系统栈和用户栈),寄存器中的状态数据。进程拥有一个完整的虚拟地址空间,不依赖于线程而独立存在;反之,线程是进程的一部分,没有自己的地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。
工具类
Concurrent.util
CyclicBarrier
初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续
CyclicBarrier就象它名字的意思一样,可看成是个障碍, 所有的线程必须到齐后才能一起通过这个障碍
CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行
UseCyclicBarrier.java
CountDownLacth
用于监听某些初始化操作,等其初始化完成后通知主线程操作
类似 wait/notify 功能 但 countdownlacth是实时的 而wait/notify 则必须等notify线程释放锁后才可以
UseCountDownLatch.java
Callable和Future
实现Future模式
funture.get();获取Future的执行结果 可以设置超时时间
UseFuture.java
semaphore 信号量
相关概念
pv 页面刷新一次记一次
UV 一台电脑一天只记录一次
QPS 每秒查询数
RT 请求响应时间
应用场景
Timer
new Timer()
TimerTask
TimerTask任务为顺序执行,如果一个任务执行时间较长则其他的会延迟执行
cancel() 将自身从任务中清除
schedule(TimerTask task,Date firstTime,Long period)
在指定日期后,按指定的间隔周期性的循环执行
schedule(TimerTask task,Long delay)
当前时间后,按指定的毫秒数后执行任务
注意点
生命周期
新建 new()
就绪 start() run())
运行
阻塞
死亡
锁对象
阻塞和释放
数据可见与同步
通信
安全性
原子性
atomic类
竞争激励时能维持常态,比lock性能好,只能同步一个值
synchronized
不可中断锁,适合竞争不激烈,可读性好
lock
可中断锁,多样化同步,竞争激烈时能维持常态
可见性
导致共享变量在线程间不可见的原因
线程交叉执行
重排序结合线程交叉执行
共享变量更新后的值没有在工作内存中与主存间同步
synchronized
线程解锁前,必须把共享变量的最新值刷新到主内存
线程加锁时,将清空工作内存中共享变量的值,从而时用共享变量时需要从主内存中重新读取最新的值
volatile
通过加入内存屏障和禁止重排序优化来是实现
写操作时,会在写操作后加入一条store屏障指令,将本低内存中的共享变量值刷新到主内存
读操作时,加入一条load屏障指令,从主内存中读取共享变量
写.png
读.png
有序性
Java内存模型,允许编译器和处理器对指令进行重排序,但重排序过程不会影响单线程程序的执行,却会影响多线程并发执行的正确性
happends-before
分类
读写锁
重入锁
悲观锁乐观锁
CAS
缺点
1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
关于ABA问题参考文档: http://blog.hesey.NET/2011/09/resolve-aba-by-atomicstampedreference.html
2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
2、比较花费CPU资源,即使没有任何争用也会做一些无用功。
3、会增加程序测试的复杂度,稍不注意就会出现问题。
设计模式
Future模式
该模型是将异步请求和代理模式联合的模型产物
流程
客户端发送一个长时间的请求,服务端不需等待该数据处理完成便立即返回一个伪造的代理数据(相当于商品订单,不是商品本身),用户也无需等待,先去执行其他的若干操作后,再去调用服务器已经完成组装的真实数据。该模型充分利用了等待的时间片段
Future模型图.png
核心结构
Main:启动系统,调用Client发出请求;&#13; Client:返回Data对象,理解返回FutureData,并开启ClientThread线程装配RealData;&#13; Data:返回数据的接口;&#13; FutureData:Future数据,构造很快,但是是一个虚拟的数据,需要装配RealData;&#13; RealData:真实数据,构造比较慢
future核心结构.png
注意事项
FutureData是对RealData的包装,是dui真实数据的一个代理,封装了获取真实数据的等待过程。它们都实现了共同的接口,所以,针对客户端程序组是没有区别的;
客户端在调用的方法中,单独启用一个线程来完成真实数据的组织,这对调用客户端的main函数式封闭的;
因为咋FutureData中的notifyAll和wait函数,主程序会等待组装完成后再会继续主进程,也就是如果没有组装完成,main函数会一直等待。
MasterWorker模式
生产者-消费者
0 条评论
下一页