多线程编程的基础知识点.docx

上传人:b****5 文档编号:5700458 上传时间:2022-12-31 格式:DOCX 页数:47 大小:257.53KB
下载 相关 举报
多线程编程的基础知识点.docx_第1页
第1页 / 共47页
多线程编程的基础知识点.docx_第2页
第2页 / 共47页
多线程编程的基础知识点.docx_第3页
第3页 / 共47页
多线程编程的基础知识点.docx_第4页
第4页 / 共47页
多线程编程的基础知识点.docx_第5页
第5页 / 共47页
点击查看更多>>
下载资源
资源描述

多线程编程的基础知识点.docx

《多线程编程的基础知识点.docx》由会员分享,可在线阅读,更多相关《多线程编程的基础知识点.docx(47页珍藏版)》请在冰豆网上搜索。

多线程编程的基础知识点.docx

多线程编程的基础知识点

多线程编程的基础知识点

   多线程编程一直是程序员比较头痛和心虚的地方,因为线程执行顺序的不可预知性和调试时候的困难,让不少人在面对多线程的情况下选择了逃避,采用单线程的方式,其实只要我们对线程有了明确的认识,再加上Java内置的对多线程的天然支持,多线程编程不再是一道难以逾越的鸿沟。

   「一」进程、线程、并发执行

   关于进程、线程、并发执行的概念,我们先来看下面的一段话:

   “一般来说,当运行一个应用程序的时候,就启动了一个进程,当然有些会启动多个进程。

启动进程的时候,操作系统会为进程分配资源,其中最主要的资源是内存空间,因为程序是在内存中运行的。

   在进程中,有些程序流程块是可以乱序执行的,并且这个代码块可以同时被多次执行。

实际上,这样的代码块就是线程体。

线程是进程中乱序执行的代码流程。

当多个线程同时运行的时候,这样的执行模式成为并发执行。

   上面这段话引自51CTO网站“熔岩”的博客一篇文章《Java多线程编程总结》,读者可参考本文获得更详细的知识。

   下面我以一个日常生活中简单的例子来说明进程和线程之间的区别和联系:

   这副图是一个双向多车道的道路图,假如我们把整条道路看成是一个“进程”的话,那么图中由白色虚线分隔开来的各个车道就是进程中的各个“线程”了。

   ①这些线程(车道)共享了进程(道路)的公共资源(土地资源)。

   ②这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。

   ③这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。

   ④这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。

   ⑤这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道

   「二」JVM与多线程

   Java编写的程序都运行在Java虚拟机(JVM)中,在JVM的内部,程序的多任务是通过线程来实现的。

   每用java命令启动一个java应用程序,就会启动一个JVM进程。

在同一个JVM进程中,有且只有一个进程,就是它自己。

在这个JVM环境中,所有程序代码的运行都是以线程来运行的。

JVM找到程序程序的入口点main(),然后运行main()方法,这样就产生了一个线程,这个线程称之为主线程。

当main方法结束后,主线程运行完成。

JVM进程也随即退出。

   操作系统将进程线程进行管理,轮流(没有固定的顺序)分配每个进程很短的一段时间(不一定是均分),然后在每个进程内部,程序代码自己处理该进程内部线程的时间分配,多个线程之间相互的切换去执行,这个切换时间也是非常短的。

   「三」Java语言对多线程的支持

   Java语言对多线程的支持通过类Thread和接口Runnable来实现。

这里就不多说了。

这里重点强调两个地方:

//主线程其它代码段

ThreadClass subThread = new ThreadClass();

subThread.start();

//主线程其它代码段

subThread.sleep(1000);

   有人认为以下的代码在调用start()方法后,肯定是先启动子线程,然后主线程继续执行。

在调用sleep()方法后CPU什么都不做,就在那里等待休眠的时间结束。

实际上这种理解是错误的。

因为:

   ①start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。

   ②Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会(也就是靠内部自己协调)。

  「四」线程的状态切换

   前面我们提到,由于线程何时执行是未知的,只有在CPU为线程分配到时间片时,线程才能真正执行。

在线程执行的过程中,有可能会因为各种各样的原因而暂停(就像前面所举的例子一样:

汽车只有在交通灯变绿的时候才能够通行,而且在行驶的过程中可能会出现塞车,等待其它车辆通行或转弯的状况)。

   这样线程就有了“状态”的概念,下面这副图,是从《Java多线程编程总结》一文中摘录出来的。

很好的反映了线程在不同情况下的状态变化

   1、新建状态(New):

新创建了一个线程对象。

   2、就绪状态(Runnable):

线程对象创建后,其他线程调用了该对象的start()方法。

该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

   3、运行状态(Running):

就绪状态的线程获取了CPU,执行程序代码。

   4、阻塞状态(Blocked):

阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。

直到线程进入就绪状态,才有机会转到运行状态。

阻塞的情况分三种:

   ①等待阻塞:

运行的线程执行wait()方法,JVM会把该线程放入等待池中。

   ②同步阻塞:

运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM把该线程放入锁池③其他阻塞:

运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。

当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

   5、死亡状态(Dead):

线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

 「五」Java中线程的调度API

   Java中关于线程调度的API最主要的有下面几个:

   ①线程睡眠:

Thread.sleep(longmillis)方法

   ②线程等待:

Object类中的wait()方法

   ③线程让步:

Thread.yield()方法

   ④线程加入:

join()方法

   ⑤线程唤醒:

Object类中的notify()方法

   关于这几个方法的详细应用,可以参考SUN的API,及《Java多线程编程总结》一文第七部分“线程的调度”。

这里我重点总结一下这几个方法的区别和使用:

   备注:

sleep方法与wait方法的区别:

①sleep方法是静态方法,wait方法是非静态方法。

   ②sleep方法在时间到后会自己“醒来”,但wait不能,必须由其它线程通过notify(All)方法让它“醒来”

   ③sleep方法通常用在不需要等待资源情况下的阻塞,像等待线程、数据库连接的情况一般用wait

   备注:

sleep/wait与yeld方法的区别:

①调用sleep或wait方法后,线程即进入block状态,而调用yeld方法后,线程进入runnable状态

   备注:

wait与join方法的区别:

①wait方法体现了线程之间的互斥关系,而join方法体现了线程之间的同步关系②wait方法必须由其它线程来解锁,而join方法不需要,只要被等待线程执行完毕,当前线程自动变为就绪③join方法的一个用途就是让子线程在完成业务逻辑执行之前,主线程一直等待直到所有子线程执行完毕。

线程简介

   一、线程概述

   线程是程序运行的基本执行单元。

当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点。

因此,在操作系统中运行的任何程序都至少有一个主线程。

   进程和线程是现代操作系统中两个必不可少的运行模型。

在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程(由用户程序建立的进程);一个进程中可以有一个或多个线程。

进程和进程之间不共享内存,也就是说系统中的进程是在各自独立的内存空间中运行的。

而一个进程中的线可以共享系统分派给这个进程的内存空间。

   线程不仅可以共享进程的内存,而且还拥有一个属于自己的内存空间,这段内存空间也叫做线程栈,是在建立线程时由系统分配的,主要用来保存线程内部所使用的数据,如线程执行函数中所定义的变量。

   注意:

任何一个线程在建立时都会执行一个函数,这个函数叫做线程执行函数。

也可以将这个函数看做线程的入口点(类似于程序中的main函数)。

无论使用什么语言或技术来建立线程,都必须执行这个函数(这个函数的表现形式可能不一样,但都会有一个这样的函数)。

如在Windows中用于建立线程的API函数CreateThread的第三个参数就是这个执行函数的指针。

   在操作系统将进程分成多个线程后,这些线程可以在操作系统的管理下并发执行,从而大大提高了程序的运行效率。

虽然线程的执行从宏观上看是多个线程同时执行,但实际上这只是操作系统的障眼法。

由于一块CPU同时只能执行一条指令,因此,在拥有一块CPU的计算机上不可能同时执行两个任务。

而操作系统为了能提高程序的运行效率,在一个线程空闲时会撤下这个线程,并且会让其他的线程来执行,这种方式叫做线程调度。

我们之所以从表面上看是多个线程同时执行,是因为不同线程之间切换的时间非常短,而且在一般情况下切换非常频繁。

假设我们有线程A和B.在运行时,可能是A执行了1毫秒后,切换到B后,B又执行了1毫秒,然后又切换到了A,A又执行1毫秒。

由于1毫秒的时间对于普通人来说是很难感知的,因此,从表面看上去就象A和B同时执行一样,但实际上A和B是交替执行的。

   二、线程给我们带来的好处

   如果能合理地使用线程,将会减少开发和维护成本,甚至可以改善复杂应用程序的性能。

如在GUI应用程序中,还以通过线程的异步特性来更好地处理事件;在应用服务器程序中可以通过建立多个线程来处理客户端的请求。

线程甚至还可以简化虚拟机的实现,如Java虚拟机(JVM)的垃圾回收器(garbagecollector)通常运行在一个或多个线程中。

因此,使用线程将会从以下五个方面来改善我们的应用程序:

   1.充分利用CPU资源

   现在世界上大多数计算机只有一块CPU.因此,充分利用CPU资源显得尤为重要。

当执行单线程程序时,由于在程序发生阻塞时CPU可能会处于空闲状态。

这将造成大量的计算资源的浪费。

