高并发问题抛去架构层面的问题,落实到代码层面就是多线程的问题。多线程的问题主要是线程安全的问题(其他还有活跃性问题,性能问题等)。

那什么是线程安全?下面这个定义来自《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.

 
  1. public class Thread implements Runnable {

  2. ThreadLocal.ThreadLocalMap threadLocals = null;

  3. }

  4. public class ThreadLocal<T> {

  5. public T get() {

  6.         Thread t = Thread.currentThread();

  7.         ThreadLocalMap map = getMap(t);

  8.         if (map != null) {

  9.             ThreadLocalMap.Entry e = map.getEntry(this);

  10.             if (e != null)

  11.                 return (T)e.value;

  12.         }

  13.         return setInitialValue();

  14.     }

  15. ThreadLocalMap getMap(Thread t) {

  16.         return t.threadLocals;

  17.     }

  18. public void set(T value) {

  19.         Thread t = Thread.currentThread();

  20.         ThreadLocalMap map = getMap(t);

  21.         if (map != null)

  22.             map.set(this, value);

  23.         else

  24.             createMap(t, value);

  25.     }

  26. }

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. 拆成小任务还是慢,那么把同步操作变成异步操作,即方法执行后立即返回,不要等待结果。由另一个线程异步地处理线程,比如采用单独的线程定时检查处理状态,或者采用异步回调的方式

聊聊高并发(二)结合实例说说线程封闭和背后的设计思想相关推荐

  1. 聊聊高并发(二十五)解析java.util.concurrent各个组件(七) 理解Semaphore

    前几篇分析了一下AQS的原理和实现,这篇拿Semaphore信号量做例子看看AQS实际是如何使用的. Semaphore表示了一种可以同时有多个线程进入临界区的同步器,它维护了一个状态表示可用的票据, ...

  2. 聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁

    上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和主要的方法,显示了如何 ...

  3. 聊聊高并发(二十二)解析java.util.concurrent各个组件(四) 深入理解AQS(二)

    上一篇介绍了AQS的基本设计思路以及两个内部类Node和ConditionObject的实现 聊聊高并发(二十一)解析java.util.concurrent各个组件(三) 深入理解AQS(一) 这篇 ...

  4. 聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类

    这篇说说java.util.concurrent.atomic包里的类,总共12个,网上有很多文章解析这几个类,这里挑些重点说说. 这12个类可以分为三组: 1. 普通类型的原子变量 2. 数组类型的 ...

  5. 聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁...

    上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和基本的方法,显示了怎样 ...

  6. 聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁

    这篇讲讲ReentrantReadWriteLock可重入读写锁,它不仅是读写锁的实现,并且支持可重入性. 聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁) 这篇讲了如何模拟一个读写锁. 可重 ...

  7. 聊聊高并发(二十七)解析java.util.concurrent各个组件(九) 理解ReentrantLock可重入锁

    这篇讲讲ReentrantLock可重入锁,JUC里提供的可重入锁是基于AQS实现的阻塞式可重入锁.这篇 聊聊高并发(十六)实现一个简单的可重入锁 模拟了可重入锁的实现.可重入锁的特点是: 1. 是互 ...

  8. 聊聊高并发(二十一)解析java.util.concurrent各个组件(三) 深入理解AQS(一)

    AQS是AbstractQueuedSynchronizer的缩写,AQS是Java并包里大部分同步器的基础构件,利用AQS可以很方便的创建锁和同步器.它封装了一个状态,提供了一系列的获取和释放操作, ...

  9. 聊聊高并发(三十三)Java内存模型那些事(一)从一致性(Consistency)的角度理解Java内存模型

    可以说并发系统要解决的最核心问题之一就是一致性的问题,关于一致性的研究已经有几十年了,有大量的理论,算法支持.这篇说说一致性这个主题一些经常提到的概念,理清Java内存模型在其中的位置. 一致性问题更 ...

最新文章

  1. 老段mysql,老段视频汇总
  2. DApp基础设施设计:借助Kubernetes、Docker和Parity实现可靠的以太坊事件跟踪
  3. 有米android sdk,有米积分墙Android SDK开发者常见问题
  4. JZOJ 3885. 【长郡NOIP2014模拟10.22】搞笑的代码
  5. POJ1220(高精度进制转换)
  6. 夫妻一方信用卡逾期,另外一方会受到牵连吗?
  7. redis setnx原子性_不支持原子性的 Redis 事务也叫事务吗?
  8. Python的第三方库xlrd
  9. 【pytorch】LSTM神经网络
  10. C语言:十进制、BCD码互换
  11. U盘格式化后容量变小了一半怎么办?
  12. contiki 操作教程
  13. 生死看淡,不服就GAN(七)----用更稳定的生成模型WGAN生成cifar
  14. windows系统如何真正隐藏文件夹[转载]
  15. 移动端网页禁止下拉刷新css
  16. EasyPoi的简介
  17. ESXI中设置高格作为旁路由并设置双机热备(VRRP)
  18. mac安装sql server
  19. 如何成为技术领袖(转载)
  20. C#/.NET 解析Cron表达式,根据Cron表达式获取最近执行时间

热门文章

  1. scrollview下拉刷新_SwiftUI之View Tree 实战3(下拉刷新)
  2. 程序员圣诞节相册源码_程序员分享圣诞刷屏源码,这次朋友圈千万不要再@微信官方了!...
  3. Python元组介绍
  4. java 虚拟机_浅谈Java虚拟机内存区
  5. ant app 心电监测_医疗级心电健康手表,随时随地监测你的健康,心电手表H1手表评测...
  6. python反转字符串的元音字母_345. 反转字符串中的元音字母-----leetcode刷题(python解题)...
  7. windows 改变文件大小 函数_手写 bind call apply 方法 与 实现节流防抖函数
  8. java 拼sql最大长度,java.sql.SQLNonTransientConnectionException: 用户 ID 长度 (0) 超出 1 到 255 的范围...
  9. 如何搭建一个打印荣誉证书的网站_如何搭建一个免费的作品集网站
  10. python antlr4需要的python 版本_python多版本管理器pyenv