当前位置:首页 » 硬盘大全 » 线程并发缓存通入口地址
扩展阅读
webinf下怎么引入js 2023-08-31 21:54:13
堡垒机怎么打开web 2023-08-31 21:54:11

线程并发缓存通入口地址

发布时间: 2023-05-24 09:28:36

数据库缓存机制是什么缓存是如何作用数据库

我们都知道MySQL的TableCache是表定义的缓存,江湖上流传着各种对这个参数的调优方法。

tablecache的作用,就是节约读取表结构文件的开销。对于tablecache是否命中,其实tablecache是针对于线程的,每个线程有自己的缓存,只缓存本线程的表结构定义。不过我们发现,strace中没有关于表结构文件的open操作(只有stat操作,定位表结构文件是否存在),也就是说tablecache不命中闹亮罩,不一定需要读取表结构文件。这种感觉好像是:在不命中tablecache时,命中了另外一个表结构缓存。

运维建议:

我们读一下MySQL的文档,关于table_open_cache的建议值公式:建议值=最大并发数*join语句涉及的表的最液闹大个数。

通过实验我们键迅容易理解:table_cache是针对于线程的,所以需要最大并发数个缓存。另外,一个语句join涉及的表,需要同时在缓存中存在。所以最小的缓存大小,等于语句join涉及的表的最大个数。将这两个数相乘,就得到了MySQL的建议值公式。

② 线程并发访问如何解决的一个简单demo程

synchronized关键字主要解决多线程共享数据同步问题。
ThreadLocal使粗销用场合主要解决多线程中数据因并发产生不一致问题。
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别:
synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线凳凳蠢程都提供了变量的副本,使 得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信 时能够获得数据共享。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。枣陪当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。

③ 求一段死循环的c#多线程并发访问一个网址的代码

首先回答你的问题:
public int A{ get{ return _a; }set{_a=value;}}
这叫封装属性,可以在get或set里对值进行处理,比如这个值不能大于100
可以修改为:
set{if(value<=100){_a=value;}} 不用属性的话,每个调用_a的地方都要加这段代码,多麻烦

刚好做了一个类似的测试,比你的要求复杂一些,你自己轮冲森看吧:
// 点击按钮,开始循环测试
private void button1_Click(object sender, EventArgs e)
{
textBox4.Text = string.Empty;
string testUrl = txtUrl.Text; // 测试地址
string postData = "killId=" + txtKillid.Text; // 测试的参判锋数,要post的数据
int threadNum = int.Parse(txtThreadNum.Text); // 发起的线程数,每个线程为一个新的Session,你腊亩要无限的话,可以设置为int.MaxValue
int threadTime = 10; // 每个线程跑10次,这是用于要用同一个Session测试10次的情况

for (int i = 0; i < threadNum; i++)
{
string[] arr = { testUrl, postData, killTime, i.ToString() };
new Thread(Post).Start(arr); // 开始当前线程测试
}
}

public void Post(object arr)
{
string[] para = arr as string[];
string testUrl = para[0];
string postData = para[1];
int killTime = int.Parse(para[2]);
int threadNum = int.Parse(para[3]);

CookieContainer cookie = new CookieContainer();
while (killTime > 0)
{
HttpWebRequest request = WebRequest.Create(testUrl) as HttpWebRequest;
request.ContentType = "application/x-www-form-urlencoded";
request.Method = "POST";
request.CookieContainer = cookie;

byte[] bodyBytes = Encoding.UTF8.GetBytes(postData);
request.ContentLength = bodyBytes.Length;
using (Stream reqStream = request.GetRequestStream())
{
reqStream.Write(bodyBytes, 0, bodyBytes.Length);
reqStream.Flush();
}

request.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1;)";

killTime--;
using (WebResponse response = request.GetResponse())
using (Stream sr = response.GetResponseStream())
using (StreamReader reader = new StreamReader(sr))
{
// 把网页返回的内容输出到TextBox中
SetText(textBox4,
threadNum + "," + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + ":" + reader.ReadToEnd());
}
cookie = request.CookieContainer;
Thread.Sleep(100);
}
}

/// <summary>
/// 用于线程里访问TextBox的线程委托
/// </summary>
/// <param name="tb"></param>
/// <param name="txt"></param>
private delegate void SetTextDelegate(TextBox tb, string txt);
private void SetText(TextBox tb, string txt)
{
if (!tb.InvokeRequired)
{
tb.Text += txt + "\r\n";
}else
{
SetTextDelegate de =SetText;
Invoke(de, tb, txt);
//de.Invoke(txt);
}
}

④ 线程和缓存是什么意思

线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

缓存是指临时文件交换区,电脑把最常用的文件从存储器里提出来临时放在缓存里,就像把工具和材料搬上工作台一样,这样会比用时现去仓库取更方便。因为缓存往往使用的是RAM(断电即掉的非永久储存),所以在忙完后还是会把文件送到硬盘等存储器里永久存储。电脑里最大的缓存就是内存条,缓存的大小与速度是直接关系到硬盘的传输速度的重要因素,能够大幅度地提高硬盘整体性能。

⑤ 线程池 ExecutorService

并发,启动大量任务线程时,频繁的线程创建/销毁会造成浪费。(创建、运行、销毁都需要时间),采用线程池,线程复用技术,提高性能。

线程池实现类,ThreadPoolExecutor 类。

ThreadPoolExecutor 构造方法,实现不同类型线程池。

corePoolSize,核心线程数。
maximumPoolSize,允许的最大线程,超过报异常。
keepAliveTime,非核心线程活跃时间。
TimeUnit,时间度量。
BlockingQueue<Runnable>,任务队列,(无限、有限队列、栈)。
ThreadFactory,线程工厂。

