点击上方疾风先生可以订阅哦
线程上下文切换
>>>>
定义
在单核CPU机器下,也可以支持并发多线程执行代码,这个时候CPU会为每一个线程分配对应的时间片,通过在指定的时间片内执行对应的线程程序代码,时间片一到,线程再继续争抢CPU资源重复上述动作,CPU需要不断地进行来回切换上下文以便能够执行到争抢到资源的线程,开发人员可以在linux系统下通过vmstat查看的context switch,即cs表示上下文
>>>>
在并发产生的影响
// cpu_test.java
// 定义业务方法
private static void meth(){
long a = 0;
long b = 100000000000000L;
for(int index = 0; index < count; index ++){
a += 2;
b -= 4;
}
}
// 当前mac机器配置: 4CPU
// 并发:创建6个线程分别执行上述方法一次
private static void cocurrent() throws Exception {
long start = System.currentTimeMillis();
// t1 - t5
Thread t1 = new Thread("Thread-1"){
@Override
public void run() {
meth();
}
};
// ... 重复代码省略 ...
meth();
// t1 - t5 start
t1.start();
// ... 重复代码省略 ...
// t1 - t5 join()
t1.join();
// ... 重复代码省略 ...
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread() + " spend time : " + (end - start));
}
// 串行:直接调用6次方法
private static void serial(){
// Thread[main,5,main] spend time : 4
long start = System.currentTimeMillis();
meth();
meth();
meth();
meth();
meth();
meth();
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread() + " spend time : " + (end - start));
}
次数(count) | 1w | 10w | 100w | 1000w | 1亿 |
---|---|---|---|---|---|
串行耗时(ms) | 2 | 5 | <10 | 53 | 466 |
并发耗时(ms) | 2 | 10 | 10-12 | 25 | 166 |
1) 在count数据不是特别多的情况,串行执行的效率比并发快,因为并发执行需要切换线程上下文
2) 随着次数的增加,串行执行的效率比并发执行效率低,原因是当前线程充分利用CPU核数的资源,利用多个线程在相应的CPU上执行,使得任务被对应的线程消费,在这种情况下,并发线程充分利用CPU空闲的资源完成任务的调度
>>>>
解决方案
// 生产者 - 消费者模型, 每生产一个数据就消费一次
void producer(Consumer consumer){
while(true){
int num = RandomUtils.nextInt(0, 100);
log.info("produce num %d", num);
// 通知消费者进行消费,当前程序中断挂起,不再继续执行,caller为调度器,协程必须有一个调度器提供子程序切换执行
Object result = caller.send(consumer, num);
log.info("conusmer num return %s", result);
}
}
// 消费者
void consumer(){
while(true){
// get num by caller, 接收从调度器返回的数据,如果没有数据则中断并挂起当前程序
int num = caller.receive();
log.info("conusmer consuming the num %s", result);
result = “consume OK”;
}
}
线程安全
>>>>
线程安全产生
>>>>
JVM资源
在JVM运行数据区中,方法区和堆内存均是属于共享资源数据,存在线程安全问题
>>>>
临界区与竞态条件
// sahred.java
int num = 0; // 在多线程中对于共享资源存在数据竞争,竞态条件
// mutil.java
run(){
num ++; // 临界区
}
>>>>
原子性
>>>>
解决线程安全方案
1) 在当前线程栈中的局部变量.方法参数,抛出异常的处理器对象,由于只在线程栈中自己使用,并没有共享给其他线程,因此这类数据是属于线程安全的,也就是不存在数据竞争的情况
2) ThreadLocal以及ThreadLocalRandom等存储的数据变量
2) 基于AQS方式的加锁方式
3) 基于JVM实现的监视器锁对象的同步关键字synchronized
4) Java加锁方式
可见性问题
>>>>
产生可见性的原因
>>>>
解决可见性
volatile
的方式保证读取的数据是内存最新的数据,可以查看volatile的原理实现final
的方式表示修饰的变量不可变,提供一次初始化的写入对后续的程序操作提供只读的方案,可以查看final关键字的原理实现线程死锁
多线程相互争抢对方相互持有的资源,由于获取不到资源一直处于挂起状态而无法继续往下执行
// threadA.java
run(){
synchronized(lockA){
// ..
synchronized(lockB){
// ...
}
}
}
// threadB.java
run(){
synchronized(lockB){
// ..
synchronized(lockA){
// ...
}
}
}
1) 使用tryLock(timeout)的方式,一旦超时将自动释放锁资源
2) 可以考虑在不影响结果的情况下调整程序指定的逻辑分先后执行
3) 其他方案: 在业务代码中如果能够使用单锁解决问题则使用单锁的方式
服务机器资源
1) 硬件方面有CPU核数以及CPU的处理读写能力, 网络带宽问题, 磁盘读写速度, 磁盘空间, 内存空间等因素;
2) 软件资源一般是并发线程池的数量,比如tomcat服务的并发线程数, 数据库连接池大小, 网络socket连接数等
1) 如果机器的CPU核数较少,比如只有一个的话,在机器启动jvm进程来创建多线程会容易导致线程切换频繁,再加上本身线程切换存在资源调度的性能消耗,容易降低程序执行效率
2) 内存空间不足也会导致创建并发线程个数受限,同时容易造成OOM的错误
3) 业务处理线程数多于数据库连接池数,如果数据库中的sql执行比较快的话,那么会导致程序很多业务进程处于阻塞等待状态,容易引起100%的CPU
1) 提前对开发的应用做好并发量的评估,通过压力测试每台机器每个JVM进程在单位时间所能承担的并发量,然后根据预估计算需要分配的资源,比如网络带宽,JVM启动的内存分配,实际的机器个数等
2) 根据业务的读写场景,对文件并发读写频繁的业务可以选择IO磁盘处理能力较强的机器,网络并发读写较强的业务需要考虑带宽以及可以动态分配的机器最大个数,以便扩容需要
3) 在实际应用中,开启线程的个数可以默认设置为系统CPU核数*2+1
你好,我是疾风先生,先后从事外企和互联网大厂的java和python工作, 记录并分享个人技术栈,欢迎关注我的公众号,致力于做一个有深度,有广度,有故事的工程师,欢迎成长的路上有你陪伴,关注后回复greek可添加私人微信,欢迎技术互动和交流,谢谢!