一、volatile 是java虚拟机提供的轻量级的同步机制;有三大特性: 1.保证可见性;2.不保证原子性;3.禁止指令重排(volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象)
二、JMM(java内存模型java Memory Model) 特性:可见性,原子性,有序性
在变量前加上volatile,一个线程对这个变量进行修改,就及时通知其他线程,主物理内存的变量值已经被修改,其他线程的变量值就修改了
本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
1、线程解锁前,必须把共享变量的值刷新回主内存;
2、线程加锁前,必须读取主内存的最新值到自己的工作内存;
3、加锁解锁是同一把锁;
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(或称栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,线程间的通信必须通过主内存来完成。
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,分三种:1、单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致; 2、处理器在进行重排序时必须要考虑指令之间的数据依赖性; 3、多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
源代码 -->编译器优化的重排 -->指令并行的重排 --> 内存系统的重排 -->最终执行的指令
内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个: 1、保证特定操作的执行顺序; 2、保证某些变量的内存可见性(利用该特性实现volatile的内存可见性) 通过插入内存屏障就禁止在内存屏障前后的指令执行重排序优化,内存屏障另一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本(保证可见性)
DCL(双端检锁)机制不一定线程安全,原始是有指令重排序的存在(某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。),加入volatile可以禁止指令重排 指令重排只会保证串行语义的执行的一致性(单线程),但并不会关系多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
CAS的全称为 Compare-And-Swap,它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
总结:CAS比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。
CAS应用:CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS并发原语体现在Java中的sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会实现出CAS汇编指令,完全依赖于硬件的功能。并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
1、Unsafe 是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
2、变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3、变量value用volatile修饰,保证了多线程之间的内存可见性
1、循环时间长,开销大 2、只能保证一个共享变量的原子操作 3、引出ABA问题
CAS --->Unsafe --->CAS底层** --->ABA ---> 原子引用更新 ---> 如何规避ABA问题
CAS会导致“ABA问题” CAS算法实现一个重要前提需要提取内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说:一个线程one从内存位置v取出A,这时候另一个线程two也从内存取出A,并且线程two进行了一些操作将值变为B,然后线程two又将V位置的数据变为A,这个时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但不代表这个过程就是没有问题的。
--理解原子引用+新增一种机制,就是修改版本号(类似时间戳)--
HashSet底层是HashMap, set在使用add方法时(实际使用map.put方法),之所以只用传一个参数,是因为传入的值被当作key,而value是一个默认的PRESENT的object。CopyOnWriteArraySet底层是CopyOnWriteArrayList
解决map线程不安全,可以用ConcurrentHashMap; 之所以不安全,是因为add方法没有加锁 常见异常:java.util.ConcurrentModificationException
java.util.ConcurrentModificationException
① 使用vector,加了锁,并发性下降: new vector<>();
② 使用Collections.synchronizedList(new ArrayList<>());
③ 使用 new CopyOnWriteArrayList<>();
CopyOnWrite容器即写时复制的容器,往一个容器添加元素时,先将当前容器进行copy,复制新的容器object[]newElements,然后新的容器里添加元素,添加完成后再将原容器的引用指向新的容器,这样的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的**,读和写不同的容器;
指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象(有一个线程一个锁也没有获取到)。
并发包中ReentrantLock的创建可以指定构造函数的Boolean类型来得到公平锁或非公平锁,默认是非公平锁。
公平锁:就是公平,在并发环境中,每个线程在获取锁时会先查看此锁维护并等待队列。
非公平锁:上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
非公平锁的优点在于吞吐量比公平锁大,对于Synchronized而言,也是一种非公平锁。
指同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说线程可以进入任何一个它已经拥有的锁所同步着的代码块。 ReentrantLock/Synchronized就是一个典型的可重入锁,可重入锁最大的作用是避免死锁。(加锁几次,解锁几次,程序不会报错,解锁少一次程序就会卡死)
指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU.(CAS就是自旋锁)
//Unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
int var5
do{
var5 = this.getIntVolatile(var1, var2);
}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));
return var5
}
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁。
共享锁:指该锁可被多个线程所持有。对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。读锁、共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
CountDownLatch:让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒; CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器值为0时,因调用await方法被阻塞的线程就会被唤醒,继续执行。
CyclicBarrier:字面意思是可循环(Cylic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。(与CountDownLatch相反)
Semaphore:信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
ArrayBlockingQueue:是一个基于数组结构的有界限阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
阻塞队列:
顾名思义,首先它是一个队列,一个阻塞队列在数据结构作用如下图:
当阻塞队列是空时,从队列获取元素的操作将会被阻塞(试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素);
当阻塞队列是满时,往队列添加元素的操作将会被阻塞(试图往已满的阻塞队列添加新元素的线程同样也会被阻塞,直到其他的线程从队列删除一个或多个元素或者清空队列,使队列变得空闲后新增)。
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都一手包办了。不需要兼顾效率和线程安全。
种类分析:
ArrayBlockingQueue:有数组结构组成的有界阻塞队列;
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列;
PriorityBlockingQueue:支持优先级排序的无界阻塞队列;
DelayQueue:使用优先级队列实现的延迟无界阻塞队列;
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列;
LinkedTransferQueue:由链表结构组成的无界阻塞队列;
LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
多线程的判断用while判断,用if会出现虚假唤醒现象。
1、原始构成:
synchronized是关键字属于JVM层面,monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor)
Lock是具体类(Java.util.concurrent.locks.lock)是api层面的锁.
2、使用方法:
synchronized 不需要用户去手动释放锁,当synchronized代码执行完成后系统会自动让线程释放对锁的占用。
ReentrantLock则需要手动释放锁,若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和unLock()方法配合try/finally语句块来完成。
3、等待是否可中断:
synchronized不可中断,除非抛出异常或者正常运行完成
ReentrantLock可中断:①设置超时方法trylock(long timeout,TimeUnit unit)。②lockInterruptibly()放代码块中,调用interrupt()方法可中断
4、加锁是否公平:
synchronized非公平锁
ReentrantLock两者都可以,默认非公平锁,构造方法可以传入Boolean值,true为公平锁,false为非公平锁。
5、锁绑定多个条件Condition
synchronized没有
ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程,要么全部唤醒。
线程池做的工作主要是控制运行线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量的线程,超出的线程就要排队等候,等待其他线程执行完毕,再从队列中取出任务来执行。
线程池主要特点或优势:线程复用,控制最大并发数量,管理线程
或者:
①:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
②:提高响应速度。当任务到达时,任务可以不需要等到线程创建,能立即执行。
③:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。(底类是ThreadPoolExecutor)
①继承线程类,②使用Runable接口(没有返回值,不抛异常),③使用Callable接口(有返回值,会抛异常),④使用线程池
1.Callable规定的方法是call(),而Runnable规定的方法是run().
2.Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
3.call() 方法可抛出异常,而run() 方法是不能抛出异常的。
4.运行Callable任务可拿到一个Future对象, Future表示异步计算的结果。
它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
5.通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。
6.Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。
Executors.newFixedThreadPool(),自己写开多少个线程,常用于执行长期的任务,性能好很多
Executors.newSingleThreadExecutor只开启一个线程,常用于一个任务一个任务执行的场景
Executors.newCachedThreadPool(),系统自己决定开多少线程。常用于执行很多短期异步的小程序或者负载较轻的服务。
corePoolSize:线程池中的常驻核心线程数;
maximumPoolSize:线程池能同时执行的最大线程数,必须大于等于1;
keepAliveTime:多余的空闲线程存活时间(只有当线程池中的线程数大于corePool Size时,才会起作用);
unit:keepAliveTime的单位
workQueue:任务队列,被提交但尚未被执行的任务;
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般默认的即可;
handler;拒绝策略,当队列满了且工作线程大于等于最大线程数时如何来拒绝请求执行的Runable请求策略
拒绝策略:AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行;
CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常;而是将某些任务回退到调用者。(如果是main线程调用线程池,则线程池任务队列满了后,某些任务会由main线程处理。)
DiscardOldestPolicy:抛出队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交,
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。(如果允许任务丢失,这是最好的策略)
答:都不用。线程池通过ThreadPoolExecutor方式创建。 Executors返回的线程池对象的弊端如下: FixedThreadPool和SingleThreadPool:允许的请求队列长度为Inter.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。 CachedThreadPool和ScheduleThreadPool: 允许的创建线程数量为Inter.MAX_VALUE,可能会堆积大量的线程,从而导致OOM。
CPU密集型:该任务需要大量的运算,没有阻塞,CPU一直运行,CPU密集型任务配置尽可能少的线程数量,CPU核数+1个线程的线程池。
IO密集型:由于不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2;如果大部分线程都阻塞,故需要多配置线程数:CPU核数/1-阻塞系数,阻塞系数在0.8~0.9之间。
指两个或两个以上的进程执行过程中,因争夺资源而造成的一种互相等待的现象。
终端输入jps -l,查看java进程的编号,再使用jstack 进程编号
步骤:先用top命令找到CPU占比最高的;ps -ef 或者jps进一步定位,得知是一个怎么样的后台程序;定位到具体线程或代码(ps -mp 进程 -o THREAD,tid,time;参数解释:-m 显示所有的线程, -p pid进程使用CPU的时间, -o 该参数后是用户自定义格式);将需要的线程ID转换为16进制格式(英文小写格式),然后printf"%x\n" 有问题的线程id;jstack 进程ID | grep tid(16进制线程ID小写英文)
相同点: 都是访问资源的令牌,都可以记录用户的信息。
不同点:token需要查库验证token是否有效。而JWT不用查库或者少查库,直接在服务端进行校验,因为用户的信息及加密信息在第二部分payload和第三部分签证中已经生成,只要在服务段进行校验就行。
token验证流程:
1.把用户的账号密码发到后端;
2.后端进行校验,校验成功生成token,把token发送到客户端;
3.客户段自己保存token,再次请求就要在http协议的请求头中带着token去访问服务端,和在服务端保存的token信息进行比对校验;
JWT验证流程:
- 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串
- 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串
- 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。
- 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。
指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录。包括单点登录和单点注销。
sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。
用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数;sso认证中心发现用户未登录,将用户引导至登录页面;用户输入用户名密码提交登录申请;sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌。sso认证中心带着令牌跳转会最初的请求地址(系统1),系统1拿到令牌,去sso认证中心校验令牌是否有效,sso认证中心校验令牌,返回有效,注册系统1,系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源。
用户访问系统2的受保护资源,系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数,sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌,系统2拿到令牌,去sso认证中心校验令牌是否有效。sso认证中心校验令牌,返回有效,注册系统2,系统2使用该令牌创建与用户的局部会话,返回受保护资源。
用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系:
局部会话存在,全局会话一定存在;
全局会话存在,局部会话不一定存在;
全局会话销毁,局部会话必须销毁。
通常而言,微服务架构是一种架构模式,或者说一种架构风格。它提倡将单一的应用程序划分成一组小的服务,彻底地去耦合,每个服务运行在其独立的进程内,服务之间互相协调,互相配置.
优点:
每个服务足够内聚,足够小,易理解,松耦合,能使用不同的语言开发,易于和第三方集成。
缺点:
开发人员要处理分布式系统的复杂性,多服务运维难度,随着服务的增加,运维压力也在增大,系统部署依赖,服务通信成本。
微服务条目 | 落地技术 |
---|---|
服务开发 | SpringBoot,Spring,SpringMVC |
服务配置与管理 | NetFlix公司的Archaius,阿里的Diamond |
服务注册与发现 | Eureka,Consul,Zookeeper |
服务调用 | Rest,RPC,gRPC |
服务熔断器 | Hystix,Envoy |
负载均衡 | Ribbon,Nginx |
服务接口地调用(客户端调用服务的简化工具) | Feign |
消息队列 | Kafka,RabbitMQ,ActiveMQ |
服务配置中心管理 | SpringCloudConfig,Chef |
服务路由(API网关) | Zuul |
服务监控 | Zabbix,Nagios,Metrics,Specatator |
全链路追踪 | Zipkin,Brave,Dapper |
服务部署 | Docker,OpenStack,Kubernetes |
数据流操作开发包 | SpringCloud Stream(封装与Redis,Rabbit,Kafka等发送接受消息) |
事件消息总线 | SpringCloud Bus |