不推荐直接使用 ThreadPoolExecutor 构造方法创建。

1,缓存

核心线禅拍燃程是0,最大线程 MAX_VALUE ,任务队列 SynchronousQueue 是一个管道,不存储,线程活跃时间60秒。
适用短期大量任务的场景。
某一时间段高并发任务,创建大量线程,任务结束后,线程变空闲,60s以内重用,处理新任务,空闲到达60s,销毁贺虚。

2,固定数量

核心线程、最大线程,自定义,LinkedBlockingQueue 任务队列,不设置空闲时间,无界队列。
适用长期任务场景。
任务队列无界,仅核心线程工作,keepAlieveTime 不需要设置。

3,单线程

核心线程、最大线程,数量1,LinkedBlockingQueue 任务队列,不设置空闲时间,无界队列。
适用单个任务顺序执行场景。
只有一个核心线程,新任务加入队列。

4,定时任务

ScheledThreadPoolExecutor 实例,ThreadPoolExecutor 子类。
核心线程自定义,最大线程 MAX_VALUE,DelayedWorkQueue 任务队列,空闲时间10s。
适用指定有延迟的任务或周期性重复任务场景。
队列不是按照 submit 时间排序,以延时时间为优先级升序排序,延时时间短的,优先级高,先执行。

线程池 ThreadPoolExecutor 创建,将任务派发给线程池,execute() 方法,自动分配线程执行。

workerCountOf(c) 方法,判断工作线程数量,当 <核心线程数量,addWorker() 方法,创建线程,设标志位参数代表核心线程,任务将由新建核心线程处理。
当 >= 核心线程,isRunning(c) 方法,表示 c<0,offer()方法,将任务加入队列。
offer() 是非阻塞方法,如果队列已满,返回失败,此时,addWorker() 方法,创建线程,不设标志位参数贺李代表非核心线程,任务将由新建的非核心线程处理。

新线程创建
如果工作线程 >CAPACITY 容量,或 >= 允许的最大值(创建核心线程 >= 核心线程数量),失败返回。

创建一个 Runnable 类型 Worker 对象,ThreadPoolExecutor 内部静态类,用户任务封装,newThread() 方法,创建新线程,将 Worker(this) 作为新线程任务主体。

将 Worker 任务加入 HashSet 集合,设置 workerAdded 标志,启动新线程( start方法),设置 workerStarted 启动标志,代表线程启动,执行 Worker 任务的 run() 方法,调用 runWorker() 方法(外部类方法)。

新线程执行 Worker 任务的 run() 方法,借助 Worker 开始为线程池工作,从 Worker 获取内部 Runnable(即 execute 派送的用户任务),并执行 run() 方法。
用户任务处理完成,线程不会马上结束,while 循环,继续 getTask() 方法,从任务队列中获取任务,该方法可能会导致阻塞,队列空则线程结束。

BlockingQueue 阻塞队列。

1,工作线程 wc > 核心线程
设置 timed 标志,队列采用阻塞等待,(poll + timeout方式),timeout 设置线程 keepAliveTime 时间 。
因此,即使队列没有任务,线程仍然存活,(任务进队列后可立即唤醒展开工作)。

2,工作线程 wc < 核心线程
(仅有核心线程),队列采用一直阻塞,( take 方式),队列是空时,线程一直阻塞,核心线程不会结束。

3,队列空,poll 超时
设置 timeOut 已超时标志,下次循环时,如果再发生工作线程 wc > 核心线程 ( timed 和 timedOut 标志并存),线程数量自减,退出 while 返回空,线程结束。

4,设置核心线程 allowCoreThreadTimeOut
不管工作线程 wc,采用 poll + timeout 方式,keepAliveTime 队列无任务,所有线程都会 timedOut 超时标志,下次循环自动结束。
即使 wc < 核心线程,线程也会结束退出,允许核心线程超时结束。

线程池本质,设置一定数量的线程集合,任务结束,但线程不结束,根据设定值,请求任务队列,继续任务,复用线程。

线程等待,利用任务队列的阻塞特性。阻塞任务队列,访问空队列时,线程等待,timeout 超时时间,实现线程 keepAliveTime 活动或一直存活。

任务队列
poll() 方法,取队列,非阻塞,poll + timeout,阻塞 timeout 时间。
take() 方法,取队列,阻塞等待。
put() 方法,存队列,阻塞等待,
offer() 方法,存队列,非阻塞,offer + timeout,阻塞 timeout 时间。

核心线程,在线程集合中,未具体标明,若 All 线程都完成任务,空闲,且队列空,在 getTask() 方法,根据超时时间,逐一唤醒,结束,剩下的数量 = 核心,它们即核心。

任重而道远

⑥ 华为技术架构师分享:高并发场景下缓存处理的一些思路

在实际的开发当中,我们经常需要进行磁盘数据的读取和搜索,因此经常会有出现从数据库读取数据的场景出现。但是当数据访问量次数增大的时候,过多的磁盘读取可能会最终成为整个系统的性能瓶颈,甚至是压垮整个数据库,导致系统卡死等严重问题。

常规的应用系统中,我们通常会在需要的时候对数据库进行查找,因此系统的大致结构如下所示:

1.缓存和数据库之间数据一致性问题

常用于缓存处理的机制我总结为了以下几种:

首先来简单说说Cache aside的这种方式:

Cache Aside模式

这种模式处理缓存通常都是先从数据库缓存查询,如果缓存没有命中则从数据库中进行查找。

这里面会发生的三种情况如下:

缓存命中:

当查询的时候发现缓存存在,那么直接从缓存中提取。

缓存失效:

当缓存没有数据的时候,则从database里面读取源数据,再加入到cache里面去。

缓存更新:

