package com.example.demo.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* Created by Administrator on 2018/11/11 0011.
*/
public class test2 {
private static int threadTotal = 200;
private static int clientTotal = 5000;
private static long count = 0;
public static void main(String[] args)
{
//在同一时间,200个线程执行5000个请求
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
for(int index = 0;index<clientTotal;index++)
{
executorService.execute(()->{
try {
semaphore.acquire();
add();
semaphore.release();
}catch (Exception e)
{
}
});
}
executorService.shutdown();
System.out.println("count=="+count);
}
public static void add(){
count++;
}
}
高并发:通过设计保证系统能够同时并行处理很多请求,服务能同时处理很多请求,提高程序性能
并发:多个线程操作相同的资源,保证线程安全,合理使用资源
JMM : java内存模型(java Memory Model, JMM),规范了java虚拟机与计算机内存是如何协同工作的,规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时,如何同步的访问共享变量。
Heap:堆, 运行时的数据区,由垃圾回收负责的,能够动态的分配内存大小,生存期不必事先告诉编译器,因为是在运行期动态分配内存的,垃圾 回收期会自动回收不再使用的数据,也有缺点,因为是在运行期间动态分配内存,所以存取速度较慢
stack:栈 存取速度比堆快,仅次于计算机的寄存器,数据可用共享,但是缺点,存在栈中的数据大小和生存期是确定的,缺乏一些灵活性,主要存一些基本变量,比如int,double,等
当两个线程,同时访问一个对象的同一个方法,都会访问对象的成员变量,两个线程都拥有该对象成员变量的私有拷贝
现在个人电脑基本都多核的,就是指多个cpu,每个cpu有多个寄存器,他们是计算机内存的基础,在寄存器上执行操作的速度远大于在主存上执行操作的速度。
高速缓存:由于计算机的存储设备与处理器的运算速度有几个数量级的差异,不得不加入读写速度接近与处理器的计算机高级缓存来作为内存与处理器之间的 缓冲,将运算所需要的数据复制到缓存中,让运算能快速的运行,当计算结束后,在从缓存同步回内存之中,这样处理器就不用等待缓慢的内存读写
java内存模型:
线程与主内存之间的交互
如下图
本地内存是一种抽象概念,是计算机主内存的优化,也就是说其实是计算机内存分配给线程的,这个内存的位置是存储速度较快的,比如寄存器,高速缓存区,每个线程拥有主存共享变量的一个拷贝副本。
两个线程a,b同时执行,同时对count变量++,比如count初始值为1,先将共享变量从主内存中读取到本地内存,然后从本地内存读取共享变量,对值进行操作然后写入主内存,值为2,线程b执行同样的操作,值也为2,注意:两个线程互相之间是不可见 的。所以这个时候需要有一些同步手段来保证程序计算的准确性。
java内存模型-同步八种操作:
同步规则规定,线程间通信必须经过主内存
并发模拟器: Apache Bench(AB)
安装apache bench,到当前目录的bin目录下,执行 ab -n 1000 -c 100 http://localhost/test
-n 后面代表请求个数
-c后面代表请求并发数
Document Path:测试页面
Document Length: 页面大小
Concurrency Level: 测试的并发数
Time taken for tests:整个测试持续的时间
Complete requests:完成的请求数量
Failed requests: 失败的请求数量
Write errors: 0
Total transferred: 整个过程中的网络传输量
HTML transferred: 整个过程中的HTML内容传输量
Requests per second: 最重要的指标之一,相当于LR中的每秒事务数,后面括号中的mean表示这是一个平均值
Time per request: 客户端平均响应值
Time per request: 服务端平均响应值
Transfer rate: 平均每秒网络上的流量,可以帮助排除是否存在网络流量过大导致响应时间延长的问题
Jmeter :并发模拟,window在目录下运行jemter.bat
CountDownLatch 可以阻塞线程,而且可用使线程在某种特定的情况下继续执行,
使用场景:我们保证线程在执行完,再执行其他的操作可以用该类。
semaphore 信号量 ,可用阻塞进程,并且可以控制同一时间请求的并发量,
这两个类一般与线程池一起使用。,使用场景:更适合控制同时并发的线程数。
原子性-atomic包
atomicxxx:CAS,Unsafe.compareAndSwapInt
AtomicLong,LongAdder
AtomicReference,AtomicReferenceFieldUpdater
AtomicStampReference:该类主要是解决CAS的ABA问题,(线程将值A改成B,又将B改回A,但当前的A已经不是之前的A,该类为被线程改的的值添加版本号,每次被修改,版本号就被修改,从而识别)
AtomicLongArray,对数组
原子性-synchronized
修饰代码块:大括号括起来的代码 ,作用于调用的对象
修饰方法:整个方法,作用于调用的对象。
修饰静态方法:整个静态方法,作用于所有对象。
修饰类,括号括起来的部分,作用于所有对象。
synchronized :不可中断锁,适合竞争不激烈,可读性好,
lock:可中断锁,多样化同步,竞争激烈时能维持常态
atomic 竞争激烈时能维持常态,比lock性能好,只能同步一个值.
线程安全性体现在:原子性,可见性,有序性
导致共享变量在线程间不可见的原因,1,线程交叉执行,2,重排序结合线程交叉执行3,共享变量更新后的值没有在工作内存与主内存间及时更新。
JMM关于synchronized的两条规定:
1,线程解锁前,必须把共享变量的最新值刷新到主内存,2,线程加锁事,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁时同一把锁).
可见性-volatile :通过加入内存屏障和禁止重排序优化来实现。Volatile有可见性的特点。用Volatile修饰的变量,在多线程情况下,每个线程对该变量的修改会立刻刷新主存中该变量的值,而不是先保存在线程自身的缓存中。使得每个线程读取该变量时,值是最新的。
有序性
重排序:
在执行程序时,编译器和处理器会对指令进行重排序,重排序分为:
编译器重排序:在不改变代码语义的情况下,优化性能而改变了代码执行顺序;
指令并行的重排序:处理器采用并行技术使多条指令重叠执行,在不存在数据依赖的情况下,改变机器指令的执行顺序;
内存系统的重排序:使用缓存和读写缓冲区时,加载和存储可能是乱序执行。
java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile,synchronized,lock均可 保证有序性
如果两个操作的执行次序不能从happend-before的原则推倒出来,则程序执行不具有有序性,虚拟机可以对他们进行重排序
发布对象:使一个对象能够被当前范围之外的代码所使用
对象逸出:一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见。(发布的同时,this逸出,this被所有线程共享)
安全发布对象的4种方法:
1,在静态初始化函数中初始化一个对象引用
2,将对象的引用保存到volatile类型域或者AtomicReference对象中
3,将对象的引用保存到某个正确构造对象的final类型域中
4,将对象的引用保存到一个由锁保护的域中
不可变对象:
发布后就是安全的,例如String类
不可变对象需要满足的条件:
1,对象创建以后其状态就不能修改
2,对象所有 域都是final类型
3,对象是正确创建的(在对象创建期间,this 引用没有逸出)
这里可以采用的方法,1,将类声明为final类型,这样类就不能继承了,将成员变量声明为私有,这样就不允许直接访问成员
2 对变量不提供set方法,将所有可变的成员修饰方法改为final 这样就只能对其赋值一次,通过构造器初始化所有成员,进行深度拷贝,在get方法中不直接返回对象的本身,而是克隆对象,返回克隆对象的拷贝。
final关键字:类,方法,变量
修饰类:不能被继承
修饰方法:1,锁定方法不能被类继承,2,效率
修饰变量:基本数据类型变量(初始化后不能修改了),引用类型变量(初始化后,不能指向其他对象)
除了final关键字之外,还有其他的不可变的类
例如Collections.unmodifiableXXX:Collection,List,Set,Map....(不可以修改其中的值,而final修饰的map可用修改,只是无法指向新的对象。
Guava:ImmutableXXX:Collection,List,Set,Map...(不可以修改其中的值,而final修饰的map可用修改,只是无法指向新的对象。
线程封闭
Ad-hoc 线程封闭:用程序控制实现,最糟糕,可以忽略,
堆栈封闭:局部变量,无并发问题。。。每个线程访问时,会拷贝一份局部变量出来,这样就不会相互影响。栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的
局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。也就是局部变量没有并发问题。
:ThreadLocal封闭
使用ThreadLocal是实现线程封闭的最好方法,有兴趣的朋友可以研究一下ThreadLocal的源码,其实ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。这里就不说ThreadLocal的使用方法了,度娘一下便知。
jdbc中的connection对象也实现了线程封闭。
Stringbuffer 线程安全,StringBuilder 线程非安全
SimpleDateFormat 线程不安全,可出现(
java.lang.NumberFormatException: multiple points
)异常,解决方法:可用利用jvm的线程封闭,将变量写到方法内,每调用一次方法创建一个对象,就不会产生并发问题。
org.joda.time.format.DateTimeFormatter; 线程安全的时间转换类,推荐使用,而且更方便,实际处理上有更多的优势。
线程不安全:ArrayList,HashSet,HashMap 等CollectIons
线程不安全的写法 先检查在执行,if(condition(a)){handle(a);}
线程不安全容器对于-》线程安全容器
ArrayList->Vector,Stack(先进后出)
HashMap->HashTable(key,value不能为null)
Collections.synchronizedXXX(List,Set,Map) 线程安全容器
在多线程的情况下,一个线程对容器进行获取一个对容器进行修改操作,容易出现并发问题。同步容器也有并发问题,这个时候我们就需要用并发容器来代替同步容器
并发容器-线程安全 J.U.C(java.util.concurrent)
ArrayList->CopyOnWriteArrayList(读写分离,保证数据最终一致性,开辟内存,写操作需要加锁)
HashSet,->CopyOnWriteArraySet
TreeSet->ConcurrentSkipListSet
单个修改或删除是线程安全的,如果是批量操作则需要程序自己加上同步性,因为该并发容器只会使每一次修改具有原子性,而不是批量的修改。\
HashMap,-.>ConcurrentHashMap
TreeMap->ConcurrentSkipListMap
AbstractQueuedSynchronizer-AQS
底层用双向列表
- 使用node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架
- 利用了一个int类型表示状态
- 使用 方法是继承
- 子类通过继承并实现它的方法管理器状态(acquire和release)的方法操纵状态
- 可以同时实现排它锁和共享锁模式(独占,共享)
AQS同步组件
1,CountDownLatch
2,Semaphore
3,CyclicBarrier
4,ReentrantLock
5,Condition
6,FutureTask
java 里面有两类锁,一种是synchronized修饰的锁,一种是ReentrantLock (j,u,c )
ReentrantLock(可重入锁)和synchronized的区别
1,可重入性,2锁的实现(synchronized 是依靠jvm实现,reentrantLock依靠jdk源码实现)
3,性能的区别
ReentrantLock 独有的功能
1,可指定是公平锁还是非公平锁,syncoronized只能是非公平锁
公平锁:先等待的线程,先获得锁。
2,提供了一个Conditiion类,可用分组唤醒需要唤醒的线程
3,能够提供中断等待锁线程的机制,lock.lockInterruptibly()
当设计的程序有次独特的功能时,可用使用ReentrantLock
竞争者少量的情况下,可用synchronized
注意:synchronized不会造成死锁,因为jvm会自动解锁,而其他的锁类可能会因为开发者的失误忘记释放锁而造成死锁。
FutureTask 可以在任务执行完后,获取执行的结果
1,Callable 与Runnable接口对比
Future接口 可以得到别的线程方法的返回值
futureTask 类
Fork/Join框架----java7 把大任务分割成小任务,再把小任务组合成大任务。(工作窃取算法https://blog.youkuaiyun.com/pange1991/article/details/80944797)
BlockIngQueue :阻塞队列,主要用在消费者生产者模式中,线程安全
队列满的情况下,不能进入入队列操作,队列空的时候不能进入出队列操作(阻塞)
实现类 :ArrayBlockingQueue,DelayQueue ,LInkedBlockingQueue,PriorityBlockIngQueue,SynchronousQueue
多线程并发最佳实战
1,使用本地变量(局部变量)
2,使用不可变类,例如string
3, 最小化锁的作用域范围:s=1/(1-a+a/n)
4,使用线程池的executor,而不是直接new Thread 执行
5,宁可使用同步也不要使用线程的wait和notify
6, 使用blockingQueue 实现生产-消费模式
7,使用并发集合而不是加了锁的同步集合。
8,使用semaphore创建有界的访问。
9,宁可使用同步代码块,也不使用同步的方法(同步方法会锁住所有对象)
10,避免使用静态变量
Spring与线程安全
Spring bean :singleton ,prototype
无状态对象,比如我们经常使用的vo,Controller这些类只是单纯的类无状态
spring无法对线程安全作出保证,主要是一些无状态类的设计,让其没有线程安全问题。
HashMap 与 ConcurrentHashMap
hashmap由数组和链表构成,数组的每一项为一个链表。
扩容
每个线程都会占用一定的内存,当线程数多了之后,就需要足够的内存,才能够满足实际情况的需要。
1,垂直扩容(纵向扩展):提高系统部件能力
2,水平扩容(横向扩展):增加更多系统成员来实现。
目前最流行的架构:分布式服务器集群+数据库主从架构
高并发,根据已有的业务场景和资源情况尽可能保证服务的可用性。不同的业务场景应对高并发可能不同。
缓存
命中率:命中数/(命中数+没有命中数)
命中率越高,代表我们使用缓存的收益越高,响应速度更快
最大元素(空间): 缓存中可以存放最大元素的数量
清空策略:FIFO,LFU,LRU,过期时间,随机等。
FIFO:先进先出,先缓存的,先被清除(当空间满时)
LFU:清除使用次数最少的元素,主要比较的元素 的命中率。在保证高频元素的情况下,可使用
LRU:无论是否先进,根据距离元素最远被get的时间,先被清除。
缓存分类和应用场景
1,本地缓存:编程实现(成员变量,局部变量,静态变量) Guava Cache
2,分布式缓存:Memcache,Redis
高并发场景下缓存常见问题
1,缓存一致性(保持缓存中的数据与数据库的一致)
2,缓存并发问题(更新与读取同步请求)
3,缓存穿透问题 (未命中缓存,大量直接查询数据库)
4,缓存的雪崩现象 (某个原因导致缓存集中失效,导致大量请求到数据库)
消息队列
避免同步执行带来的各种问题,各个模块解耦,基本无并发问题。
下单-消息队列的应用
消息队列特性
与业务无关:只做消息分发
FIFO先投递先到达
容灾:节点的动态增删和消息的持久化
性能:吞吐量提升,系统内部通信效率提高
为什么需要消息队列???
1,生产和消费的速度或稳定性等因素不一致。
1,业务系统触发了发送短信的发送申请,但是短信发送系统发送短信的速度跟不上,需要将来不及处理的消息暂存一下,缓冲压力,可以将消息存放到消息队列中,消息发送模块可用慢慢的从消息队列中取出消息进行处理。,
2,当前系统调用 远程系统下订单成本较高,且因为网络因素等不稳定,可用通过消息队列赞一批一起来发送。
消息队列的好处:
1,业务解耦,基于消息的模型只关心通知,不关心结果
2,最终一致性
3,广播
4,错峰与流控
强调时时性可用rpc远程调用。
消息队列: Kafka ,RabbitMQ
高并发下的应用拆分
比如 股票系统(用户信息,开户,股票行情,交易,订单)融合在一起的,如果行情由于高并发出现问题会影响其他的应用
将其拆分为 交易中心,账号中心,用户中心,行情中心,通知中心。
应用拆分-原则
1,业务优先
2,循序渐进
3,兼顾技术,重构,分层
4,可靠测试
应用之间通信(RPC(dubbo等)时效性高),消息队列(时效性低)
应用之间数据库设计:每个应用都有独立的数据库
避免事务操作跨应用
应用拆分采取的组件:
Dubbbo框架,与springclub(微服务,是把一个大型的单个应用程序和服务,拆分成数个甚至数十个支持的微服务,可扩展单个组件而不是整个的应用程序堆栈,围绕业务领域组件来创建应用,这些应用可以单独的开发管理和迭代,。
微服务通常是直接面对用户的,每个服务直接为用户提供某些功能,类似的功能可能针对手机有一个服务,对机顶盒有一个服务。
客户端通过API Gateway与后端进行通信,apiAateway相当于是对微服务的统一管理
应用之间通信分为异步与同步调用,异步一般为消息队列(例如kafka)
同步调用,一般使用rest或者rpc
rest夸语言夸客户端(http)
rpc传输协议更高效,安全。
应用限流
控制一段代码在一段时间内的访问次数,控制访问流量(高并发条件下)
应用限流-算法
计数器法,滑动窗口,漏桶算法,令牌桶算法(rateLimiter)。
分布式限流实现:
Redis:incrby key num 执行后根据key(当前时间) value会+1 ,当达到限制后,可以做相应操作。(阻塞或丢弃)
比如请求量比较大,使用rateLimiter.
服务降级
当服务器压力剧增的时候,根据当前业务情况及流量,对一些服务和页面有策略的降级,以此缓解服务器压力,从而保证核心任务的正常运行,同时也保证了大部分用户的正常响应。
也就是说如果当前服务处理不了了。会给一个默认的返回。
服务熔断
保护措施(过载保护)
股票熔断,指的是当股指波动到一定点时,为了控制风险,会暂停交易。
服务降级分类:
自动降级:超时,失败次数,故障,限流
人工降级:秒杀,双11大促
服务降级要考虑的问题
1,清楚核心服务,与非核心服务
2,是否支持降级,降级的策略是什么
3,业务方通场景,策略
Hystrix
1,在通过第三方客户端访问(通常是通过网络)依赖服务出现高延迟或者失败时,为系统提供保护和控制
2,在分布式系统中防止级联失败
3,快速失败(Fail fast)同时能快速恢复。
4,提供失败回退,和优雅的服务降级机制。
5,提供实时的监控报警和运维控制手段。
数据库切库,分库,分表
数据库瓶颈
1,单个库数据量太大(1T-2T):多个库
2,单个数据库服务器压力过大,读写瓶颈:多个库
3,单个表数据量过大:分表
一个主库多个分库
1,切库的基础及实际运用:读写分离
2,自定义注解完成数据库切库-代码实现(手记)
支持多数据源,分库
每个服务一个库
行情服务-行情库
交易服务-订单库
什么时候考虑分表:表的数据量很大,大到查询很慢和做索引都很慢 。
股票一张表-变500张表。,提供磁盘io读取速度
横向分表(表的结构一样--)和纵向分表 (根据活跃度划分)
数据库分表:mybatis分表插件 :shardbatis2.0
高可用的一些手段
任务调度系统分布式:elastic-job +zookeeper(当当网)
主备切换:apache curator + zookeeper 分布式锁实现。
监控报警机制 (手记)