聊聊高并发(二)结合实例说说线程封闭和背后的设计思想
高并发问题抛去架构层面的问题,落实到代码层面就是多线程的问题。多线程的问题主要是线程安全的问题(其他还有活跃性问题,性能问题等)。
那什么是线程安全?下面这个定义来自《Java并发编程实战》,这本书强烈推荐,是几个Java语言的作者合写的,都是并发编程方面的大神。
线程安全指的是:当多个线程访问某个类时,这个类始终都能表现出正确的行为。
正确指的是“所见即所知”,程序执行的结果和你所预想的结果一致。
理解线程安全的概念很重要,所谓线程安全问题,就是处理对象状态的问题。如果要处理的对象是无状态的(不变性),或者可以避免多个线程共享的(线程封闭),那么我们可以放心,这个对象可能是线程安全的。当无法避免,必须要共享这个对象状态给多线程访问时,这时候才用到线程同步的一系列技术。
这个理解放大到架构层面,我们来设计业务层代码时,业务层最好做到无状态,这样就业务层就具备了可伸缩性,可以通过横向扩展平滑应对高并发。
所以我们处理线程安全可以有几个层次:
1. 能否做成无状态的不变对象。无状态是最安全的。
2. 能否线程封闭
3. 采用何种同步技术
我理解为能够“逃避”多线程问题,能逃则逃,实在不行了再来处理。
了解了线程封闭的背景,来说说线程封闭的具体技术和思路
1. 栈封闭
2. ThreadLocal
3. 程序控制线程封闭
栈封闭说白了就是多使用局部变量。理解Java运行时模型的同学都知道局部变量的引用是保持在线程栈中的,只对当前线程可见,其他线程不可见。所以局部变量是线程安全的。
ThreadLocal机制本质上是程序控制线程封闭,只不过是Java本身帮忙处理了。来看Java的Thread类和ThreadLocal类
1. Thread线程类维护了一个ThreadLocalMap的实例变量
2. ThreadLocalMap就是一个Map结构
3. ThreadLocal的set方法取到当前线程,拿到当前线程的threadLocalMap对象,然后把ThreadLocal对象作为key,把要放入的值作为value,放到Map
4. ThreadLocal的get方法取到当前线程,拿到当前线程的threadLocalMap对象,然后把ThreadLocal对象作为key,拿到对应的value.
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
}
ThreadLocal的设计很简单,就是给线程对象设置了一个内部的Map,可以放置一些数据。JVM从底层保证了Thread对象之间不会看到对方的数据。
使用ThreadLocal前提是给每个ThreadLocal保存一个单独的对象,这个对象不能是在多个ThreadLocal共享的,否则这个对象也是线程不安全的。
Structs2就用了ThreadLocal来保存每个请求的数据,用了线程封闭的思想。但是ThreadLocal的缺点也显而易见,必须保存多个副本,采用空间换取效率。
程序控制线程封闭,这个不是一种具体的技术,而是一种设计思路,从设计上把处理一个对象状态的代码都放到一个线程中去,从而避免线程安全的问题。
有很多这样的实例,Netty5的EventLoop就采用这样的设计,我们的游戏后台处理用户请求是也采用了这种设计。
具体的思路是这样的:
1. 把和用户状态相关的代码放到一个队列中去,由一个线程处理
2. 考虑是否隔离用户之间的状态,即一个用户使用一个队列,还是多个用户使用一个队列
拿Netty举例,EventLoop被设计成了一个线程的线程池。我们知道线程池的组成是工作线程 + 任务队列。EventLoop的工作线程只有一个。
用户请求过来后被随机放到一个EventLoop去,也就是放到EventLoop线程池的任务队列,由一个线程来处理。并且处理用户请求的代码都使用Pipeline职责链封装好了,一个Pipeline交给一个线程来处理,从而保证了跟同一个用户的状态被封闭到了一个线程中去。
更多Netty EventLoop相关的内容看这篇 Netty5源码分析(二) -- 线程模型分析
这里有个问题也显而易见,就是如果把多个用户都放到一个队列,交给一个线程处理,那么前一个用户的处理速度会影响到后一个用户被处理的时间。
我们的游戏服务器的设计采用了一个用户一个任务队列的方式,处理任务的代码被做成了Runnable,这样多个Runnable可以交给一个线程池执行,从而多个用户可以同时被处理,而同一个用户的状态处理被封闭到了唯一的一个任务队列中,互不干扰。
但是也有问题,即线程池内的工作线程和任务队列是有界的,所以单个线程处理的时间必须要快,否则大量请求被积压在任务队列来不及处理,一旦任务队列也满了,那么后续的请求都进不来了。
如果使用无界的任务队列,所有请求能进来,但是问题是高并发情况下大量请求过来,会把系统内存撑爆,倒置OOM。
所以一个常用的设计思路如下:
1. 采用有界的任务队列和不限个数的工作线程,这样可以平滑地处理高并发,不至于内存被撑爆
2. 单个线程请求时间必须要快,尽量不超过100ms
3. 如果单个线程处理的时间由于任务太大必须耗时,那么把任务拆个小任务来多次执行
4. 拆成小任务还是慢,那么把同步操作变成异步操作,即方法执行后立即返回,不要等待结果。由另一个线程异步地处理线程,比如采用单独的线程定时检查处理状态,或者采用异步回调的方式
聊聊高并发(二)结合实例说说线程封闭和背后的设计思想相关推荐
- 聊聊高并发(二十五)解析java.util.concurrent各个组件(七) 理解Semaphore
前几篇分析了一下AQS的原理和实现,这篇拿Semaphore信号量做例子看看AQS实际是如何使用的. Semaphore表示了一种可以同时有多个线程进入临界区的同步器,它维护了一个状态表示可用的票据, ...
- 聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁
上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和主要的方法,显示了如何 ...
- 聊聊高并发(二十二)解析java.util.concurrent各个组件(四) 深入理解AQS(二)
上一篇介绍了AQS的基本设计思路以及两个内部类Node和ConditionObject的实现 聊聊高并发(二十一)解析java.util.concurrent各个组件(三) 深入理解AQS(一) 这篇 ...
- 聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类
这篇说说java.util.concurrent.atomic包里的类,总共12个,网上有很多文章解析这几个类,这里挑些重点说说. 这12个类可以分为三组: 1. 普通类型的原子变量 2. 数组类型的 ...
- 聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁...
上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和基本的方法,显示了怎样 ...
- 聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁
这篇讲讲ReentrantReadWriteLock可重入读写锁,它不仅是读写锁的实现,并且支持可重入性. 聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁) 这篇讲了如何模拟一个读写锁. 可重 ...
- 聊聊高并发(二十七)解析java.util.concurrent各个组件(九) 理解ReentrantLock可重入锁
这篇讲讲ReentrantLock可重入锁,JUC里提供的可重入锁是基于AQS实现的阻塞式可重入锁.这篇 聊聊高并发(十六)实现一个简单的可重入锁 模拟了可重入锁的实现.可重入锁的特点是: 1. 是互 ...
- 聊聊高并发(二十一)解析java.util.concurrent各个组件(三) 深入理解AQS(一)
AQS是AbstractQueuedSynchronizer的缩写,AQS是Java并包里大部分同步器的基础构件,利用AQS可以很方便的创建锁和同步器.它封装了一个状态,提供了一系列的获取和释放操作, ...
- 聊聊高并发(三十三)Java内存模型那些事(一)从一致性(Consistency)的角度理解Java内存模型
可以说并发系统要解决的最核心问题之一就是一致性的问题,关于一致性的研究已经有几十年了,有大量的理论,算法支持.这篇说说一致性这个主题一些经常提到的概念,理清Java内存模型在其中的位置. 一致性问题更 ...
最新文章
- 老段mysql,老段视频汇总
- DApp基础设施设计:借助Kubernetes、Docker和Parity实现可靠的以太坊事件跟踪
- 有米android sdk,有米积分墙Android SDK开发者常见问题
- JZOJ 3885. 【长郡NOIP2014模拟10.22】搞笑的代码
- POJ1220(高精度进制转换)
- 夫妻一方信用卡逾期,另外一方会受到牵连吗?
- redis setnx原子性_不支持原子性的 Redis 事务也叫事务吗?
- Python的第三方库xlrd
- 【pytorch】LSTM神经网络
- C语言:十进制、BCD码互换
- U盘格式化后容量变小了一半怎么办?
- contiki 操作教程
- 生死看淡,不服就GAN(七)----用更稳定的生成模型WGAN生成cifar
- windows系统如何真正隐藏文件夹[转载]
- 移动端网页禁止下拉刷新css
- EasyPoi的简介
- ESXI中设置高格作为旁路由并设置双机热备(VRRP)
- mac安装sql server
- 如何成为技术领袖(转载)
- C#/.NET 解析Cron表达式,根据Cron表达式获取最近执行时间
热门文章
- scrollview下拉刷新_SwiftUI之View Tree 实战3(下拉刷新)
- 程序员圣诞节相册源码_程序员分享圣诞刷屏源码,这次朋友圈千万不要再@微信官方了!...
- Python元组介绍
- java 虚拟机_浅谈Java虚拟机内存区
- ant app 心电监测_医疗级心电健康手表,随时随地监测你的健康,心电手表H1手表评测...
- python反转字符串的元音字母_345. 反转字符串中的元音字母-----leetcode刷题(python解题)...
- windows 改变文件大小 函数_手写 bind call apply 方法 与 实现节流防抖函数
- java 拼sql最大长度,java.sql.SQLNonTransientConnectionException: 用户 ID 长度 (0) 超出 1 到 255 的范围...
- 如何搭建一个打印荣誉证书的网站_如何搭建一个免费的作品集网站
- python antlr4需要的python 版本_python多版本管理器pyenv