(5) - 多线程 (图)

来源:互联网 发布:如何备份网站数据库 编辑:程序博客网 时间:2024/04/28 16:54

---------------------- ASP.Net+Android+IO开发S、.Net培训、期待与您交流! ----------------------


个人学习后归纳的知识点,利于整体记忆。

1、 线程概述

一个程序(执行中)我们也叫一个进程,而进程中又包含很多独立运行的程序处理片段,这些控制小单元我们就叫线程,进程可以看做一个大的线程。如,在java中我们一般叫main()函数为主线程。


多线程的特点:能并发执行,像现在的迅雷下载,就是多线程下载。执行具有随机性,在不人为干涉下谁抢到处理器执行权谁就执行。线程概述较简单,不细说了。

 

2、 创建方式

(1)   继承Thread类

步骤:①定义类继承Thread

            ②复写Thread类中的run方法

             ③调用线程start方法,该方法有两个作用:

                     *启动线程

                      *start自动调用run方法

class Demo extends Thread{public void run(){        //线程的执行体        System.out.println("创建线程!");}}public class Test{public static void main(String[] args){        Demo d = new Demo();        d.start();//启动线程}}


(2)   实现Runnable接口

步骤:

①    定义类实现Runnable接口。

②    覆盖Runnable接口中的run方法。将线程要运行的方法存放在run方法中。

③    通过Thread类建立线程对象。

④    将Runnable接口的子类对象作为实际参数传给Thread类的构造函数。

class RunDemo implements Runnable{       //复写run       public void run()       {              System.out.println("Runnable实现方法创建线程!");       }}public class Test{       public static voidmain(String[] args)       {              RunDemo rd = new RunDemo();              newThread(rd).start();       }}

问题:为什么要将Runnable接口的子类对象传递给Thread的构造函数呢?

我们在不需要去增强线程子类的情况下,仅仅执行run运行功能代码,不建议去创建Thread的子类对象,而Runnable中仅含run,只需复写run。但Runnable却不是线程类,不能启动,这时就需要Thread去调用指定对象的run运行。


(3)   两种方式的区别:

继承Thread:线程代码存放在Thread子类方法中。

实现Runnable:线程代码存放在接口的子类run方法中。


用extends继承Thread的方式,从代码格式我们不难看出,每开启一个线程是一个新Thread子类对象,每一个对象都有自己复写的run方法,更重要的都有自己的this,我们在学习后面的同步锁时会知道对象中同步方法的锁是this,只有两线程使用同一把锁,才能同步共享数据,而继承的方式使得各线程对象都使用各自的锁(this),所以我们说继承方式的多个线程,不适合同步操作共享数据,即不能解决同步的问题


用implements实现Runnale的方式,我们从代码不难看出,Runnable的子类,只复写了一份run,并且调用时Runnable的子类只new一个对象,把该对象传入到你new的Thread对象中,要几个线程就传几个new Thread(...),但这多个Thread都是用Runnable的子类中的run(),即一个run方法。说到线程的同步时,这个run方法对于所有的线程都是使用一个锁,即Runnable的子类对象的this。所以说implements的方式,不仅能少复写run方法(调用一份,简化代码),还能解决线程同步问题,即多线程操作共享数据。


并且implements实现的方式避免了单继承的局限性(因多实现,功能可扩展),所以建议使用实现方式。


3、 线程的运行状态


(1)   创建状态:当用构造方法创建一个线程,它就已经有了相应的内存空间和资源,但还没有执行权。

(2)   就绪状态:建立线程后调用start(),便进入就绪状态,它将进入线程队列排队等待处理器服务。

(3)   运行状态:当就绪的线程获得处理器执行权后就进入了运行状态,执行线程中run方法的代码。

(4)   阻塞状态:一个正在执行的线程,在某些情况下,如sleep,wait或等待输入输出,线程将让出处理器的执行权,进入阻塞状态,在阻塞状态的线程不在就绪队列里,只有当阻塞原因消除后,才能进入就绪队列,而后运行。

(5)   终止状态:调用stop或run执行完,线程生命周期结束.


4、 安全问题及同步

(1)多线程在并行执行时,它们若对同一个共享数据进行操作时,一个线程还未执行完,另一个线程也参与进来,那么这段共享数据就会出错,会引起安全问题。

这样的安全问题其解决办法为:多线程在操作共享数据时,只能让一个线程执行完,其他线程才能参与执行。

 

(2)在java中对于线程的安全问题才用的是同步代码块的解决方式:

synchronized(对象)

{

       需要同步的代码

}

多现场在访问该代码块时,只允许一个线程进入,不待该线程执行完,其它线程不得进入。对象如同锁,持有锁的线程可以在同步代码中执行。

 

同步函数:public synchronized void add(){},其功能类似同步代码块。

 

同步的前提:

(a)必须要有两个或者两个以上的线程。

(b) 必须是多个线程使用同一个锁。

必须保证同步代码中只能有一个线程执行。

弊端:多个线程都需要判断锁,较为消耗资源。


5、 同步函数的锁

同步代码块的锁,较为随意,它只是一个充当互斥量的标记,这里只下说同步函数。

非静态同步函数的锁是this。

通过函数验证(简述):

使用两个线程来买票100张票。

一个线程在同步代码块中。

一个线程在同步函数中。

都在执行买票动作,用if控制,有票才买,买一张票少一。

买票代码都用while(true){}包围,一开起线程就无限买票。

