基础知识:
CPU多级缓存:CPU速度远大于内存速度。
CPU多级缓存的问题:
缓存一致性(MESI协议):用于保证多核CPU cache缓存共享数据的一致性问题。
乱序执行优化:处理器为提高运算速度而作出违背代码原有顺序的优化。(单核稳定,多核易错)
如何解决?=====》首先弄清部分概念:Java内存模型(Java Memory Model)
同步的八种操作和规则:
1.锁定(lock):作用于主内存的变量,把一个变量标识为一条线程独占状态。
2.解锁(unlock):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3.读取(read):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。
4.load(载入):作用于工作内存的变量,他把read操作从主内存中得到的变量值放入工作内存的变量副本中。
5.使用(use):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎。
6.赋值(assign):作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量。
7.存储(store):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
8.写入(write):作用于主内存的变量,把store操作从工作内存中一个变量的值传送到主内存的变量中。
规则:
1.如果把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作;如果把变量从工作内存中同步回主内存中,就要按顺序的执行store和write操作。但是Java内存模型只要求上述操作必须按照顺序执行,而没有保证必须是连续执行的。
2.不允许read和load、store和write操作之一单独出现。
3.不允许一个线程丢弃它最近assign操作,即变量在工作内存中改变了之后必须同步到主内存中。
4.不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存中同步回主内存中。
5.一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是一个变量实施use和store操作之前,必循先执行assign和load操作。
6.一个变量在同一时刻只允许一条线程对其进行lock操作,但是lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操纵,变量才能被解锁。lock和unlock必须成对出现。
7.如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。
8.如果一个变量事先没有被lock操作锁定,则不允许对它进行unlock操作,也不允许unlock一个被其他线程锁定的变量。
9.对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
并发的优势和风险:
优势1:速度:同时处理多个请求,响应更快;复杂的操作可以分成多个进程同时进行。
优势2:设计:程序设计在某些情况下可以有更多的选择。
优势3:资源利用:CPU能够在等待IO的时候做一些其他的事情。
风险1:安全性:多个线程共享数据可能会产生期望不相符的结果。
风险2:活跃性:某个操作无法继续执行下去会产生活跃性问题,比如死锁、饥饿等问题。
风险3:性能:线程过多使的CPU频繁切换,调度时间增多;同步机制;消耗过多内存
解决方案:
一、线程安全类:
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要额外的同步或者协同,这些类都能表现出正确行为,那么这个类是线程安全的。
体现在三个方面:
1.原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
原子性---Atomic包:
(1)AtomicXXX:CAS/Unsafe.compareAndSwapInt
public static AtomicInteger count=new AtomicInteger(0);
count.incrementAndGet();
该方法核心代码:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
//工作内存中的var2值和主内存中var5值不断比较,相同执行var5 + var4操作
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
(2)AutomicLong、LongAdder(区别)
LongAdder的高明之处可能在于将之前单个节点的并发分散到各个节点的,这样从而提高在高并发时候的效率。
(3)AtomicReference、AtomicReferenceFieldUpdater类====》更新数据
private static AtomicReference<Integer> count=new AtomicReference<Integer>(0);
//原子性更新类的某个实例指定的某个字段count,count需要特殊关键词volatile修饰
private static AtomicIntegerFieldUpdater<AtomicExample5> updater=
AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");
(4)AtmoicStampReference:CAS的ABA问题:变量值虽然相同,但是版本不同了。
(5)AtomicBoolean
//如何让某段代码只执行一次
private static AtomicBoolean ishappend=new AtomicBoolean(false);
原子性---锁:
synchronized:依赖JVM---不可中断锁,适合竞争不激烈,可读性好。
1.修饰代码块:大括号起来的代码,作用于调用的对象
2.修饰方法:整个方法,作用于调用的对象
-----子类无法继承父类的synchronized
3.修饰静态方法:整个静态方法,作用于所有的对象
4.修饰类:括号起来的部分,作用于所有对象
Lock:依赖特殊的CPU指令,代码实现,ReentrantLock---可中断锁,多样化同步,竞争激烈时能维持常态
Atomic:竞争激烈时能维持常态,比Lock性能好,只能同步一个值。
2.可见性:一个线程对内存的修改可以及时的被其他线程观察到。
导致共享变量在线程中不可见的原因:线程交叉执行;重排序结合线程交叉执行;共享变量更新后的值没有在工作内存与主存间及时更新。
可见性:
synchronized--JMM关于synchronized的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存中;
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
volatile--通过加入内存屏障和禁止重排序优化来实现
对volatile变量写操作时,会在写操作后加入一条store平渣指令,将本地内存中的共享变量值刷新到主内存中。
对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
---CPU指令级实现,并不是线程安全的,不具有原子性。
适合场景:状态标识量;doubleCheck(安全发布对象双重检测机制);
例子: volatile boolean inited=false;
//线程1:
context=loadContext();
inited=true;
//线程2:
while(!inited){
sleep();
}
doSomethingWithConfig(context);
3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响单线程程序的执行,却会影响到多线程并发执行的正确性。
Java先天有序性:happen...before原则
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
-----虽然java虚拟机会重排序,但是只是对数据不依赖的指令重排序,最后执行的顺序看起来就是代码书写的顺序;无法保证多线程执行的正确性。
2.锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
3.volatile变量原则:对一个变量的写操作先行发生于后面对这个变量的读操作。
4.传递原则:A在B前,B在C前,则A在C前。
5-8:线程启动原则、线程中断原则、线程终结原则、对象终结原则。
-----如果两个操作的执行次序无法从该八个原则中推导出来,就不能保证有序性,虚拟机可以对他们进行重排序。
二、安全发布对象:
发布对象:使一个对象能够被当前范围之外的代码所使用。
对象逸出:一个错误的发布。当一个对象还没有构造完成时,就使它被其他线程所见。
安全发布对象的四种方法(对应视频5-2-5.3):
1.在静态初始化函数中初始化一个对象引用。
2.将对象的引用保存到volatile类型域或者AtomicReference对象中。
3.将对象的引用保存到某个正确构造对象的final类型域中。
4.将对象的引用保存到一个由锁保护的域中。
example1: 懒汉模式,单例实例在第一次使用时创建===>线程不安全
example2: 饿汉模式,单例实例在类装载创建=>线程安全;不足:构造方法中有过多处理会使得类加载慢;如果不调用会造成资源浪费;
example3: synchronized使懒汉模式线程安全,但是不推荐
example4:懒汉模式+双重检测机制+同步锁===》线程不安全
example5:懒汉模式+双重检测机制+同步锁+volatile===》线程安全
example6: 饿汉模式,使用静态块
example7: 枚举模式:最安全的===》对象的创建放在枚举内
public class SingletonExample7 {
//私有构造函数
private SingletonExample7(){
}
//静态的工厂方法
public static SingletonExample7 getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
private SingletonExample7 singleton;
//jvm保证这个方法只会被调用一次
Singleton(){
singleton=new SingletonExample7();
}
public SingletonExample7 getInstance(){
return singleton;
}
}
三、不可变对象:
1.对象创建后其状态不能修改
2.对象所有域都是final类型
3.对象是正确创建的(创建期间,this引用没有逸出)
创建不可变对象的方法:
1.final(如果是引用型变量初始化以后不能指向另外一个对象,但是可以修改)
2.Collections.unmodifiableXXX:Collection、List、Set、Map...(Collections.unmodifiableMap处理后不可修改,如果修改执行会抛出异常)
3.Guava:ImmutableXXX:Collection、List、Set、Map...(生成对象初始化以后不可以修改,如果修改执行会抛出异常)
四、线程封闭:将对象封装到一个线程中来保证线程安全。
1.堆栈封闭:局部变量不会被多个线程共享,无并发问题。
2.ThreadLocal线程封闭:特别好的封闭方法;Map-->key对应线程,value对应对象。
ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。
1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。
2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。
4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。
ThreadLocal用法详解和原理:https://www.cnblogs.com/coshaho/p/5127135.html
总结和补充:
常见线程不安全类与写法:
1.StringBuilder---->StringBuffer(方法都使用了synchronized)
2.SimpleDateFormat---->JodaTime
3.ArrayList、HashSet、HashMap等Collections
4.先检查后执行:if(condition(a)){handle(a)}
五、线程安全---同步容器类(并不一定线程安全)
1.ArrayList--->Vector,Stack
2.HashMap--->HashTable(key和value不能为null)
3.Collections.synchronizedXXX(List、Set、Map)
缺陷:多数容器类都是非线程安全的,即使部分容器是线程安全的,由于使用sychronized进行锁控制,导致读/写均需进行锁操作,性能很低。
同步容器类详情:https://www.cnblogs.com/dolphin0520/p/3933404.html
六、线程安全---并发容器J.U.C
ArrayList-->CopyOnwriteArrayList(适合读多写少)
HashSet、TreeSet--->CopyOnWriteArraySet、ConcurrentSkipListSet
HashMap、TreeMap--->CopyOnWriteArrayMap、ConcurrentSkipListMap(不允许null)
并发容器J.U.C相对于同步器:https://www.cnblogs.com/daoqidelv/p/6753162.html
总结和补充:
安全共享对象策略:
1.线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改。
2.共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它。
3.线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它。
4.被守护对象:被守护对象只能通过获取特定的锁来访问。
J.U.C(java.util.concurrent)包:
核心:AQS(AbstractQueuedSynchronized)
AQS设计思想:
1.使用Node实现FIFO队列,可以用于构建或者其他同步装置的基础框架。
2.利用一个int类型表示状态。
3.使用方法是继承,子类通过继承并通过实现它的方法管理其状态,(acquire和release)的方法操纵状态。
4.可以同时实现排他锁和共享锁模式(独占和共享)。
AQS同步组件:CountDownLatch、Semaphore、CyclicBarrier、ReentrantLock、Condition、FutureTask
CountDownLatch:
CountDownLatch是一个同步工具,它主要用线程执行之间的协作。CountDownLatch 的作用和 Thread.join()方法类似,让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。在直接创建线程的年代(Java 5.0 之前),我们可以使用 Thread.join()。在线程池出现后,因为线程池中的线程不能直接被引用,所以就必须使用 CountDownLatch了。
实现原理:计数器的值由构造函数传入,并用它初始化AQS的state值。当线程调用await方法时会检查state的值是否为0,如果是就直接返回(即不会阻塞);如果不是,将表示该节点的线程入列,然后将自身阻塞。当其它线程调用countDown方法会将计数器减1,然后判断计数器的值是否为0,当它为0时,会唤醒队列中的第一个节点,由于CountDownLatch使用了AQS的共享模式,所以第一个节点被唤醒后又会唤醒第二个节点,以此类推,使得所有因await方法阻塞的线程都能被唤醒而继续执行。
Semaphore:
Semaphore是一个计数信号量,它的本质是一个”共享锁”。
信号量维护了一个信号量许可集。线程可以通过调用acquire(),来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。线程可以通过release()来释放它所持有的信号量许可。
适用场景:例如数据库连接的数量有限,远低于并发访个数。
CyclicBarrier:
CyclicBarrier字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CountDownLatch和CyclicBarrier的区别:
CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待,而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。
而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。
CountDownLatch 是计数器, 线程完成一个就记一个, 就像 报数一样, 只不过是递减的.
而CyclicBarrier更像一个水闸, 线程执行就想水流, 在水闸处都会堵住, 等到水满(线程到齐)了, 才开始泄流.
ReentrantLock:
synchronized是jvm层面的锁,是不会引发死锁的。ReentrantLock需要加锁和解锁,有死锁可能。
在JDK5.0版本之前,重入锁的性能远远好于synchronized关键字,JDK6.0版本之后synchronized 得到了大量的优化,二者性能也不分伯仲,但是重入锁是可以完全替代synchronized关键字的。除此之外,重入锁又自带一系列高逼格:可中断响应、锁申请等待限时、公平锁。另外可以结合Condition来使用。
适用场景:
1.只有少量线程的时候适合Synchronized
2.线程不少,但是增长可以预估的适合ReentrantLock
Condition:
Condition的作用是对锁进行更精确的控制。Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是Object中的wait(),notify(),notifyAll()方法是和"同步锁" (synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。
FutureTask:
在Java中一般通过继承Thread类或者实现Runnable接口这两种方式来创建多线程,但是这两种方式都有个缺陷,就是不能在执行完成后获取执行的结果,因此Java1.5之后提供了Callable和Future接口,通过它们就可以在任务执行完毕之后得到任务的执行结果。
详见: http://www.importnew.com/25286.html
Fork/Join框架详解:
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork/Join框架要完成两件事情:
1.任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割。
2.执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
详见: https://www.cnblogs.com/senlinyang/p/7885964.html
BlockingQueue:
在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的 多线程程序带来极大的便利。多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。
详见:https://www.cnblogs.com/KingIceMou/p/8075343.html
线程池:
new Thread()弊端:
1.每次new Thread 新建对象,性能差。
2.线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM(Out Of Memory)
3.缺少更多功能,如更多执行、定期执行、线程中断。
线程池的好处:
1.重用存在的线程,减少对象创建、消亡的开销,性能佳。
2.可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。
3.提供定时执行、定期执行、单线程、并发数控制等功能。
线程池类:ThreadPoolExecutor
1.corePoolSize:核心线程数量
2.maxmumPoolSize:线程最大线程数
3.workQueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响。
4.keepAliveTime:线程没有任务执行时最多保持多久时间终止
5.unit:keepAliveTime时间单位
6.threadFactory:线程工厂,用来创建线程
7.rejectHandler:当拒绝处理任务时的策略
ThreadPoolExecutor提供的方法:
基础方法:
1.execute():提交任务,交给线程池执行
2.submit():提交任务,能够返回执行结果=execute+Future
3.shutdown():关闭线程池,等待任务都完成
4.shutdownNow():关闭线程池,不等待任务执行完
监控方法:
1.getTaskCount():线程池已执行和未执行的任务总数。
2.getCompletedTaskCount():已完成的任务数量
3.getPoolSize():线程池当前的线程数量。
4.getPoolSize():线程池当前的线程数量
5.getActiveCount():当前线程池中正在执行任务的线程数量。
线程池---Executor框架接口:
1.Executors.newCachedThreadPool
2.Executors.newFixedThreadPool
3.Executrors.newSingleThreadPool
4.Executors.newScheduledTreadPool
多线程扩展知识:
多线程产生死锁的条件:互斥条件、请求和保持条件、不剥夺条件、环路等待条件
多线程并发最佳实践:
1.使用本地变量
2.使用不可变类
3.最小化锁的作用于范围:s=1/(1-a+a/n)---->阿姆达尔公式
阿姆达尔曾致力于并行处理系统的研究。对于固定负载情况下描述并行处理效果的加速比s,阿姆达尔经过深入研究给 出了S=1/(1-a+a/n)其中,a为并行计算部分所占比例,n为并行处理结点个数。这样,当1-a=0时,(即没有串行,只有并行)最大加速比s=n;当a=0时(即只有串行,没有并行),最小加速比s=1;当n→∞时,极限加速比s→ 1/(1-a),这也就是加速比的上限例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。
4.使用线程池的Executor,而不是直接new Thread执行
5.宁可使用同步也不要使用线程的wait和notify
6.使用BlockingQueue实现生产-消费模式
7.使用并发集合而不是加了锁的同步集合
8.使用Semaphore创建有界的访问
9.宁可使用同步代码块也不要使用同步方法
10.避免使用静态变量
Spring与线程安全:https://blog.csdn.net/qq_38306026/article/details/79220911
HashMap和ConcurrentHashMap的原理、区别