javaee论坛

普通会员

225648

帖子

329

回复

343

积分

楼主
发表于 2019-11-02 06:53:48 | 查看: 422 | 回复: 0

1995年,在java诞生之初,诸如JamesGosling等java的主要设计者,非常明智的选择让java内置支持“多线程”,这使得java相比同时期的其它编程语言,有着非常明显的优势。

我今天的讨论,也就由此拉开序幕:(声明:只是学术研究和讨论,有任何疑问欢迎指出!)

java线程的概念

官方给出的解释是:线程是彼此互相独立的,能独立运行的子任务。这句话一点没错,只是太过官方,我们来具体一点!什么是“进程”:好比你打开了电脑,打开QQ,就是一个进程,打开idea,这也是一个进程。进程之间往往会制造出“同步”的假象(当然,这一点在线程中有过之而无不及)。什么是“线程”:你打开QQ后,既想发消息,又想听音乐。QQ这个进程为你运行了两个“线程”,同样的,这两者同步的只是电脑给人的假象,这一切,还要“归罪”到强大的CPU上。(开个玩笑)

分时通俗来讲,就是可以同一时间执行多个程序的操作系统,在自己电脑上,可以一边听歌,一边看视频,一边浏览网页。但实际上,CPU只是将时间切割成时间片,然后将这些时间片分配给这些程序,这样一个个的程序在极短时间内获得时间片从而运行,达到“同时”的效果。

学线程,你需要知道什么

用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先建立一个数据字典,以免产生误会。多线程:指的是这个程序(一个进程)运行时产生了不止一个线程并行与并发:并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

并发与并行线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:

voidtransferMoney(Userfrom,Userto,floatamount){to.setMoney(to.getBalance()+amount);from.setMoney(from.getBalance()-amount);}

同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

进入主题:线程的状态

每个线程的教学都会讲这个部分,infact,他的确是线程不可分割的一部分,但是,我并不打算在这里给你们消磨时间的机会。各种状态一目了然,值得一提的是"blocked"这个状态:线程在Running的过程中可能会遇到阻塞(Blocked)情况

调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。调用wait(),使该线程处于等待池(waitblockedpool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lockblockedpool),释放同步锁使线程回到可运行状态(Runnable)对Running状态的线程加同步锁(Synchronized)使其进入(lockblockedpool),同步锁被释放进入可运行状态(Runnable)。此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

小叙——继承?实现?

1.扩展java.lang.Thread类这里继承Thread类的方法是比较常用的一种,如果说你只是想起一条线程。没有什么其它特殊的要求,那么可以使用Thread.(笔者推荐使用Runable,后头会说明为什么)。下面来看一个简单的实例[java]viewplaincopy

packagecom.multithread.learning;/***@functon多线程学习*@author林炳文*@time2015.3.9*/classThread1extendsThread{privateStringname;publicThread1(Stringname){this.name=name;}publicvoidrun(){for(inti=0;i<5;i++){System.out.println(name+"运行:"+i);try{sleep((int)Math.random()*10);}catch(InterruptedExceptione){e.printStackTrace();}}}}publicclassMain{publicstaticvoidmain(String[]args){Thread1mTh1=newThread1("A");Thread1mTh2=newThread1("B");mTh1.start();mTh2.start();}}输出:A运行:0B运行:0A运行:1A运行:2A运行:3A运行:4B运行:1B运行:2B运行:3B运行:4

再运行一下:

A运行:0B运行:0B运行:1B运行:2B运行:3B运行:4A运行:1A运行:2A运行:3A运行:4

说明:程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。

注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。——start()—>调度线程,让其做准备。

从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。但是start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。[java]viewplaincopy在CODE上查看代码片派生到我的代码片Thread1mTh1=newThread1(“A”);Thread1mTh2=mTh1;mTh1.start();mTh2.start();

输出:

Exceptioninthread"main"java.lang.IllegalThreadStateExceptionatjava.lang.Thread.start(UnknownSource)atcom.multithread.learning.Main.main(Main.java:31)A运行:0A运行:1A运行:2A运行:3A运行:4

