java多线程有哪些实际的应⽤场景?
多线程使⽤的主要⽬的在于:
1、吞吐量:你做WEB,容器帮你做了多线程,但是他只能帮你做请求层⾯的。简单的说,可能就是⼀个请求⼀个线程。或多个请求⼀个线程。如果是单线程,那同时只能处理⼀个⽤户的请求。
2、伸缩性:也就是说,你可以通过增加CPU核数来提升性能。如果是单线程,那程序执⾏到死也就利⽤了单核,肯定没办法通过增加CPU 核数来提升性能。鉴于你是做WEB的,第1点可能你⼏乎不涉及。那这⾥我就讲第⼆点吧。--举个简单的例⼦:假设有个请求,这个请求服务端的处理需要执⾏3个很缓慢的IO操作(⽐如数据库查询或⽂件查询),那么正常的顺序可能是(括号⾥⾯代表执⾏时间):
a、读取⽂件1 (10ms)
b、处理1的数据(1ms)
c、读取⽂件2 (10ms)
d、处理2的数据(1ms)
e、读取⽂件3 (10ms)
f、处理3的数据(1ms)
g、整合1、2、3的数据结果(1ms)
单线程总共就需要34ms。
那如果你在这个请求内,把ab、cd、ef分别分给3个线程去做,就只需要12ms了。
所以多线程不是没怎么⽤,⽽是,你平常要善于发现⼀些可优化的点。然后评估⽅案是否应该使⽤。假设还是上⾯那个相同的问题:但是每个步骤的执⾏时间不⼀样了。
a、读取⽂件1 (1ms)
b、处理1的数据(1ms)
c、读取⽂件2 (1ms)
d、处理2的数据(1ms)
e、读取⽂件3 (28ms)
f、处理3的数据(1ms)
g、整合1、2、3的数据结果(1ms)单线程总共就需要34ms。
如果还是按上⾯的划分⽅案(上⾯⽅案和⽊桶原理⼀样,耗时取决于最慢的那个线程的执⾏速度),在这个例⼦中是第三个线程,执⾏
29ms。那么最后这个请求耗时是30ms。⽐起不⽤单线程,就节省了4ms。但是有可能线程调度切换也要花费个1、2ms。
因此,这个⽅案显得优势就不明显了,还带来程序复杂度提升。不太值得。那么现在优化的点,就不是第⼀个例⼦那样的任务分割多线程完成。⽽是优化⽂件3的读取速度。可能是采⽤缓存和减少⼀些重复读取。⾸先,假设有⼀种情况,所有⽤户都请求这个请求,那其实相当于所有⽤户都需要读取⽂件3。
那你想想,100个⼈进⾏了这个请求,相当于你花在读取这个⽂件上的时间就是28×100=2800ms了。那么,如果你把⽂件缓存起来,那只要第⼀个⽤户的请求读取了,第⼆个⽤户不需要读取了,从内存取是很快速的,可能1ms都不到。伪代码:
看起来好像还不错,建⽴⼀个⽂件名和⽂件数据的映射。如果读取⼀个map中已经存在的数据,那么就不不⽤读取⽂件了。可是问题在
于,Servlet是并发,上⾯会导致⼀个很严重的问题,死循环。因为,HashMap在并发修改的时候,可能是导致循环链表的构成(具体你可以⾃⾏阅读HashMap源码)如果你没接触过多线程,可能到时候发现服务器没请求也巨卡,也不知道什么情况!好的,那就⽤ConcurrentHashMap,正如他的名字⼀样,他是⼀个线程安全的HashMap,这样能轻松解决问题。
这样真的解决问题了吗,这样虽然只要有⽤户访问过⽂件a,那另⼀个⽤户想访问⽂件a,也会从fileName2Data中拿数据,然后也不会引起死循环。可是,如果你觉得这样就已经完了,那你把多线程也想的太简单了,骚年!你会发现,1000个⽤户⾸次访问同⼀个⽂件的时候,居然读取了1000次⽂件(这是最极端的,可能只有⼏百)。What the fuckin hell难道代码错了吗,难道我就这样过我的⼀⽣!好好分析下。Servlet是多线程的,那么
上⾯注释的“偶然”,这是完全有可能的,因此,这样做还是有问题。因此,可以⾃⼰简单的封装⼀个任务来处理。
以上所有代码都是直接在bbs打出来的,不保证可以直接运⾏。
多线程最多的场景:web服务器本⾝;各种专⽤服务器(如游戏服务器);多线程的常见应⽤场景:
1、后台任务,例如:定时向⼤量(100w以上)的⽤户发送邮件;
2、异步处理,例如:发微博、记录⽇志等;
3、分布式计算
======================================================================
在java中,每⼀个线程有⼀块⼯作内存区,其中存放着被所有线程共享的主内存中的变量的值的拷贝。当线程执⾏时,它在⾃⼰的⼯作内存中操作这些变量。
为了存取⼀个共享的变量,⼀个线程通常先获取锁定并且清除它的⼯作内存区,这保证该共享变量从所有线程的共享内存区正确地装⼊到线程的⼯作内存区,当线程解锁时保证该⼯作内存区中变量的值协会到共享内存中。
当⼀个线程使⽤某⼀个变量时,不论程序是否正确地使⽤线程同步操作,它获取的值⼀定是由它本⾝或者其他线程存储到变量中的值。例如,如果两个线程把不同的值或者对象引⽤存储到同⼀个共享变量中,那么该变量的值要么是这个线程的,要么是那个线程的,共享变量的值不会是由两个线程的引⽤值组合⽽成。
⼀个变量时Java程序可以存取的⼀个地址,它不仅包括基本类型变量、引⽤类型变量,⽽且还包括数组类型变量。保存在主内存区的变量可以被所有线程共享,但是⼀个线程存取另⼀个线程的参数或者
局部变量时不可能的,所以开发⼈员不必担⼼局部变量的线程安全问题。
volatile变量–多线程间可见
由于每个线程都有⾃⼰的⼯作内存区,因此当⼀个线程改变⾃⼰的⼯作内存中的数据时,对其他线程来说,可能是不可见的。为此,可以使⽤volatile关键字破事所有线程军读写内存中的变量,从⽽使得volatile变量在多线程间可见。
声明为volatile的变量可以做到如下保证:
1、其他线程对变量的修改,可以及时反应在当前线程中;
2、确保当前线程对volatile变量的修改,能及时写回到共享内存中,
并被其他线程所见;3、使⽤volatile声明的变量,编译器会保证其有序性。
同步关键字synchronized
同步关键字synchronized是Java语⾔中最为常⽤的同步⽅法之⼀。在JDK早期版本中,synchronized的性能并不是太好,值适合于锁竞争不是特别激烈的场合。在JDK6中,synchronized和⾮公平锁的差距已经缩⼩。更为重要的是,synchronized更为简洁明了,代码可读性和维护性⽐较好。
锁定⼀个对象的⽅法:
当method()⽅法被调⽤时,调⽤线程⾸先必须获得当前对象所,若当前对象锁被其他线程持有,这调⽤线程会等待,犯法结束后,对象锁会被释放,以上⽅法等价于下⾯的写法:
其次,使⽤synchronized还可以构造同步块,与同步⽅法相⽐,同步块可以更为精确控制同步代码范围。⼀个⼩的同步代码⾮常有离与锁的快进快出,从⽽使系统拥有更⾼的吞吐量。
synchronized也可以⽤于static函数:
这个地⽅⼀定要注意,synchronized的锁是加在当前Class对象上,因此,所有对该⽅法的调⽤,都必须获得Class对象的锁。
虽然synchronized可以保证对象或者代码段的线程安全,但是仅使⽤synchronized还是不⾜以控制拥有复杂逻辑的线程交互。为了实现多线程间的交互,还需要使⽤Object对象的wait()和notify()⽅法。
典型⽤法:
在使⽤wait()⽅法前,需要获得对象锁。在wait()⽅法执⾏时,当前线程或释放obj的独占锁,供其他线程使⽤。jdk怎么使用
当等待在obj上线程收到ify()时,它就能重新获得obj的独占锁,并继续运⾏。注意了,notify()⽅法是随机唤起等待在当前对象的某⼀个线程。
下⾯是⼀个阻塞队列的实现:
synchronized配合wait()、notify()应该是Java开发者必须掌握的基本技能。
Reentrantlock重⼊锁
Reentrantlock称为重⼊锁。它⽐synchronized拥有更加强⼤的功能,它可以中断、可定时。在⾼并发的情况下,它⽐synchronized有明显的性能优势。
Reentrantlock提供了公平和⾮公平两种锁。公平锁是对锁的获取是先进先出,⽽⾮公平锁是可以插队的。当然从性能上分析,⾮公平锁的性能要好得多。因此,在⽆特殊需要,应该优选⾮公平锁,但是synchronized提供锁业不是绝对公平的。Reentrantlock在构造的时候可以指定锁是否公平。
在使⽤重⼊锁时,⼀定要在程序最后释放锁。⼀般释放锁的代码要写在finally⾥。否则,如果程序出现异常,Loack就永远⽆法释放了。synchronized的锁是JVM最后⾃动释放的。
经典使⽤⽅式如下:
Reentrantlock提供了⾮常丰富的锁控制功能,灵活应⽤这些控制⽅法,可以提⾼应⽤程序的性能。不过这⾥并⾮是极⼒推荐使⽤Reentrantlock。重⼊锁算是JDK中提供的⾼级开发⼯具。
ReadWriteLock读写锁
读写分离是⼀种⾮常常见的数据处理思想。在sql中应该算是必须⽤到的技术。ReadWriteLock是在JDK5中提供的读写分离锁。读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。读写分离使⽤场景主要是如果在系统中,读操作次数远远⼤于写操作。使⽤⽅式如下:
Condition对象
Conditiond对象⽤于协调多线程间的复杂协作。主要与锁相关联。通过Lock接⼝中的newCondition()⽅法可以⽣成⼀个与Lock绑定的Condition实例。Condition对象和锁的关系就如⽤Object.wait()、ify()两个函数以及synchronized关键字⼀样。
这⾥可以把ArrayBlockingQueue的源码摘出来看⼀下:
此实例简单实现了⼀个对象池,对象池最⼤容量为100。因此,当同时有100个对象请求时,对象池就会出现资源短缺,未能获得资源的线程就需要等待。当某个线程使⽤对象完毕后,就需要将对象返回给对象池。此时,由于可⽤资源增加,因此,可以激活⼀个等待该资源的线程。
ThreadLocal线程局部变量
在刚开始接触ThreadLocal,笔者很难理解这个线程局部变量的使⽤场景。当现在回过头去看,ThreadLocal是⼀种多线程间并发访问变量的解决⽅案。与synchronized等加锁的⽅式不同,ThreadLocal完全不提供锁,⽽使⽤了以空间换时间的⼿段,为每个线程提供变量的独⽴副本,以保障线程安全,因此它不是⼀种数据共享的解决⽅案。
ThreadLocal是解决线程安全问题⼀个很好的思路,ThreadLocal类中有⼀个Map,⽤于存储每⼀个线程的变量副本,Map中元素的键为线程对象,⽽值对应线程的变量副本,由于Key值不可重复,每⼀个“线程对象”对应线程的“变量副本”,⽽到达了线程安全。
特别值得注意的地⽅,从性能上说,ThreadLocal并不具有绝对的⼜是,在并发量不是很⾼时,也⾏加锁的性能会更好。但作为⼀套与锁完全⽆关的线程安全解决⽅案,在⾼并发量或者所竞争激烈的场合,使⽤ThreadLocal可以在⼀定程度上减少锁竞争。
下⾯是⼀个ThreadLocal的简单使⽤:
输出结果:
输出的结果信息可以发现每个线程所产⽣的序号虽然都共享同⼀个TestNum实例,但它们并没有发⽣
相互⼲扰的情况,⽽是各⾃产⽣独⽴的序列号,这是因为ThreadLocal为每⼀个线程提供了单独的副本。
锁的性能和优化
“锁”是最常⽤的同步⽅法之⼀。在平常开发中,经常能看到很多同学直接把锁加很⼤⼀段代码上。还有的同学只会⽤⼀种锁⽅式解决所有共享问题。显然这样的编码是让⼈⽆法接受的。特别的在⾼并发的环境下,激烈的锁竞争会导致程序的性能下降德更加明显。因此合理使⽤锁对程序的性能直接相关。
1、线程的开销
在多核情况下,使⽤多线程可以明显提⾼系统的性能。但是在实际情况中,使⽤多线程的⽅式会额外增加系统的开销。相对于单核系统任务本⾝的资源消耗外,多线程应⽤还需要维护额外多线程特有的信息。⽐如,线程本⾝的元数据,线程调度,线程上下⽂的切换等。
2、减⼩锁持有时间
在使⽤锁进⾏并发控制的程序中,当锁发⽣竞争时,单个线程对锁的持有时间与系统性能有着直接的关系。如果线程持有锁的时间很长,那么相对地,锁的竞争程度也就越激烈。因此,在程序开发过程中,应该尽可能地减少对某个锁的占有时间,以减少线程间互斥的可能。⽐如
下⾯这⼀段代码:
此实例如果只有mutexMethod()⽅法是有同步需要的,⽽在beforeMethod(),和afterMethod()并不需要做同步控制。如果beforeMethod(),和afterMethod()分别是重量级的⽅法,则会花费较长的CPU时间。在这个时候,如果并发量较⼤时,使⽤这种同步⽅案会导致等待线程⼤量增加。因为当前执⾏的线程只有在执⾏完所有任务后,才会释放锁。
下⾯是优化后的⽅案,只在必要的时候进⾏同步,这样就能明显减少线程持有锁的时间,提⾼系统的吞吐量。代码如下:
3、减少锁粒度
减⼩锁粒度也是⼀种削弱多线程锁竞争的⼀种有效⼿段,这种技术典型的使⽤场景就是ConcurrentHashMap这个类。在普通的HashMap中每当对集合进⾏add()操作或者get()操作时,总是获得集合对象的锁。这种操作完全是⼀种同步⾏为,因为锁是在整个集合对象上的,因此,在⾼并发时,激烈的锁竞争会影响到系统的吞吐量。
如果看过源码的同学应该知道HashMap是数组+链表的⽅式做实现的。ConcurrentHashMap在HashMap的基础上将整个HashMap分成若⼲个段(Segment),每个段都是⼀个⼦HashMap。如果需要
在增加⼀个新的表项,并不是将这个HashMap加锁,⼆⼗搜线根据hashcode得到该表项应该被存放在哪个段中,然后对该段加锁,并完成put()操作。这样,在多线程环境中,如果多个线程同时进⾏写⼊操作,只要被写⼊的项不存在同⼀个段中,那么线程间便可以做到真正的并⾏。具体的实现希望读者⾃⼰花点时间读⼀读ConcurrentHashMap这个类的源码,这⾥就不再做过多描述了。
4、锁分离
在前⾯提起过ReadWriteLock读写锁,那么读写分离的延伸就是锁的分离。同样可以在JDK中到锁分离的源码LinkedBlockingQueue。
这⾥需要说明⼀下的就是,take()和put()函数是相互独⽴的,它们之间不存在锁竞争关系。只需要在take()和put()各⾃⽅法内部分别对takeLock和putLock发⽣竞争。从⽽,削弱了锁竞争的可能性。
5、锁粗化
上⾯说到的减⼩锁时间和粒度,这样做就是为了满⾜每个线程持有锁的时间尽量短。但是,在粒度上应该把握⼀个度,如果对⽤⼀个锁不停地进⾏请求、同步和释放,其本⾝也会消耗系统宝贵的资源,反⽽加⼤了系统开销。
我们需要知道的是,虚拟机在遇到⼀连串连续的对同⼀锁不断进⾏请求和释放的操作时,便会把所有
的锁操作整合成对锁的⼀次请求,从⽽减少对锁的请求同步次数,这样的操作叫做锁的粗化。下⾯是⼀段整合实例演⽰:
JVM整合后的形式:
因此,这样的整合给我们开发⼈员对锁粒度的把握给出了很好的演⽰作⽤。
⽆锁的并⾏计算
上⾯花了很⼤篇幅在说锁的事情,同时也提到过锁是会带来⼀定的上下⽂切换的额外资源开销,在⾼并发时,”锁“的激烈竞争可能会成为系统瓶颈。因此,这⾥可以使⽤⼀种⾮阻塞同步⽅法。这种⽆锁⽅式依然能保证数据和程序在⾼并发环境下保持多线程间的⼀致性。
1、⾮阻塞同步/⽆锁⾮阻塞同步⽅式其实在前⾯的ThreadLocal中已经有所体现,每个线程拥有各⾃独⽴的变量副本,因此在并⾏计算时,⽆需相互等待。这⾥笔者主要推荐⼀种更为重要的、基于⽐较并交换(Compare And Swap)CAS算法的⽆锁并发控制⽅法。
CAS算法的过程:它包含3个参数CAS(V,E,N)。V表⽰要更新的变量,E表⽰预期值,N表⽰新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后CAS返回当前V的真实值。CAS操作时抱着乐观的态度进⾏的,它总是认为
⾃⼰可以成功完成操作。当多个线程同时使⽤CAS操作⼀个变量时,只有⼀个会胜出,并成功更新,其余俊辉失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作及时没有锁,也可以发现其他线程对当前线程的⼲扰,并且进⾏恰当的处理。
2、原⼦量操作
JDK的urrent.atomic包提供了使⽤⽆锁算法实现的原⼦操作类,代码内部主要使⽤了底层native代码的实现。有兴趣的同学可以继续跟踪⼀下native层⾯的代码。这⾥就不贴表层的代码实现了。
下⾯主要以⼀个例⼦来展⽰普通同步⽅法和⽆锁同步的性能差距:
测试结果如下:
相信这样的测试结果将内部锁和⾮阻塞同步算法的性能差异体现的⾮常明显。因此笔者更推荐直接视同atomic下的这个原⼦类

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。