夜光序言:
两个人在一起久了,就象左手和右手,即使不再相爱也会选择相守,因为放弃这么多年的时光需要很大的勇气。也许生命中会出现你爱的人,但那终归是过客,你还是会牵着你的左手或者右手一直走下去。——幸福有时候真的与爱情无关。
正文:嗯唔,都是一些必会知识噢~~
一、并发编程
1.线程安全:
当多个线程访问某一个类(对象)时这个类始终都能表现出正确的行为,那么这个类(对象和方法)就是线程安全的。
2.synchronized:
可以在任意对象以及方法上加锁,而加锁的这段代码称为"互斥区"或者"临界区"。
一个线程想要执行synchronized修饰的方法里的内容,首先是尝试获得锁,如果拿到锁,执行synchronized方法体里面的内容
如果拿不到那么这个线程会不断的尝试获得这把锁,直到拿到为止,而且是多个线程去竞争这把锁。
3.多个线程多个锁:
多个线程,每个线程都将可以拿到自己指定的锁,分别获得锁之后,执行synchronized方法体的内容,
关键字synchronized获得的锁都是对象锁,而不是把一段代码(方法)当做锁,在静态方法上机上synchronized获得的锁为类级别的锁,表示锁定类。
4.对象锁的同步和异步:
同步synchronized:同步就是共享,同步的目的是为了线程安全,对于线程安全需要满足两个特性:原子性(同步)、可见性。
异步asynchronized:异步就是独立,相互之间不受任何制约。
5.脏读:
对于对象的同步和异步方法,我们在设计程序的时候,一定要考虑问题的整体,不然就会出现数据不一致错误,很经典的错误就是脏读(dityread)。
在我们对一个对象的方法加锁的时候,需要考虑业务的整体性,即为setValue和getValue方法同时加锁synchronized同步关键字
保证(service)业务逻辑层的原子性,不然会出现业务逻辑错误。
6.synchronized锁重入:
关键字synchronized拥有重入锁的功能,也就是在使用synchronized时,当一个线程得到一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。
7.出现异常,锁自动释放:
对于web应用程序,异常释放锁的情况,如果不及时处理,很可能对应用程序业务逻辑产生严重的错误。
比如:现在执行一个队列任务,很多对象都去在等待一个对象正确执行完毕再释放锁,
但是一个对象出现由于异常的出现,导致业务逻辑没有正常执行完毕,就是释放了锁,那么后面的对象执行的都是错误的业务逻辑。
8.synchronized代码块:
使用synchronized声明的方法在某些情况下是有弊端的,比如:A线程方法调用一个时间很长的任务,那么B线程
必须等待很长的时间才能执行,这样的情况下可以使用synchronized代码块去优化代码执行时间,也就是减小锁的时间。
synchronized可以使用任何object对象进行加锁,用法比较灵活,不要使用string的常量加锁,会出现死循环问题。
9.锁对象改变的问题:
当使用一个对象进行加锁的时候,要注意对象本身发生改变的时候,那么持有的锁就不同,如果对象不发生改变,那么依然是同步的,即使是对象的属性发生了改变。
10.volatile关键字:
volatile的作用主要是使变量在多个线程间可见,在java中每一个线程都有一块工作内存区,其中存放着所有线程共享的变量值的拷贝,
当线程执行时,在自己的工作内存区中操作这些变量,为了存取一个共享的变量,一个线程通常先获取锁定并去清除它的工作内存区,
把这些共享变量从所有线程的共享内存区中正确的装入到他所在的工作内存区中,当线程解锁时保证该工作内存区中变量的值写回到共享内存中。
一个线程可以执行的操作有:使用(use)、赋值(assign)、装载(load)、存储(store)、锁定(lock)、解锁(unlock)。
主线程可以执行的操作有:读(read)、写(write)、锁定(lock)、解锁(unlock),每个操作都是原子的。
volatile的作用是强制线程到主内存(共享内存)里去读取变量,而不去工作内存区读取,从而实现了多个线程间变量可见,
也就是满足线程安全的可见性。
11.volatile关键字的非原子性:
volatile关键字虽然有多个线程间的可见性,但不具备同步性(原子性),可以算的上是一个轻量级的synchronized,
性能比synchronized强很多,不会造成阻塞,volatile只针对于多个线程可见的变量操作,并不能代替synchronized的同步功能。
如要实现原子性使用atomic类的系列对象,支持原子性操作,atomic只支持本身方法原子性,并不保证多个线程的原子性。
12.多线程通信wait和notify:
线程是操作系统中独立的个体,但这些个体不经过处理就不能成为一个整体,线程间的通信就成为整体的比用方法之一,
当线程存在线程指挥,系统间的交互性会更强大,在提高CPU利用率的同时还会使开发人员对线程任务在处理的过程中进行有效的把控和监督。
使用wait和notify方法实现线程间的通信,这两个方法都是object类的方法,java为所有的类都提供了这两个方法,wait和notify都
必须配合synchronized关键字使用,wait方法释放锁,notify不释放锁。
13.使用wait和notify模拟queue:
blockingqueue:它是一个队列,并且支持一个阻塞的机制,阻塞的放入和得到数据, 要实现linkedblockingqueue的方法是put()和take(),
put(anobject):把anObject放入blockingqueue,如果blockingqueue没有空间,则调用此方法的线程被阻断,直到blockingqueue里面有空间再继续。
take:取走blockingqueue里排在首位的对象,如果blockingqueue为空,阻断进入等待状态,直到blockingqueue里有新的方法加入。
14.ThreadLocal:
线程局部变量,是一种并发访问变量的解决方案,与其synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用空间换时间的手段,
为每个线程提供变量的独立副本,以保证线程安全。从性能上说,不具有绝对的优势,在并发不是很高的时候加锁的性能会更好,
但作为一套完全与锁无关的线程安全解决方案,在高并发量或者激烈的场景,用ThreadLocal可以在一定程度上减少锁的竞争。
15.单例和多线程:
单例模式最常见的是饿汉模式和懒汉模式,一个直接实例化对象,一个在调用方法时进行实例化对象,在多线程的模式中,考虑到性能和线程安全问题,
一般选择最近经典的两种单例模式,在性能提高的同时,又保证了线程安全。
17.同步类容器:
同步类容器都是线程安全的。但在某些场景下加锁来保护复合操作,复合操作如:迭代(反复访问元素,遍历完容器中所有的元素)、
跳转(根据指定的顺序找到当前元素的下一个元素),以及条件的运算,
这些复合操作在多线程并发的修改容器时,可能会表现出意外的行为,最经典的便是concurrentmodificationException,原因是当容器迭代的过程中,
被并发修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题。
同步类容器:如:古老的vetor、hashtable这些容器的同步功能都是JDK的conllections.synchronized等工厂方法去创建实现的,
其底层的机制是传统的synchronized关键字对每个公用的方法都进行同步,使得每次只有一个线程访问容器的状态,这很明显不能满足互联网时代高并发的需求,在保证线程安全的同时,也必须有足够好的性能。
18.并发类容器:
JDK1.5以后提供了多种并发类容器来替代同步类容器,从而来改善性能。同步类容器的状态都是串行化的,他们虽然实现了线程安全,但是严重降低了并发性,严重降低了应用程序的吞吐量,
并发类容器是专门针对并发设计的,使用concurrentMap来代替给予散列的传统的hashtable,而且在concurrentMap中,添加了一些常见复合操作的支持,
以及使用了CopyOnWriteArrayList代替voctor,并发CopyOnWriteArraySet,以及并发的Queue,concurrentLinkedQueue和LinkedBlockingQueue,前者是高性能的队列,
后者是以阻塞形式的队列,具体实现Queue还有很多,例如:ArraListBlockingQueue、priorityBlockingQueue、synchronousQueue等。
19.concurrentMap:
concurrentMap接口有两中重要的实现:concurrentHashMap和concurrentSkipListMap,concurrentSkipListMap支持并发排序功能,弥补concurrentHashMap,
concurrentHashMap内部使用段(segment)来表示不同的部分,每个段是一个小的hashtable,它们有自己的锁,只要多个修改操作发生在不同的段上就可并发执行,
把一个整体分成16个段(segment),也就是最高支持16个线程的并发修改操作,这也是在多线程场景时减少锁的粒度,从而降低了锁竞争的一种方案,
并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能好。
20.CopyOnWrite容器:
简称COW,是一种程序设计的优化策略,JDK里的COW容器有两种:CopyOnWriteArrayList和CopyOnWriteArraySet,COW非常有用,可以在非常多的应用场景下使用到,
CopyOnWrite容器是写时复制的容器,通俗的理解就是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将容器进行copy,复制一个新的容器,
然后在新的容器里添加元素,添加完元素后再原容器的引用指向新的容器,这样做的好处是可以对CopyOnWrite容器进行并发的读,,而不需要加锁,因为当前容器不会添加任何元素,
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。CopyOnWrite在读多写少的应用场景下使用。
21.并发Queue:
在并发队列上JDK提供了两套实现,一个是以concurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列,它们都继承Queue。
concurrentLinkedQueue:是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常concurrentLinkedQueue性能高于BlockingQueue,它是一个基于链接节点的无界线程安全队列,该队列的元素遵循先进先出的原则,头是最先加入的,尾是最近加入的,该队列不允许null元素。
concurrentLinkedQueue重要方法:add()和offer()都是加入元素的方法,在concurrentLinkedQueue中这两种方法没有任何区别,poll()和peek()都是取头元素节点,区别在于poll()方法会删除元素,peek()则不会。
22.BlockingQueue接口:
1.ArrayBlockingQueue:
基于数组阻塞队列的实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没有实现读写分离,也就是意味着生产者和消费者不能同时并行,
长度是需要定义的,可以指定先进先出或者先进后出,也叫有界队列,在很多场合适合使用。
2.LinkedBlockingQueue:
基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓存队列,该队列由一个链表构成,LinkedBlockingQueue之所以能够高效的处理并发数据,是因为其内部实现了分离锁,
从而实现生产者和消费者可以并行执行,它是一个无界队列。
3.priorityBlockingQueue:
基于优先级的阻塞队列,优先级的判断通过构造函数传入的compator对象来决定,也就是说传入队列的对象必须实现compatible接口,在实现priorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,它也是一个无界队列。
4.DelayQueue:
带有延迟时间的Queue,其中的元素只有当其指定延迟的时间到了才能够从该队列中获取到该元素,DelayQueue中的元素必须实现Delayed接口,
DelayQueue是一个没有大小限制的队列,应用场景很多,比如:对缓存缓存超时的数据进行移除,任务超时处理,空闲连接的关闭等。
5.synchronousQueue:
一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并且消费。
23.Deque双端队列:
Deque允许在队列的头部和尾部进行出队和入队操作,LinkedBlockingDeque是一个线程安全的双端队列实现,也是最为复杂的一个队列,在内部实现维护了前端和后端的节点,
但是没有实现读写分离,因此同一时间只能有一个线程对其操作,在高并发中性能低于其他BlockingQueue,更低于concurrentLinkedQueue,
在JDK早起有一个非线程安全的Deque是ArrayDeque,JDK1.6里添加了LinkedBlockingDeque来弥补了多线程场景下线程安全的问题。
24.多线程的设计模式:
并行设计模式属于设计优化的一部分,它是对一些常用的多线程结构的总结和抽象,与串行程序相比,并行程序的结构更为复杂,因此合理的使用并行模式在多线程开发中更具有意义,主要学习
Future、Master-Worker和生产者-消费者模型。
25.Future模式:
Future模式类似于商品订单,比如网购的时候当看中某一件商品时,就可以提交订单,,当订单处理完成后,在家等待商品送货上门即可,或者更形象的,
我们发送AJAX请求的时候,页面是异步的进行后台处理,用户无需一直等待请求的结果,可以继续浏览或操作其他内容。
26.Master-Worker模式:
是常用的并行计算模式,核心思想是系统由两类进程协作工作,Master进程和Worker进程,Master负责接收和分配任务,Worker负责处理子业务,当各个Worker子进程处理完成后会将结果返回给Master,
由Master做归纳和总结,好处是能将一个大任务分解成若干个小任务并行执行,从而提高系统的吞吐量。
27.生产者-消费者模式:
生产者和消费者也是一种经典的多线程模式,在实际的开发中应用非常广泛的思想理念,在生产和消费模式中:通常由两类线程,即若干个生产者的线程和若干个消费者的线程,
生产者线程负责提交用户请求,消费者线程则负责处理生产者提交的任务,在生产者和消费者之间通过共享内存缓存区进行通信。
28.Executor框架:
为了更好的控制多线程,JDK提供了一套线程框架Executor帮助开发人员有效的进行线程控制,它们在java.util.concurrent包中,是JDK并发包的核心,其中一个比较重要的类:Executors,它扮演着线程工厂的角色,通过Executor可以创建特定功能的线程池,
Executors创建线程池的方法:1.newFixedThreadPoll(),该方法返回一个固定数量的线程池,该方法的线程数始终不变,当有一个任务提交时,若线程池空闲,则立即执行,若没有则会暂缓在一个任务队列中等待有空闲的线程去执行。
2.newSingleThreadExecutor(),创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
3.newCachedThreadPool(),返回一个可根据实际情况调整线程个数的线程池,不限制线程最大数量,若用空闲的线程则执行任务,若无任务则不创建线程,并且每一个空闲线程会在60秒后自动回收。
4.newScheduledThreadPool(),该方法返回一个ScheduledExecutorService对象,但该线程池可以指定线程的数量。
29.自定义线程池:
若Executors工厂无法满足我们的需求,可以自己创建自定义线程池,Executors工厂类中的创建线程方法其内部实现是用了ThrreadPoolExecutor类,这个类可以自定义线程。
30.自定义线程池详细说明:
构造方法对于队列是什么类型比较关键:
1.在使用有界队列时,若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,若大于corePoolSize,则会将任务加入队列,若队列已满,则在总线程数不大于maxmumPoolSize的前提下创建新的线程,若线程数大于maxmumPoolSize,则执行拒绝策略,或其他自定义方式。
2.无界队列任务时:LinkedBlockingQueue,与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况,当有新的任务到来,系统的线程数小于corePoolSize时则新建线程任务,当达到corePoolSize后就不会继续增加,若后续仍有新的任务加入,没有新的空闲线程资源,则任务直接进入队列等待,若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统资源。
3.JDK拒绝策略:
AbortPolicy:直接抛出异常组织系统正常工作。
CallerRunPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。
DiscardOldestPolicy:丢弃最老的一个请求,尝试再次提交当前任务。
DiscardPolicy:丢弃无法处理的任务,不给予任何处理。
如果需要自定义拒绝策略可以实现RejectedExecutionHandler接口。
31.concurrentUtil常用类:
1.CyclicBarrier使用:假设只有一个场景,每个线程代表一个运动员,当运动员都准备好后才一起出发,只要有一个没准备好,大家都得等待。
2.CountDownLacth使用:经常用于某些初始化操作,等初始化完成后,通知主线程继续工作。
3.callable和Future使用:Future模式适合在处理很耗时很长业务逻辑时进行使用,可以有效的减少系统的响应时间,提高系统吞吐量。
4.Semaphore信号量:在semaphore信号量非常适合高并发访问,新系统在上线之前,要对系统的访问量进行评估,这个值不是随便拍拍脑袋就能想出来的,是经过以往的经验、数据、历史的访问量,以及推广力度进行一个合理的评估,评估标准不能太大,也不能太小,太大投入的资源达不到实际效果,浪费资源,太小某个时间点一个高峰值的访问量可以直接压垮系统。
PV(page view):网站的总访问量,页面浏览量和点击量,用户每刷新一次就会被记录一次。
UV(unique visitor):访问网站的一台电脑客户端为一个访客,时间上以00:00-24:00之内相同的ip的客户端只记录一次。
QPS(query per second):即每秒查询数,qps很大程度上代表了系统业务上的繁忙程度,每次请求的背后可能对应着多次磁盘I/O,多次网络请求多个时间片等,通过qps可以非常直观的了解当前系统业务情况,一旦当前qps超过所设定的预警阀值,可以考虑增加机器对集群扩容,以免压过过大导致宕机,可以根据前期压力测试得到估值,在结合后期综合运维情况估算出阀值。
RT(response time):即请求响应时间,这个指标非常关键,直接说明前端用户的体验,因此任何系统设计师都想降低rt时间,还涉及cpu、内存、网络、磁盘等情况,更细节的问题如:select、update、delete/ps等数据库层面的设计。
5.容量评估:通过开发、运维、测试、以及业务相关人员综合出系统的一一系列阀值,然后根据关键阀值如:qps、rt等对系统进行有效的变更,一般来讲进行多轮测试以后可以对系统进行峰值评估,采用80/20原则,即80%的访问请求将在20%的时间内达到,这样就可以根据系统对应的PV计算出峰值qps。峰值qps:(总PV * 80%)/(60*60*24*20%)然后再将总的峰值qps除以单台机器所承受的最高的qps值等于所需要机器的数量:机器数=总的峰值qps/压测得出的单机极限qps。
6.Semaphore可以控制系统的流量:拿到信号量的线程可以进入,否则就等待,通过acquire()和release()方法获取和释放访问许可。
32.重入锁(ReenttrantLock):
重入锁在需要进行同步的代码部分加上锁定,但不要忘记最后一定要释放锁定,不然会造成锁永远无法释放,其他线程永远进不来的情况。
33.锁等待/等待通知:
在使用synchronized时,如果需要多线程之间进行协作工作则需要object的wait()、notify()和notifyAll()方法进行配合工作,同样在使用lock时可以使用一个新的等待/通知类,它就是condition,这个condition一定是针对具体的某一把锁,也就是在只有锁的基础之上才会产生condition。
34.多个condition:
我们可以通过Lock对象产生多个condition进行多线程间的交互,可以使得部分需要唤醒的线程唤醒,其他线程继续等待通知。