当有新的写操作去修改database里面的数据时,需要在写操作完成之后,让cache里面对应的数据失效。

关于这种模式下依然会存在缺陷。比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

Facebook的大牛们也曾经就缓存处理这个问题发表过相关的论文,链接如下:

分布式环境中要想完全的保证数据一致性是一件极为困难的事情,我们只能够尽可能的减低这种数据不一致性问题产生的情况。

Read Through模式

Read Through模式是指应用程序始终从缓存中请求数据。 如果缓存没有数据,则它负责使用底层提供程序插件从数据库中检索数据。 检索数据后,缓存会自行更新并将数据返回给调用应用程序。使用Read Through 有一个好处。

我们总是使用key从缓存中检索数据, 调用的应用程序不知道数据库, 由存储方来负责自己的缓存处理,这使代码更具可读性, 代码更清晰。但是这也有相应的缺陷,开发人员需要给编写相关的程序插件,增加了开发的难度性。

Write Through模式

Write Through模式和Read Through模式类似,当数据发生更新的时候,先去Cache里面进行更新,如果命中了,则先更新缓存再由Cache方来更新database。如果没有命中的话,就直接更新Cache里面的数据。

2.缓存穿透问题

在高并发的场景中,缓存穿透是一个经常都会遇到的问题。

什么是缓存穿透?

大量的请求在缓存中没有查询到指定的数据,因此需要从数据库中进行查询,造成缓存穿透。

会造成什么后果?

大量的请求短时间内涌入到database中进行查询会增加database的压力,最终导致database无法承载客户单请求的压力,出现宕机卡死等现象。

常用的解决方案通常有以下几类:

1.空值缓存

在某些特定的业务场景中,对于数据的查询可能会是空的,没有实际的存在,并且这类数据信息在短时间进行多次的反复查询也不会有变化,那么整个过程中,多次的请求数据库操作会显得有些多余。

不妨可以将这些空值(没有查询结果的数据)对应的key存储在缓存中,那么第二次查找的时候就不需要再次请求到database那么麻烦,只需要通过内存查询即可。这样的做法能够大大减少对于database的访问压力。

2.布隆过滤器

通常对于database里面的数据的key值可以预先存储在布隆过滤器里面去,然后先在布隆过滤器里面进行过滤,如果发现布隆过滤器中没有的话,就再去redis里面进行查询,如果redis中也没有数据的话,再去database查询。这样可以避免不存在的数据信息也去往存储库中进行查询情况。

什么是缓存雪崩?

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

如何避免缓存雪崩问题?

1.使用加锁队列来应付这种问题。当有多个请求涌入的时候,当缓存失效的时候加入一把分布式锁,只允许抢锁成功的请求去库里面读取数据然后将其存入缓存中,再释放锁,让后续的读请求从缓存中取数据。但是这种做法有一定的弊端,过多的读请求线程堵塞,将机器内存占满,依然没有能够从根本上解决问题。

2.在并发场景发生前,先手动触发请求,将缓存都存储起来,以减少后期请求对database的第一次查询的压力。数据过期时间设置尽量分散开来,不要让数据出现同一时间段出现缓存过期的情况。

3.从缓存可用性的角度来思考,避免缓存出现单点故障的问题,可以结合使用 主从+哨兵的模式来搭建缓存架构,但是这种模式搭建的缓存架构有个弊端,就是无法进行缓存分片,存储缓存的数据量有限制,因此可以升级为Redis Cluster架构来进行优化处理。(需要结合企业实际的经济实力,毕竟Redis Cluster的搭建需要更多的机器)

4.Ehcache本地缓存 + Hystrix限流&降级,避免MySQL被打死。

使用 Ehcache本地缓存的目的也是考虑在 Redis Cluster 完全不可用的时候,Ehcache本地缓存还能够支撑一阵。

使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。

然后去调用我们自己开发的降级组件(降级),比如设置的一些默认值呀之类的。以此来保护最后的 MySQL 不会被大量的请求给打死。

⑦ Tomcat使用线程池配置高并发连接

1:配置executor属性

打开/conf/server.xml文件,在Connector之前配置一个线程池:

重要参数说明:name :共享线程池的名字。这是Connector为了共享线程池要引用的名字,该名字必须唯一。默认值:None; namePrefix :在JVM上,每个运行线程都可以有一个name 字符串。这一属性为线程池中每个线程的name字符串设置了一个前缀,Tomcat将把线程号追加到这一前缀的后面。默认值:tomcat-exec-; maxThreads :该线程池可以容纳的最大线程数。默认值:200; maxIdleTime :在Tomcat关闭一个空闲线程之前,允许空闲线程持续的时间(以毫秒为单位)。只有当前活跃的线程数大于minSpareThread的值,才会关闭空闲线程。默认值:60000(一分钟)。 minSpareThreads :Tomcat应该始终打开的最小不活跃线程数。默认值:25。

2:配置Connector

重要参数说明:executor :表示使用该参数值对应的线程池; minProcessors :服务器启动时创建的处理请求的线程数; maxProcessors :最大可以创建的处理请求的线程数; acceptCount :指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。

一.Tomcat内存优化

Tomcat内存优化主要是对 tomcat 启动参数优化,我们可以在 tomcat 的启动脚本 catalina.sh 中设置JAVA_OPTS 参数。

1.JAVA_OPTS参数说明

现公司服务器内存一般都可以加到最大2G ,所以可以采取以下配置:

在cygwin=false前添加

配置完成后可重启Tomcat ,通过以下命令进行查看配置是否生效:

首先查看Tomcat 进程号:

result

我们可以看到Tomcat 进程号是27698 。

查看是否配置生效:

能在输出的信息中找到Heap Configuration中看到MaxHeapSize 等参数已经生效。

二.Tomcat并发优化