而在程序中使用多线程可以在某一个线程处于休眠或阻塞时,而CPU又恰好处于空闲状态时来运行其他的线程。

这样CPU就很难有空闲的时候。

因此,CPU资源就得到了充分地利用。

   2. 简化编程模型

   如果程序只完成一项任务,那只要写一个单线程的程序,并且按着执行这个任务的步骤编写代码即可。

但要完成多项任务,如果还使用单线程的话,那就得在在程序中判断每项任务是否应该执行以及什么时候执行。

如显示一个时钟的时、分、秒三个指针。

使用单线程就得在循环中逐一判断这三个指针的转动时间和角度。

如果使用三个线程分另来处理这三个指针的显示,那么对于每个线程来说就是指行一个单独的任务。

这样有助于开发人员对程序的理解和维护。

   3. 简化异步事件的处理

   当一个服务器应用程序在接收不同的客户端连接时最简单地处理方法就是为每一个客户端连接建立一个线程。

然后监听线程仍然负责监听来自客户端的请求。

如果这种应用程序采用单线程来处理,当监听线程接收到一个客户端请求后,开始读取客户端发来的数据,在读完数据后,read方法处于阻塞状态,也就是说,这个线程将无法再监听客户端请求了。

而要想在单线程中处理多个客户端请求,就必须使用非阻塞的Socket连接和异步I/O.但使用异步I/O方式比使用同步I/O更难以控制,也更容易出错。

因此,使用多线程和同步I/O可以更容易地处理类似于多请求的异步事件。

   4.使GUI更有效率

   使用单线程来处理GUI事件时,必须使用循环来对随时可能发生的GUI事件进行扫描,在循环内部除了扫描GUI事件外,还得来执行其他的程序代码。

如果这些代码太长,那么GUI事件就会被“冻结”,直到这些代码被执行完为止。

   在现代的GUI框架(如SWING、AWT和SWT)中都使用了一个单独的事件分派线程(eventdispatchthread,EDT)来对GUI事件进行扫描。

当我们按下一个按钮时,按钮的单击事件函数会在这个事件分派线程中被调用。

由于EDT的任务只是对GUI事件进行扫描,因此,这种方式对事件的反映是非常快的。

   5. 节约成本

   提高程序的执行效率一般有三种方法:

   

(1)增加计算机的CPU个数。

   

(2)为一个程序启动多个进程

   (3)在程序中使用多进程。

   第一种方法是最容易做到的,但同时也是最昂贵的。

这种方法不需要修改程序,从理论上说,任何程序都可以使用这种方法来提高执行效率。

第二种方法虽然不用购买新的硬件,但这种方式不容易共享数据,如果这个程序要完成的任务需要必须要共享数据的话,这种方式就不太方便,而且启动多个线程会消耗大量的系统资源。

第三种方法恰好弥补了第一种方法的缺点,而又继承了它们的优点。

也就是说,既不需要购买CPU,也不会因为启太多的线程而占用大量的系统资源(在默认情况下,一个线程所占的内存空间要远比一个进程所占的内存空间小得多),并且多线程可以模拟多块CPU的运行方式,因此,使用多线程是提高程序执行效率的最廉价的方式。

   三、Java的线程模型

   由于Java是纯面向对象语言,因此,Java的线程模型也是面向对象的。

Java通过Thread类将线程所必须的功能都封装了起来。

要想建立一个线程,必须要有一个线程执行函数,这个线程执行函数对应Thread类的run方法。

Thread类还有一个start方法,这个方法负责建立线程,相当于调用Windows的建立线程函数CreateThread.当调用start方法后,如果线程建立成功,并自动调用Thread类的run方法。

因此,任何继承Thread的Java类都可以通过Thread类的start方法来建立线程。

如果想运行自己的线程执行函数,那就要覆盖Thread类的run方法。

   在Java的线程模型中除了Thread类,还有一个标识某个Java类是否可作为线程类的接口Runnable,这个接口只有一个抽象方法run,也就是Java线程模型的线程执行函数。

因此,一个线程类的唯一标准就是这个类是否实现了Runnable接口的run方法,也就是说,拥有线程执行函数的类就是线程类。

   从上面可以看出,在Java中建立线程有两种方法,一种是继承Thread类,另一种是实现Runnable接口,并通过Thread和实现Runnable的类来建立线程,其实这两种方法从本质上说是一种方法,即都是通过Thread类来建立线程,并运行run方法的。

但它们的大区别是通过继承Thread类来建立线程,虽然在实现起来更容易,但由于Java不支持多继承,因此,这个线程类如果继承了Thread,就不能再继承其他的类了,因此,Java线程模型提供了通过实现Runnable接口的方法来建立线程,这样线程类可以在必要的时候继承和业务有关的类,而不是Thread类。

