黑马程序员------Java基础学习------多线程

来源:互联网 发布:算法第四版英文版pdf 编辑:程序博客网 时间:2024/05/21 06:02

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------



一进程和线程

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程。比如在Windows系统中,一个运行的xx.exe就是一个进程。

Java程序的进程里有几个线程:主线程, 垃圾回收线程(后台线程)

线程是指进程中的一个执行任务(控制单元),一个进程中可以运行多个线程,多个线程可共享数据。

多进程:操作系统中同时运行的多个程序;多线程:在同一个进程中同时运行的多个任务;

一个进程至少有一个线程,为了提高效率,可以在一个进程中开启多个控制单元并发运行。如:多线程下载软件。 

可以完成同时运行,但是通过程序运行的结果发现,虽然同时运行,但是每一次结果都不一致。 

因为多线程存在一个特性:随机性。 造成的原因:CPU在瞬间不断切换去处理各个线程而导致的。 可以理解成多个线程在抢cpu资源。


线程与进程的比较

 

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—WeightProcess)或进程元;而把传统的进程称为重型进程(Heavy—WeightProcess),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少需要一个线程。

 

进程与线程的区别:

        1.进程有独立的进程空间,进程中的数据存放空间(堆空间和栈空间)是独立的。

        2.线程的堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间可以影响的。


创建线程的方式有两种

1、继承Thread类

子类覆写父类中的run方法,将线程运行的代码存放在run中。

建立子类对象的同时线程也被创建。

通过调用start方法开启线程。

 

class Test extends Thread {    String name;    public TimePrinter(String name) {        this.name = name;    }       //复写run方法    public void run() {       //需要执行的方法                     System.out.print(name);    }       }          class ThreadTest{       static public void main(String args[]) {        //创建teat对象,并启动线程              Test test = new Test("zhu");              test.start();    }}

2、实现Runnable接口

子类覆盖接口中的run方法。

通过Thread类创建线程,并将实现了Runnable接口的子类对象作为参数传递给Thread类的构造函数。

Thread类对象调用start方法开启线程。

可使用匿名内部类来写

class Test implements Runnable {    String name;    public TimePrinter(String name) {        this.name = name;    }       //复写run方法    public void run() {       //需要执行的方法                     System.out.print(name);    }       }          class ThreadTest{       static public void main(String args[]) {        //创建teat对象,作为参数传入Thread构造函数中,通过Thread实例r启动线程              Test test = new Test("zhu");              Thread r = new Thread(test);              r.start();    }}

  注意:当使用 runnable 接口时,您不能直接创建所需类的对象并运行它;必须从 Thread 类的一个实例内部运行它。许多程序员更喜欢 runnable 接口,因为从 Thread 类继承会强加类层次。

 

2、线程的生命周期

      当线程被创建并启动,它要经过新建、就绪、运行、阻塞和死亡5中状态。即:


  1. 新建(new): 当new一个线程后,该线程处于新建状态,此时它和Java对象一样,仅仅由Java虚拟机为其分配内存空间,并初始化成员变量。此时线程对象没有表现出任何的动态特征,程序也不会执行线程的执行体。
  2. 就绪(Keady):线程能够运行,但是在等待可用的处理器。可能刚刚启动,或者刚刚从阻塞中恢复,或者被其它线程抢占。
  3. 运行(Kunning):线程正在运行。在多处理器系统中,可能有多个线程处于运行态。
  4. 阻塞(Blocked):线程由于等待"处理器"外的其它条件而无法运行,如:条件变量的改变,加锁互质量或者等待I/O操作结束。
  5. 死亡(Terminated):线程从启动函数中返回,或者调用pthread_exit,或者被取消,终止自己并完成所有资源清理工作。不是被分离,也不是被连接,一旦被分离或者被连接,它就可以被回收。