1.Tomcat连接相关参数

在Tomcat 配置文件 server.xml 中的 配置中

1.参数说明

minProcessors :最小空闲连接线程数,用于提高系统处理性能,默认值为 10 maxProcessors :最大连接线程数,即:并发处理的最大请求数,默认值为 75 acceptCount :允许的最大连接数,应大于等于 maxProcessors ,默认值为 100 enableLookups :是否反查域名,取值为: true 或 false 。为了提高处理能力,应设置为 false connectionTimeout :网络连接超时,单位:毫秒。设置为 0 表示永不超时,这样设置有隐患的。通常可设置为 30000 毫秒。其中和最大连接数相关的参数为maxProcessors 和 acceptCount 。如果要加大并发连接数,应同时加大这两个参数。web server允许的最大连接数还受制于操作系统的内核参数设置,通常 Windows 是 2000 个左右, Linux是 1000 个左右。

2.Tomcat中的配置示例

2.调整连接器connector的并发处理能力

1.参数说明

maxThreads :客户请求最大线程数 minSpareThreads :Tomcat初始化时创建的 socket 线程数 maxSpareThreads :Tomcat连接器的最大空闲 socket 线程数 enableLookups :若设为true, 则支持域名解析,可把 ip 地址解析为主机名 redirectPort :在需要基于安全通道的场合,把客户请求转发到基于SSL 的 redirectPort 端口 acceptAccount :监听端口队列最大数,满了之后客户请求会被拒绝(不能小于maxSpareThreads ) connectionTimeout :连接超时 minProcessors :服务器创建时的最小处理线程数 maxProcessors :服务器同时最大处理线程数 URIEncoding :URL统一编码

2.Tomcat中的配置示例

3.Tomcat缓存优化

1.参数说明

compression :打开压缩功能 compressionMinSize :启用压缩的输出内容大小,这里面默认为2KB compressableMimeType :压缩类型 connectionTimeout :定义建立客户连接超时的时间. 如果为 -1, 表示不限制建立客户连接的时间

2.Tomcat中的配置示例

4.参考配置

1.旧有的配置

参考网络对服务器做过如下配置,拿出来分享下:

后来发现在访问量达到3 百万多的时候出现性能瓶颈。

2.更改后的配置

⑧ 7.单线程并发

单线程并发 意味着貌似可以在单个线程中同时完成多个任务。 从表面上看,单线程并发听起来有点矛盾。 以前,在多线程体系结构中,多个任务将在多个线程之间分配,以并行执行。 因此察敏,不同任务之间的切换是通过操作系统和CPU在不同线程之间的切换来完成的。 但是,单个线程实际上可以几乎同时处理多个任务。 在本单线程并发教程中,我将解释单线程并发如何设计的,以及有何好处。请注意:本教程仍在进行中。 在不久的将来会添加更多!

请注意:本教程仍在进行中。 在不久的将来会添加更多!

在经典的多线程体系架构中,通常将每个任务分配给一个单独的线程以执行。 每个线程一次只执行一个任务。 在某些设计中,将为每个任务创建一个新线程,因此一旦任务完成,该线程就会死掉。 在其他设计中,线程池保持活动状态,该线程池一次从任务队列中执行一个任务,然后执行另一任务,如此往复。有关更多信息,请参阅我的 线程池 教程。

多线程体系架构的优点是简没岁,相对容易地在多个线程和多个CPU之间分配工作负载。 只需将任务分配给线程,然后让OS / CPU将线程调度到CPU。

但是,如果正在执行的任务需要共享数据,则多线程体系架构可能会导致许多并发问题,例如 竞态条件 , 死锁 , 饥饿 , 滑动条件 , 嵌套监视器锁定 等。通常,越多的线程共享相同的数据和数据结构,发生并发问题的可能性就越高。 换句话说,您需要在设计时留意更多内容。

当多个线程试图同时访问同一个数据结构时,经典的多线程体系结构有时还会导致拥塞。 这取决于给定数据结构的实现方式,某些线程可能会被阻塞,以等待其他正在访问该数据结构线程访问完成。

经典多线程体系结构的替代方法是单线程或 同线程 。 通过仅使用一个线程来执行应用程序中的所有任务,就可以完全避免前一部分(经典的多线程并发架构)中列出的所有并发问题。

您可以扩展单线程体系结构以使用多个线程,其中每个线程的行为就像是一个单独的隔离的单线程系统。 在那种情况下,我将此架构称为相同线程。 执行任务所需的所有数据仍保持隔离在单个线程内-在同一线程内。

如果只有一个线程执行应用程序的所有任务,则可能会导致一些问题:

    ● 从任务中阻止IO操作,将阻止线程,从而阻止整个应用程序。

    ● 长时间运行的任务,可能会产生无法接受的延迟其他任务的执行。

    ● 单个线程只能使用到单个CPU。

可以解决这些问题,但又不会失去单线程并发体系结构的简单性的优势,也不会使整体设计过于复杂。

大多数长时间运行的应用程序以某种循环执行,其中应用程序主线程正在等待来自应用程序外部的输入,处理该输入,然后返回等待状态。

这种线程循环在服务器应用程序(Web服务,服务等)和GUI应用程序中都可以使用。 有时,您可以看到该线程循环, 而有时则看不到。

您可能会想知道,在一遍又一遍地密集循环中,执行的线程是否会浪费大量CPU时间。 如果线程在运行时没有任何实际工作要做,那么可能会浪费掉大量CPU时间。拦睁 因此,如果执行循环的线程判断出休眠几毫秒是可行的,则可能会使用“休眠”,而非循环, 这样可以减少CPU的时间浪费。

线程循环通常在其生命周期内执行两种类型的任务:

    ● 重复任务

    ● 一次性任务