用Thread类创建线程

   在Java中创建线程有两种方法:

使用Thread类和使用Runnable接口。

在使用Runnable接口时需要建立一个Thread实例。

因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例。

Thread类的构造方法被重载了八次,构造方法如下:

public Thread( );

public Thread(Runnable target);

public Thread(String name);

public Thread(Runnable target, String name);

public Thread(ThreadGroup group, Runnable target);

public Thread(ThreadGroup group, String name);

public Thread(ThreadGroup group, Runnable target, String name);

public Thread(ThreadGroup group, Runnable target, String name, long stackSize);

   Runnabletarget

   实现了Runnable接口的类的实例。

要注意的是Thread类也实现了Runnable接口,因此,从Thread类继承的类的实例也可以作为target传入这个构造方法。

   Stringname

   线程的名子。

这个名子可以在建立Thread实例后通过Thread类的setName方法设置。

如果不设置线程的名子,线程就使用默认的线程名:

Thread-N,N是线程建立的顺序,是一个不重复的正整数。

   ThreadGroupgroup

   当前建立的线程所属的线程组。

如果不指定线程组,所有的线程都被加到一个默认的线程组中。

关于线程组的细节将在后面的章节详细讨论。

   longstackSize

   线程栈的大小,这个值一般是CPU页面的整数倍。

如x86的页面大小是4KB.在x86平台下,默认的线程栈大小是12KB.

   一个普通的Java类只要从Thread类继承,就可以成为一个线程类。

并可通过Thread类的start方法来执行线程代码。

虽然Thread类的子类可以直接实例化,但在子类中必须要覆盖Thread类的run方法才能真正运行线程的代码。

下面的代码给出了一个使用Thread类建立线程的例子:

  001  package mythread;

  002  

  003  public class Thread1 extends Thread

  004  {

  005      public void run()

  006      {

  007          System.out.println(this.getName());

  008      }

  009      public static void main(String[] args)

  010      {

  011          System.out.println(Thread.currentThread().getName());

  012          Thread1 thread1 = new Thread1();

  013          Thread1 thread2 = new Thread1 ();

  014          thread1.start();

  015          thread2.start();

  016      }

  017  }

   上面的代码建立了两个线程:

thread1和thread2.上述代码中的005至008行是Thread1类的run方法。

当在014和015行调用start方法时,系统会自动调用run方法。

在007行使用this.getName()输出了当前线程的名字,由于在建立线程时并未指定线程名,因此,所输出的线程名是系统的默认值,也就是Thread-n的形式。

在011行输出了主线程的线程名。

   上面代码的运行结果如下:

main

Thread-0

Thread-1

   从上面的输出结果可以看出,第一行输出的main是主线程的名子。

后面的Thread-1和Thread-2分别是thread1和thread2的输出结果。

   注意:

任何一个Java程序都必须有一个主线程。

一般这个主线程的名子为main.只有在程序中建立另外的线程,才能算是真正的多线程程序。

也就是说,多线程程序必须拥有一个以上的线程。

   Thread类有一个重载构造方法可以设置线程名。

除了使用构造方法在建立线程时设置线程名,还可以使用Thread类的setName方法修改线程名。

要想通过Thread类的构造方法来设置线程名,必须在Thread的子类中使用Thread类的publicThread(Stringname)构造方法,因此,必须在Thread的子类中也添加一个用于传入线程名的构造方法。

下面的代码给出了一个设置线程名的例子:

    package mythread;

    

    public class Thread2 extends Thread

    {

        private String who;

    

        public void run()

        {

            System.out.println(who + ":

" + this.getName());

        }

        public Thread2(String who)

        {

            super();

            this.who = who;

        }

        public Thread2(String who, String name)

        {

            super(name);

            this.who = who;

        }

        public static void main(String[] args)

       {

            Thread2 thread1 = new Thread2 ("thread1", "MyThread1");

           Thread2 thread2 = new Thread2 ("thread2");

           Thread2 thread3 = new Thread2 ("thread3");

            thread2.setName("MyThread2");

            thread1.start();

           thread2.start();

           thread3.start();

        }

 

   在类中有两个构造方法:

   第011行:

publicsample2_2(Stringwho)

   这个构造方法有一个参数:

who.这个参数用来标识当前建立的线程。

在这个构造方法中仍然调用Thread的默认构造方法publicThread()。

   第016行:

publicsample2_2(Stringwho,Stringname)

   这个构造方法中的who和第一个构造方法的who的含义一样,而name参数就是线程的名名。

在这个构造方法中调用了Thread类的publicThread(Stringname)构造方法,也就是第018行的super(name)。

   在main方法中建立了三个线程:

thread1、thread2和thread3.其中thread1通过

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工程科技 > 建筑土木

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1