       结果及分析:同步代码块中使用定义的Object obj对象当锁,执行结果发现票号会出现0号的错票,不可能买到0号票。

这是没有满足线程同步的前提,即没有用同一个锁,那么两线程都会访问共享的tick变量,导致错误,而当把同步代码块中锁obj改成this,发现正常,那么证明同步函数中锁为this。

 

同样的方法证明: 静态同步函数的锁是该方法所在类的字节码对象,即类名.class。


6、 死锁

两线程持有对方想要的锁不放,又想要对方的锁去执行,这时会是一个死锁状态,想要但是双方都不给,会出现僵局。在java中出现较多为同步嵌套同步的情况。

class Lock{//定义A,B锁static Object lockA = new Object();static Object lockB = new Object();}class Test1implements Runnable {public void run(){        //要A锁        synchronized(Lock.lockA){               System.out.println("Test1--------");               //要B锁               synchronized(Lock.lockB){                      System.out.println("Test1--------");               }        }}}class Test2implements Runnable {public void run(){        //要B锁        synchronized(Lock.lockB){               System.out.println("Test2--------");               //要A锁               synchronized(Lock.lockA){                      System.out.println("Test2--------");               }        }    }}class Test{public static void main(String[] args){        new Thread(new Test1()).start();        new Thread(new Test2()).start();}}


出现死锁的原因,Test1有外同步A锁,需要B锁进入内部同步,而Test2有外同步的B锁,需要A锁进内部的,但是两个外同步的锁都不能放出,又想要内部的,于是就僵持了,也就出现了死锁的状况。


7、 线程通信

简单说,就是多个线程在操作同一个数据,但操作动作不同。

 

通信时的安全问题解决:需要认清同步的两个前提条件,同步区域要包含两个或多个线程的通讯数据,锁要使用同一个锁。

 

等待唤醒机制:即一般线程要求一个产生你才能消费一个或者说我叫你你才动的方式进行,线程中用变量通信,wait()-notify()成对实现等待-唤醒机制。

 

特点:等待的线程被存在线程池中,当notify时,会唤醒线程池中的等待线程,通常唤醒顺序最前的一个等待线程。notifyAll()唤醒所有等待线程。

wait被声明在Object中,抛出了异常,调用需处理。

wait和notify用在同步中(只有同步拥有锁的概念),要求等待和唤醒使用同一个锁(件监视器),这个锁是同步代码或方法的锁。

      