以下各节将对这两项任务进行更详细的说明。

重复任务是一个重复执行的任务,它在执行该任务的线程的生命周期内一次又一次地执行。 通常,对于任务的每次调用,将完全执行重复的任务。

重复任务的一个示例是检查一组入站网络连接上的传入数据。 如果检测到任何传入数据,将对其进行处理,并且在处理之后,将针对此特定调用执行重复的任务。 但是,需要一次又一次地检查入站数据,以使应用程序能够连续响应传入的数据。

一次性任务是只需要执行一次的任务。一次性任务可以是短期运行,也可以是长期运行。

短时任务是一个足够短的任务,可以在一个执行阶段中完成,而又不会使执行该任务的线程因该线程承担的其他职责(它必须执行的其他任务)而延迟。

一次性长时间运行的任务是在单个执行阶段中花费太长时间才能完成的任务。 “花费太长时间”是指执行任务中的全部工作量将占用太多的线程时间,因此其他重复任务或一次性任务将被延迟太多,以至于应用程序的总响应速度受到伤害。

为了避免单个长时间运行的任务占用过多的线程执行时间,将完成任务所需的全部工作分解为较小的块,可以一次执行一个块。每个块必须足够小,以免延迟线程执行过多任务所需的其他任务。

长时间运行的任务在内部跟踪其执行块。执行长时间运行的任务的线程将多次调用其执行方法,直到所有任务块均已完全执行。在调用特定长时间运行任务的执行方法之间,线程可以调用其他长时间运行任务,其他重复任务或线程承担的任何职责的执行方法。

一次性的长期运行任务可能是处理目录中的N个文件。可以将N个文件的处理分解成较小的块,而不是在单个执行阶段中处理所有N个文件,而每个块都在单个执行阶段中进行处理。例如,每个执行阶段可以处理1个文件。要处理所有N个文件的任务

在线程循环中,一次性任务通常由重复任务检测并执行,如下所示。

为了能够似乎同时在一个以上的任务上取得进展,在任务上取得进展的线程必须能够在这些任务之间进行切换。 这也称为任务切换。

任务切换的确切工作方式取决于任务的类型-线程是在重复任务还是一次性任务之间进行切换。 虽然总的原理还是一样的。 我将在以下各节中对这两者进行更详细的说明。

重复的任务通常只有一个方法,该方法被同一线程重复调用。 重复任务是应在应用程序的整个生命周期中重复的任务,因此它永远不会真正“完成”。 重复的任务执行了所需的操作,然后退出其执行方法,将控制权交还给调用线程。

通过以循环方式调用它们的执行方法,单个线程可以在多个重复任务之间进行切换。 首先重复执行的任务A有执行的机会,然后是B,然后是C,然后是A,依此类推。

万一重复任务没有完全完成它开始的任何工作,它可以记录它在内部走了多远,并在下次调用重复任务时从那里继续。

一次性任务与重复任务的不同之处在于,一次性任务有望在某个时间点完成。 这意味着,有时需要从任务池中删除一次性任务。

除此之外,一次完成任务之间的切换类似于重复任务之间的切换。 执行线程调用给定的一次性任务的执行方法,该任务在短时间内取得进展,然后在内部记录其执行的距离,然后退出其执行方法,将控制权交还给调用线程。 现在,调用线程可以循环方式调用任务池中的下一个一次性任务。

每次调用一次性任务的执行方法后,调用线程将检查任务是否已完成。 如果已删除,则一次性任务将从任务池中删除。

在实践中,一个应用程序可能包含一个调用一个或多个重复任务的线程循环,重复任务可以执行一次任务作为重复行为的一部分。 下图说明了这一点。 该图仅描述了一个重复的任务,但根据具体应用,可能还会有更多任务。

当单个线程要在多个任务(无论是重复任务还是一次性任务)之间切换时,必须确保在一次调用任务时,这些任务不会占用过多的线程执行时间。换句话说,确保每个任务之间执行时间的公平平衡是每个任务帮助的职责。

任务应该允许自己执行多长时间,具体取决于系统设计者。对于一次性任务,这可能会有些复杂。有些任务自然很快就完成了,而另一些任务自然要花费更长的时间才能完成。对于运行时间较长的任务,由任务的实现者来估计如何将工作分解为足够小的分区,以便可以在不延迟其他任务过多的情况下执行每个分区。

需要注意的一件有趣的事是,如果线程以循环方式调用每个一次性任务,那么任务执行器包含的一次性任务越多,每个线程获得的执行时间就越短,因为在执行任务之前需要更长的时间接下来的执行时间。

可以实现一个将某些任务优先于其他任务的任务执行器。 例如,任务执行者可以在内部将任务保存在不同的列表中,例如 执行低优先级任务列表中的任务每执行1次,就执行2次高优先级列表中的任务。

确切地说,如何执行优先任务执行器将取决于具体需求。 还有多少个优先级,例如 低/高,或低/中/高等

如果一次性任务正在等待某些异步操作完成,例如 如果来自远程服务器的答复,则一次性任务将无法继续进行下去,直到它正在等待的异步操作完成为止。 在那种情况下,一次又一次地调用该任务可能没有意义,只是为了使该任务意识到它无法取得任何进展并立即将控制权返回给调用线程。

在这种情况下,一次性任务能够将自己“停放”在任务执行器内部可能是有意义的,因此不再被调用。 异步操作完成后,一次性任务可以取消停放,然后重新插入到活动任务中,这些活动将连续调用以取得进展。 当然,要能够取消任务,系统的其他部分必须检测到异步操作已完成,以及要为该异步操作取消任务。

