今天我们来聊聊Java多线程的问题,多线程在并发编程中尤其重要,从jdk1.0引入的Thread 类和Runable接口,以及到后来的jdk1.5版本引入的Callable 接口,已经让多线程编程变的越来重要,功能越来越强大,从之前一个线程执行完后没有返回结果,到后期的一个线程执行完后可以返回执行结果,都在直接影响我们的程序的设计思路。本文章会把通过不同的多线程实现用详细的例子给大家展示出来,其中还涉及到部分线程池和其他相关的东西,希望通过本章的讲解能够让大家对多线程有一个深入的了解。
本文的内容还是通过先理论,后实践,在衍生、最后总结的方式一条线给大家顺下来,网上有一些关于多线程相关的技术文章,但是大部分都支离破碎,碎片化的知识加上跳跃式的章节让一些初学者没有头绪。如果大家耐心的读完本文章,并且根据本文章的示例运行一遍相关的程序,你基本就掌握了Java多线程技术,后期结合自己在工作种的一些经验,我相信大家的技术会更上一层。在这里用一句话和大家共勉:天行健,君子以自强不息。地势坤,君子以厚德载物。
第一个问题,就是线程和进程的关系,可能好多人初学者搞不清楚这个概念,什么是进程什么线程从不同的理解角度去看,可能有不同种说法,网上有人从CPU执行时间角度去说的,有人从程序调度角度去说的,各有各的说法,但是只要你理解得当,都可以说没问题了。就比如形容“幸福”,每个人理解的角度不同,在你饿的时候吃一顿饭,是幸福,渴的时候喝一瓶水喝,是幸福,累的时候有觉睡,是幸福,无聊的时候打个游戏都是幸福,不同的角度阐述的问题不同,今天我们就从程序员理解的角度去说,什么是进程,什么是线程?
进程就是操作系统对一个应用程序分配资源(比如:CPU,内存,磁盘、GPU,上下文环境等)的一个单位,而线程是在这个单元内进一步颗粒化的一个资源利用,比如说:我们启动QQ程序,那么系统就会给QQ程序分配各种资源,这个QQ程序我们就可以统称为一个进程。在QQ程序中,我们可以聊天,可以一对一聊天,也可以群聊,也可以下载文件等,但是我们这些操作都是在QQ这个程序(我们称之为进程)中执行了,在这里面的每一步操作,都是基于QQ进程的环境和上下文为依据的,这里每一个操作都被称为一个线程,那么他们是如何执行呢?比如说我同时和10个人聊天,那么这10个线程首先会放到一个线程池里,然后根据CPU执行的情况,去调用每一个线程。由于CPU执行的速度非常快,给我们感觉是一次行执行完毕,其实他内部是挨个去执行的。下面我们看个进程的图例:
TIM这个进程,在widow情况下,理论可以申请到4G的内存空间,CPU可以分配的情况不同环境下不同,假如TIM现在CPU可以分配到40%,内存可以分配到2G,GPU最大可以分配20%的情况下,我们如果TIM同时启动1000个线程进行文件下载操作,那么这部分线程的资源环境都是基于TIM进程分配的最大资源环境下进行的,假如我们这1000个线程是按照编号从1到1000.CPU执行完编号1的线程后,如果编号3准备好,那么他可能就线执行3号线程,然后在执行2号线程,由于CPU执行的速度很快,给我们感觉是在一起执行,其实CPU内部的调度也是挨个去执行的。
对线程有了一个初步认识后,我们在大脑建立一个抽象的模型,可以构思一下,线程也是有生命周期的。比如我们人类,从出生到死亡,在整个生命轴上,我们可以吃饭睡觉,生病健康。线程也类似,他被创建和死亡,有各种状态,下面我们通过图例说说线程的六种状态:
第一种:新建状态(new):创建一个线程,但线程并未启动。线程没有执行run()、start()或者execute()方法。
第二种:可运行状态(runable):线程执行run()、start()或者execute()方法,进入线程池等待被线程调度选中。
第三种:阻塞状态(blocked):当前线程在等待另一个线程的执行,比如等待一个synchronized修饰的方法或者块。
第四种:等待状态(waitting):处于这种状态的线程不会被CPU分配时间片。如果没有其他线程唤醒,将无限期等待下去
第五种:有时间等待状态(timed_waitting):处于这种状态的线程不会被CPU分配时间片,在指定的时间段内线程将被自动换醒。
第六种:死亡状态(terminated):线程结束后的一种状态。
这六种状态都来自Enum Thread.State枚举类中,我们通过程序先来看第一种状态(new):
这种状态就是线程通过new的方式刚被创建,没有调用run()方法,可以称之为一个简单的线程壳。
第二状态是在线程执行run()或者start()方法后,在java 虚拟机上的一种执行,他可能在等待来自操作系统的资源分配,比如等待获CPU时间片。下面我通过示例来看:
控制台可以看到当前俩个线程的状态,main线程是一个runable状态,一直在等带分配资源,而WaitingThread线程是一个waiting状态,此时如果不手动终止程序执行,这俩个线程将一直保持该状态直到天长地久。
那么大家通过上面的示例,我们可以明白,通过调用wait()方法,可以使线程进入waiting状态,其实线程进入waiting状态有三种方式:
1. 调用wait()方法,没有设置时间,。
2. 调用join()方法,在等待其他线程终止时。
下面我们通过示例演示调用join()方法,使线程进行等待状态,第三种方式,不在这里演示,大家自行测试。
通过join()方法的源码,我们可以看到,在join()方法内部其实也是调用 的wait()方法:
接下来我们time_waiting 状态,他是线程在指定的时间内等待,通过javaAPI我们可以看到,他有五种方法,可以让线程进行等待状态:
三:多线程三种实现方式
多线程的实现有三种方式,从jdk1.0的THread 类Runable 接口,到jdk1.5的Callable接口。下面我们通过详细的示例讲解三种实现方式。
通过继承Thread类实现,该类位于java.lang包下,他实现了Run able接口,有8个构造方法和若干个实现的方法,下面我们通过例子在展示:
这是一个非常简单的通过继承Thread类实现的一个线程,在他的run()方法里面主要干了一件事,就是每隔2秒循环一个数字,从0开始直到9结束。在这里我们是用的是用的java.util.concurrent.TimeUnit枚举类,该类下面有7个枚举常量,分别是
通过该枚举常量然后调用sleep()方法,可以给我们非常直观的时间概念,比如说我想让想让当前线程睡眠1秒,那么我们就可以TimeUnit.SECONDS.sleep(1),是不是比Thread.sleep(1000) 看起来更加符合人类的语言呢??
第二种放方式是通过实现Runable接口创建线程,对于通过实现接口比继承的好处有很多,我就步一一在这里阐述, 但是在多线程中,通过实现接口实现多线程一个重要的优势是,线程池只接受接口。我们看一下jdkAPI的几个线程池提交方法:
他都是只接受接口,其中就是Runable 和Callable接口。当然了,类也不是说一无是处,他有自己的优点,比如说类中start()方法,就是一个异步启动执行线程的,后面我们在区别和联系中会详细讲解到。
下面我们看通过Runbale运行的后示例:
通过控制台,我们可以看到输出的详细信息,如果大家仔细查看的话,发现是按顺序先执行线程1,然后在执行线程2,这个就是run()执行的步骤:
Callable接口的出现是为了满足线程执行完毕后返回结果而出现的,我们都知道,通过实现Runable接口和继承Thread类运行run()方法后, 该类是没有返回值的,假设我对一个班级的学生成绩统计后返回一个list,那么我就没法通过run()方法去实现,我只能通过一个类的全局变量,然后在run()方式里面复制去实现,如果一个线程没有问题,线程多的话,就涉及到一个变量共享问题,导致我存的数据可能不是我想要的。那么Callable就能解决该问题,下面我们通过示例展示callable接口的实现:
通过上述例子我们可以看到,通过实现Callable 接口,调用他对于的call方法可以返回具体执行的对象,在本例程序中,我们处理一个人员名称对象列表,然后获取姓氏,可以通过统计该列表中每个姓氏人员有多少个,通过call()方法实现后,我们将处理的lists返回,通过控制台打印后,我们可以看到返回的结果:
其中,在call()方法中,我们使用了jdk1.8最新的lambda表达式,在以后的文章中,我会详细简绍lambda表达式。
通过上面的示例,我们基本了解了三种线程的实现方式,也对三种实现方式有了最基本认识,那么下面我们就一起来看看他们的区别和联系:
2. Thread类有个一个start()方法,他是可以做的多线程异步执行,通过上面的示例,我们可以很明确的了解到,start()方式执行后,线程并不是按照顺序执行的。
3. Runable是一个接口,最后的好处是接口可以多实现,而类是个单继承,其次,如果我们在实际业务中用到线程池及时,那么我们就的使用Runable或则Callable接口了,上面我们也讲到线程池只接受接口参数。
4. Callable也是一个接口,他可以接受一个泛型,是为了返回值做准备的,通过上面的示例我们也可以看到,Callable最大最好用的是call()方法可以返回你想要的任何数据。
5. 关于线程优先级这块,在这里不做详细讲解,在实际业务的涉及到线程优先级的很少,而且如果控制不当,很容易造成严重的后果,在后面我们会重点介绍一下线程锁的详细使用。
上面我们详细了介绍了线程的几种状态,并且通过各种示例对线程的状态进行详细的展示,下面我们总结一下线程之间这几种状态的转换。大家可以结合上面示例和本图进行一个详细的理解,这样有助于对线程状态的进一步深入了解。
首先我们看start方法源码:
在start()方法源码中,调用了一个start0()方法,我们可以看到start0()方法是通过private native void start0() native进行修饰的,该方法是一个原生态的方法,方法的实现不在当前文件中,下面我们通过示例展示run()方法和start()方法的的区别:
通过输出控制台我们可以看到,run()方法是直接运行在main函数中的主线程中执行的(准确的说是被创建新线程的当前线程所执行,如果我们在Thread2中执行Thread1的run()方法,你会发出他输出的是Thead2的名称)。:
而我们通过start()方法执行后,通过控制台发现他是在当前线程下执行的:
通过上述我们发现,run()方法其实他是当作一个普通的方法执行的,他必须等run()方法体内程序逻辑执行完毕后,才会执行后续的代码,而start()方法是真是意义上的多线程,他的执行不会去等待run()方法体内的逻辑,下面我们在通过一个打印序列进一步了解这俩个方法执行的逻辑。
虽然我们执行过程中计划是先循环1到10,然后在循环10到20,可是实际上通过start()方法后,他马上把该线程状态设置为可运行状态,放入等待队列,然后执行下个操作,他不会等run()方法体的具体执行情况,通过控制台我们可以看到,他的输出不是有序的,start()方法可以说才是真正的多线程模式。
然后我们在看run()方法执行过程,run()方法是按顺序执行的,只有第一个线程执行完毕后,才会执行第二个线程,不管你执行多少次都是有序的输出,和start()方法不同,start()方法我们可以多次尝试发现,每次执行输出的结果都会不一样。:
接下来我们看call()方法,该方法是Callable接口中唯一的一个方法,返回一个Object对象,如果我们通过实现Callable接口然后调用call()方法,发现他出和run()方法有点类似,除了他能返回结果外。其中都是在main主线程中执行的,并且执行的过程中都是有顺序的,下面我们看看具体是示例:
在实际应用中,Callable会和Future以及ExecutorService线程池结合使用,具体详细的讲解我们在下面线程池技术中一一列出。
我们在讨论多线程的时候,不得不提线程池,在实际生产环境应用中,有好多大名鼎鼎的相关插件在帮我们管理者线程池,不需要我们自己去写程序实现,实际上,线程方法面的技术比较复制,有时候我们多线程运用不当,会造成非常严重的问题,而这些问题有时候又是无法即使讲解和定位的。比如说我们在本地模拟一个聊天程序,一个服务器处理1万个用户简单的聊天请求,如果我们在本地气1万个线程去响应用户请求,我们估计电脑马上就会奔溃,这时候我们就需要用到线程池。
我相信大家看到这里的时候,多前面线程有一个基本的了解和应用,那么在这里讲解线程池技术我会穿插一些新的知识点,帮助大家进一步加深理解,比如:线程安全和共享资源问题、线程避免死锁问题、阻塞队列和同步器问题、以及讲解一下阿姆达尔定律。那么下面我们就言归正传,先看一下jdk API中和线程池相关的几个类和接口。在jdk1.5之后Java通过突出推出一套executor框架来专门用与处理线程池相关问题,他能够分解任务、任务调度和执行相关线程,下面我们通过我们通过一个
上面是用powerdesigner 简单的画了一个类图,在这里变常用到并且设计到的类和接口大致这么多,有一部分由于篇幅有限,我在这里没有一一罗列,具体的一些继承和实现类的方法大家可以查看API。从上面我们可以看到,创建线程池是通过Executors类的五个方法,也就是五种创建线程池方式。
创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程会代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newScheduledThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
创建新的线程。他适用于一些生命周期非常短的任务,因为没有线程池上线,如果操作IO时间长,处理业务复制的问题,将会导致创建大量线程,最终会导致内存溢出问题。
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。API官方文档是这么描述的:
核心点:创建固定数量的线程,
我是指标先生,翔博软件的创始人。
如果你了解翔博软件,你就应该知道,我有很多不为人知的赚钱交易战法。但在众多的炒股战法之中,以下链接炒股战法配套指标却始终是我的最爱。为什么呢?
2,一线天筹码选股器3.0版面世了!
9,打虎要打腰,杀猪要捅喉。把它应用在股票里,用那一招呢?
11,十年老股民操盘心得:一旦出现“三点共振”,后市必大涨!
28,一位股市天才悟出的“平台突破”买入法,长期坚持,你将赚多亏少
29,史上最强抄底战法—“V”型反转,一旦出现这种形态,不要傻傻的
30,著名游资的炒股传奇:从身无分文至财务自由,短线只因反复死记“2560”战法,几乎吃掉全部涨幅
31,终于有人把“缠论123战法”讲透彻了,备受散户喜爱,晚上睡不着时看一看
32,中国股市,为什么说炒次新股最赚钱呢?次新战法为你解密。
33,隔壁老王用了3年时间,从7万本金赚到710万,只因反复死啃量学倍缩洗盘战法的"缩量上涨必将暴涨,缩量下跌必将暴跌"16字铁则
44,“成交量突破”才是王者指标——连操盘手都钟爱不已的操作手段,散户值得拥有!
45,吃透“MACD+KDJ”黄金组合,精准捕抓买卖点,掌握少走10年弯路!
46,上海女博士说破股市“均线粘合排序”庄家听后跪求删帖,受益一辈子!
47,三剑合一优化版该如何做好?为什么有失败的,别人也能赚钱
48,股票急跌后出现“阳包阴”,暗示下跌空间有限,或为买入信号
49,“价值连城”的MACD水上金叉战法,堪称经典,摸透你就是股市高手
50,20年牛散实战秘笈:一拳打出大阳线战法,掌握出手都是抓强势股!
51,"MACD+KDJ"双剑合璧了,背熟了,你也足以秒杀那些所谓的股神
52,挖掘金叉买点:日周月KDJ共同金叉选股法,参透领悟,终身受用!
53,股市赚钱法则:始终坚持操作“低位下影线买入,高位上影线卖出”
54,抄底须谨慎!详解"双底选股战法"研判技巧,学会未来将救你一命!
55,给那些股票不会赚钱的股民的建议----一位股市老人
56,炒股唯有做波段才是王道,越简单越靠谱。 你认同吗?
57,必须收藏的干货:什么是涨停回调买入法?这里说的一清二楚
58,犹太人炒股无价之宝:抛弃MACD、KDJ指标,布林线才是真正的赚钱
59,均线多头排列超强选股法:这招战法很少有散户可以做的好,收藏好!
60,黄金眼出击战法,一眼能看出大牛股的起涨信号
61,A股回调,当下最好的策略:强势股回调买入战法,强者恒强!原因有哪些?
69,一位血亏千万顶尖操盘手的总结:不想继续亏损,牢记“CCI底背离指标”
70,切记!股票一旦遇到“一阳穿多线”的形态,后市拉升行情不容错过!
71,不妨试试“老鸭头”均线战法(附选股公式),大胆买进,坐等主力拉升
72,股票一旦出现“空中加油”形态,加急满仓干,股价将会暴涨!
73,一旦出现“多方炮”形态,当机立断跑步进场,后市股价将一飞冲天!
74,翔博软件一线天筹码系统之一线天选股器v3.0
75,翔博软件一线天筹码系统的回踩模式之一线天波段主图和回踩龙门线
76,A股赚钱黄金指标:出现“上升三法”形态,定有一波大涨行情!
77,很多散户可能永远都不会知道:MACD指标上的零轴线到底有多重要!
78,中国股市:“美人肩”形态一出,不是涨停就是涨个不停,建议收藏!
80,一旦出现“放量打拐”信号,散户就可以满仓进场了,股价即将起飞!
你渴望掌握精品炒股战法的绝活吗?
你渴望随时随地能够赚钱吗?
你渴望自由潇洒的生活吗?
特别提醒#1:精品炒股战法是你获取自由富足人生的最佳工具,请你务必把握,立即行动。
特别提醒#2:作为本次指标的特色,也为了向你展示精品炒股战法的威力,其中精彩敬请实战中体验。