精华内容
下载资源
问答
  • 多线程优化比较好的软件
    千次阅读
    2021-10-24 17:09:15

    (一)前言

    最近一段时间整个公司有不少应用上线,上线后慢慢开始暴露一些问题,除去bug之外,一个很值得关注的点就是系统的优化。毕竟优化系统不仅可以使得程序更加稳定,还能节省一些资源的浪费。作为一个技术氛围很不错的公司,很多人会把自己的优化方案发出来和大家一起讨论,只不过没想到这一讨论把公司CEO以及几位P8、P9大佬都给炸出来了。

    本文将会介绍系统优化的常用方式,以及优化的一些误区。

    (二)程序优化

    2.1 慢sql

    应用上线后,我们可以通过各种云产品监控慢Sql,以阿里云的产品为例,通过应用实时监控服务ARMS就能将一段时间内的慢Sql统计出来。慢Sql出现的最大可能就是Sql走了全表扫描,没有经过索引。检查Sql语句可以通过explain命令查看执行过程,如果没有建立索引就建索引,如果已经有索引就优化索引。另外要注意避免索引失效,比如一下六点就会导致索引失效:

    • 1、复合索引不要跨列或无序使用(最佳左前缀):索引的顺序和sql语句查询时的顺序一致
    • 2、复合索引尽量使用全索引匹配
    • 3、不要在索引上进行任何操作(计算、函数、类型转换)
    • 4、like尽量以“常量”开头,不要以%开头,否则索引失效
    • 5、尽量不要使用类型转换(显示、隐式),否则索引失效,如:
    name的属性是varchar,这里变成了int
    select * from teacher where name=123
    
    • 6、 尽量不要使用or,否则索引失效

    2.2 在循环中调用查询外部服务的语句

    在系统优化时,我们还看到了在for循环中去调用查询外部服务或者数据库的情况,比如像下面这样:

    for (String user:userList) {
        User resultUser = UserMapper.selectByName(user);
        result.add(resultUser);
    }
    

    在一个循环中,多次调用数据库查询语句,要知道每次调用sql命令都是有网络开销的。遇到这种情况,建议将单次查询改成批量一次或几次查询,节省网络开销的时间。

    2.3 接口只返回必要信息

    这个要求主要针对那些调用量很大的接口,有些接口会返回大量的数据,有很多都是非必要的,导致网络传输开销大。

    优化方案也很简单,调整返回对象,只返回需要的信息。

    2.4 异步线程一定要用线程池

    如果代码中涉及到异步线程的逻辑,一定要用线程池。有一个业务就出现了自己开线程,结果程序运行过程开了N多线程,直接把应用搞崩了。

    (三)产品优化

    3.1 查询增加时间约束

    以我们现在所做的大数据应用为例,数据动不动就上亿,这个时候做好查询时时间范围的限制能给系统带来极大的性能提升。比如我们现在搜索用的ElasticSearch,就在产品测让用户每次进入时只看到最近三个月的信息。

    如果用户想查看更多就让他们自己去选时间范围。根据正常的使用情况,每一百次查询,可能只会有一次用户自定义查询时间范围的动作,极大提高效率。

    3.2 不让用户进行深分页

    目前常用的前端分页主要有两种,第一种是展示给用户开头几页和最后几页,比如下面这种:

    这种分页仅适合于数据量少的应用,另外一种分页方式是只展示当前页的前后几页,而不会给用户直接跳转到最后一页的机会,比如百度的分页:

    我们要知道不管是哪种数据库的分页,页数越深,效率就越低。尽可能在产品测就不让深分页出现。

    3.3 批量操作业务进行约束

    很多产品喜欢设计一些很牛逼的功能,比如全选下载不设置任何限制。这在技术上是很不合理的设计,这一点上最好和产品沟通,批量操作设置勾选上限。

    (四)系统优化误区

    4.1 使用多线程提高效率

    在大部分人的认知里,把单线程改成多线程总能提高效率,但就是这个问题的讨论直接把公司CEO以及好几位P8、P9大佬都炸出来了。这几位在阿里干了超过十年的大佬给出的一致意见就是,在系统优化的时候,不要轻易用多线程,你以为你提高了效率,其实可能已经埋下了很多坑

    他们的结论来源于这么多年的经验,无论是http调用,springboot内嵌的tomcat;还是rpc的dubbo调用,他们本身都维护了线程池。所以除非业务调用时的RT时间无法忍受,否则真的不建议在服务方法里起多线程。除了带来代码维护的不确定性外,从机器整体cpu能支撑的qps来看其实并没有多大提升

    有位P8表示他以前在一个部门待过,那里尽喜欢在代码里加多线程,结果就是写完代码之后,先找了一波人专门监控代码运行情况,然后再找一波人来优化代码。费时费力不讨好。

    (五)总结

    系统的优化不是什么高大上的技术,其实就是很多基本的知识。但是他又很重要,保证了系统的稳定和资源的节省。另外系统优化的目的是为了让应用运行更加稳定,如果系统优化导致应用变得不确定了,那就得不偿失了。

    最后祝所有同行1024程序员节快乐,我是鱼仔,我们下期再见。

    更多相关内容
  • 多处理器多线程软件性能优化.pdf
  • 多线程是一种基于硬件或软件的处理技术,它的首要目标是计算型工作中利用并发来提高性能。多线程也可以用于区别各种任务,以便可以将优先权分配给更多时间敏感的流量,如语音、视频或关键数据。
  • 利用多核多线程进行程序优化

    千次阅读 2019-03-28 13:19:40
    对于应用层面的优化,将采用多线程和 CPU 亲和力技术;在微架构层面,采用 Cache 优化。 并行设计 利用并行程序设计模型来设计应用程序,就必须把自己的思维从线性模型中拉出来,重新审视整个处理流程,...

    样例程序

    程序功能:求从1一直到 APPLE_MAX_VALUE (100000000) 相加累计的和,并赋值给 apple 的 a 和 b ;求 orange 数据结构中的 a[i]+b[i ] 的和,循环 ORANGE_MAX_VALUE(1000000) 次。

    说明:

    1. 由于样例程序是从实际应用中抽象出来的模型,所以本文不会进行 test.a=test.b= test.b+sum 、中间变量(查找表)等类似的优化。
    2. 以下所有程序片断均为部分代码,完整代码请参看本文最下面的附件。

    清单 1. 样例程序

    #define ORANGE_MAX_VALUE      1000000
    #define APPLE_MAX_VALUE       100000000
    #define MSECOND               1000000
    
    struct apple
    {
         unsigned long long a;
    	unsigned long long b;
    };
    
    struct orange
    {
    	int a[ORANGE_MAX_VALUE];
    	int b[ORANGE_MAX_VALUE];
    	
    };
    
    int main (int argc, const char * argv[]) {
        // insert code here...
         struct apple test;
    	struct orange test1;
    	
    	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
    	{
    		test.a += sum;
    		test.b += sum;
    	}
    	
         sum=0;
    	for(index=0;index<ORANGE_MAX_VALUE;index++)
    	{
    		sum += test1.a[index]+test1.b[index];
    	}
    
         return 0;
    }

    K-Best 测量方法

    在检测程序运行时间这个复杂问题上,将采用 Randal E.Bryant 和 David R. O’Hallaron 提出的 K 次最优测量方法。假设重复的执行一个程序,并纪录 K 次最快的时间,如果发现测量的误差 ε 很小,那么用测量的最快值表示过程的真正执行时间, 称这种方法为“ K 次最优(K-Best)方法”,要求设置三个参数:

    K: 要求在某个接近最快值范围内的测量值数量。

    ε 测量值必须多大程度的接近,即测量值按照升序标号 V1, V2, V3, … , Vi, … ,同时必须满足(1+ ε)Vi >= Vk

    M: 在结束测试之前,测量值的最大数量。

    按照升序的方式维护一个 K 个最快时间的数组,对于每一个新的测量值,如果比当前 K 处的值更快,则用最新的值替换数组中的元素 K ,然后再进行升序排序,持续不断的进行该过程,并满足误差标准,此时就称测量值已经收敛。如果 M 次后,不能满足误差标准,则称为不能收敛。

    在接下来的所有试验中,采用 K=10,ε=2%,M=200 来获取程序运行时间,同时也对 K 次最优测量方法进行了改进,不是采用最小值来表示程序执行的时间,而是采用 K 次测量值的平均值来表示程序的真正运行时间。由于采用的误差 ε 比较大,在所有试验程序的时间收集过程中,均能收敛,但也能说明问题。

    为了可移植性,采用 gettimeofday() 来获取系统时钟(system clock)时间,可以精确到微秒。

    测试环境

    硬件:联想 Dual-core 双核机器,主频 2.4G,内存 2G

    软件:Suse Linunx Enterprise 10,内核版本:linux-2.6.16

    软件优化的三个层次

    医生治病首先要望闻问切,然后才确定病因,最后再对症下药,如果胡乱医治一通,不死也残废。说起来大家都懂的道理,但在软件优化过程中,往往都喜欢犯这样的错误。不分青红皂白,一上来这里改改,那里改改,其结果往往不如人意。

    一般将软件优化可分为三个层次:系统层面,应用层面及微架构层面。首先从宏观进行考虑,进行望闻问切,即系统层面的优化,把所有与程序相关的信息收集上来,确定病因。确定病因后,开始从微观上进行优化,即进行应用层面和微架构方面的优化。

    1. 系统层面的优化:内存不够,CPU 速度过慢,系统中进程过多等
    2. 应用层面的优化:算法优化、并行设计等
    3. 微架构层面的优化:分支预测、数据结构优化、指令优化等

    软件优化可以在应用开发的任一阶段进行,当然越早越好,这样以后的麻烦就会少很多。

    在实际应用程序中,采用最多的是应用层面的优化,也会采用微架构层面的优化。将某些优化和维护成本进行对比,往往选择的都是后者。如分支预测优化和指令优化,在大型应用程序中,往往采用的比较少,因为维护成本过高。

    本文将从应用层面和微架构层面,对样例程序进行优化。对于应用层面的优化,将采用多线程和 CPU 亲和力技术;在微架构层面,采用 Cache 优化。

    并行设计

    利用并行程序设计模型来设计应用程序,就必须把自己的思维从线性模型中拉出来,重新审视整个处理流程,从头到尾梳理一遍,将能够并行执行的部分识别出来。

    可以将应用程序看成是众多相互依赖的任务的集合。将应用程序划分成多个独立的任务,并确定这些任务之间的相互依赖关系,这个过程被称为分解(Decomosition)。分解问题的方式主要有三种:任务分解、数据分解和数据流分解。关于这部分的详细资料,请参看参考资料一。

    仔细分析样例程序,运用任务分解的方法 ,不难发现计算 apple 的值和计算 orange 的值,属于完全不相关的两个操作,因此可以并行。

    改造后的两线程程序:

    清单 2. 两线程程序

    void* add(void* x)
    {		
    	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
    	{
    		((struct apple *)x)->a += sum;
    		((struct apple *)x)->b += sum;	
    	}
    		
    	return NULL;
    }
    	
    int main (int argc, const char * argv[]) {
    		// insert code here...
    	struct apple test;
    	struct orange test1={{0},{0}};
    	pthread_t ThreadA;
    		
    	pthread_create(&ThreadA,NULL,add,&test);
    		
    	for(index=0;index<ORANGE_MAX_VALUE;index++)
    	{
    		sum += test1.a[index]+test1.b[index];
    	}		
    	
         pthread_join(ThreadA,NULL);
    
    	return 0;
    }

    更甚一步,通过数据分解的方法,还可以发现,计算 apple 的值可以分解为两个线程,一个用于计算 apple a 的值,另外一个线程用于计算 apple b 的值(说明:本方案抽象于实际的应用程序)。但两个线程存在同时访问 apple 的可能性,所以需要加锁访问该数据结构。

    改造后的三线程程序如下:

    清单 3. 三线程程序

    struct apple
    {
         unsigned long long a;
    	unsigned long long b;
    	pthread_rwlock_t rwLock;
    };
    
    void* addx(void* x)
    {
    	pthread_rwlock_wrlock(&((struct apple *)x)->rwLock);
    	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
    	{
    		((struct apple *)x)->a += sum;
    	}
    	pthread_rwlock_unlock(&((struct apple *)x)->rwLock);
    	
    	return NULL;
    }
    
    void* addy(void* y)
    {
    	pthread_rwlock_wrlock(&((struct apple *)y)->rwLock);
    	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
    	{
    		((struct apple *)y)->b += sum;
    	}
    	pthread_rwlock_unlock(&((struct apple *)y)->rwLock);
    	
    	return NULL;
    }
    
    
    
    int main (int argc, const char * argv[]) {
        // insert code here...
         struct apple test;
    	struct orange test1={{0},{0}};
    	pthread_t ThreadA,ThreadB;
    	
    	pthread_create(&ThreadA,NULL,addx,&test);
    	pthread_create(&ThreadB,NULL,addy,&test);
    
    	for(index=0;index<ORANGE_MAX_VALUE;index++)
    	{
    		sum+=test1.a[index]+test1.b[index];
    	}
    	
         pthread_join(ThreadA,NULL);
         pthread_join(ThreadB,NULL);
    	
         return 0;
    }

    这样改造后,真的能达到我们想要的效果吗?通过 K-Best 测量方法,其结果让我们大失所望,如下图:

    图 1. 单线程与多线程耗时对比图

    单线程与多线程耗时对比图

    为什么多线程会比单线程更耗时呢?其原因就在于,线程启停以及线程上下文切换都会引起额外的开销,所以消耗的时间比单线程多。

    为什么加锁后的三线程比两线程还慢呢?其原因也很简单,那把读写锁就是罪魁祸首。通过 Thread Viewer 也可以印证刚才的结果,实际情况并不是并行执行,反而成了串行执行,如图2:

    图 2. 通过 Viewer 观察三线程运行情况

    通过 Viewer 观察三线程运行情况

    其中最下面那个线程是主线程,一个是 addx 线程,另外一个是 addy 线程,从图中不难看出,其他两个线程为串行执行。

    通过数据分解来划分多线程,还存在另外一种方式,一个线程计算从1到 APPLE_MAX_VALUE/2 的值,另外一个线程计算从APPLE_MAX_VALUE/2+1 到 APPLE_MAX_VALUE 的值,但本文会弃用这种模型,有兴趣的读者可以试一试。

    在采用多线程方法设计程序时,如果产生的额外开销大于线程的工作任务,就没有并行的必要。线程并不是越多越好,软件线程的数量尽量能与硬件线程的数量相匹配。最好根据实际的需要,通过不断的调优,来确定线程数量的最佳值。

     

    加锁与不加锁

    针对加锁的三线程方案,由于两个线程访问的是 apple 的不同元素,根本没有加锁的必要,所以修改 apple 的数据结构(删除读写锁代码),通过不加锁来提高性能。

    测试结果如下:

    图 3. 加锁与不加锁耗时对比图

    加锁与不加锁耗时对比图

    其结果再一次大跌眼镜,可能有些人就会越来越糊涂了,怎么不加锁的效率反而更低呢?将在针对 Cache 的优化一节中细细分析其具体原因。

    在实际测试过程中,不加锁的三线程方案非常不稳定,有时所花费的时间相差4倍多。

    要提高并行程序的性能,在设计时就需要在较少同步和较多同步之间寻求折中。同步太少会导致错误的结果,同步太多又会导致效率过低。尽量使用私有锁,降低锁的粒度。无锁设计既有优点也有缺点,无锁方案能充分提高效率,但使得设计更加复杂,维护操作困难,不得不借助其他机制来保证程序的正确性。

    回页首

    针对 Cache 的优化

    在串行程序设计过程中,为了节约带宽或者存储空间,比较直接的方法,就是对数据结构做一些针对性的设计,将数据压缩 (pack) 的更紧凑,减少数据的移动,以此来提高程序的性能。但在多核多线程程序中,这种方法往往有时会适得其反。

    数据不仅在执行核和存储器之间移动,还会在执行核之间传输。根据数据相关性,其中有两种读写模式会涉及到数据的移动:写后读和写后写 ,因为这两种模式会引发数据的竞争,表面上是并行执行,但实际只能串行执行,进而影响到性能。

    处理器交换的最小单元是 cache 行,或称 cache 块。在多核体系中,对于不共享 cache 的架构来说,两个独立的 cache 在需要读取同一 cache 行时,会共享该 cache 行,如果在其中一个 cache 中,该 cache 行被写入,而在另一个 cache 中该 cache 行被读取,那么即使读写的地址不相交,也需要在这两个 cache 之间移动数据,这就被称为 cache 伪共享,导致执行核必须在存储总线上来回传递这个 cache 行,这种现象被称为“乒乓效应”。

    同样地,当两个线程写入同一个 cache 的不同部分时,也会互相竞争该 cache 行,也就是写后写的问题。上文曾提到,不加锁的方案反而比加锁的方案更慢,就是互相竞争 cache 的原因。

    在 X86 机器上,某些处理器的一个 cache 行是64字节,具体可以参看 Intel 的参考手册。

    既然不加锁三线程方案的瓶颈在于 cache,那么让 apple 的两个成员 a 和 b 位于不同的 cache 行中,效率会有所提高吗?

    修改后的代码片断如下:

    清单 4. 针对Cache的优化

    struct apple
    {
    	unsigned long long a;
    	char c[128];  /*32,64,128*/
    	unsigned long long b;
    };

    测量结果如下图所示:

    图 4. 增加 Cache 时间耗时对比图

    增加 Cache 时间耗时对比图

    小小的一行代码,尽然带来了如此高的收益,不难看出,我们是用空间来换时间。当然读者也可以采用更简便的方法: __attribute__((__aligned__(L1_CACHE_BYTES))) 来确定 cache 的大小。

    如果对加锁三线程方案中的 apple 数据结构也增加一行类似功能的代码,效率也是否会提升呢?性能不会有所提升,其原因是加锁的三线程方案效率低下的原因不是 Cache 失效造成的,而是那把锁。

    在多核和多线程程序设计过程中,要全盘考虑多个线程的访存需求,不要单独考虑一个线程的需求。在选择并行任务分解方法时,要综合考虑访存带宽和竞争问题,将不同处理器和不同线程使用的数据放在不同的 Cache 行中,将只读数据和可写数据分离开。

    CPU 亲和力

    CPU 亲和力可分为两大类:软亲和力和硬亲和力。

    Linux 内核进程调度器天生就具有被称为 CPU 软亲和力(affinity) 的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。但不代表不会进行小范围的迁移。

    CPU 硬亲和力是指进程固定在某个处理器上运行,而不是在不同的处理器之间进行频繁的迁移。这样不仅改善了程序的性能,还提高了程序的可靠性。

    从以上不难看出,在某种程度上硬亲和力比软亲和力具有一定的优势。但在内核开发者不断的努力下,2.6内核软亲和力的缺陷已经比2.4的内核有了很大的改善。

    在双核机器上,针对两线程的方案,如果将计算 apple 的线程绑定到一个 CPU 上,将计算 orange 的线程绑定到另外一个 CPU 上,效率是否会有所提高呢?

    程序如下:

    清单 5. CPU 亲和力

    struct apple
    {
    	unsigned long long a;
    	unsigned long long b;
    };
    	
    struct orange
    {
    	int a[ORANGE_MAX_VALUE];
    	int b[ORANGE_MAX_VALUE];		
    };
    		
    inline int set_cpu(int i)
    {
    	CPU_ZERO(&mask);
    	
    	if(2 <= cpu_nums)
    	{
    		CPU_SET(i,&mask);
    		
    		if(-1 == sched_setaffinity(gettid(),sizeof(&mask),&mask))
    		{
    			return -1;
    		}
    	}
    	return 0;
    }
    
    	
    void* add(void* x)
    {
    	if(-1 == set_cpu(1))
    	{
    		return NULL;
    	} 
    		
    	for(sum=0;sum<APPLE_MAX_VALUE;sum++)
    	{
    		((struct apple *)x)->a += sum;
    		((struct apple *)x)->b += sum;
    	}	
    	
    	return NULL;
    }
    	
    int main (int argc, const char * argv[]) {
    		// insert code here...
    	struct apple test;
    	struct orange test1;
    	
    	cpu_nums = sysconf(_SC_NPROCESSORS_CONF);
    	
    	if(-1 == set_cpu(0))
    	{
    		return -1;
    	} 
    		
    	pthread_create(&ThreadA,NULL,add,&test);
    				
    	for(index=0;index<ORANGE_MAX_VALUE;index++)
    	{
    		sum+=test1.a[index]+test1.b[index];
    	}		
    		
    	pthread_join(ThreadA,NULL);
    		
    	return 0;
    }

    测量结果为:

    图 5. 采用硬亲和力时间对比图(两线程)

    采用硬亲和力时间对比图(两线程)

    其测量结果正是我们所希望的,但花费的时间还是比单线程的多,其原因与上面分析的类似。

    进一步分析不难发现,样例程序大部分时间都消耗在计算 apple 上,如果将计算 a 和 b 的值,分布到不同的 CPU 上进行计算,同时考虑 Cache 的影响,效率是否也会有所提升呢?

    图 6. 采用硬亲和力时间对比图(三线程)

    采用硬亲和力时间对比图(三线程)

    从时间上观察,设置亲和力的程序所花费的时间略高于采用 Cache 的三线程方案。由于考虑了 Cache 的影响,排除了一级缓存造成的瓶颈,多出的时间主要消耗在系统调用及内核上,可以通过 time 命令来验证:

    #time ./unlockcachemultiprocess
        real   0m0.834s      user  0m1.644s       sys    0m0.004s
    #time ./affinityunlockcacheprocess
        real   0m0.875s      user  0m1.716s       sys    0m0.008s

    通过设置 CPU 亲和力来利用多核特性,为提高应用程序性能提供了捷径。同时也是一把双刃剑,如果忽略负载均衡、数据竞争等因素,效率将大打折扣,甚至带来事倍功半的结果。

    在进行具体的设计过程中,需要设计良好的数据结构和算法,使其适合于应用的数据移动和处理器的性能特性。

     

    总结

    根据以上分析及实验,对所有改进方案的测试时间做一个综合对比,如下图所示:

    图 7. 各方案时间对比图

    各方案时间对比图

    单线程原始程序平均耗时:1.049046s,最慢的不加锁三线程方案平均耗时:2.217413s,最快的三线程( Cache 为128)平均耗时:0.826674s,效率提升约26%。当然,还可以进一步优化,让效率得到更高的提升。

    从上图不难得出结论:采用多核多线程并行设计方案,能有效提高性能,但如果考虑不全面,如忽略带宽、数据竞争及数据同步不当等因素,效率反而降低,程序执行越来越慢。

    如果抛开本文开篇时的限制,采用上文曾提到的另外一种数据分解模型,同时结合硬亲和力对样例程序进行优化,测试时间为0.54s,效率提升了92%。

    软件优化是一个贯穿整个软件开发周期,从开始设计到最终完成一直进行的连续过程。在优化前,需要找出瓶颈和热点所在。正如最伟大的 C 语言大师 Rob Pike 所说:

    如果你无法断定程序会在什么地方耗费运行时间,瓶颈经常出现在意想不到的地方,所以别急于胡乱找个地方改代码,除非你已经证实那儿就是瓶颈所在。

    将这句话送给所有的优化人员,和大家共勉。

     

    参考资料

    • 请参考书籍《多核程序设计技术》,了解更多关于多线程设计的理念
    • 请参考书籍《软件优化技术》,了解更多关于软件优化的技术
    • 请参考书籍《UNIX编程艺术》, 了解更多关于软件架构方面的知识
    • 参考文章《CPU Affinity》,了解更多关于CPU亲和力的信息
    • 参考文章《管理处理器的亲和性(affinity)》,了解更多关于CPU亲和力的信息

    --------------------- 
    作者:源景 
    来源:CSDN 
    原文:https://blog.csdn.net/yuanjingjiang/article/details/17473951 
    版权声明:本文为博主原创文章,转载请附上博文链接!

    展开全文
  • 图片压缩工具,采用TinyPng接口,更描述信息请参考博客:https://blog.csdn.net/ByAlick
  • 代码说明 对sm4进行了优化加速,具体方法包括多线程运行、SMID指令集优化、循环展开等。 运行指导 将源码clone到本地运行main函数即可运行。 软件环境:Visual Studio 2019 硬件环境:PC机
  • CAS最大的意义,就是让我们写这种多线程安全的代码,提供了一个新的思路和方向 (就和锁不一样了) 很多功能,既可以是硬件实现,也可以是软件实现 就像刚才这段比较交换逻辑,这就相当于硬件直接实现出来了,通过这...

    一、常见的锁策略

    锁策略,和普通程序猿基本没啥关系,和 "实现锁” 的人才有关系的
    这里所提到的锁策略,和 Java 本身没关系,适用于所有和 “锁” 相关的情况

    1、乐观锁 vs 悲观锁

    处理锁冲突的态度(原因)

    悲观锁:预期锁冲突的概率很高
    乐观锁:预期锁冲突的概率很低

    乐观的态度:认为,下一波疫情即使来了,但是菜应该是能买到 (根据前两波疫情的经验),就不必专门做特殊的准备

    悲观的态度:认为,下一波疫情来了之后,可能就买不到菜,于是换一个大的冰箱,去超市定期屯一些米面油肉+方便面+矿泉水+常用药…… [要做的事情更多,付出更多的成本和待见]

    悲观锁,做的工作更多,付出的成本更多,更低效
    乐观锁,做的工作更少,付出的成本更低,更高效


    2、读写锁 vs 普通的互斥锁

    对于普通的互斥锁,只有两个操作,加锁和解锁
    只要两个线程针对同一个对象加锁,就会产生互斥

    对于读写锁来说,分成了三个操作:

    1. 加读锁 – 如果代码只是进行读操作,就加读锁

    2. 加写锁 – 如果代码中进行了修改操作,就加写锁

    3. 解锁

    针对读锁和读锁之间,是不存在互斥关系的。读锁和写锁之间,写锁和写锁之间,才需要互斥
    多线程同时读同一个变量,不会有线程安全问题!
    而且在很多场景中,都是读操作多,写操作少 (数据库索引)


    3、重量级锁 vs 轻量级锁

    处理锁冲突的结果

    和上面的悲观乐观有一定重叠

    重量级锁,就是做了更多的事情,开销更大
    轻量级锁,做的事情更少,开销更小

    也可以认为,通常情况下,悲观锁一般都是重量级锁,乐观锁一般都是轻量级锁 (不绝对)

    在使用的锁中,如果锁是基于内核的一些功能来实现的 (比如调用了操作系统提供的 mutex接口),此时一般认为这是重量级锁 (操作系统的锁会在内核中做很多的事情,比如让线程阻塞等待…)
    如果锁是纯用户态实现的,此时一般认为这是轻量级锁 (用户态的代码更可控,也更高效)


    4、挂起等待锁 vs 自旋锁

    挂起等待锁,往往就是通过内核的一些机制来实现的,往往较重,[重量级锁的一种典型实现]

    自旋锁,往往就是通过用户态代码来实现的,往往较轻, [轻量级锁的一种典型实现]


    5、公平锁 vs 非公平锁

    公平锁: 多个线程在等待一把锁的时候,谁是先来的,谁就能先获取到这个锁,(遵守先来后到)
    非公平锁: 多个线程在等待—把锁的时候,不遵守先来后到,(每个等待的线程获取到锁的概率都是均等的)

    注意:此处约定的是,遵守先来后到,才是公平

    对于操作系统来说,本身线程之间的调度就是随机的 (机会均等的),操作系统提供的 mutex这个锁,就是属于非公平锁
    –> 考虑到相同优先级的情况,实际开发中很少会手动修改线程的优先级,(改了之后在宏观上的体会并不明显)

    要想实现公平锁,反而要付出更多的代价,(得整个队列,来把这些参与竞争的线程给排一排先来后到


    6、可重入锁 vs 不可重入锁

    一个线程,针对一把锁,咔咔连续加锁两次,如果会死锁,就是不可重入锁;如果不会死锁,就是可重入锁


    7、synchronized 特性

    1.既是—个乐观锁,也是一个悲观锁 (根据锁竞争的激烈程度,自适应)

    2.不是读写锁只是一个普通互斥锁

    3.既是一个轻量级锁,也是一个重量级锁 (根据锁竞争的激烈程度,自适应)

    4.轻量级锁的部分基于自旋锁来实现,重量级的部分基于挂起等待锁来实现

    5.非公平锁

    6.可重入锁


    二、CAS

    1、什么是 CAS

    CAS:全称 Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

    我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

    1. 比较 A 与 V 是否相等。(比较)

    2. 如果比较相等,将 B 写入 V。(交换)

    3. 返回操作是否成功

    CAS 伪代码:

    下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解
    CAS 的工作流程.

    boolean CAS(address, expectValue, swapValue) {
    	if (&address == expectedValue) {
    		&address = swapValue;
    		return true;
    	}
    	return false;
    }
    

    在这里插入图片描述

    此处所谓的 CAS 指的是,CPU 提供了一个单独的 CAS 指令,通过这一条指令,就完成上述伪代码描述的过程

    如果上述过程都是这 “—条指令" 就干完了,
    就相当于这是原子的了 (CPU上面执行的指令就是—条—条执行的…指令已经是不可分割的最小单位)
    此时线程就安全了

    CAS最大的意义,就是让我们写这种多线程安全的代码,提供了一个新的思路和方向 (就和锁不一样了)

    很多功能,既可以是硬件实现,也可以是软件实现
    就像刚才这段比较交换逻辑,这就相当于硬件直接实现出来了,通过这一条指令,封装好,直接让咱们用了


    2、CAS 有哪些应用

    2.1、基于CAS能够实现"原子类"

    Java 标准库中提供了一组原子类,针对所常用多一些 int, long, int array… 进行了封装,可以基于 CAS 的方式进行修改,并且线程安

    java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的
    典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.

    public class TestDemo {
        public static void main(String[] args) throws InterruptedException {
           AtomicInteger num = new AtomicInteger(0); // 创建一个整数 值是 0
            // 此方法相当于 num++
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    num.getAndIncrement();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    num.getAndIncrement();
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            // 通过 get 方法得到原子类 内部的数值
            System.out.println(num.get());
        }
    }
    

    运行结果:100000

    这个代码里面不存在线程安全问题,基于 CAS 实现的 ++ 操作
    这里面就可以保证既能够线程安全,又能够比 synchronized 高效,``synchronized会涉及到锁的竞争,两个线程要相互等待CAS` 不涉及到线程阻塞等待

    方法:

    num.incrementAndGet(); // ++num
    num.decrementAndGet(); // --num
    num.getAndIncrement(); // num++;
    num.getAndDecrement(); // num--;
    num.getAndAdd(10); // += 10
    

    伪代码实现:

    class AtomicInteger {
    	private int value;
    	
        public int getAndIncrement() {
    		int oldValue = value;
    		while ( CAS(value, oldValue, oldValue+1) != true) {
    			oldValue = value;
    		}
    		return oldValue;
    	}
    }
    

    在这里插入图片描述
    在这里插入图片描述

    假设两个线程同时调用 getAndIncrement

    1. 两个线程都读取 value 的值到 oldValue 中. (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)
    2. 线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值
    3. 线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要进入循环.
      在循环里重新读取 value 的值赋给 oldValue
    4. 线程2 接下来第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作
    5. 线程1 和 线程2 返回各自的 oldValue 的值即可

    通过形如上述代码就可以实现一个原子类,不需要使用重量级锁,就可以高效的完成多线程的自增操作
    本来 check and set 这样的操作在代码角度不是原子的,但是在硬件层面上可以让一条指令完成这个操作, 也就变成原子的了


    2.2、基于CAS能够实现"自旋锁"

    基于 CAS 实现更灵活的锁,获取到更多的控制权

    自旋锁伪代码 :

    public class SpinLock {
        private Thread owner = null; // 记录下当前锁被哪个线程持有了,为 null 表示当前未加锁
        
        public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有.
        // 如果这个锁已经被别的线程持有, 那么就自旋等待.
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
            while(!CAS(this.owner, null, Thread.currentThread())){
            }
        }
        
        public void unlock (){
            this.owner = null;
        }
    }
    

    和刚才的原子类类似,也是通过—个循环来实现的,循环里面调用CAS

    CAS 会比较当前的 owner 值是否是 null
    如果是 null 就改成当前线程,意思就是当前线程拿到了锁;如果不是 null,就返回false,进入下次循环
    下次循环仍然是进行CAS操作

    如果当前这个锁**一直被别人持有,**当前尝试加锁的线程就会在这个 while 的地方快速反复的进行循环 => 自旋 (忙等)

    自旋锁是一个轻量级锁,也可以视为是一个乐观锁
    当前这把锁虽然没能立即拿到,预期很快就能拿到 (假设锁冲突不激烈)
    短暂的自旋几次,浪费点CPU,问题都不大,好处就是只要这边锁─释放,就能立即的拿到锁

    例如:滑稽老铁追女神,女神说滚,滑稽滚了,过一会儿又问,这样的锲而不舍的过程,就相当于"自旋”的过程,也就是"忙等"的过程

    好处:一旦女神分手了,心里处在空虚的情况下,就容易趁虚而入

    坏处:浪费大量的时间精力 (不如多看看书学习学习)
    如果滑稽老哥比较乐观,已经洞察到了女生的感情即将发生危机,短时间付出这些成本也是值得的
    如果要是情况比较悲观的话,显然自旋锁就不合适


    3、CAS 的 ABA 问题

    3.1、ABA 问题

    CAS 中的关键,是先比较,再交换
    比较其实是在比较当前值和旧值是不是相同,把这两个值相同视为是中间没有发生过改变

    但是这里的结论存在漏洞
    当前值和旧值相同可能是中间确实没改变过,也有可能变了,但是又变回来了

    假设存在两个线程 t1 和 t2,有一个共享变量 num,初始值为 A
    接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要

    1. 先读取 num 的值,记录到 oldNum 变量中

    2. 使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z

    但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A

    线程 t1 的 CAS 是期望 num 不变就修改。但是 num 的值已经被 t2 给改了,只不过又改成 A 了,这
    个时候 t1 究竟是否要更新 num 的值为 Z 呢?

    到这一步, t1 线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程

    这样的漏洞,在大多数情况下,其实没啥影响,但是,极端情况下也会引起 bug

    这就好比,我们买一个手机,无法判定这个手机是刚出厂的新手机,还是别人用旧了,又翻新过的手

    举—个典型的例子,ABA问题产生的bug:

    假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作
    我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.
    如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.
    正常的过程:

    1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
      望更新为 50.

    2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

    3. 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

    异常的过程:

    1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
      望更新为 50.

    2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

    1. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !

    2. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作

    这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题搞的鬼

    int oldValue = value; //读取旧值
    CAS(&value, oldValue, oldValue - 50)
    

    当按下取款的操作的时候,机器卡了一下,滑稽多按了—下取款~
    这就相当于,一次取钱操作,执行了两遍,(两个线程,并发的去执行这个取钱操作),咱们的预期效果应该是只能取成功一次!(希望取走50,账户还剩50)

    假设在取款的一瞬间,滑稽的朋友给他转了50此时就会触发ABA问题

    在这里插入图片描述

    卡了和转了50,这两次巧合导致了一个存在 BUG 的 ABA 问题 (极端场景的问题)
    哪怕这样的 bug 出现概率是 0.01%,咱们也需要处理!!!
    一个互联网产品每天接收的请求,处理的用户量可能是非常大的!!


    3.2、解决方案

    给要修改的值, 引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期

    这个版本号只能变大,不能变小,修改变量的时候,比较就不是比较变量本身了,而是比较版本号了

    这里不一定非得用“版本号",也可以用“时间戳"

    • CAS 操作在读取旧值的同时,也要读取版本号
    • 真正修改的时候
      • 如果当前版本号和读到的版本号相同,则修改数据,并把版本号 + 1.
      • 如果当前版本号高于读到的版本号,就操作失败 (认为数据已经被修改过了

    这就好比, 判定这个手机是否是翻新机, 那么就需要收集每个手机的数据, 第一次挂在电商网站上的
    手机记为版本1, 以后每次这个手机出现在电商网站上, 就把版本号进行递增. 这样如果买家不在意
    这是翻新机, 就买. 如果买家在意, 就可以直接略过

    对比理解上面的转账例子:

    假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50操作.
    我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.
    为了解决 ABA 问题, 给余额搭配一个版本号, 初始设为 1.

    1. 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100,
      版本号为 1, 期望更新为 50.

    2. 线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.

    3. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100, 版本号变成3.

    4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读
      到的版本号为 1, 版本小于当前版本, 认为操作失败

    在这里插入图片描述

    此处就要求,每次针对余额进行修改,都让版本+1
    每次修改之前,也要先对比版本看看旧值和当前值是否一致

    当引入版本号之后,t2再尝试进行这里的比较版本操作,就发现版本的旧值和当前值并不匹配.
    因此就放弃进行修改
    如果直接拿变量本身进行判定,因为变量的值有加有减, 就容易出现 ABA 的情况,现在是拿版本号来进行判定,要求版本号只能增加, 这个时候就不会有ABA问题了

    这种基于版本号的方式来进行多线程数据的控制,也是一种乐观锁的典型实现

    1. 数据库里
    2. 版本管理工具 (SVN) 通过版本号来进行多人开发的协同

    在 Java 标准库中提供了 AtomicStampedReference<E> 类. 这个类可以对某个类进行包装, 在内部就提
    供了上面描述的版本管理功能.

    相关面试题:

    1. 讲解下你自己理解的 CAS 机
      全称 Compare and swap, 即 “比较并交换”. 相当于通过一个原子的操作, 同时完成 “读取内存, 比
      较是否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的支撑.

    2. ABA问题怎么解决?
      给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
      如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当
      前版本号比之前读到的版本号大, 就认为操作失败


    三、synchronized 中的锁优化机制

    1、加锁工作过程

    Java 的版本非常多,在这些版本变迁的过程中,很多地方都有了不少的变化,我们只考虑 JDK 1.8

    JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级 。


    1.1、偏向锁

    偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程),如果没有其他线程参与竞争锁,那么就不会真正执行加锁操作,从而降低程序开销,一旦真的涉及到其他的线程竞争,再取消偏向锁状态,进入轻量级锁状态

    在这里插入图片描述
    举个栗子理解偏向锁 :

    有一天我看上了一个小哥哥,长的又帅又有钱
    万一后面有一天,我腻歪了,然后想把他甩了,但是他要是对我纠缠不休,这还麻烦

    • 我就只是和这个小哥哥搞暧昧。同时,又不明确我们彼此的关系。
    • 这样做的目的就是为了有朝一日,我想换男朋友了,就直接甩了就行
    • 但是如果再这个过程中,有另外一个妹子,也在对这个小哥哥频频示好
      我就需要提高警惕了,对于这种情况,就要立即和小哥哥确认关系 (男女朋友的关系),立即对另外的妹子进行回击:他是我男朋友。你离他远点

    偏向锁 并不是真的加锁,只是做了一个标记
    带来的好处就是,后续如果没人竞争的时候,就避免了加锁解锁的开销
    偏向锁,升级到轻量级锁的过程
    如果没有其他的妹子和我竞争,就一直不去确立关系,(节省了确立关系 / 分手的开销)
    如果没有其他的线程来竞争这个锁,就不必真的加锁,(节省了加锁解锁的开销)

    文里的偏向锁,和懒汉模式也有点像,思路都是一致的,只是在必要的时候,才进行操作,如果不必要,则能省就省


    1.2、轻量级锁

    着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态 (自适应的自旋锁).
    此处的轻量级锁就是通过 CAS 来实现

    • 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
    • 如果更新成功, 则认为加锁成功
    • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU)

    自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.
    因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.
    也就是所谓的 “自适应”


    1.3、重量级锁

    如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁
    此处的重量级锁就是指用到内核提供的 mutex

    • 执行加锁操作, 先进入内核态.
    • 在内核态判定当前锁是否已经被占用
    • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
    • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
    • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁

    2、synchronized 几个典型的优化手段

    2.1、锁膨胀 / 锁升级

    体现了 synchronized 能够"自适应"这样的能力


    2.2、锁粗化

    一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化

    此处的粗细指的是“锁的粒度",锁的粒度: 粗和细

    加锁代码涉及到的范围,加锁代码的范围越大,认为锁的粒度越粗范围越小,则认为粒度越细

    在这里插入图片描述
    实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.
    但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁

    举个栗子理解锁粗化 :

    滑稽老哥当了领导, 给下属交代工作任务:
    方式一:

    • 打电话, 交代任务1, 挂电话.
      打电话, 交代任务2, 挂电话.
      打电话, 交代任务3, 挂电话.

    方式二:

    • 打电话, 交代任务1, 任务2, 任务3, 挂电话.

    显然, 方式二是更高效的方案.

    到底锁粒度是粗好还是细好?各有各的好
    如果锁粒度比较细,多个线程之间的并发性就更高
    如果锁粒度比较粗,加锁解锁的开销就更小
    编译器就会有一个优化,就会自动判定,如果某个地方的代码锁的粒度太细了就会进行粗化
    如果两次加锁之间的间隔较大 (中间隔的代码多),一般不会进行这种优化;如果加锁之间间隔比较小 (中间隔的代码少),就很可能触发这个优化


    2.3、锁消除

    有些代码,明明不用加锁,结果你给加上锁了
    编译器就会判断锁没有什么必要,就直接把锁给去掉了
    有的时候加锁操作并不是很明显,稍不留神就做出了这种错误的决定
    StringBuffer,Vector…在标准库中进行了加锁操作
    在单个线程中用到了上述的类,就是单线程进行了加锁解锁

    StringBuffer sb = new StringBuffer();
    sb.append("a");
    sb.append("b");
    sb.append("c");
    sb.append("d");
    

    此时每个 append 的调用都会涉及加锁和解锁,但如果只是在单线程中执行这个代码,那么这些加锁解锁操作是没有必要的,白白浪费了一些资源开销


    四、Callable 接口

    Java 中的 JUC:``java.util.concurrent`
    并发 (多线程相关的操作)

    Callable 是一个 interface ,也是一种创建线程的方式,相当于把线程封装了一个 “返回值”,方便程序猿借助多线程的方式计算结果. Runnable 不太适合于让线程计算出一个结果这样的代码

    例如,像创建一个线程,让这个线程计算 1+2+ 3 + …+ 1000。如果基于 Runnable 来实现,就会比较麻烦,Callable 就是要解决 Runnable 不方面返回结果这个问题的

    代码示例: 创建线程计算 1 + 2 + 3 + … + 1000, 不使用 Callable 版本

    • 创建一个类 Result , 包含一个 sum 表示最终结果,lock 表示线程同步使用的锁对象.
    • main 方法中先创建 Result 实例,然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + … + 1000.
    • 主线程同时使用 wait 等待线程 t 计算结束. (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了).
    • 当线程 t 计算完毕后,通过 notify 唤醒主线程,主线程再打印结果
    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    public class Test {
        public static void main(String[] args) {
            //通过callable来描述一个这样的任务~~
            Callable<Integer> callable = new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int sum = 6;
                    for (int i = 1; i <= 1000; i++){
                        sum += i;
                    }
                    return sum;
                }
            };
            //为了让线程执行 callable中的任务,光使用构造方法还不够,还需要一个辅助的类.
            FutureTask<Integer> task = new FutureTask<>(callable);
            //创建线程,来完成这里的大算工作
            Thread t = new Thread(task);
    
    		// 凭小票去获取自己地麻辣烫
            // 如果线程的任务没有完成,get 就会阻塞,一直到任务完成了,结果算出来了
            try {
                System.out.println(task.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    

    运行结果:500500

    可以看到, 使用 Callable 和 FutureTask 之后, 代码简化了很多, 也不必手动写线程同步代码了

    在这里插入图片描述

    理解 Callable

    Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,
    Runnable 描述的是不带返回值的任务.

    Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为
    Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.

    FutureTask 就可以负责这个等待结果出来的工作

    理解 FutureTask

    想象去吃麻辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你一张 “小票” . 这个小票就是
    FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没


    五、JUC(java.util.concurrent) 的常见类

    1、ReentrantLock

    可重入互斥锁,和 synchronized 定位类似,都是用来实现互斥效果,保证线程安全
    ReentrantLock 也是可重入锁,“Reentrant” 这个单词的原意就是 “可重入”

    ReentrantLock 的用法:
    lock(): 加锁, 如果获取不到锁就死等
    trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁
    unlock():解锁

    把加锁和解锁两个操作分开了

    ReentrantLock lock = new ReentrantLock();
    // -----------------------------------------
    
    lock.lock();
    try {
    	// working
    } finally {
    	lock.unlock() // 保证不管是否异常都能执行到 unlock, 这么写比较麻烦
    }
    

    这种分开的做法不太好,很容易遗漏 unlock (容易出现死锁),当多个线程竞争同一个锁的时候就会阻塞…

    ReentrantLock 和 synchronized 的区别:

    1. synchronized 是一个关键字,是 JVM 内部实现的(大概率是基于 C++ 实现)。 ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).

    2. synchronized 使用时不需要手动释放锁,出了代码块,锁自然释放。 ReentrantLock 使用时需要手动释放. 使用起来更灵活,但是也容易遗漏 unlock,要谨防忘记释放。

    3. synchronized 在竞争锁锁失败时,会阻塞等待,死等。ReentrantLock 除了阻塞等待这一手之外,还有一手 trylock ,给了我们更多的回旋余地,等待一段时间就放弃,直接返回。

    4. synchronized 是非公平锁,ReentrantLock 默认是非公平锁,提供了非公平和公平锁两个版本,可以通过构造方法传入一个 true 开启公平锁模式

    5. 更强大的唤醒机制,基于 synchronized 衍生出来的等待机制是通过 Objectwait / notify 实现等待–唤醒,每次唤醒的是一
      个随机等待的线程,功能是相对有限的。基于 ReentrantLock 衍生出来的等待机制,是 Condition 类 (条件变量) 实现等待–唤醒,功能要更丰富一些,可以更精确控制唤醒某个指定的线程

    // ReentrantLock 的构造方法
    public ReentrantLock(boolean fair) {
    	sync = fair ? new FairSync() : new NonfairSync();
    }  
    

    如何选择使用哪个锁?
    锁竞争不激烈的时候,使用 synchronized,效率更高,自动释放更方便
    锁竞争激烈的时候,使用 ReentrantLock,搭配 trylock 更灵活控制加锁的行为,而不是死等
    如果需要使用公平锁,使用 ReentrantLock

    日常开发中,绝大部分情况下,synchronized就够用了!


    2、原子类

    原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个

    AtomicBoolean
    AtomicInteger
    AtomicIntegerArray
    AtomicLong
    AtomicReference
    AtomicStampedReference

    以 AtomicInteger 举例,常见方法有

    num.incrementAndGet(); // ++num
    num.decrementAndGet(); // --num
    num.getAndIncrement(); // num++;
    num.getAndDecrement(); // num--;
    num.getAndAdd(10);     // += 10
    

    3、线程池

    虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销毁线程的时候还是会比较低效

    线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 "池子"中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了

    1)ExecutorService 和 Executors:

    代码示例:

    • ExecutorService 表示一个线程池实例.

    • Executors 是一个工厂类, 能够创建出几种不同风格的线程池.

    • ExecutorService 的 submit 方法能够向线程池中提交若干个任务.

    ExecutorService pool = Executors.newFixedThreadPool(10);
    pool.submit(new Runnable() {
    	@Override
    	public void run() {
    		System.out.println("hello");
    	}
    });
    

    Executors 创建线程池的几种方式 :

    newFixedThreadPool: 创建固定线程数的线程池
    newCachedThreadPool: 创建线程数目动态增长的线程池.
    newSingleThreadExecutor: 创建只包含单个线程的线程池.
    newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
    Executors 本质上是 ThreadPoolExecutor 类的封装

    2)ThreadPoolExecutor:

    ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定

    ThreadPoolExecutor 的构造方法

    理解 ThreadPoolExecutor 构造方法的参数
    把创建一个线程池想象成开个公司. 每个员工相当于一个线程.

    • corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)
    • maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).
    • keepAliveTime: 临时工允许的空闲时间.
    • unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
    • workQueue: 传递任务的阻塞队列
    • threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
    • RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.
    • AbortPolicy(): 超过负荷, 直接抛出异常.
    • CallerRunsPolicy(): 调用者负责处理
    • DiscardOldestPolicy(): 丢弃队列中最老的任务.
    • DiscardPolicy(): 丢弃新来的任务

    代码示例:

    ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
                                                new SynchronousQueue<Runnable>(),
                                                Executors.defaultThreadFactory(),
                                                new ThreadPoolExecutor.AbortPolicy());
    for(int i=0;i<3;i++) {
            pool.submit(new Runnable() {
            @Override
            void run() {
                System.out.println("hello");
            }
            });
    }
    

    4、信号量 Semaphore

    是一个更广义的锁
    锁是信号量里第一种特殊情况,叫做 “二元信号量"

    理解信号量

    开车经常会遇到一个情况,停车,停车场入口一般会有个牌子,上面写着 “当前空闲xx个车位”,
    每次有个车开出来,车位数+1
    这个牌子就是信号量,描述了可用资源 (车位)的个数,每次申请一个可用资源,计数器就 -1 (称为Р操作)

    • 当有车开进去的时候,就相当于申请一个可用资源,可用车位就 -1 (这个称为信号量的 P 操作)

    • 当有车开出来的时候,就相当于释放一个可用资源,可用车位就 +1 (这个称为信号量的 V 操作)

    如果计数器的值已经为 0 了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源

    P 和 V 没有对应的英文单词,提出信号量的人,叫做"迪杰斯特拉” (数学家) ,数据结构中的图=>迪杰斯特拉算法,能够计算两点之间的最短路径

    可以看成英文: P acquire 申请,V release释放

    锁就可以视为 “二元信号量”,可用资源就一个,计数器的取值非 0 即 1
    信号量就把锁推广到了一般情况,可用资源更多的时候,如何处理的
    实际开发中,并不会经常用到信号量

    信号量,用来表示 “可用资源的个数”,本质上就是一个计数器
    使用信号量可以实现 “共享锁”,比如某个资源允许 3 个线程同时使用,那么就可以使用 P 操作作为加锁,V 操作作为解锁,前三个线程的 P 操作都能顺利返回,后续线程再进行 P 操作就会阻塞等待,直到前面的线程执行了 V 操作.

    Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用

    代码示例:

    • 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
    • acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
    • 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果
    Semaphore semaphore = new Semaphore(4);
    
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println("申请资源");
                semaphore.acquire();
                System.out.println("我获取到资源了");
                Thread.sleep(1000);
                System.out.println("我释放资源了");
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    
    for (int i = 0; i < 20; i++) {
        Thread t = new Thread(runnable);
        t.start();
    }
    

    5、CountDownLatch

    同时等待 N 个任务执行结束

    假设有一场跑步比赛,当所有的选手都冲过终点,此时认为是比赛结束

    这样的场景在开发中,也是存在的
    例如,多线程下载
    迅雷…下载一个比较大的资源 (电影),通过多线程下载就可以提高下载速度
    把一个文件拆成多个部分,每个线程负责下载其中的一个部分,得是所有的线程都完成自己的下载,才算整个下载完

    countDown 给每个线程里面去调用,就表示到达终点了
    await 是给等待线程去调用,当所有的任务都到达终点了,await 就从阻塞中返回,就表示任务完成

    好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩

    • 构造 CountDownLatch 实例,初始化 10 表示有 10 个任务需要完成
    • 每个任务执行完毕,都调用 latch.countDown() ,在 CountDownLatch 内部的计数器同时自减.
    • 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕,相当于计数器为 0 了
    import java.util.concurrent.CountDownLatch;
    
    public class Test2 {
        public static void main(String[] args) throws InterruptedException {
            // 构造方法的参数表示有几个选手
            CountDownLatch latch = new CountDownLatch(10);
            for (int i = 0; i < 10; i++) {
                Thread t = new Thread(() -> {
                    try {
                        Thread.sleep(3000);
                        System.out.println(Thread.currentThread().getName() + " 到达终点");
                        latch.countDown(); // 调用 countDown 的次数和个数一致,此时就会await返回的情况
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
                t.start();
            }
    
            // 裁判要等所有线程到达
            // 当这些线程没有执行完的时候,wait 就会阻塞,所有线程执行完了,await 才返回
            latch.await();
            System.out.println("比赛结束");
        }
    }
    

    运行结果:

    (等待几秒后)

    Thread-7 到达终点
    Thread-8 到达终点
    Thread-6 到达终点
    Thread-9 到达终点
    Thread-1 到达终点
    Thread-0 到达终点
    Thread-3 到达终点
    Thread-2 到达终点
    Thread-4 到达终点
    Thread-5 到达终点
    比赛结束

    Process finished with exit code 0


    六、线程安全的集合类

    1)、自己使用同步机制 (``synchronized或者ReentrantLock`)

    2)、 Collections.synchronizedList(new ArrayList);

    synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List
    synchronizedList 的关键操作上都带有 synchronized

    全部加锁,不如第一种自己加锁灵活

    3)、使用 CopyOnWriteArrayLis

    写时拷贝,在修改的时候,会创建一份副本出来

    CopyOnWrite容器即写时复制的容器。

    • 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,
    • 添加完元素之后,再将原容器的引用指向新的容器。
      这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
      所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。

    优点:

    • 在读多写少的场景下, 性能很高, 不需要加锁竞争.

    缺点:

    • 占用内存较多.
    • 新写的数据不能被第一时间读取到

    举例:
    有一个 ArrayList
    如果咱们是多线程去读这个 ArrayList,此时没有线程安全问题,完全不需要加锁,也不需要其他方面的控制。如果有多线程去写,就是把这个ArrayList 给**复制了一份,先修改副本*

    {1,2,3,4} 把 1 改成 100

    • {100,2,3,4} [副本]
    • 当修改完毕,再让副本转正

    这样做的好处,就是修改的同时对于读操作,是没有任何影响的,读的时候优先读旧的版本
    不会说出现读到一个 “修改了一半” 的中间状态

    也叫做 “双缓冲区" 策略
    操作系统,创建进程的时候,也是通过写时拷贝。显卡在渲染画面的时候,也是通过类似的机制。

    也是适合于读多写少的情况,也是适合于数据小的情况
    更新配置数据,经常会用到这种类似的操作


    2、多线程环境使用队列

    1). ArrayBlockingQueue 基于数组实现的阻塞队列
    2). LinkedBlockingQueue 基于链表实现的阻塞队列
    3). PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
    4). TransferQueue  最多只包含一个元素的阻塞队列  
    

    3、多线程下使用哈希表 [最常考的问题之一]

    HashMap 本身不是线程安全的
    在多线程环境下使用哈希表可以使用:

    • Hashtable [不推荐]
    • ConcurrentHashMap [推荐]

    3.1、HashTable

    HashTable 如何保证线程安全的?就是给关键方法加锁:

    public synchronized V put(K key, V value) {
    
    public synchronized V get(Object key) {
    

    针对 this 来加锁
    当有多个线程来访问这个 HashTable 的时候,无论是啥样的操作,无论是啥样的数据,都会出现锁竞争
    这样的设计就会导致锁竞争的概率非常大,效率就比较低!

    如果多线程访问同一个 Hashtable 就会直接造成锁冲突
    size 属性也是通过 synchronized 来控制同步,也是比较慢的
    一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率会非常低

    举例:

    有个公司,老板。若干部门,每个部门有领导,也有基层员工
    设老板规定:
    员工要想请假,都得找他当面来申请,他好签字
    由于公司里的人很多,很多人都要请假
    这个时候大家都在老板门口排队,非常不方便!

    1. 每个 HashTable 对象只有一把锁

    在这里插入图片描述

    如果元素多了,链表就会长,就影响 hash 表的效率,就需要扩容 (增加数组的长度)
    扩容就需要创建一个更大的数组,然后把之前旧的元素都给搬运过去 (非常耗时)

    • 解决方案:
      老板自己也不耐烦,直接放权,让批示请假的权限,下放到部门领导

    3.2、ConcurrentHashMap

    1. ConcurrentHashMap 里面的情况就是:

    在这里插入图片描述

    操作元素的时候,是针对这个 元素所在的链表的头结点 来加锁的
    如果你两个线程操作是针 对两个不同的链表上的元素, 没有线程安全问题,其实不必加锁
    由于 hash 表中,链表的数目非常多,每个链表的长度是相对短的,因此就可以保证锁冲突的概率就非常小了

    1. ConcurrentHashMap 减少了锁冲突,就让锁加到每个链表的头结点上 (锁桶)
    2. ConcurrentHashMap 只是针对写操作加锁了,读操作没加锁,而只是使用
    3. ConcurrentHashMap 中更广泛的使用 CAS,进一步提高效率 (比如维护 size 操作)
    4. ConcurrentHashMap 针对扩容,进行了巧妙的化整为零
    • 对于 HashTable 来说,只要你这次 put 触发了扩容,就一口气搬运完,会导致这次 put 非常卡顿
    • 对于 ConcurrentHashMap,每次操作只搬运一点点,通过多次操作完成整个搬运的过程
    • 同时维护一个新的 HashMap 和一个旧的,查找的时候,既需要查旧的也要查新的插入的时候**只插入新的,直到搬运完毕再销毁旧的

    4、相关面试题

    1)、ConcurrentHashMap的读是否要加锁,为什么?

    读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了volatile 关键字

    2)、介绍下 ConcurrentHashMap的锁分段技术?

    这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁.
    目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.

    3)、ConcurrentHashMap在jdk1.8做了哪些优化?

    取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象).
    将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于 8 个元素)就转换成红黑树

    4)、Hashtable和HashMap、ConcurrentHashMap 之间的区别?

    HashMap: 线程不安全. key 允许为 null
    Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
    ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用CAS 机制. 优化了扩容方式. key 不允许为 null

    其他面试题:
    1)、谈谈 volatile关键字的用法?

    volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰
    的变量, 可以第一时间读取到最新的值

    2)、Java多线程是如何实现数据共享的?

    JVM 把内存分成了这几个区域:

    方法区, 堆区, 栈区, 程序计数器.

    其中堆区这个内存区域是多个线程之间共享的.

    只要把某个数据放到堆内存中, 就可以让多个线程都能访问到

    3)、Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?

    创建线程池主要有两种方式:

    • 通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
    • 通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.

    LinkedBlockingQueue 表示线程池的任务队列. 用户通过 submit / execute 向这个任务队列中添加任务, 再由线程池中的工作线程来执行任务.

    4)、Java线程共有几种状态?状态之间怎么切换的?

    • NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.
    • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在
      CPU 上运行/在即将准备运行 的状态.
    • BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状
      态.
    • WAITING: 调用 wait 方法会进入该状态.
    • TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
    • TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态.

    5)、在多线程下,如果对一个数进行叠加,该怎么做?

    • 使用 synchronized / ReentrantLock 加锁
    • 使用 AtomInteger 原子操作
    1. Servlet是否是线程安全的?

    Servlet 本身是工作在多线程环境下.

    如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进行
    操作, 是可能出现线程不安全的情况的

    7)、Thread和Runnable的区别和联系?

    Thread 类描述了一个线程.

    Runnable 描述了一个任务.

    在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用
    Runnable 来描述这个任务

    8)、多次start一个线程会怎么样

    第一次调用 start 可以成功调用.

    后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常

    9)、有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?

    synchronized 加在非静态方法上, 相当于针对当前对象加锁.
    如果这两个方法属于同一个实例:

    • 线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到
      锁之后才能执行方法内容.

    如果这两个方法属于不同实例:

    • 两者能并发执行, 互不干扰

    10)、进程和线程的区别?

    • 进程是包含线程的,每个进程至少有一个线程存在,即主线程。
    • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
    • 进程是系统分配资源的最小单位,线程是系统调度的最小单位
    展开全文
  • 笔者一直以来也都是用的IDM,IDM最核心的功能包括:多线程下载、断点续传以及网页资源嗅探等。但IDM是需要收费的,而且不支持Mac。所以今天给大家推荐另一款多线程下载神器,可完全替代IDM,也支持Mac系统,大家再也...

    引言

    提到下载软件,大家最常用的可能就是迅雷或者IDM了。笔者一直以来也都是用的IDM,IDM最核心的功能包括:多线程下载、断点续传以及网页资源嗅探等。但IDM是需要收费的,而且不支持Mac。所以今天给大家推荐另一款多线程下载神器,可完全替代IDM,也支持Mac系统,大家再也不用去花时间找IDM的破解版了。笔者目前也已经不用IDM了,改用这款软件,速度和IDM不相上下,甚至更快。

    简介

    言归正转,今天给大家分享的这款软件名为NeatDownloadManager,是一款非常轻量级的网络下载工具。最关键的是,它目前是完全免费的,也没有任何广告。不仅支持Windows,也支持Mac。软件体积也非常小,不到1MB。

    功能

    • 采用优化的动态分割算法
    • 充分利用网络带宽
    • 根据下载状态(完整或不完整)和文件类型(视频、文档等)自动组织下载
    • 支持HTTP、HTTPS和FTP协议
    • 支持HTTP和SOCKS代理协议
    • 支持HTTP身份验证和代理身份验证
    • 支持暂停/恢复下载的功能,可断点续传
    • 可通过拓展插件从网页捕捉音视频资源
    • 即使在下载过程中,也可以设置带宽限制和最大下载连接数
    • 可下载HLS视频的所有.ts片段,最后自动合并为一个.ts文件
    • 支持拖放(仅限Mac端)

    下载

    Windows:点击下载
    Mac:点击下载

    安装

    下载后双击安装包,默认安装即可。
    在这里插入图片描述

    用法

    本地下载

    打开软件,点击 New URL,输入待下载文件的直链。

    例如,这里下载一个Ubuntu系统镜像:https://releases.ubuntu.com/22.04/ubuntu-22.04-desktop-amd64.iso

    在这里插入图片描述

    资源嗅探

    安装软件的同时会自动安装一个浏览器拓展插件,用于网页资源(音视频等)的动态捕捉。

    以谷歌浏览器为例,你需要先开启该拓展插件:

    在这里插入图片描述

    然后,随便打开一个带有音视频的网页,比如B站,播放视频的时候就能看到下载按钮了。

    在这里插入图片描述

    提示

    可能很多同学会好奇为什么下载速度会这么快?我们先抛开多线程这个因素不谈,从软件官方介绍中我们得知,该款软件是基于C++下载引擎编写的,即下载功能是调用C++的下载内核来实现的,该软件其实只是做了一个简单的封装和逻辑处理,让普通用户也能轻松使用。C++,永远滴神!不过,人生苦短,我用Python~~哇库哇库= ̄ω ̄=

    下载最大连接数,即线程数不要设置太高,推荐的设置是8或16,对于高性能的游戏本可以考虑设置为16或32。

    当然,你也可以根据电脑的逻辑核心数来设置。使用 Ctrl+Shift+Esc快捷键打开任务管理器,在性能->CPU中就能看到逻辑处理器的数量。可以看到这里是6,那我设置最大线程数最好就设置为8,不要太大了。太大了对CPU的资源占用太高,下载速度可能不升反降。

    在这里插入图片描述

    注意

    值得注意的是,并不是所有的网络文件都支持多线程下载。对于不支持多线程下载的文件,我们可能需要关闭网页自动捕获功能,然后使用浏览器自带下载功能。

    参考

    http://www.neatdownloadmanager.com/

    展开全文
  • 这个回答非常形象,系统思维的模式。------------------------------------------------------作者:...主要是任务可分解性和结果可预期性。数据类的工作是很容易做到这点的。游戏并不是这种类型。多核、多线程、多g...
  • 日常开发中如果用到多线程编程,也一定会涉及到线程安全问题 线程安全这个问题就不太理解 正因为如此,程序猿们才尝试发明出更多的编程模型来处理并发编程的任务 例如:多进程、多线程、actor、csp、async+await、...
  • C# 多线程详细讲解

    万次阅读 多人点赞 2021-05-27 11:31:25
    C#多线程 一、基本概念 1、进程 首先打开任务管理器,查看当前运行的进程: 从任务管理器里面可以看到当前所有正在运行的进程。那么究竟什么是进程呢? 进程(Process)是Windows系统中的一个基本概念,它...
  • 操作系统 --- 多线程(初阶)

    千次阅读 多人点赞 2022-03-16 14:03:45
    多线程(初阶) 1. 线程 1.1 为什么会有线程 上一节进程的课程我们学习了,引入进程就是为了"并发编程",虽然进程能解决并发的问题,但是我们认为还不是不够理想. 因为创建进程/销毁进程/调度进程,开销有点大, 创建进程...
  • 存的多线程编程技术,由一些具有国际影响力的大规模软件和硬件厂商共同定义标准。它是 一种编译指导语句指导多线程、共享内存并行的应用程序编程接口(API)。本章介绍OpenMP 编程的概况、编写OpenMP 程序所需要的...
  • C# 多线程详解

    千次阅读 2020-07-31 20:25:32
    一、基本概念 1、进程 首先打开任务管理器,查看当前运行的进程: 从任务管理器里面可以看到当前...一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或线程线程是操作系统分配处理器时...
  • 多线程技术和多核技术

    千次阅读 2020-08-10 11:07:38
    多核与多线程都是提升处理器处理性能的重要手段,如今多核处理器随处可见,多线程处理器似乎鲜有提及,其实多线程并不是一个新鲜的概念,在很多地方也有广泛的应用。到底多核处理器与多线程处理两者之间有何差异?各...
  • Java多线程案例之单例模式(懒汉,饿汉,枚举)

    千次阅读 多人点赞 2022-04-05 15:33:25
    本篇文章将介绍Java多线程中的几个典型案例之单例模式,所谓单例模式,就是一个类只有一个实例对象,本文将着重介绍在多线程的背景下,单例模式的简单实现。
  • C++多线程以及线程池

    千次阅读 2020-06-10 16:11:46
    在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更指内核线程(kernel thread),而把用户线程(user thread)称为线程。   同一进程中的线程将共享该进程中的全部系统资源
  • C语言开启多线程

    千次阅读 2022-03-30 09:39:41
    计算机发展初期为单核单任务,windows开始发展为单核多任务,而后是多核多任务,多任务开始为多进程,后来出现了多线程,多核为多进程和多线程提供了更的支持,不仅可以分配时间线,还可以分配空闲的核心。...
  • 多线程开发指南

    2015-08-28 12:01:50
    讲述多线程在iOS中的应用及注意点。更优化软件
  • Java多线程(一文看懂!)

    千次阅读 2021-07-28 17:36:15
    百度中多线程的介绍(multithreading):是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括...
  • 多线程与多进程

    千次阅读 2019-02-27 10:26:49
    多线程 优劣 数据共享、同步 数据是分开的:共享复杂,需要用IPC;同步简单 多线程共享进程数据:共享简单;同步复杂 各有优势 内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换...
  • Java开发中多线程与高并发需要注意的 15 个细节

    千次阅读 多人点赞 2022-03-29 10:22:44
    CAS(compare and set缩写,无锁优化,自旋锁,也叫乐观锁,CPU原语级别的支持所有中间不可以被打断),java.util.concurrent.atomic.AtomicInteger,凡是类加了Atomic的类都是用了CAS优化线程安全。 -- END -- - ...
  • 多线程与多进程详细

    千次阅读 2021-02-04 11:05:25
    进程和线程的概念 1.进程(最小的资源单位):  进程:就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。  程序:我们编写的程序用来描述进程要完成哪些功能以及如何...
  • 多线程在web中的使用

    千次阅读 2018-09-28 15:01:05
    整理网络上的 最典型的如: 1、用户注册完成送大礼包/积分之类,且积分等也是另一个系统并...最典型的应用比如tomcat,tomcat内部采用的就是多线程。 上百个客户端访问同一个web应用,tomcat接入后都是把后续的处理...
  • 多线程和高并发介绍

    万次阅读 多人点赞 2021-06-06 05:00:46
    本文主要是针对多线程和高并发的概念做了简单的描述,介绍了什么是多线程,什么是高并发,并且对多线程和高并发的关系做了比较描述。 一、什么是多线程? 1.多线程介绍 什么是多线程,首先看下百度百科对多线程...
  • 多线程高并发编程】二 实现多线程的几种方式

    万次阅读 多人点赞 2020-02-17 23:32:54
    本文我们来看看多线程的应用场景,为什么要用多线程,以及实现一个多线程有几种方式。
  • 多线程基本概念多线程有什么用?线程、进程、协程的区别什么是多线程上下文切换?什么是线程安全2. java多线程创建创建线程的方式Runnable和Callable的区别Thread类中的start()和run()方法有什么区别?什么导致线程...
  • 硬件线程软件线程的区别

    千次阅读 2020-06-24 17:16:31
    硬件线程软件线程的...每一个逻辑处理器可以运行软件线程的代码,windows调度器可以决定将一个软件线程赋给一个硬件线程,通过这种方式均衡每一个硬件线程的工作负载,以达到并行优化的作用。 打个比方,如果把...
  • 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。...多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务, 也...
  • 一、同步问题 1.管程 ...Java内存模型:描述多线程的逻辑结构 JMM 线程工作内存(线程私有,不同线程间相互隔离) 所有变量的读写必须在工作内存中进行,使用的变量均是从主内存中拷贝的副本。 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 166,825
精华内容 66,730
热门标签
关键字:

多线程优化比较好的软件