显然,如果在应用程序中只有一个线程正在执行,则不能利用多个CPU。 解决方案是启动多个线程。 通常,每个CPU一个线程-取决于您的线程需要执行哪种任务。 如果您有需要执行阻塞IO工作的任务,例如从文件系统或网络中读取数据,则每个CPU可能需要多个线程。 每个线程将在等待阻塞的IO操作完成时被阻塞,不执行任何操作。

当您将单线程体系结构扩展到多个单线程子系统时,从技术上讲,它不再是单线程的。 但是,每个单线程子系统通常都将被设计为一个单线程系统,并表现为一个单线程系统。 我曾经将这样的多线程单线程系统称为同线程系统,尽管我不确定这实际上是最精确的术语。 我们可能需要重新审视这些不同的设计,并在将来为它们提供更具描述性的术语。

译自: Singlethreaded Concurrency

Jakob Jenkov

Last update: 2020-12-11

⑨ 并发编程解惑之线程

主要内容:

进程是资源分配的最小单位,每个进程都有独立的代码和数据空间,一个进程包含 1 到 n 个线程。线程是 CPU 调度的最小单位,每个线程有独立的运行栈和程序计数器,线程切换开销小。

Java 程序总是从主类的 main 方法开始执行,main 方法就是 Java 程序默认的主线程,而在 main 方法中再创建的线程就是其他线程。在 Java 中,每次程序启动至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。每次使用 Java 命令启动一个 Java 程序,就相当于启动一个 JVM 实例,而每个 JVM 实例就是在操作系统中启动的一个进程。

多线程可以通过继承或实现接口的方式创建。

Thread 类是 JDK 中定义的用于控制线程对象的类,该类中封装了线程执行体 run() 方法。需要强调的一点是,线程执行先后与创建顺序无关。

通过 Runnable 方式创建线程相比通过继承 Thread 类创建线程的优势是避免了单继承的局限性。若一个 boy 类继承了 person 类,boy 类就无法通过继承 Thread 类的方式来实现多线程。

使用 Runnable 接口创建线程的过程:先是创建对象实例 MyRunnable,然后将对象 My Runnable 作为 Thread 构造方法的入参,来构造出线程。对于 new Thread(Runnable target) 创建的使用同一入参目标对象的线程,可以共享该入参目标对象 MyRunnable 的成员变量和方法,但 run() 方法中的局部变量相互独立,互不干扰。

上面代码是 new 了三个不同的 My Runnable 对象,如果只想使用同一个对象,可以只 new 一个 MyRunnable 对象给三个 new Thread 使用。

实现 Runnable 接口比继承 Thread 类所具有的优势:

线程有新建、可运行、阻塞、等待、定时等待、死亡 6 种状态。一个具有生命的线程,总是处于这 6 种状态之一。 每个线程可以独立于其他线程运行,也可和其他线程协同运行。线程被创建后,调用 start() 方法启动线程,该线程便从新建态进入就绪状态。

NEW 状态(新建状态) 实例化一个线程之后,并且这个线程没有开始执行,这个时候的状态就是 NEW 状态:

RUNNABLE 状态(就绪状敬春则态):

阻塞状态有 3 种:

如果一个线程调用了一个对象的 wait 方法, 那么这个线程就会处于等待状态(waiting 状态)直到另外一个线程调用这个对象的 notify 或者 notifyAll 方法后才会解除这个状态。

run() 里的代码执行完毕后,线程进入终结状态(TERMINATED 状态)。

线程状态有 6 种:新建、可运行、阻塞、等待、定时等待、死亡。

我们看下 join 方法的使用:

运行结果:

我们来看下 yield 方法的使用:

运行结果:

线程与线程之间是无法直接通信的,A 线程无法直接通知 B 线程,Java 中线程之间交换信息是通过共享的内存来实现的,控制共享资源的读写的访问,使得多个线程轮流执行对共享数据的操作,线程之间通信是通过对共享资源上锁或释放锁来实现的。线程排队轮流执行共享资源,这称为线程的同步。

Java 提供了很多同森历步操作(也就是线程间的通信方式),同步可使用 synchronized 关键字、Object 类的 wait/notifyAll 方法、ReentrantLock 锁、无锁同步 CAS 等方式来实现。

ReentrantLock 是 JDK 内置的一个锁对象,用于线程同步(线程通信),需要用户手动释放锁。

运行结果:

这表明同一时间段只能有 1 个线程执行 work 方法,因为 work 方法里的代码需要获取到锁才能执行,这就实现了多个线程间的通信,线程 0 获取锁,先执行,线程 1 等待,线程 0 释放锁,线程 1 继续执行。

synchronized 是一亮棚种语法级别的同步方式,称为内置锁。该锁会在代码执行完毕后由 JVM 释放。

输出结果跟 ReentrantLock 一样。

Java 中的 Object 类默认是所有类的父类,该类拥有 wait、 notify、notifyAll 方法,其他对象会自动继承 Object 类,可调用 Object 类的这些方法实现线程间的通信。

除了可以通过锁的方式来实现通信,还可通过无锁的方式来实现,无锁同 CAS(Compare-and-Swap,比较和交换)的实现,需要有 3 个操作数:内存地址 V,旧的预期值 A,即将要更新的目标值 B,当且仅当内存地址 V 的值与预期值 A 相等时,将内存地址 V 的值修改为目标值 B,否则就什么都不做。

我们通过计算器的案例来演示无锁同步 CAS 的实现方式,非线程安全的计数方式如下:

线程安全的计数方式如下:

运行结果:

线程安全累加的结果才是正确的,非线程安全会出现少计算值的情况。JDK 1.5 开始,并发包里提供了原子操作的类,AtomicBoolean 用原子方式更新的 boolean 值,AtomicInteger 用原子方式更新 int 值,AtomicLong 用原子方式更新 long 值。 AtomicInteger 和 AtomicLong 还提供了用原子方式将当前值自增 1 或自减 1 的方法,在多线程程序中,诸如 ++i 或 i++ 等运算不具有原子性,是不安全的线程操作之一。 通常我们使用 synchronized 将该操作变成一个原子操作,但 JVM 为此种操作提供了原子操作的同步类 Atomic,使用 AtomicInteger 做自增运算的性能是 ReentantLock 的好几倍。

上面我们都是使用底层的方式实现线程间的通信的,但在实际的开发中,我们应该尽量远离底层结构,使用封装好的 API,例如 J.U.C 包(java.util.concurrent,又称并发包)下的工具类 CountDownLath、CyclicBarrier、Semaphore,来实现线程通信,协调线程执行。

CountDownLatch 能够实现线程之间的等待,CountDownLatch 用于某一个线程等待若干个其他线程执行完任务之后,它才开始执行。

CountDownLatch 类只提供了一个构造器:

CountDownLatch 类中常用的 3 个方法:

运行结果:

CyclicBarrier 字面意思循环栅栏,通过它可以让一组线程等待至某个状态之后再全部同时执行。当所有等待线程都被释放以后,CyclicBarrier 可以被重复使用,所以有循环之意。

相比 CountDownLatch,CyclicBarrier 可以被循环使用,而且如果遇到线程中断等情况时,可以利用 reset() 方法,重置计数器,CyclicBarrier 会比 CountDownLatch 更加灵活。

CyclicBarrier 提供 2 个构造器:

上面的方法中,参数 parties 指让多少个线程或者任务等待至 barrier 状态;参数 barrierAction 为当这些线程都达到 barrier 状态时会执行的内容。

CyclicBarrier 中最重要的方法 await 方法,它有 2 个重载版本。下面方法用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续任务。

而下面的方法则是让这些线程等待至一定的时间,如果还有线程没有到达 barrier 状态就直接让到达 barrier 的线程执行任务。

运行结果:

CyclicBarrier 用于一组线程互相等待至某个状态,然后这一组线程再同时执行,CountDownLatch 是不能重用的,而 CyclicBarrier 可以重用。

Semaphore 类是一个计数信号量,它可以设定一个阈值,多个线程竞争获取许可信号,执行完任务后归还,超过阈值后,线程申请许可信号时将会被阻塞。Semaphore 可以用来 构建对象池,资源池,比如数据库连接池。

假如在服务器上运行着若干个客户端请求的线程。这些线程需要连接到同一数据库,但任一时刻只能获得一定数目的数据库连接。要怎样才能够有效地将这些固定数目的数据库连接分配给大量的线程呢?

给方法加同步锁,保证同一时刻只能有一个线程去调用此方法,其他所有线程排队等待,但若有 10 个数据库连接,也只有一个能被使用,效率太低。另外一种方法,使用信号量,让信号量许可与数据库可用连接数为相同数量,10 个数据库连接都能被使用,大大提高性能。

上面三个工具类是 J.U.C 包的核心类,J.U.C 包的全景图就比较复杂了:

J.U.C 包(java.util.concurrent)中的高层类(Lock、同步器、阻塞队列、Executor、并发容器)依赖基础类(AQS、非阻塞数据结构、原子变量类),而基础类是通过 CAS 和 volatile 来实现的。我们尽量使用顶层的类,避免使用基础类 CAS 和 volatile 来协调线程的执行。J.U.C 包其他的内容,在其他的篇章会有相应的讲解。

Future 是一种异步执行的设计模式,类似 ajax 异步请求,不需要同步等待返回结果,可继续执行代码。使 Runnable(无返回值不支持上报异常)或 Callable(有返回值支持上报异常)均可开启线程执行任务。但是如果需要异步获取线程的返回结果,就需要通过 Future 来实现了。

Future 是位于 java.util.concurrent 包下的一个接口,Future 接口封装了取消任务,获取任务结果的方法。

在 Java 中,一般是通过继承 Thread 类或者实现 Runnable 接口来创建多线程, Runnable 接口不能返回结果,JDK 1.5 之后,Java 提供了 Callable 接口来封装子任务,Callable 接口可以获取返回结果。我们使用线程池提交 Callable 接口任务,将返回 Future 接口添加进 ArrayList 数组,最后遍历 FutureList,实现异步获取返回值。

运行结果:

上面就是异步线程执行的调用过程,实际开发中用得更多的是使用现成的异步框架来实现异步编程,如 RxJava,有兴趣的可以继续去了解,通常异步框架都是结合远程 HTTP 调用 Retrofit 框架来使用的,两者结合起来用,可以避免调用远程接口时,花费过多的时间在等待接口返回上。

线程封闭是通过本地线程 ThreadLocal 来实现的,ThreadLocal 是线程局部变量(local vari able),它为每个线程都提供一个变量值的副本,每个线程对该变量副本的修改相互不影响。

在 JVM 虚拟机中,堆内存用于存储共享的数据(实例对象),也就是主内存。Thread Local .set()、ThreadLocal.get() 方法直接在本地内存(工作内存)中写和读共享变量的副本,而不需要同步数据,不用像 synchronized 那样保证数据可见性,修改主内存数据后还要同步更新到工作内存。

Myabatis、hibernate 是通过 threadlocal 来存储 session 的,每一个线程都维护着一个 session,对线程独享的资源操作很方便,也避免了线程阻塞。

ThreadLocal 类位于 Thread 线程类内部,我们分析下它的源码:

ThreadLocal 和 Synchonized 都用于解决多线程并发访问的问题,访问多线程共享的资源时,Synchronized 同步机制采用了以时间换空间的方式,提供一份变量让多个线程排队访问,而 ThreadLocal 采用了以空间换时间的方式,提供每个线程一个变量,实现数据隔离。