二、实现java.lang.Runnable接口采用Runnable也是非常常见的一种,我们只需要重写run方法即可。下面也来看个实例。[java]viewplaincopy

/***@functon多线程学习*@author林炳文*@time2015.3.9*/packagecom.multithread.runnable;classThread2implementsRunnable{privateStringname;publicThread2(Stringname){this.name=name;}@Overridepublicvoidrun(){for(inti=0;i<5;i++){System.out.println(name+"运行:"+i);try{Thread.sleep((int)Math.random()*10);}catch(InterruptedExceptione){e.printStackTrace();}}}}publicclassMain{publicstaticvoidmain(String[]args){newThread(newThread2("C")).start();newThread(newThread2("D")).start();}}输出:C运行:0D运行:0D运行:1C运行:1D运行:2C运行:2D运行:3C运行:3D运行:4C运行:4

说明:Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnabletarget)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

Thread和Runnable的区别如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。当然,事无绝对,如果在Thread里对如变量使用static,也是可以的。。。

正式见面:线程境界-常用函数说明

线程的各个状态的实际操作方法,才是我们需要讨论的问题,

1.线程的休眠sleep与挂起yield线程可以使用sleep()让当前线程暂停一会后继续执行,其目的就是给其他线程执行的机会。而yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。结论:yield()-暂停执行,允许其它(优先级相同或更高的)线程运行,但本身仍处于可运行状态(并不阻塞),可以与其他等待执行的线程继续竞争CPU资源。

说到这,其实有一个地方挺有意思的:线程的调度分为两种模型:分时调度模型和抢占式调度模型。所谓分时调度,就是所有县城轮流使用CPU,平均分配时间片;而抢占式,就是靠优先级说话!而不巧的是,java的线程调度模型是抢占式,

这就意味着:我们需要知道另一个概念:优先级

优先级,分为:MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY(标准)而优先级不是“先天”的,我们可以通过下面方式来为线程设置优先级:

Thread对象.setPriority(...);

值得注意的是,设置优先级这一步骤,必须要在线程启动(start)前设置完成。

而线程的挂起,不能有用户指定线程暂停多长时间,但可以限制只有同优先级的线程有才机会执行。如下:

publicvoidrun(){for(intk=0;k<10;k++){if(k==5&&Thread.currentThread().getName().equals("test1")){Thread.yield();}System.out.println(Thread.currentThread().getName()+":"+k);}}publicstaticvoidmain(String[]args){Testtest1=newTest();Testtest2=newTest();Threadt1=newThread(test1,"test1");Threadt2=newThread(test2,"test2");t1.setPriority(Thread.MAX_PRIORITY);t2.setPriority(Thread.MIN_PRIORITY);t1.start();t2.start();}

运行以后,我们会发现,事情并没有按我们想象中的来,怎么回事?仔细看前面我说过的内容,可以发现,倒数第四、五行为两个线程设置了不同的优先级,原因一览无余。

我们可以通过设置相同优先级来达到目的,不过,有没有其他方法呢?

2.线程的联合——join通常,我们再一个线程中加入join后,次线程会暂停执行,一直等到加入的线程执行完毕后再继续执行。格式:

第一个线程执行try{Thread.sleep(1000);第二个线程的对象.join();}catch(Exceptione){e.printStack();}

3.线程同步——synchronized许多线程在执行过程中必须考虑与其他线程的数据关系,我们称为“共享数据或协调执行状态”,这就需要同步机制。在java中每个对象都有一把锁与其对应,但java不提供单独的lock与unlock操作,它由高层的结构隐式实现。就像我们开头讨论的Thread与Runnable,不就是因为“数据共享”的问题吗?

但是,事无绝对,有一点必须注意,你必须清楚的知道哪些数据是共享的。请看下面例子:

public**synchronized**voidrun(){for(inti=0;i<10;i++){System.out.println(""+i);}}

这样,你就算运行1000次程序,它的结果也一定是01234567890123456789,而不会发生错乱,因为这里synchronized起到了保护作用。在可能的情况下,应该把保护范围缩到最小,也就是“使用代码块保护数据”(使用this代表当前对象):