       由经典的生产消费者模型看唤醒等待机制

class Res{       privateString name;       privateint count =1;       //通信量       privateboolean flag = false;             //生产者执行函数       publicsynchronized void set(String name)       {              //有多个生产者时需循环判断标志              //避免唤醒本方线程而不进行判断的错误              while(flag)                     try{this.wait();}catch(Exceptione){}              this.name= name +"----"+count++;//生产一个                           System.out.println(Thread.currentThread().getName()+"..生产者.."+this.name);              flag= true;              this.notifyAll();//唤醒所有线程池的等待线程       }       //消费者执行函数       publicsynchronized void out()       {              while(!flag)//有多个消费者,要循环判断表志              {                     try{wait();}catch(Exceptione){}              }              System.out.println(Thread.currentThread().getName()+"..消费者........................"+this.name);              flag= false;              this.notifyAll();       }}class Produce implements Runnable {       privateRes r;       Produce(Resr)       {              this.r= r;       }       publicvoid run()       {              while(true)              {                     r.set("商品");              }       }}class Consume implements Runnable {       privateRes r;       Consume(Resr)       {              this.r= r;       }       publicvoid run()       {              while(true)              {                     r.out();              }       }}class Test{       publicstatic void main(String[] args)       {              Resr = new Res();              //多个生产和消费者              //两个生产者              newThread(new Produce(r)).start();              newThread(new Produce(r)).start();              //两个消费者              newThread(new Consume(r)).start();              newThread(new Consume(r)).start();       }}


在有多个生产消费的代码中等待需进行循环判断,而不能用if,因为唤醒机制,只按序唤醒线程池中最先等待的,这样,当前一个生产者生产后唤醒的可能依旧是生产者线程(有多个生产线程),它由等待处唤醒向下执行生产,没有经过判断,这将导致两次生产,而只消费一次的错误现象,为避免上述现象,在等待处用while判断,本方唤醒都进行判断。但这也可能导致全部等待的现象,因为唤醒一个线程时本方的,其他都没唤醒,而这个醒着的线程,循环进入判断,又wait,结果唯一醒着的线程也没了,所以没有一个执行线程,程序卡死。为避免,要把对方线程也唤醒,所以唤醒时用了notifyAll全部唤醒,不至于只有一个线程醒着。当然,只有一个生产者和一个消费者不用这样考虑。


8、 线程的一些方法

线程停止:stop()已过时,它可能会产生死锁,所以被java舍弃,现在唯一的做法就是让run方法结束。

 

中断线程:Interrupt()是中断线程,中断状态不是停止状态,与停止状态不同,interrupt是将冻结状态的线程,强制结束冻结状态,恢复到运行状态,但会抛出异常,需处理。Interrupt后,在异常处理处,可进行标志的设置,使循环退出,达到线程run的结束目的。

 

守护线程:setDaemon(),将线程标记为守护线程或用户线程,当在运行的线程都是守护线程时,java虚拟机退出。该方法必须在启动线程前调用。其实相当于后台线程,它依赖于前台线程,没有一个前台线程,守护线程就自动结束。

 

礼让方法:jion()方法,其他线程等待调用jion方法的该线程终止在执行。即占据cup的执行权。

       作用,临时加入一个线程立即获得执行权,让出执行权的线程得等其运行完。

       当a线程执行到了b线程的b.join()方法,a就会等待,等b线程运行完。

       t1.start();

       t1.join()

       t2.start();//在jion后开启

       其上,main主线程让出执行权,等t1执行完方可再执行。

   而t1.start();//开启

       t2.start();//开启

       t1.join()

主线程让出执行权给t1,但能执行的还有t2,所以t1和t2都在运行。

 

优先级:线程抢到执行权的频率或说权大小。优先级共10级,默认是5,由setPriority()设置。

 

暂停线程:yield方法暂停当前正在执行的线程对象,并执行其他线程。


---------------------- ASP.Net+Android+IO开发S、.Net培训、期待与您交流! ----------------------

原创粉丝点击