ThreadLocal 可用于数据库连接 Connection 对象的隔离,使得每个请求线程都可以复用连接而又相互不影响。

在 Java 里面,存在强引用、弱引用、软引用、虚引用。我们主要来了解下强引用和弱引用:

上面 a、b 对实例 A、B 都是强引用

而上面这种情况就不一样了,即使 b 被置为 null,但是 c 仍然持有对 C 对象实例的引用,而间接的保持着对 b 的强引用,所以 GC 不会回收分配给 b 的空间,导致 b 无法回收也没有被使用,造成了内存泄漏。这时可以通过 c = null; 来使得 c 被回收,但也可以通过弱引用来达到同样目的:

从源码中可以看出 Entry 里的 key 对 ThreadLocal 实例是弱引用:

Entry 里的 key 对 ThreadLocal 实例是弱引用,将 key 值置为 null,堆中的 ThreadLocal 实例是可以被垃圾收集器(GC)回收的。但是 value 却存在一条从 Current Thread 过来的强引用链,只有当当前线程 Current Thread 销毁时,value 才能被回收。在 threadLocal 被设为 null 以及线程结束之前,Entry 的键值对都不会被回收,出现内存泄漏。为了避免泄漏,在 ThreadLocalMap 中的 set/get Entry 方法里,会对 key 为 null 的情况进行判断,如果为 null 的话,就会对 value 置为 null。也可以通过 ThreadLocal 的 remove 方法(类似加锁和解锁,最后 remove 一下,解锁对象的引用)直接清除,释放内存空间。

总结来说,利用 ThreadLocal 来访问共享数据时,JVM 通过设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露,同时通过调用 remove、get、set 方法的时候,回收弱引用(Key 为 null 的 Entry)。当使用 static ThreadLocal 的时候(如上面的 Spring 多数据源),static 变量在类未加载的时候,它就已经加载,当线程结束的时候,static 变量不一定会被回收,比起普通成员变量使用的时候才加载,static 的生命周期变长了,若没有及时回收,容易产生内存泄漏。

使用线程池,可以重用存在的线程,减少对象创建、消亡的开销,可控制最大并发线程数,避免资源竞争过度,还能实现线程定时执行、单线程执行、固定线程数执行等功能。

Java 把线程的调用封装成了一个 Executor 接口,Executor 接口中定义了一个 execute 方法,用来提交线程的执行。Executor 接口的子接口是 ExecutorService,负责管理线程的执行。通过 Executors 类的静态方法可以初始化

ExecutorService 线程池。Executors 类的静态方法可创建不同类型的线程池:

但是,不建议使用 Executors 去创建线程池,而是通过 ThreadPoolExecutor 的方式,明确给出线程池的参数去创建,规避资源耗尽的风险。

如果使用 Executors 去创建线程池:

最佳的实践是通过 ThreadPoolExecutor 手动地去创建线程池,选取合适的队列存储任务,并指定线程池线程大小。通过线程池实现类 ThreadPoolExecutor 可构造出线程池的,构造函数有下面几个重要的参数:

参数 1:corePoolSize

线程池核心线程数。

参数 2:workQueue

阻塞队列,用于保存执行任务的线程,有 4 种阻塞队列可选:

参数 3:maximunPoolSize

线程池最大线程数。如果阻塞队列满了(有界的阻塞队列),来了一个新的任务,若线程池当前线程数小于最大线程数,则创建新的线程执行任务,否则交给饱和策略处理。如果是无界队列就不存在这种情况,任务都在无界队列里存储着。

参数 4:RejectedExecutionHandler

拒绝策略,当队列满了,而且线程达到了最大线程数后,对新任务采取的处理策略。

有 4 种策略可选:

最后,还可以自定义处理策略。

参数 5:ThreadFactory

创建线程的工厂。

参数 6:keeyAliveTime

线程没有任务执行时最多保持多久时间终止。当线程池中的线程数大于 corePoolSize 时,线程池中所有线程中的某一个线程的空闲时间若达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。但如果调用了 allowCoreThread TimeOut(boolean value) 方法,线程池中的线程数就算不超过 corePoolSize,keepAlive Time 参数也会起作用,直到线程池中的线程数量变为 0。

参数 7:TimeUnit

配合第 6 个参数使用,表示存活时间的时间单位最佳的实践是通过 ThreadPoolExecutor 手动地去创建线程池,选取合适的队列存储任务,并指定线程池线程大小。

运行结果:

线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后,还会不断的去获取队列里的任务来执行。Worker 的加锁解锁机制是继承 AQS 实现的。

我们来看下 Worker 线程的运行过程:

总结来说,如果当前运行的线程数小于 corePoolSize 线程数,则获取全局锁,然后创建新的线程来执行任务如果运行的线程数大于等于 corePoolSize 线程数,则将任务加入阻塞队列 BlockingQueue 如果阻塞队列已满,无法将任务加入 BlockingQueue,则获取全局所,再创建新的线程来执行任务

如果新创建线程后使得线程数超过了 maximumPoolSize 线程数,则调用 Rejected ExecutionHandler.rejectedExecution() 方法根据对应的拒绝策略处理任务。

CPU 密集型任务,线程执行任务占用 CPU 时间会比较长,应该配置相对少的线程数,避免过度争抢资源,可配置 N 个 CPU+1 个线程的线程池;但 IO 密集型任务则由于需要等待 IO 操作,线程经常处于等待状态,应该配置相对多的线程如 2*N 个 CPU 个线程,A 线程阻塞后,B 线程能马上执行,线程多竞争激烈,能饱和的执行任务。线程提交 SQL 后等待数据库返回结果时间较长的情况,CPU 空闲会较多,线程数应设置大些,让更多线程争取 CPU 的调度。