  下面给出了Thread类中和这四种状态相关的方法:

// 开始线程publicvoid start( ); publicvoid run( ); // 挂起和唤醒线程publicstaticvoid sleep(long millis); publicstaticvoid sleep(long millis, int nanos); publicvoid resume( ); //少用publicvoid suspend( );  //少用// 终止线程publicvoid stop( );  //少用publicvoid interrupt( );

  线程在建立后并不马上执行run方法中的代码,而是处于等待状态。线程处于等待状态时,可以通过Thread类的方法来设置线程不各种属性,如线程的优先级(setPriority)、线程名(setName)和线程的类型(setDaemon)等。

      当调用start方法后,线程开始执行run方法中的代码。线程进入运行状态。可以通过Thread类的isAlive方法来判断线程是否处于运行状态。当线程处于运行状态时,isAlive返回true,当isAlive返回false时,可能线程处于等待状态,也可能处于停止状态。下面的代码演示了线程的创建、运行和停止三个状态之间的切换,并输出了相应的isAlive返回值。

      一但线程开始执行run方法,就会一直到这个run方法执行完成这个线程才退出。但在线程执行的过程中,可以通过两个方法使线程暂时停止执行。这两个方法是suspend和sleep。在使用suspend挂起线程后,可以通过resume方法唤醒线程。而使用sleep使线程休眠后,只能在设定的时间后使线程处于就绪状态(在线程休眠结束后,线程不一定会马上执行,只是进入了就绪状态,等待着系统进行调度)。

 

3、线程同步

      由于系统线程调度具有一定的随机性,造成它很容易出现“错误情况”,这就造成了安全问题。为解决这个问题,jaa多线程引入了同步代码块的概念,具体语法如下:

synchronized (obj){       //此处的代码就是同步代码块}

  与同步代码块对应的是同步方法,也就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。通过使用同步方法可以非常方便地实现线程安全的类,具体特征如下:

  1. 该类的对象可以被多个线程安全地访问
  2. 每个线程调用该对象的任意方法之后都将得到正确结果
  3. 每个线程调用该对象的任意方法后,该对象状态依然保持合理状态。

下面是具体的语法:


public void method3(SomeObject so) {       synchronized(so){     //…..方法执行体}}

  注意:在使用synchronized关键字时候,应该尽可能避免在synchronized方法或synchronized块中使用sleep或者yield方法,因为synchronized程序块占有着对象锁,你休息那么其他的线程只能一边等着你醒来执行完了才能执行。不但严重影响效率,也不合逻辑。

  同样,在同步程序块内调用yeild方法让出CPU资源也没有意义,因为你占用着锁,其他互斥线程还是无法访问同步程序块。当然与同步程序块无关的线程可以获得更多的执行时间。

 

4、线程通信

      线程之间除了同步互斥,还要考虑通信。在Java5之前我们的通信方式为:wait 和 notify。那么Condition的优势是支持多路等待,就是我可以定义多个Condition,每个condition控制线程的一条执行通路。传统方式只能是一路等待。我们可以先分析下Java5 Api中的缓冲队列的实现:

  假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行take操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行put操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个condition实例来做到这一点。

4.1 使用condition控制线程通信

  如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。

  当使用Lock对象来保证同步时,java提供了一个condition类来保持协调,使用condition对象可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,condition对象也可以唤醒其他处于等待的线程。

  Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了3种方法:await()、signal()和signalAll()。

 

4.2 使用阻塞队列(BlockingQueue)控制线程通信

  阻塞队列具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果队列已满,则该线程阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。

  程序的两个线程通过交替向BlockingQueue中放入元素,即可很好地控制线程的通信。BlockingQueue提供了两个支持阻塞的方法:put(E e)和take()。

 

三、总结

  本文主要简单的介绍了如何创建和启动多线程,并说了两种创建多线程方式之间的优势和劣势,也比较详细的介绍了线程的生命周期,然后简单的说明了线程同步方法,最后介绍了两种实现线程通信的方式,这些都是学习多线程应该掌握的基本知识。

 

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------


0 0