publicvoidrun(){**synchronized(this)**{for(inti=0;i<10;i++){System.out.println(""+i);}}线程的同步锁机制

wait()、notify()、和notifyAll()在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁标志,进入等待状态,并且可以调用notify或者notifyAll方法通知正在等待的其他线程。notify通知等待队列的第一个线程任务,而notifyAll通知的是等待线程队列中的所有线程。

//被线程1调用synchronizedmethod-1(...){//访问数据变量available=true;notify();}//被线程2调用synchronizedmethof-2(...){while(!available){try{wait();//等待notify唤醒}catch(Exceptione){e.printStack();}}}

对于这一部分,仅靠以上代码是难以掌握的,故一下给出实例操作:

小叙收尾——线程实际操做练习

我们定义这样一个场景:学生钱不够了问父母要,同时停止消费(线程),唤醒充值线程,父母向卡里打钱,同时唤醒学生线程,通知学生已经打过钱,,,等等,(涉及知识点有synchroized,wait,sleep,yield,线程唤醒,锁的恢复,映射…)

package线程的实例操作;/***银行类*/classBankCard{intsum=0;//存款//加线程锁,使共享资源publicsynchronizedvoidsave(Stringname,intcount){//如果存够了足够的钱就不再存了while(sum>5000){try{System.out.println(name+"\t存款,发现钱够了");//等待,并且从这里退出pushwait();}catch(Exceptione){e.printStackTrace();}}//注意,notifyAll()以后,并没有退出,而是继续执行直到完成this.sum+=count;System.out.println(name+"\t存入了[¥"+count+"]\t余额[¥"+this.sum+"]");//因为我们不确定有没有线程在wait,所以我们既然存了钱,就唤醒孩子线程,让他们准备取款notifyAll();System.out.println("\t"+name+"告诉孩子存了钱");}//取款publicsynchronizedvoidcost(Stringname,intcount){//如果钱不够了,就不再取款while(sum<count){try{System.out.println(name+"\t取款:等钱花"+count);//等待,并从这里退出popwait();}catch(Exceptione){e.printStackTrace();}}//注意,notifyAll以后,并没有退出,而是继续执行直到完成this.sum-=count;System.out.println(name+"\t取走了[¥"+count+"]\t余额[¥"+this.sum+"]");//因为不确定有没有线程在wait,所以我们既然消费了产品,就唤醒有可能的生产者notifyAll();System.out.println("\t"+name+"告诉父母取了钱");}}/***父母类*/classParentimplementsRunnable{BankCardcard=null;//这里是映射——对象实现从属关系Stringname;intcount;intinterval;//存款时间间隔Parent(BankCardcard,Stringname,intcount,intinterval){this.card=card;this.count=count;this.name=name;this.interval=interval;}publicvoidrun(){while(true){card.save(name,count);try{Thread.sleep(interval);}catch(Exceptione){e.printStackTrace();}}}}/***孩子类*/classChildrenimplementsRunnable{BankCardcard=null;Stringname;intcount;intinterval;Children(BankCardcard,Stringname,intcount,intinterval){this.card=card;this.count=count;this.name=name;this.interval=interval;}publicvoidrun(){while(true){//intcount=(int)(Math.random()*degree);card.cost(name,count);try{Thread.sleep(interval);}catch(Exceptione){e.printStackTrace();}}}}/***存取款类*/publicclassCheckInOut{publicstaticvoidmain(String[]args){BankCardcard=newBankCard();Parentpx=newParent(card,"爸爸",1500,500);Parentpy=newParent(card,"妈妈",1000,800);Parentpz=newParent(card,"爷爷",800,1000);Childrenca=newChildren(card,"大女儿",400,600);Childrencb=newChildren(card,"二女儿",300,600);Childrencc=newChildren(card,"三女儿",500,600);newThread(px).start();newThread(py).start();newThread(pz).start();newThread(ca).start();newThread(cb).start();newThread(cc).start();}}

运行一下,你会有额外的体会呦~


您需要登录后才可以回帖 登录 | 立即注册

触屏版| 电脑版

技术支持 历史网 V2.0 © 2016-2017