目录

10、IO技术

10.1 基本概念和IO入门

10.1.1 数据源

10.1.2 流的概念

10.1.3 第一个简单的IO流程序即深入理解

10.1.4 Java中流的概念细分

10.1.5 Java中的IO流类的体系

10.1.6 四大IO抽象类

10.2 常用流详解

10.2.1 文件字节流

10.2.2 文件字符流

10.2.3 缓冲字节流

10.2.4 缓冲字符流

10.2.5 字节数组流

10.2.6 数据流

10.2.7 对象流

10.2.8 转换流

10.3 Java对象的序列化和反序列化

10.3.1 序列化涉及的类和接口

10.3.2 序列化/反序列化的步骤和实例

10.4 装饰器模式介绍

第十章 总结

11、多线程技术

11.1 基本概念

11.1.1 程序

11.1.2 进程

11.1.3 线程

11.1.4 线程和进程的区别

11.1.5 进程和程序的区别

11.2 Java中实现多线程

11.2.1 通过继承Thread类实现多线程

11.2.2 通过Runnable接口实现多线程

11.3 线程状态和生命周期

11.3.1 线程状态

11.3.2 终止线程的典型方式

11.3.3 暂停线程执行sleep/yield

11.3.4 线程的联合join()

11.4 线程的基本信息和优先级别

11.4.1 获取线程基本信息的方法

11.4.2 线程的优先级

11.5 线程同步

11.5.1 什么是线程同步

11.5.2 实现线程同步

11.5.3 死锁及解决方案

11.6 线程并发协作(生产者/消费者模式)

11.7 任务定时调度

第十一章 总结

12、网络编程

12.1 基本概念

12.1.1 计算机网络

12.1.2 网络通讯协议

12.1.3 数据封装和解封

12.1.4 IP地址和接口

12.1.5 URL资源定位符

12.1.6 Socket

12.2 TCP协议和UDP协议

12.2.1 区别和练习

12.2.2 TCP协议

12.2.3 UDP协议

12.3 Java网络编程

12.3.1 InetAddress

12.3.2 InetSocketAddress

12.3.3 URL类

12.3.4 基于TCP协议的Socket编程和通讯

12.3.5 UDP通讯的实现

第十二章 总结


10、IO技术

10.1 基本概念和IO入门

对于任何程序设计语言而言,输入输出(Input/Output)系统都是非常核心的功能。程序运行需要数据,数据的获取往往需要跟外部系统进行通信,外部系统可能是文件、数据库、其他程序、网络、IO设备等等。外部系统比较复杂多变,那么我们有必要通过某种手段进行抽象、屏蔽外部的差异,从而实现更加便捷的编程。

输入(Input)指的是:可以让程序从外部系统获得数据(核心含义是“读”,读取外部数据)。常见的应用:

Ø 读取硬盘上的文件内容到程序。例如:播放器打开一个视频文件、word打开一个doc文件。

Ø 读取网络上某个位置内容到程序。例如:浏览器中输入网址后,打开该网址对应的网页内容;下载网络上某个网址的文件。

Ø 读取数据库系统的数据到程序。

Ø 读取某些硬件系统数据到程序。例如:车载电脑读取雷达扫描信息到程序;温控系统等。

输出(Output)指的是:程序输出数据给外部系统从而可以操作外部系统(核心含义是“写”,将数据写出到外部系统)。常见的应用有:

Ø 将数据写到硬盘中。例如:我们编辑完一个word文档后,将内容写到硬盘上进行保存。

Ø 将数据写到数据库系统中。例如:我们注册一个网站会员,实际就是后台程序向数据库中写入一条记录。

Ø 将数据写到某些硬件系统中。例如:导弹系统导航程序将新的路径输出到飞控子系统,飞控子系统根据数据修正飞行路径。

java.io包为我们提供了相关的API,实现了对所有外部系统的输入输出操作,这就是我们这章所要学习的技术。

10.1.1 数据源

数据源data source,提供数据的原始媒介。常见的数据源有:数据库、文件、其他程序、内存、网络连接、IO设备。如图10-1所示。

数据源分为:源设备、目标设备。

1. 源设备:为程序提供数据,一般对应输入流。

2. 目标设备:程序数据的目的地,一般对应输出流。

10.1.2 流的概念

流是一个抽象、动态的概念,是一连串连续动态的数据集合。

对于输入流而言,数据源就像水箱,流(stream)就像水管中流动着的水流,程序就是我们最终的用户。我们通过流(A Stream)将数据源(Source)中的数据(information)输送到程序(Program)中。

对于输出流而言,目标数据源就是目的地(dest),我们通过流(A Stream)将程序(Program)中的数据(information)输送到目的数据源(dest)中。

图  流与源数据源和目标数据源之间的关系

菜鸟雷区

输入/输出流的划分是相对程序而言的,并不是相对数据源。

10.1.3 第一个简单的IO流程序即深入理解

当程序需要读取数据源的数据时,就会通过IO流对象开启一个通向数据源的流,通过这个IO流对象的相关方法可以顺序读取数据源中的数据。

【示例 】使用流读取文件内容(不规范的写法,仅用于测试)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

import java.io.*;

public class TestIO1 {

    public static void main(String[] args) {

        try {

            //创建输入流

            FileInputStream fis = new FileInputStream("d:/a.txt"); // 文件内容是:abc

            //一个字节一个字节的读取数据

            int s1 = fis.read(); // 打印输入字符a对应的ascii码值97

            int s2 = fis.read(); // 打印输入字符b对应的ascii码值98

            int s3 = fis.read(); // 打印输入字符c 对应的ascii码值99

            int s4 = fis.read(); // 由于文件内容已经读取完毕,返回-1

            System.out.println(s1);

            System.out.println(s2);

            System.out.println(s3);

            System.out.println(s4);

            // 流对象使用完,必须关闭!不然,总占用系统资源,最终会造成系统崩溃!

            fis.close();

        catch (Exception e) {

            e.printStackTrace();

        }

    }

}

执行结果如图 所示:

通过示例 我们要注意以下几点:

1. 在示例中我们读取的文件内容是已知的,因此可以使用固定次数的“int s= fis.read();”语句读取内容,但是在实际开发中通常我们根本不知道文件的内容,因此我们在读取的时候需要配合while循环使用。

2. 为了保证出现异常后流的正常关闭,通常要将流的关闭语句要放到finally语句块中,并且要判断流是不是null。

IO流的经典写法如示例 所示。

【示例 】使用流读取文件内容(经典代码,一定要掌握)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

import java.io.*;

public class TestIO2 {

    public static void main(String[] args) {

        FileInputStream fis = null;

        try {

            fis = new FileInputStream("d:/a.txt"); // 内容是:abc

            StringBuilder sb = new StringBuilder();

            int temp = 0;

            //当temp等于-1时,表示已经到了文件结尾,停止读取

            while ((temp = fis.read()) != -1) {

                sb.append((char) temp);

            }

            System.out.println(sb);

        catch (Exception e) {

            e.printStackTrace();

        finally {

            try {

                //这种写法,保证了即使遇到异常情况,也会关闭流对象。

                if (fis != null) {

                    fis.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

执行结果如图 所示:

10.1.4 Java中流的概念细分

按流的方向分类:

1. 输入流:数据流向是数据源到程序(以InputStream、Reader结尾的流)。

2. 输出流:数据流向是程序到目的地(以OutPutStream、Writer结尾的流)。

图  输入/输出流示意图

按处理的数据单元分类:

1. 字节流:以字节为单位获取数据,命名上以Stream结尾的流一般是字节流,如FileInputStream、FileOutputStream。

2. 字符流:以字符为单位获取数据,命名上以Reader/Writer结尾的流一般是字符流,如FileReader、FileWriter。

按处理对象不同分类:

1. 节点流:可以直接从数据源或目的地读写数据,如FileInputStream、FileReader、DataInputStream等。

2.  处理流:不直接连接到数据源或目的地,是”处理流的流”。通过对其他流的处理提高程序的性能,如BufferedInputStream、BufferedReader等。处理流也叫包装流。

节点流处于IO操作的第一线,所有操作必须通过它们进行;处理流可以对节点流进行包装,提高性能或提高程序的灵活性。

10.1.5 Java中的IO流类的体系

Java为我们提供了多种多样的IO流,我们可以根据不同的功能及性能要求挑选合适的IO流,如图 所示,为Java中IO流类的体系。

注:这里只列出常用的类,详情可以参考JDK API文档。粗体标注为常用!

从上图发现,很多流都是成对出现的,比如:FileInputStream/FileOutputStream,显然是对文件做输入和输出操作的。我们下面简单做个总结:

1. InputStream/OutputStream

字节流的抽象类。

2. Reader/Writer

字符流的抽象类。

3. FileInputStream/FileOutputStream

节点流:以字节为单位直接操作“文件”。

4. ByteArrayInputStream/ByteArrayOutputStream

节点流:以字节为单位直接操作“字节数组对象”。

5. ObjectInputStream/ObjectOutputStream

处理流:以字节为单位直接操作“对象”。

6. DataInputStream/DataOutputStream

处理流:以字节为单位直接操作“基本数据类型与字符串类型”。

7. FileReader/FileWriter

节点流:以字符为单位直接操作“文本文件”(注意:只能读写文本文件)。

8. BufferedReader/BufferedWriter

处理流:将Reader/Writer对象进行包装,增加缓存功能,提高读写效率。

9. BufferedInputStream/BufferedOutputStream

处理流:将InputStream/OutputStream对象进行包装,增加缓存功能,提高 读写效率。

10. InputStreamReader/OutputStreamWriter

处理流:将字节流对象转化成字符流对象。

11. PrintStream

处理流:将OutputStream进行包装,可以方便地输出字符,更加灵活。

10.1.6 四大IO抽象类

InputStream/OutputStream和Reader/writer类是所有IO流类的抽象父类,我们有必要简单了解一下这个四个抽象类的作用。然后,通过它们具体的子类熟悉相关的用法。

·InputStream

此抽象类是表示字节输入流的所有类的父类。InputSteam是一个抽象类,它不可以实例化。 数据的读取需要由它的子类来实现。根据节点的不同,它派生了不同的节点流子类 。

继承自InputSteam的流都是用于向程序中输入数据,且数据的单位为字节(8 bit)。

        常用方法:

int read():读取一个字节的数据,并将字节的值作为int类型返回(0-255之间的一个值)。如果未读出字节则返回-1(返回值为-1表示读取结束)。

void close():关闭输入流对象,释放相关系统资源。

· OutputStream

此抽象类是表示字节输出流的所有类的父类。输出流接收输出字节并将这些字节发送到某个目的地。

常用方法:

void write(int n):向目的地中写入一个字节。

void close():关闭输出流对象,释放相关系统资源。

· Reader

Reader用于读取的字符流抽象类,数据单位为字符。

int read(): 读取一个字符的数据,并将字符的值作为int类型返回(0-65535之间的一个值,即Unicode值)。如果未读出字符则返回-1(返回值为-1表示读取结束)。

void close() : 关闭流对象,释放相关系统资源。

· Writer

Writer用于写入的字符流抽象类,数据单位为字符。

void write(int n): 向输出流中写入一个字符。

void close() : 关闭输出流对象,释放相关系统资源。

10.2 常用流详解

10.2.1 文件字节流

FileInputStream通过字节的方式读取文件,适合读取所有类型的文件(图像、视频、文本文件等)。Java也提供了FileReader专门读取文本文件。

FileOutputStream 通过字节的方式写数据到文件中,适合所有类型的文件。Java也提供了FileWriter专门写入文本文件。

【示例 】将字符串/字节数组的内容写入到文件中

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

import java.io.FileOutputStream;

import java.io.IOException;

public class TestFileOutputStream {

    public static void main(String[] args) {

        FileOutputStream fos = null;

        String string = "北京尚学堂欢迎您!";

        try {

            // true表示内容会追加到文件末尾;false表示重写整个文件内容。

            fos = new FileOutputStream("d:/a.txt"true);

            //该方法是直接将一个字节数组写入文件中; 而write(int n)是写入一个字节

            fos.write(string.getBytes());

        catch (Exception e) {

            e.printStackTrace();

        finally {

            try {

                if (fos != null) {

                    fos.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

在示例 中,用到一个write方法:void write(byte[ ] b),该方法不再一个字节一个字节地写入,而是直接写入一个字节数组;另外其还有一个重载的方法:void write(byte[ ] b, int off, int length),这个方法也是写入一个字节数组,但是我们程序员可以指定从字节数组的哪个位置开始写入,写入的长度是多少。

执行结果如图 所示:

图 示例 运行后a.txt文件内容

现在我们已经学习了使用文件字节流分别实现文件的读取与写入操作,接下来我们将两种功能综合使用就可以轻松实现文件的复制了。

【示例 】利用文件流实现文件的复制

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

public class TestFileCopy {

    public static void main(String[] args) {

        //将a.txt内容拷贝到b.txt

        copyFile("d:/a.txt""d:/b.txt"); 

    }

    /**

     * 将src文件的内容拷贝到dec文件

     * @param src 源文件

     * @param dec 目标文件

     */

    static void copyFile(String src, String dec) {

        FileInputStream fis = null;

        FileOutputStream fos = null;

        //为了提高效率,设置缓存数组!(读取的字节数据会暂存放到该字节数组中)

        byte[] buffer = new byte[1024];

        int temp = 0;

        try {

            fis = new FileInputStream(src);

            fos = new FileOutputStream(dec);

            //边读边写

            //temp指的是本次读取的真实长度,temp等于-1时表示读取结束

            while ((temp = fis.read(buffer)) != -1) {

                /*将缓存数组中的数据写入文件中,注意:写入的是读取的真实长度;

                 *如果使用fos.write(buffer)方法,那么写入的长度将会是1024,即缓存

                 *数组的长度*/

                fos.write(buffer, 0, temp);

            }

        catch (Exception e) {

            e.printStackTrace();

        finally {

            //两个流需要分别关闭

            try {

                if (fos != null) {

                    fos.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (fis != null) {

                    fis.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

执行结果如图 和图所示:

图  示例 运行后d盘部分目录

图  b.txt文件的内容

注意

在使用文件字节流时,我们需要注意以下两点:

1. 为了减少对硬盘的读写次数,提高效率,通常设置缓存数组。相应地,读取时使用的方法为:read(byte[] b);写入时的方法为:write(byte[ ] b, int off, int length)。

2. 程序中如果遇到多个流,每个流都要单独关闭,防止其中一个流出现异常后导致其他流无法关闭的情况。

10.2.2 文件字符流

前面介绍的文件字节流可以处理所有的文件,但是字节流不能很好的处理Unicode字符,经常会出现“乱码”现象。所以,我们处理文本文件,一般可以使用文件字符流,它以字符为单位进行操作。

【示例 】使用FileReader与FileWriter实现文本文件的复制

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

import java.io.FileNotFoundException;

import java.io.FileReader;

import java.io.FileWriter;

import java.io.IOException;

public class TestFileCopy2 {

    public static void main(String[] args) {

        // 写法和使用Stream基本一样。只不过,读取时是读取的字符。

        FileReader fr = null;

        FileWriter fw = null;

        int len = 0;

        try {

            fr = new FileReader("d:/a.txt");

            fw = new FileWriter("d:/d.txt");

            //为了提高效率,创建缓冲用的字符数组

            char[] buffer = new char[1024];

            //边读边写

            while ((len = fr.read(buffer)) != -1) {

                fw.write(buffer, 0, len);

            }

        catch (FileNotFoundException e) {

            e.printStackTrace();

        catch (IOException e) {

            e.printStackTrace();

        finally {

            try {

                if (fw != null) {

                    fw.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (fr != null) {

                    fr.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

执行结果如图 和图 所示:

图  示例 运行后d盘部分目录

10.2.3 缓冲字节流

Java缓冲流本身并不具有IO流的读取与写入功能,只是在别的流(节点流或其他处理流)上加上缓冲功能提高效率,就像是把别的流包装起来一样,因此缓冲流是一种处理流(包装流)。

当对文件或者其他数据源进行频繁的读写操作时,效率比较低,这时如果使用缓冲流就能够更高效的读写信息。因为缓冲流是先将数据缓存起来,然后当缓存区存满后或者手动刷新时再一次性的读取到程序或写入目的地。

因此,缓冲流还是很重要的,我们在IO操作时记得加上缓冲流来提升性能。

BufferedInputStream和BufferedOutputStream这两个流是缓冲字节流,通过内部缓存数组来提高操作流的效率。

下面我们通过两种方式(普通文件字节流与缓冲文件字节流)实现一个视频文件的复制,来体会一下缓冲流的好处。

【示例 】使用缓冲流实现文件的高效率复制

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

import java.io.BufferedInputStream;

import java.io.BufferedOutputStream;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

public class TestBufferedFileCopy1 {

    public static void main(String[] args) {

        // 使用缓冲字节流实现复制

        long time1 = System.currentTimeMillis();

        copyFile1("D:/电影/华语/大陆/尚学堂传奇.mp4", "D:/电影/华语/大陆/尚学堂越

                 "+"来越传奇.mp4");

        long time2 = System.currentTimeMillis();

        System.out.println("缓冲字节流花费的时间为:" + (time2 - time1));

        // 使用普通字节流实现复制

        long time3 = System.currentTimeMillis();

        copyFile2("D:/电影/华语/大陆/尚学堂传奇.mp4", "D:/电影/华语/大陆/尚学堂越

                 "+"来越传奇2.mp4");

        long time4 = System.currentTimeMillis();

        System.out.println("普通字节流花费的时间为:" + (time4 - time3));

    }

    /**缓冲字节流实现的文件复制的方法*/

    static void copyFile1(String src, String dec) {

        FileInputStream fis = null;

        BufferedInputStream bis = null;

        FileOutputStream fos = null;

        BufferedOutputStream bos = null;

        int temp = 0;

        try {

            fis = new FileInputStream(src);

            fos = new FileOutputStream(dec);

            //使用缓冲字节流包装文件字节流,增加缓冲功能,提高效率

            //缓存区的大小(缓存数组的长度)默认是8192,也可以自己指定大小

            bis = new BufferedInputStream(fis);

            bos = new BufferedOutputStream(fos);

            while ((temp = bis.read()) != -1) {

                bos.write(temp);

            }

        catch (Exception e) {

            e.printStackTrace();

        finally {

            //注意:增加处理流后,注意流的关闭顺序!“后开的先关闭!”

            try {

                if (bos != null) {

                    bos.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (bis != null) {

                    bis.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (fos != null) {

                    fos.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (fis != null) {

                    fis.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

    /**普通节流实现的文件复制的方法*/

    static void copyFile2(String src, String dec) {

        FileInputStream fis = null;

        FileOutputStream fos = null;

        int temp = 0;

        try {

            fis = new FileInputStream(src);

            fos = new FileOutputStream(dec);

            while ((temp = fis.read()) != -1) {

                fos.write(temp);

            }

        catch (Exception e) {

            e.printStackTrace();

        finally {

            try {

                if (fos != null) {

                    fos.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (fis != null) {

                    fis.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

执行结果如图 所示:

图  示例 运行效果图

注意

1. 在关闭流时,应该先关闭最外层的包装流,即“后开的先关闭”。

2. 缓存区的大小默认是8192字节,也可以使用其它的构造方法自己指定大小。

10.2.4 缓冲字符流

BufferedReader/BufferedWriter增加了缓存机制,大大提高了读写文本文件的效率,同时,提供了更方便的按行读取的方法:readLine(); 处理文本时,我们一般可以使用缓冲字符流。

【示例 】使用BufferedReader与BufferedWriter实现文本文件的复制

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.FileNotFoundException;

import java.io.FileReader;

import java.io.FileWriter;

import java.io.IOException;

public class TestBufferedFileCopy2 {

    public static void main(String[] args) {

        // 注:处理文本文件时,实际开发中可以用如下写法,简单高效!!

        FileReader fr = null;

        FileWriter fw = null;

        BufferedReader br = null;

        BufferedWriter bw = null;

        String tempString = "";

        try {

            fr = new FileReader("d:/a.txt");

            fw = new FileWriter("d:/d.txt");

            //使用缓冲字符流进行包装

            br = new BufferedReader(fr);

            bw = new BufferedWriter(fw);

            //BufferedReader提供了更方便的readLine()方法,直接按行读取文本

            //br.readLine()方法的返回值是一个字符串对象,即文本中的一行内容

            while ((tempString = br.readLine()) != null) {

                //将读取的一行字符串写入文件中

                bw.write(tempString);

                //下次写入之前先换行,否则会在上一行后边继续追加,而不是另起一行

                bw.newLine();

            }

        catch (FileNotFoundException e) {

            e.printStackTrace();

        catch (IOException e) {

            e.printStackTrace();

        finally {

            try {

                if (bw != null) {

                    bw.close();

                }

            catch (IOException e1) {

                e1.printStackTrace();

            }

            try {

                if (br != null) {

                    br.close();

                }

            catch (IOException e1) {

                e1.printStackTrace();

            }

            try {

                if (fw != null) {

                    fw.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (fr != null) {

                    fr.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

注意

1. readLine()方法是BufferedReader特有的方法,可以对文本文件进行更加方便的读取操作。

2. 写入一行后要记得使用newLine()方法换行。

10.2.5 字节数组流

ByteArrayInputStream和ByteArrayOutputStream经常用在需要流和数组之间转化的情况!

说白了,FileInputStream是把文件当做数据源。ByteArrayInputStream则是把内存中的”某个字节数组对象”当做数据源。

【示例 】简单测试ByteArrayInputStream 的使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

import java.io.ByteArrayInputStream;

import java.io.IOException;

public class TestByteArray {

    public static void main(String[] args) {

        //将字符串转变成字节数组

        byte[] b = "abcdefg".getBytes();

        test(b);

    }

    public static void test(byte[] b) {

        ByteArrayInputStream bais = null;

        StringBuilder sb = new StringBuilder();

        int temp = 0;

        //用于保存读取的字节数

        int num = 0

        try {

            //该构造方法的参数是一个字节数组,这个字节数组就是数据源

            bais = new ByteArrayInputStream(b);

            while ((temp = bais.read()) != -1) {

                sb.append((char) temp);

                num++;

            }

            System.out.println(sb);

            System.out.println("读取的字节数:" + num);

        finally {

            try {

                if (bais != null) {

                    bais.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

执行结果如图 所示:

10.2.6 数据流

数据流将“基本数据类型与字符串类型”作为数据源,从而允许程序以与机器无关的方式从底层输入输出流中操作Java基本数据类型与字符串类型。

DataInputStream和DataOutputStream提供了可以存取与机器无关的所有Java基础类型数据(如:int、double、String等)的方法。

DataInputStream和DataOutputStream是处理流,可以对其他节点流或处理流进行包装,增加一些更灵活、更高效的功能。

【示例 】DataInputStream和DataOutputStream的使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

import java.io.BufferedInputStream;

import java.io.BufferedOutputStream;

import java.io.DataInputStream;

import java.io.DataOutputStream;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

public class TestDataStream {

    public static void main(String[] args) {

        DataOutputStream dos = null;

        DataInputStream dis = null;

        FileOutputStream fos = null;

        FileInputStream  fis = null;

        try {

            fos = new FileOutputStream("D:/data.txt");

            fis = new FileInputStream("D:/data.txt");

            //使用数据流对缓冲流进行包装,新增缓冲功能

            dos = new DataOutputStream(new BufferedOutputStream(fos));

            dis = new DataInputStream(new BufferedInputStream(fis));

            //将如下数据写入到文件中

            dos.writeChar('a');

            dos.writeInt(10);

            dos.writeDouble(Math.random());

            dos.writeBoolean(true);

            dos.writeUTF("北京尚学堂");

            //手动刷新缓冲区:将流中数据写入到文件中

            dos.flush();

            //直接读取数据:读取的顺序要与写入的顺序一致,否则不能正确读取数据。

            System.out.println("char: " + dis.readChar());

            System.out.println("int: " + dis.readInt());

            System.out.println("double: " + dis.readDouble());

            System.out.println("boolean: " + dis.readBoolean());

            System.out.println("String: " + dis.readUTF());

        catch (IOException e) {

            e.printStackTrace();

        finally {

            try {

                if(dos!=null){

                    dos.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if(dis!=null){

                    dis.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if(fos!=null){

                    fos.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if(fis!=null){

                    fis.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

执行结果如 图所示:

10.2.7 对象流

我们前边学到的数据流只能实现对基本数据类型和字符串类型的读写,并不能读取对象(字符串除外),如果要对某个对象进行读写操作,我们需要学习一对新的处理流:ObjectInputStream/ObjectOutputStream。

ObjectInputStream/ObjectOutputStream是以“对象”为数据源,但是必须将传输的对象进行序列化与反序列化操作。

序列化与反序列化的具体内容,请见<10.3 Java对象的序列化和反序列化>。示例10-11仅演示对象流的简单应用。

【示例 】ObjectInputStream/ObjectOutputStream的使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

import java.io.BufferedInputStream;

import java.io.BufferedOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.InputStream;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.io.OutputStream;

import java.util.Date;

public class TestObjectStream {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        write();

        read();

    }

    /**使用对象输出流将数据写入文件*/

    public static void write(){

        // 创建Object输出流,并包装缓冲流,增加缓冲功能

        OutputStream os = null;

        BufferedOutputStream bos = null;

        ObjectOutputStream oos = null;

        try {

            os = new FileOutputStream(new File("d:/bjsxt.txt"));

            bos = new BufferedOutputStream(os);

            oos = new ObjectOutputStream(bos);

            // 使用Object输出流

            //对象流也可以对基本数据类型进行读写操作

            oos.writeInt(12);

            oos.writeDouble(3.14);

            oos.writeChar('A');

            oos.writeBoolean(true);

            oos.writeUTF("北京尚学堂");

            //对象流能够对对象数据类型进行读写操作

            //Date是系统提供的类,已经实现了序列化接口

            //如果是自定义类,则需要自己实现序列化接口

            oos.writeObject(new Date());

        catch (IOException e) {

            e.printStackTrace();

        finally {

            //关闭输出流

            if(oos != null){

                try {

                    oos.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if(bos != null){

                try {

                    bos.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if(os != null){

                try {

                    os.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

    }

    /**使用对象输入流将数据读入程序*/

    public static void read() {

        // 创建Object输入流

        InputStream is = null;

        BufferedInputStream bis = null;

        ObjectInputStream ois = null;

        try {

            is = new FileInputStream(new File("d:/bjsxt.txt"));

            bis = new BufferedInputStream(is);

            ois = new ObjectInputStream(bis);

            // 使用Object输入流按照写入顺序读取

            System.out.println(ois.readInt());

            System.out.println(ois.readDouble());

            System.out.println(ois.readChar());

            System.out.println(ois.readBoolean());

            System.out.println(ois.readUTF());

            System.out.println(ois.readObject().toString());

        catch (ClassNotFoundException e) {

            e.printStackTrace();

        catch (IOException e) {

            e.printStackTrace();

        finally {

            // 关闭Object输入流

            if(ois != null){

                try {

                    ois.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if(bis != null){

                try {

                    bis.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if(is != null){

                try {

                    is.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

    }

}

执行结果如图 所示:

图  示例 运行效果图

注意

1. 对象流不仅可以读写对象,还可以读写基本数据类型。

2. 使用对象流读写对象时,该对象必须序列化与反序列化。

3. 系统提供的类(如Date等)已经实现了序列化接口,自定义类必须手动实现序列化接口。

10.2.8 转换流

InputStreamReader/OutputStreamWriter用来实现将字节流转化成字符流。比如,如下场景:

System.in是字节流对象,代表键盘的输入,如果我们想按行接收用户的输入时,就必须用到缓冲字符流BufferedReader特有的方法readLine(),但是经过观察会发现在创建BufferedReader的构造方法的参数必须是一个Reader对象,这时候我们的转换流InputStreamReader就派上用场了。

而System.out也是字节流对象,代表输出到显示器,按行读取用户的输入后,并且要将读取的一行字符串直接显示到控制台,就需要用到字符流的write(String str)方法,所以我们要使用OutputStreamWriter将字节流转化为字符流。

【示例 】使用InputStreamReader接收用户的输入,并输出到控制台

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStreamWriter;

public class TestConvertStream {

    public static void main(String[] args) {

        // 创建字符输入和输出流:使用转换流将字节流转换成字符流

        BufferedReader br = null;

        BufferedWriter bw = null;

        try {

            br = new BufferedReader(new InputStreamReader(System.in));

            bw = new BufferedWriter(new OutputStreamWriter(System.out));

            // 使用字符输入和输出流

            String str = br.readLine();

            // 一直读取,直到用户输入了exit为止

            while (!"exit".equals(str)) {

                // 写到控制台

                bw.write(str);

                bw.newLine();// 写一行后换行

                bw.flush();// 手动刷新

                // 再读一行

                str = br.readLine();

            }

        catch (IOException e) {

            e.printStackTrace();

        finally {

            // 关闭字符输入和输出流

            if (br != null) {

                try {

                    br.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if (bw != null) {

                try {

                    bw.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

    }

}

执行结果如图 所示:

10.3 Java对象的序列化和反序列化

当两个进程远程通信时,彼此可以发送各种类型的数据。 无论是何种类型的数据,都会以二进制序列的形式在网络上传送。比如,我们可以通过http协议发送字符串信息;我们也可以在网络上直接发送Java对象。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象才能正常读取。

把Java对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为Java对象的过程称为对象的反序列化。

对象序列化的作用有如下两种:

1. 持久化: 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中,比如:休眠的实现。以后服务器session管理,hibernate将对象持久化实现。

2. 网络通信:在网络上传送对象的字节序列。比如:服务器之间的数据通信、对象传递。

10.3.1 序列化涉及的类和接口

ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

只有实现了Serializable接口的类的对象才能被序列化。 Serializable接口是一个空接口,只起到标记作用。

10.3.2 序列化/反序列化的步骤和实例

【示例 】将Person类的实例进行序列化和反序列化

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.io.Serializable;

//Person类实现Serializable接口后,Person对象才能被序列化

class Person implements Serializable {

    // 添加序列化ID,它决定着是否能够成功反序列化!

    private static final long serialVersionUID = 1L;

    int age;

    boolean isMan;

    String name;

    public Person(int age, boolean isMan, String name) {

        super();

        this.age = age;

        this.isMan = isMan;

        this.name = name;

    }

    @Override

    public String toString() {

        return "Person [age=" + age + ", isMan=" + isMan + ", name=" + name + "]";

    }

}

public class TestSerializable {

    public static void main(String[] args) {

        FileOutputStream fos = null;

        ObjectOutputStream oos = null;

        ObjectInputStream ois = null;

        FileInputStream fis = null;

        try {

            // 通过ObjectOutputStream将Person对象的数据写入到文件中,即序列化。

            Person person = new Person(18true"高淇");

            // 序列化

            fos = new FileOutputStream("d:/c.txt");

            oos = new ObjectOutputStream(fos);

            oos.writeObject(person);

            oos.flush();

            // 反序列化

            fis = new FileInputStream("d:/c.txt");

            // 通过ObjectInputStream将文件中二进制数据反序列化成Person对象:

            ois = new ObjectInputStream(fis);

            Person p = (Person) ois.readObject();

            System.out.println(p);

        catch (ClassNotFoundException e) {

            e.printStackTrace();

        catch (IOException e) {

            e.printStackTrace();

        finally {

            if (oos != null) {

                try {

                    oos.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if (fos != null) {

                try {

                    fos.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if (ois != null) {

                try {

                    ois.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if (fis != null) {

                try {

                    fis.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

    }

}

执行结果如图 所示:

图 示例 运行效果图

注意

1. static属性不参与序列化。

2. 对象中的某些属性如果不想被序列化,不能使用static,而是使用transient修饰。

3. 为了防止读和写的序列化ID不一致,一般指定一个固定的序列化ID。

10.4 装饰器模式介绍

装饰器模式是GOF23种设计模式中较为常用的一种模式。它可以实现对原有类的包装和装饰,使新的类具有更强的功能。

我这里有智能手机iphone, 我们可以通过加装投影组件,实现原有手机功能的扩展。这就是一种“装饰器模式”。 我们在未来给普通人加装“外骨骼”装饰,让普通人具有力扛千斤的能力,也是一种“装饰器模式”。

图  手机经过投影套件“装饰后”,成为功能更强的“投影手机”

【示 例】装饰器模式演示

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

class Iphone {

    private String name;

    public Iphone(String name) {

        this.name = name;

    }

    public void show() {

        System.out.println("我是" + name + ",可以在屏幕上显示");

    }

}

class TouyingPhone {

    public Iphone phone;

    public TouyingPhone(Iphone p) {

        this.phone = p;

    }

    // 功能更强的方法

    public void show() {

        phone.show();

        System.out.println("还可以投影,在墙壁上显示");

    }

}

public class TestDecoration {

    public static void main(String[] args) {

        Iphone phone = new Iphone("iphone30");

        phone.show();

        System.out.println("===============装饰后");

        TouyingPhone typhone = new TouyingPhone(phone);

        typhone.show();

    }

}

执行结果如图 所示:

IO流体系中的装饰器模式

IO流体系中大量使用了装饰器模式,让流具有更强的功能、更强的灵活性。比如:

1

2

FileInputStream fis = new FileInputStream(src);

BufferedInputStream bis = new BufferedInputStream(fis);

显然BufferedInputStream装饰了原有的FileInputStream,让普通的FileInputStream也具备了缓存功能,提高了效率。 大家举一反三,可以翻看本章代码,看看还有哪些地方使用了装饰器模式。

第十章 总结

1. 按流的方向分类:

输入流:数据源到程序(InputStream、Reader读进来)。

输出流:程序到目的地(OutPutStream、Writer写出去)。

2. 按流的处理数据单元分类:

字节流:按照字节读取数据(InputStream、OutputStream)。

字符流:按照字符读取数据(Reader、Writer)。

3. 按流的功能分类:

节点流:可以直接从数据源或目的地读写数据。

处理流:不直接连接到数据源或目的地,是处理流的流。通过对其他流的处理提高程序的性能。

4. IO的四个基本抽象类:InputStream、OutputStream、Reader、Writer

5. InputStream的实现类:

FileInputStream

ByteArrayInutStream

BufferedInputStream

DataInputStream

ObjectInputStream

6. OutputStream的实现类:

FileOutputStream

ByteArrayOutputStream

BufferedOutputStream

DataOutputStream

ObjectOutputStream

PrintStream

7. Reader的实现类

FileReader

BufferedReader

InputStreamReader

8. Writer的实现类

FileWriter

BufferedWriter

OutputStreamWriter

9. 把Java对象转换为字节序列的过程称为对象的序列化。

10. 把字节序列恢复为Java对象的过程称为对象的反序列化。

11、多线程技术

多线程是Java语言的重要特性,大量应用于网络编程、服务器端程序的开发,最常见的UI界面底层原理、操作系统底层原理都大量使用了多线程。

我们可以流畅的点击软件或者游戏中的各种按钮,其实,底层就是多线程的应用。UI界面的主线程绘制界面,如果有一个耗时的操作发生则启动新的线程,完全不影响主线程的工作。当这个线程工作完毕后,再更新到主界面上。

我们可以上百人、上千人、上万人同时访问某个网站,其实,也是基于网站服务器的多线程原理。如果没有多线程,服务器处理速度会极大降低。

多线程应用于计算机的各个方面,但是对于初学者,我们只需掌握基本的概念即可。在入门阶段,暂时没有必要钻研过深。

11.1 基本概念

11.1.1 程序

“程序(Program)”是一个静态的概念,一般对应于操作系统中的一个可执行文件,比如:我们要启动酷狗听音乐,则对应酷狗的可执行程序。当我们双击酷狗,则加载程序到内存中,开始执行该程序,于是产生了“进程”。

11.1.2 进程

执行中的程序叫做进程(Process),是一个动态的概念。现代的操作系统都可以同时启动多个进程。比如:我们在用酷狗听音乐,也可以使用eclipse写代码,也可以同时用浏览器查看网页。进程具有如下特点:

1. 进程是程序的一次动态执行过程, 占用特定的地址空间。

2. 每个进程由3部分组成:cpu、data、code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西,这样的缺点是:浪费内存,cpu的负担较重。

3. 多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占CPU的使用权。

4. 进程的查看

Windows系统: Ctrl+Alt+Del,启动任务管理器即可查看所有进程。

Unix系统: ps or top。

11.1.3 线程

一个进程可以产生多个线程。同多个进程可以共享操作系统的某些资源一样,同一进程的多个线程也可以共享此进程的某些资源(比如:代码、数据),所以线程又被称为轻量级进程(lightweight process)。

1. 一个进程内部的一个执行单元,它是程序中的一个单一的顺序控制流程。

2. 一个进程可拥有多个并行的(concurrent)线程。

3. 一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,而且它们从同一堆中分配对象并进行通信、数据交换和同步操作。

4. 由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。

5. 线程的启动、中断、消亡,消耗的资源非常少。

11.1.4 线程和进程的区别

1. 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。

2. 线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。

3. 线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位。

4. 多进程: 在操作系统中能同时运行多个任务(程序)。

5. 多线程: 在同一应用程序中有多个顺序流同时执行。

6. 线程是进程的一部分,所以线程有的时候被称为轻量级进程。

7. 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。

8.  系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。

11.1.5 进程和程序的区别

程序是一组指令的集合,它是静态的实体,没有执行的含义。而进程是一个动态的实体,有自己的生命周期。一般说来,一个进程肯定与一个程序相对应,并且只有一个,但是一个程序可以有多个进程,或者一个进程都没有。除此之外,进程还有并发性和交往性。简单地说,进程是程序的一部分,程序运行的时候会产生进程。

11.2 Java中实现多线程

在Java中使用多线程非常简单,我们先学习如何创建和使用线程,然后再结合案例深入剖析线程的特性。

11.2.1 通过继承Thread类实现多线程

继承Thread类实现多线程的步骤:

1. 在Java中负责实现线程功能的类是java.lang.Thread 类。

2. 可以通过创建 Thread的实例来创建新的线程。

3. 每个线程都是通过某个特定的Thread对象所对应的方法run( )来完成其操作的,方法run( )称为线程体。

4. 通过调用Thread类的start()方法来启动一个线程。

【示例11-1】通过继承Thread类实现多线程

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class TestThread extends Thread {//自定义类继承Thread类

    //run()方法里是线程体

    public void run() {

        for (int i = 0; i < 10; i++) {

            System.out.println(this.getName() + ":" + i);//getName()方法是返回线程名称

        }

    }

    public static void main(String[] args) {

        TestThread thread1 = new TestThread();//创建线程对象

        thread1.start();//启动线程

        TestThread thread2 = new TestThread();

        thread2.start();

    }

}

执行结果如图11-3所示:

图11-3 示例11-1运行效果图

此种方式的缺点:如果我们的类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类。

11.2.2 通过Runnable接口实现多线程

在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了11.2.1节中实现线程类的缺点,即在实现Runnable接口的同时还可以继承某个类。所以实现Runnable接口的方式要通用一些。

【示例11-2】通过Runnable接口实现多线程

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class TestThread2 implements Runnable {//自定义类实现Runnable接口;

    //run()方法里是线程体;

    public void run() {

        for (int i = 0; i < 10; i++) {

            System.out.println(Thread.currentThread().getName() + ":" + i);

        }

    }

    public static void main(String[] args) {

        //创建线程对象,把实现了Runnable接口的对象作为参数传入;

        Thread thread1 = new Thread(new TestThread2());

        thread1.start();//启动线程;

        Thread thread2 = new Thread(new TestThread2());

        thread2.start();

    }

}

11.3 线程状态和生命周期

11.3.1 线程状态

一个线程对象在它的生命周期内,需要经历5个状态。

图11-4 线程生命周期图

▪ 新生状态(New)

用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

▪ 就绪状态(Runnable)

处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有4中原因会导致线程进入就绪状态:

1. 新建线程:调用start()方法,进入就绪状态;

2. 阻塞线程:阻塞解除,进入就绪状态;

3. 运行线程:调用yield()方法,直接进入就绪状态;

4. 运行线程:JVM将CPU资源从本线程切换到其他线程。

▪ 运行状态(Running)

在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

▪ 阻塞状态(Blocked)

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:

1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。

2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。

3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。

4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

▪ 死亡状态(Terminated)

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。

当一个线程进入死亡状态以后,就不能再回到其它状态了。

11.3.2 终止线程的典型方式

终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。

【示例11-3】终止线程的典型方法(重要)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public class TestThreadCiycle implements Runnable {

    String name;

    boolean live = true;// 标记变量,表示线程是否可中止;

    public TestThreadCiycle(String name) {

        super();

        this.name = name;

    }

    public void run() {

        int i = 0;

        //当live的值是true时,继续线程体;false则结束循环,继而终止线程体;

        while (live) {

            System.out.println(name + (i++));

        }

    }

    public void terminate() {

        live = false;

    }

    public static void main(String[] args) {

        TestThreadCiycle ttc = new TestThreadCiycle("线程A:");

        Thread t1 = new Thread(ttc);// 新生状态

        t1.start();// 就绪状态

        for (int i = 0; i < 100; i++) {

            System.out.println("主线程" + i);

        }

        ttc.terminate();

        System.out.println("ttc stop!");

    }

}

执行结果如图11-5所示:

11.3.3 暂停线程执行sleep/yield

暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:

1. sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。

2. yield()方法:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。

【示例11-4】暂停线程的方法-sleep()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public class TestThreadState {

    public static void main(String[] args) {

        StateThread thread1 = new StateThread();

        thread1.start();

        StateThread thread2 = new StateThread();

        thread2.start();

    }

}

//使用继承方式实现多线程

class StateThread extends Thread {

    public void run() {

        for (int i = 0; i < 100; i++) {

            System.out.println(this.getName() + ":" + i);

            try {

                Thread.sleep(2000);//调用线程的sleep()方法;

            catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

执行结果如图11-6所示(注:以下图示只是部分结果,运行时可以感受到每条结果输出之前的延迟,是Thread.sleep(2000)语句在起作用):

图11-6示例11-4运行效果图

【示例11-5】暂停线程的方法-yield()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class TestThreadState {

    public static void main(String[] args) {

        StateThread thread1 = new StateThread();

        thread1.start();

        StateThread thread2 = new StateThread();

        thread2.start();

    }

}

//使用继承方式实现多线程

class StateThread extends Thread {

    public void run() {

        for (int i = 0; i < 100; i++) {

            System.out.println(this.getName() + ":" + i);

            Thread.yield();//调用线程的yield()方法;

        }

    }

}

执行结果如图11-7所示(注:以下图示只是部分结果,可以引起线程切换,但运行时没有明显延迟):

11.3.4 线程的联合join()

线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。如下面示例中,“爸爸线程”要抽烟,于是联合了“儿子线程”去买烟,必须等待“儿子线程”买烟完毕,“爸爸线程”才能继续抽烟。

【示例11-6】线程的联合-join()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

public class TestThreadState {

    public static void main(String[] args) {

        System.out.println("爸爸和儿子买烟故事");

        Thread father = new Thread(new FatherThread());

        father.start();

    }

}

class FatherThread implements Runnable {

    public void run() {

        System.out.println("爸爸想抽烟,发现烟抽完了");

        System.out.println("爸爸让儿子去买包红塔山");

        Thread son = new Thread(new SonThread());

        son.start();

        System.out.println("爸爸等儿子买烟回来");

        try {

            son.join();

        catch (InterruptedException e) {

            e.printStackTrace();

            System.out.println("爸爸出门去找儿子跑哪去了");

            // 结束JVM。如果是0则表示正常结束;如果是非0则表示非正常结束

            System.exit(1);

        }

        System.out.println("爸爸高兴的接过烟开始抽,并把零钱给了儿子");

    }

}

class SonThread implements Runnable {

    public void run() {

        System.out.println("儿子出门去买烟");

        System.out.println("儿子买烟需要10分钟");

        try {

            for (int i = 1; i <= 10; i++) {

                System.out.println("第" + i + "分钟");

                Thread.sleep(1000);

            }

        catch (InterruptedException e) {

            e.printStackTrace();

        }

        System.out.println("儿子买烟回来了");

    }

}

执行结果如图11-8所示:

11.4 线程的基本信息和优先级别

线程也是对象,系统为线程定义了很多方法、优先级、名字等,以便于对线程进行有效管理。

11.4.1 获取线程基本信息的方法

【示例11-7】线程的常用方法一

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public class TestThread {

    public static void main(String[] argc) throws Exception {

        Runnable r = new MyThread();

        Thread t = new Thread(r, "Name test");//定义线程对象,并传入参数;

        t.start();//启动线程;

        System.out.println("name is: " + t.getName());//输出线程名称;

        Thread.currentThread().sleep(5000);//线程暂停5分钟;

        System.out.println(t.isAlive());//判断线程还在运行吗?

        System.out.println("over!");

    }

}

class MyThread implements Runnable {

    //线程体;

    public void run() {

        for (int i = 0; i < 10; i++)

            System.out.println(i);

    }

}

执行结果如图11-9所示:

11.4.2 线程的优先级

1. 处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选。

2. 线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。

3. 使用下列方法获得或设置线程对象的优先级。

int getPriority();

void setPriority(int newPriority);

注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。

【示例11-8】线程的常用方法二

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class TestThread {

    public static void main(String[] args) {

        Thread t1 = new Thread(new MyThread(), "t1");

        Thread t2 = new Thread(new MyThread(), "t2");

        t1.setPriority(1);

        t2.setPriority(10);

        t1.start();

        t2.start();

    }

}

class MyThread extends Thread {

    public void run() {

        for (int i = 0; i < 10; i++) {

            System.out.println(Thread.currentThread().getName() + ": " + i);

        }

    }

}

执行结果如图11-10所示:

11.5 线程同步

在处理多线程问题时,如果多线程同时访问同一个对象,并且某些线程还想修改这个对象时,就需要用到“线程同步”机制。

11.5.1 什么是线程同步

▪ 同步问题的提出

现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。 比如:教室里,只有一台电脑,多个人都想使用。天然的解决办法就是,在电脑旁边,大家排队。前一人使用完后,后一人再使用。

▪ 线程同步的概念

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

【示例11-9】多线程操作同一个对象(未使用线程同步)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

public class TestSync {

    public static void main(String[] args) {

        Account a1 = new Account(100"高");

        Drawing draw1 = new Drawing(80, a1);// 定义取钱线程对象;

        Drawing draw2 = new Drawing(80, a1);// 定义取钱线程对象;

        draw1.start(); // 你取钱

        draw2.start(); // 你老婆取钱

    }

}

/*

 * 简单表示银行账户

 */

class Account {

    int money;

    String aname;

    public Account(int money, String aname) {

        super();

        this.money = money;

        this.aname = aname;

    }

}

/**

 * 模拟提款操作

 */

class Drawing extends Thread {

    int drawingNum; // 取多少钱

    Account account; // 要取钱的账户

    int expenseTotal; // 总共取的钱数

    public Drawing(int drawingNum, Account account) {

        super();

        this.drawingNum = drawingNum;

        this.account = account;

    }

    @Override

    public void run() {

        if (account.money - drawingNum < 0) {

            return;

        }

        try {

            Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。

        catch (InterruptedException e) {

            e.printStackTrace();

        }

        account.money -= drawingNum;

        expenseTotal += drawingNum;

        System.out.println(this.getName() + "--账户余额:" + account.money);

        System.out.println(this.getName() + "--总共取了:" + expenseTotal);

    }

}

执行结果如图11-11所示:

图11-11示例11-9运行效果图

没有线程同步机制,两个线程同时操作同一个账户对象,竟然从只有100元的账户,轻松取出80*2=160元,账户余额竟然成为了-60。这么大的问题,显然银行不会答应的。

11.5.2 实现线程同步

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。

由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。

▪ synchronized 方法

通过在方法声明中加入 synchronized关键字来声明,语法如下:

1

public  synchronized  void accessVal(int newVal);

synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

▪ synchronized块

synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。

Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。

synchronized 块:通过 synchronized关键字来声明synchronized 块,语法如下:

1

2

3

4

synchronized(syncObject)

   { 

   //允许访问控制的代码 

   }

【示例11-10】多线程操作同一个对象(使用线程同步)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

public class TestSync {

    public static void main(String[] args) {

        Account a1 = new Account(100"高");

        Drawing draw1 = new Drawing(80, a1);

        Drawing draw2 = new Drawing(80, a1);

        draw1.start(); // 你取钱

        draw2.start(); // 你老婆取钱

    }

}

/*

 * 简单表示银行账户

 */

class Account {

    int money;

    String aname;

    public Account(int money, String aname) {

        super();

        this.money = money;

        this.aname = aname;

    }

}

/**

 * 模拟提款操作

 

 * @author Administrator

 *

 */

class Drawing extends Thread {

    int drawingNum; // 取多少钱

    Account account; // 要取钱的账户

    int expenseTotal; // 总共取的钱数

    public Drawing(int drawingNum, Account account) {

        super();

        this.drawingNum = drawingNum;

        this.account = account;

    }

    @Override

    public void run() {

        draw();

    }

    void draw() {

        synchronized (account) {

            if (account.money - drawingNum < 0) {

                System.out.println(this.getName() + "取款,余额不足!");

                return;

            }

            try {

                Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。

            catch (InterruptedException e) {

                e.printStackTrace();

            }

            account.money -= drawingNum;

            expenseTotal += drawingNum;

        }

        System.out.println(this.getName() + "--账户余额:" + account.money);

        System.out.println(this.getName() + "--总共取了:" + expenseTotal);

    }

}

执行结果如图11-12和图11-13所示:

图11-12示例11-10运行效果图1

图11-13示例11-10运行效果图2

“synchronized (account)” 意味着线程需要获得account对象的“锁”才有资格运行同步块中的代码。 Account对象的“锁”也称为“互斥锁”,在同一时刻只能被一个线程使用。A线程拥有锁,则可以调用“同步块”中的代码;B线程没有锁,则进入account对象的“锁池队列”等待,直到A线程使用完毕释放了account对象的锁,B线程得到锁才可以开始调用“同步块”中的代码。

11.5.3 死锁及解决方案

死锁的概念

“死锁”指的是:

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。

因此, 某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。下面案例中,“化妆线程”需要同时拥有“镜子对象”、“口红对象”才能运行同步块。那么,实际运行时,“小丫的化妆线程”拥有了“镜子对象”,“大丫的化妆线程”拥有了“口红对象”,都在互相等待对方释放资源,才能化妆。这样,两个线程就形成了互相等待,无法继续运行的“死锁状态”。

【示例11-11】死锁问题演示

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

class Lipstick {//口红类

}

class Mirror {//镜子类

}

class Makeup extends Thread {//化妆类继承了Thread类

    int flag;

    String girl;

    static Lipstick lipstick = new Lipstick();

    static Mirror mirror = new Mirror();

    @Override

    public void run() {

        // TODO Auto-generated method stub

        doMakeup();

    }

    void doMakeup() {

        if (flag == 0) {

            synchronized (lipstick) {//需要得到口红的“锁”;

                System.out.println(girl + "拿着口红!");

                try {

                    Thread.sleep(1000);

                catch (InterruptedException e) {

                    e.printStackTrace();

                }

                synchronized (mirror) {//需要得到镜子的“锁”;

                    System.out.println(girl + "拿着镜子!");

                }

            }

        else {

            synchronized (mirror) {

                System.out.println(girl + "拿着镜子!");

                try {

                    Thread.sleep(2000);

                catch (InterruptedException e) {

                    e.printStackTrace();

                }

                synchronized (lipstick) {

                    System.out.println(girl + "拿着口红!");

                }

            }

        }

    }

}

public class TestDeadLock {

    public static void main(String[] args) {

        Makeup m1 = new Makeup();//大丫的化妆线程;

        m1.girl = "大丫";

        m1.flag = 0;

        Makeup m2 = new Makeup();//小丫的化妆线程;

        m2.girl = "小丫";

        m2.flag = 1;

        m1.start();

        m2.start();

    }

}

执行结果如图11-14所示(两线程都在等对方的资源,都处于停滞状态):

图11-14示例11-11运行效果图

死锁的解决方法

死锁是由于“同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。 如上面的死锁案例,修改成示例10-11所示。

【示例11-12】死锁问题的解决

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

class Lipstick {//口红类

}

class Mirror {//镜子类

}

class Makeup extends Thread {//化妆类继承了Thread类

    int flag;

    String girl;

    static Lipstick lipstick = new Lipstick();

    static Mirror mirror = new Mirror();

    @Override

    public void run() {

        // TODO Auto-generated method stub

        doMakeup();

    }

    void doMakeup() {

        if (flag == 0) {

            synchronized (lipstick) {

                System.out.println(girl + "拿着口红!");

                try {

                    Thread.sleep(1000);

                catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

            synchronized (mirror) {

                System.out.println(girl + "拿着镜子!");

            }

        else {

            synchronized (mirror) {

                System.out.println(girl + "拿着镜子!");

                try {

                    Thread.sleep(2000);

                catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

            synchronized (lipstick) {

                System.out.println(girl + "拿着口红!");

            }

        }

    }

}

public class TestDeadLock {

    public static void main(String[] args) {

        Makeup m1 = new Makeup();// 大丫的化妆线程;

        m1.girl = "大丫";

        m1.flag = 0;

        Makeup m2 = new Makeup();// 小丫的化妆线程;

        m2.girl = "小丫";

        m2.flag = 1;

        m1.start();

        m2.start();

    }

}

执行结果如图11-15和图11-16所示(两线程都可以得到需要的资源,程序正常运行结束):

图11-15示例11-12运行效果图

11.6 线程并发协作(生产者/消费者模式)

多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。

Ø 什么是生产者?

生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。

Ø 什么是消费者?

消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。

Ø 什么是缓冲区?

消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

图11-17 生产者消费者示意图

缓冲区是实现并发的核心,缓冲区的设置有3个好处:

Ø 实现线程的并发协作

有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。

Ø 解耦了生产者和消费者

生产者不需要和消费者直接打交道。

Ø 解决忙闲不均,提高效率

生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。

【示例11-13】生产者与消费者模式

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

public class TestProduce {

    public static void main(String[] args) {

        SyncStack sStack = new SyncStack();// 定义缓冲区对象;

        Shengchan sc = new Shengchan(sStack);// 定义生产线程;

        Xiaofei xf = new Xiaofei(sStack);// 定义消费线程;

        sc.start();

        xf.start();

    }

}

class Mantou {// 馒头

    int id;

    Mantou(int id) {

        this.id = id;

    }

}

class SyncStack {// 缓冲区(相当于:馒头筐)

    int index = 0;

    Mantou[] ms = new Mantou[10];

    public synchronized void push(Mantou m) {

        while (index == ms.length) {//说明馒头筐满了

            try {

               //wait后,线程会将持有的锁释放,进入阻塞状态;

               //这样其它需要锁的线程就可以获得锁;

                this.wait();

                //这里的含义是执行此方法的线程暂停,进入阻塞状态,

                //等消费者消费了馒头后再生产。

            catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

        // 唤醒在当前对象等待池中等待的第一个线程。

        //notifyAll叫醒所有在当前对象等待池中等待的所有线程。

        this.notify();

        // 如果不唤醒的话。以后这两个线程都会进入等待线程,没有人唤醒。

        ms[index] = m;

        index++;

    }

    public synchronized Mantou pop() {

        while (index == 0) {//如果馒头筐是空的;

            try {

                //如果馒头筐是空的,就暂停此消费线程(因为没什么可消费的嘛)。

                this.wait();                //等生产线程生产完再来消费;

            catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

        this.notify();

        index--;

        return ms[index];

    }

}

class Shengchan extends Thread {// 生产者线程

    SyncStack ss = null;

    public Shengchan(SyncStack ss) {

        this.ss = ss;

    }

    @Override

    public void run() {

        for (int i = 0; i < 10; i++) {

            System.out.println("生产馒头:" + i);

            Mantou m = new Mantou(i);

            ss.push(m);

        }

    }

}

class Xiaofei extends Thread {// 消费者线程;

    SyncStack ss = null;

    public Xiaofei(SyncStack ss) {

        this.ss = ss;

    }

    @Override

    public void run() {

        for (int i = 0; i < 10; i++) {

            Mantou m = ss.pop();

            System.out.println("消费馒头:" + i);

        }

    }

}

执行结果如图11-18所示:

图11-18 示例11-13运行效果图

线程并发协作总结:

线程并发协作(也叫线程通信),通常用于生产者/消费者模式,情景如下:

1. 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

2. 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。

3. 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。

4. 在生产者消费者问题中,仅有synchronized是不够的。

· synchronized可阻止并发更新同一个共享资源,实现了同步;

· synchronized不能用来实现不同线程之间的消息传递(通信)。

5. 那线程是通过哪些方法来进行消息传递(通信)的呢?见如下总结:

6. 以上方法均是java.lang.Object类的方法;

都只能在synchronized同步方法或者synchronized同步代码块中使用,否则会抛出异常。

11.7 任务定时调度

通过Timer和Timetask,我们可以实现定时启动某个线程。

java.util.Timer

在这种实现方式中,Timer类作用是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。

java.util.TimerTask

TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。

在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。

【示例11-14】java.util.Timer的使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public class TestTimer {

    public static void main(String[] args) {

        Timer t1 = new Timer();//定义计时器;

        MyTask task1 = new MyTask();//定义任务;

        t1.schedule(task1,3000);  //3秒后执行;

        //t1.schedule(task1,5000,1000);//5秒以后每隔1秒执行一次!

        //GregorianCalendar calendar1 = new GregorianCalendar(2010,0,5,14,36,57); 

        //t1.schedule(task1,calendar1.getTime()); //指定时间定时执行; 

    }

}

class MyTask extends TimerTask {//自定义线程类继承TimerTask类;

    public void run() {

        for(int i=0;i<10;i++){

            System.out.println("任务1:"+i);

        }

    }

}

执行结果如图11-19所示:

图11-19 示例11-14运行效果图

运行以上程序时,可以感觉到在输出之前有明显的延迟(大概就是3秒!)。还有几个方法,我注释掉了,大家自己试试吧!

在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask实现。

第十一章 总结

1. 程序:Java源程序和字节码文件被称为“程序(Program)”,是一个静态的概念。

2. 进程:执行中的程序叫做进程(Process),是一个动态的概念。每个进程由3部分组成:cpu、data、code。

3. 线程:是进程中一个“单一的连续控制流程 (a single sequential flow of control)”。

4. 在Java中实现多线程的方式:

▪ 继承Thread类实现多线程

▪ 实现Runnable接口实现多线程

5. 线程的状态:

▪ 新生状态

▪ 就绪状态

▪ 运行状态

▪ 死亡状态

▪ 阻塞状态

6. 暂停线程执行的方法:

▪ sleep()

▪ yield()

▪ join()

7. 实现线程同步的两种方式:

▪ synchronized 方法 :

1

public  synchronized  void accessVal(int newVal);

▪ synchronized 块:

1

2

3

4

synchronized(syncObject)

//允许访问控制的代码 

}

8. 同步解决问题的另一种典型方式:生产者/消费者模式。

9. 线程通信的方法:

▪ wait()

▪  notify()

▪ notifyAll()

都是Object类的方法,只能在同步方法和同步代码块中使用。

12、网络编程

如今,计算机已经成为人们学习、工作、生活必不可少的工具。我们利用计算机可以和亲朋好友网上聊天,也可以玩网游、发邮件等等,这些功能实现都离不开计算机网络。计算机网络实现了不同计算机之间的通信,这必须依靠编写网络程序来实现。下面,我们将教大家如何编写网络程序。

在学习编程之前,我们首先要了解关于网络通信的一些概念。

12.1 基本概念

本节介绍一些在网络编程中涉及到的基本概念。了解这些概念有助于大家建立起网络编程的基本认知体系。当然,许多细节知识,比如通信协议的内容、数据封装和解封的具体细节等知识在此不会详解。本章的目的是帮助大家建立网络编程的整体认知而不是深钻细节。

12.1.1 计算机网络

计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

从其中我们可以提取到以下内容:

1. 计算机网络的作用:资源共享和信息传递。

2. 计算机网络的组成:

a) 计算机硬件:计算机(大中小型服务器,台式机、笔记本等)、外部设备(路由器、交换机等)、通信线路(双绞线、光纤等)。

b) 计算机软件:网络操作系统(Windows 2000 Server/Advance Server、Unix、Linux等)、网络管理软件(WorkWin、SugarNMS等)、网络通信协议(如TCP/IP协议栈等)。

3. 计算机网络的多台计算机是具有独立功能的,而不是脱离网络就无法存在的。

12.1.2 网络通讯协议

1、网络通讯协议

通过计算机网络可以实现不同计算机之间的连接与通信,但是计算机网络中实现通信必须有一些约定即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准。就像两个人想要顺利沟通就必须使用同一种语言一样,如果一个人只懂英语而另外一个人只懂中文,这样就会造成没有共同语言而无法沟通。

国际标准化组织(ISO,即International Organization for Standardization)定义了网络通信协议的基本框架,被称为OSI(Open System Interconnect,即开放系统互联)模型。要制定通讯规则,内容会很多,比如要考虑A电脑如何找到B电脑,A电脑在发送信息给B电脑时是否需要B电脑进行反馈,A电脑传送给B电脑的数据格式又是怎样的?内容太多太杂,所以OSI模型将这些通讯标准进行层次划分,每一层次解决一个类别的问题,这样就使得标准的制定没那么复杂。OSI模型制定的七层标准模型,分别是:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。

OSI七层协议模型如图12-1所示:

图12-1 七层协议模型

虽然国际标准化组织制定了这样一个网络通信协议的模型,但是实际上互联网通讯使用最多的网络通信协议是TCP/IP网络通信协议。

TCP/IP 是一个协议族,也是按照层次划分,共四层:应用层,传输层,互连网络层,网络接口层(物理+数据链路层)。

那么TCP/IP协议和OSI模型有什么区别呢?OSI网络通信协议模型,是一个参考模型,而TCP/IP协议是事实上的标准。TCP/IP协议参考了OSI 模型,但是并没有严格按照OSI规定的七层标准去划分,而只划分了四层,这样会更简单点,当划分太多层次时,你很难区分某个协议是属于哪个层次的。TCP/IP协议和OSI模型也并不冲突,TCP/IP协议中的应用层协议,就对应于OSI中的应用层,表示层,会话层。就像以前有工业部和信息产业部,现在实行大部制后只有工业和信息化部一个部门,但是这个部门还是要做以前两个部门一样多的事情,本质上没有多大的差别。TCP/IP中有两个重要的协议,传输层的TCP协议和互连网络层的IP协议,因此就拿这两个协议做代表,来命名整个协议族了,再说TCP/IP协议时,是指整个协议族。

2、网络协议的分层

由于网络结点之间联系很复杂,在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与再下一层不发生关系。

把用户应用程序作为最高层,把物理通信线路作为最低层,将其间的协议处理分为若干层,规定每层处理的任务,也规定每层的接口标准。

ISO模型与TCP/IP模型的对应关系如图12-2所示。

图12-2 开放系统互连参考模型与TCP/IP参考模型对比

12.1.3 数据封装和解封

由于用户传输的数据一般都比较大,有的可以达到MB字节,一次性发送出去十分困难,于是就需要把数据分成许多片段,再按照一定的次序发送出去。这个过程就需要对数据进行封装。

数据封装(Data Encapsulation)是指将协议数据单元(PDU)封装在一组协议头和协议尾中的过程。在OSI七层参考模型中,每层主要负责与其它机器上的对等层进行通信。该过程是在协议数据单元(PDU)中实现的,其中每层的PDU一般由本层的协议头、协议尾和数据封装构成。

1.数据发送处理过程

(1)应用层将数据交给传输层,传输层添加上TCP的控制信息(称为TCP头部),这个数据单元称为段(Segment),加入控制信息的过程称为封装。然后,将段交给网络层。

(2)网络层接收到段,再添加上IP头部,这个数据单元称为包(Packet)。然后,将包交给数据链路层。

(3) 数据链路层接收到包,再添加上MAC头部和尾部,这个数据单元称为帧(Frame)。然后,将帧交给物理层。

(4)物理层将接收到的数据转化为比特流,然后在网线中传送。

2.数据接收处理过程

(1)物理层接收到比特流,经过处理后将数据交给数据链路层。

(2)数据链路层将接收到的数据转化为数据帧,再除去MAC头部和尾部,这个除去控制信息的过程称为解封,然后将包交给网络层。

(3)网络层接收到包,再除去IP头部,然后将段交给传输层。

(4)传输层接收到段,再除去TCP头部,然后将数据交给应用层。

从以上传输过程中,可以总结出以下规则:

(1)发送方数据处理的方式是从高层到底层,逐层进行数据封装。

(2)接收方数据处理的方式是从底层到高层,逐层进行数据解封装。

接收方的每一层只把对该层有意义的数据拿走,或者说每一层只能处理发送方同等层的数据,然后把其余的部分传递给上一层,这就是对等层通信的概念。

数据封装与解封如图12-3和图12-4所示:

图12-3 数据封装

图12-4 数据解封

12.1.4 IP地址和接口

▪ IP地址:

用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。 比如互联网的每个服务器都要有自己的IP地址,而每个局域网的计算机要通信也要配置IP地址。路由器是连接两个或多个网络的网络设备。

目前主流使用的IP地址是IPV4,但是随着网络规模的不断扩大,IPV4面临着枯竭的危险,所以推出了IPV6。

IPV4:32位地址,并以8位为一个单位,分成四部分,以点分十进制表示,如192.168.0.1。因为8位二进制的计数范围是00000000---11111111,对应十进制的0-255,所以-4.278.4.1是错误的IPV4地址。

IPV6:128位(16个字节)写成8个16位的无符号整数,每个整数用四个十六进制位表示,每个数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984

注意事项

1. 127.0.0.1 本机地址

2. 192.168.0.0--192.168.255.255为私有地址,属于非注册地址,专门为组织机构内部使用。

▪ 端口:

IP地址用来标识一台计算机,但是一台计算机上可能提供多种网络应用程序,如何来区分这些不同的程序呢?这就要用到端口。

端口是虚拟的概念,并不是说在主机上真的有若干个端口。通过端口,可以在一个主机上运行多个网络应用程序。 端口的表示是一个16位的二进制整数,对应十进制的0-65535。

Oracle、MySQL、Tomcat、QQ、msn、迅雷、电驴、360等网络程序都有自己的端口。

总结

1. IP地址好比每个人的地址(门牌号),端口好比是房间号。必须同时指定IP地址和端口号才能够正确的发送数据。

2. IP地址好比为电话号码,而端口号就好比为分机号。

12.1.5 URL资源定位符

在www上,每一信息资源都有统一且唯一的地址,该地址就叫URL(Uniform Resource Locator),它是www的统一资源定位符。URL由4部分组成:协议 、存放资源的主机域名、资源文件名和端口号。如果未指定该端口号,则使用协议默认的端口。例如http 协议的默认端口为 80。 在浏览器中访问网页时,地址栏显示的地址就是URL。

在java.net包中提供了URL类,该类封装了大量复杂的涉及从远程站点获取信息的细节。

12.1.6 Socket

我们开发的网络应用程序位于应用层,TCP和UDP属于传输层协议,在应用层如何使用传输层的服务呢?在应用层和传输层之间,则是使用套接Socket来进行分离。

套接字就像是传输层为应用层开的一个小口,应用程序通过这个小口向远程发送数据,或者接收远程发来的数据;而这个小口以内,也就是数据进入这个口之后,或者数据从这个口出来之前,是不知道也不需要知道的,也不会关心它如何传输,这属于网络其它层次工作。

Socket实际是传输层供给应用层的编程接口。Socket就是应用层与传输层之间的桥梁。使用Socket编程可以开发客户机和服务器应用程序,可以在本地网络上进行通信,也可通过Internet在全球范围内通信。

图12-5 Socket的作用

12.2 TCP协议和UDP协议

12.2.1 区别和练习

TCP协议和UDP协议是传输层的两种协议。Socket是传输层供给应用层的编程接口,所以Socket编程就分为TCP编程和UDP编程两类。

在网络通讯中,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。

这两种传输方式都在实际的网络编程中使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则可以通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据传递。

由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。

总结

1. TCP是面向连接的,传输数据安全,稳定,效率相对较低。

2. UDP是面向无连接的,传输数据不安全,效率较高。

12.2.2 TCP协议

TCP(Transfer Control Protocol)是面向连接的,所谓面向连接,就是当计算机双方通信时必需经过先建立连接,然后传送数据,最后拆除连接三个过程。

TCP在建立连接时又分三步走:

第一步,是请求端(客户端)发送一个包含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号。

第二步,服务器在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示客户端的请求被接受,同时TCP序号被加一,ACK即确认(Acknowledgement)。

第三步,客户端也返回一个确认报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成。然后才开始通信的第二步:数据处理。

这就是所说的TCP的三次握手(Three-way Handshake)。

12.2.3 UDP协议

基于TCP协议可以建立稳定连接的点对点的通信。这种通信方式实时、快速、安全性高,但是很占用系统的资源。

在网络传输方式上,还有另一种基于UDP协议的通信方式,称为数据报通信方式。在这种方式中,每个数据发送单元被统一封装成数据报包的方式,发送方将数据报包发送到网络中,数据报包在网络中去寻找它的目的地。

12.3 Java网络编程

Java为了可移植性,不允许直接调用操作系统,而是由java.net包来提供网络功能。Java虚拟机负责提供与操作系统的实际连接。下面我们来介绍几个java.net包中的常用的类。

12.3.1 InetAddress

作用:封装计算机的IP地址和DNS(没有端口信息)。

注:DNS是Domain Name System,域名系统。

特点:这个类没有构造方法。如果要得到对象,只能通过静态方法:getLocalHost()、getByName()、 getAllByName()、 getAddress()、getHostName()。

【示例12-1】使用getLocalHost方法创建InetAddress对象

1

2

3

4

5

6

7

8

9

10

11

import java.net.InetAddress;

import java.net.UnknownHostException;

public class Test1 {

    public static void main(String[] args) throws UnknownHostException {

        InetAddress addr = InetAddress.getLocalHost();

        //返回IP地址:192.168.1.110

        System.out.println(addr.getHostAddress()); 

        //输出计算机名:gaoqi

        System.out.println(addr.getHostName());     

    }

}

【示例12-2】根据域名得到InetAddress对象

1

2

3

4

5

6

7

8

9

10

11

import java.net.InetAddress;

import java.net.UnknownHostException;

public class Test2 {

    public static void main(String[] args) throws UnknownHostException {

        InetAddress addr = InetAddress.getByName("www.sxt.cn");

        // 返回 sxt服务器的IP:59.110.14.7

        System.out.println(addr.getHostAddress());

        // 输出:www.sxt.cn

        System.out.println(addr.getHostName());

    }

}

【示例12-3】根据IP得到InetAddress对象

1

2

3

4

5

6

7

8

9

10

11

12

13

14

import java.net.InetAddress;

import java.net.UnknownHostException;

public class Test3 {

    public static void main(String[] args) throws UnknownHostException {

        InetAddress addr = InetAddress.getByName("59.110.14.7");

        // 返回sxt服务器的IP:59.110.14.7

        System.out.println(addr.getHostAddress());

        /*

         * 输出ip而不是域名。如果这个IP地址不存在或DNS服务器不允许进行IP地址

         * 和域名的映射,getHostName方法就直接返回这个IP地址。

         */

        System.out.println(addr.getHostName());

    }

}

12.3.2 InetSocketAddress

作用:包含IP和端口信息,常用于Socket通信。此类实现 IP 套接字地址(IP 地址 + 端口号),不依赖任何协议。

【示例12-4】InetSocketAddress的使用

1

2

3

4

5

6

7

8

9

import java.net.InetSocketAddress;

public class Test4 {

    public static void main(String[] args) {

        InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1"8080);

        InetSocketAddress socketAddress2 = new InetSocketAddress("localhost"9000);

        System.out.println(socketAddress.getHostName());

        System.out.println(socketAddress2.getAddress());

    }

}

12.3.3 URL类

IP地址唯一标识了Internet上的计算机,而URL则标识了这些计算机上的资源。类 URL 代表一个统一资源定位符,它是指向互联网“资源”的指针。资源可以是简单的文件或目录,也可以是对更为复杂的对象的引用,例如对数据库或搜索引擎的查询。

为了方便程序员编程,JDK中提供了URL类,该类的全名是java.net.URL,有了这样一个类,就可以使用它的各种方法来对URL对象进行分割、合并等处理。

【示例12-5】URL类的使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

import java.net.MalformedURLException;

import java.net.URL;

public class Test5 {

    public static void main(String[] args) throws MalformedURLException {

        URL u = new URL("http://www.google.cn:80/webhp#aa?canhu=33");

        System.out.println("获取与此url关联的协议的默认端口:" + u.getDefaultPort());

        System.out.println("getFile:" + u.getFile()); // 端口号后面的内容

        System.out.println("主机名:" + u.getHost()); // www.google.cn

        System.out.println("路径:" + u.getPath()); // 端口号后,参数前的内容

        // 如果www.google.cn:80则返回80.否则返回-1

        System.out.println("端口:" + u.getPort()); 

        System.out.println("协议:" + u.getProtocol());

        System.out.println("参数部分:" + u.getQuery());

        System.out.println("锚点:" + u.getRef());

        URL u1 = new URL("http://www.abc.com/aa/");

        URL u2 = new URL(u, "2.html"); // 相对路径构建url对象

        System.out.println(u2.toString()); // http://www.abc.com/aa/2.html

    }

}

【示例12-6】最简单的网络爬虫

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.net.MalformedURLException;

import java.net.URL;

public class Test6 {

    public static void main(String[] args) {

        basicSpider();

    }

    //网络爬虫

    static void basicSpider() {

        URL url = null;

        InputStream is = null;

        BufferedReader br = null;

        StringBuilder sb = new StringBuilder();

        String temp = "";

        try {

            url = new URL("http://www.baidu.com");

            is = url.openStream();

            br = new BufferedReader(new InputStreamReader(is));

            /* 

             * 这样就可以将网络内容下载到本地机器。

             * 然后进行数据分析,建立索引。这也是搜索引擎的第一步。

             */

            while ((temp = br.readLine()) != null) {

                sb.append(temp);

            }

            System.out.println(sb);

        catch (MalformedURLException e) {

            e.printStackTrace();

        catch (IOException e) {

            e.printStackTrace();

        finally {

            try {

                br.close();

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                is.close();

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

12.3.4 基于TCP协议的Socket编程和通讯

在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client)程序,简称客户端,而在第一次通讯中等待连接的程序被称作服务器端(Server)程序,简称服务器。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。

“请求-响应”模式:

1. Socket类:发送TCP消息。

2. ServerSocket类:创建服务器。

套接字是一种进程间的数据交换机制。这些进程既可以在同一机器上,也可以在通过网络连接的不同机器上。换句话说,套接字起到通信端点的作用。单个套接字是一个端点,而一对套接字则构成一个双向通信信道,使非关联进程可以在本地或通过网络进行数据交换。一旦建立套接字连接,数据即可在相同或不同的系统中双向或单向发送,直到其中一个端点关闭连接。套接字与主机地址和端口地址相关联。主机地址就是客户端或服务器程序所在的主机的IP地址。端口地址是指客户端或服务器程序使用的主机的通信端口。

在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样,客户端和服务器通过套接字所建立的连接使用输入输出流进行通信。

TCP/IP套接字是最可靠的双向流协议,使用TCP/IP可以发送任意数量的数据。

实际上,套接字只是计算机上已编号的端口。如果发送方和接收方计算机确定好端口,他们就可以通信了。

如图12-6所示为客户端与服务器端的通信关系图:

图12-6 客户端与服务器端的通信关系图

TCP/IP通信连接的简单过程:

位于A计算机上的TCP/IP软件向B计算机发送包含端口号的消息,B计算机的TCP/IP软件接收该消息,并进行检查,查看是否有它知道的程序正在该端口上接收消息。如果有,他就将该消息交给这个程序。

要使程序有效地运行,就必须有一个客户端和一个服务器。

通过Socket的编程顺序:

1. 创建服务器ServerSocket,在创建时,定义ServerSocket的监听端口(在这个端口接收客户端发来的消息)。

2. ServerSocket调用accept()方法,使之处于阻塞状态。

3. 创建客户端Socket,并设置服务器的IP及端口。

4. 客户端发出连接请求,建立连接。

5. 分别取得服务器和客户端Socket的InputStream和OutputStream。

6. 利用Socket和ServerSocket进行数据传输。

7. 关闭流及Socket。

【示例12-7】TCP:单向通信Socket之服务器端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.OutputStreamWriter;

import java.net.ServerSocket;

import java.net.Socket;

/**

 * 最简单的服务器端代码

 * @author Administrator

 */

public class BasicSocketServer {

    public static void main(String[] args) {

        Socket socket = null;

        BufferedWriter bw = null;

        try {

            // 建立服务器端套接字:指定监听的接口

            ServerSocket serverSocket = new ServerSocket(8888);

            System.out.println("服务端建立监听");

            // 监听,等待客户端请求,并愿意接收连接

            socket = serverSocket.accept();

            // 获取socket的输出流,并使用缓冲流进行包装

            bw = new BufferedWriter(new

                                    OutputStreamWriter(socket.getOutputStream()));

            // 向客户端发送反馈信息

            bw.write("hhhh");

        catch (IOException e) {

            e.printStackTrace();

        finally {

            // 关闭流及socket连接

            if (bw != null) {

                try {

                    bw.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if (socket != null) {

                try {

                    socket.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

    }

}

【示例12-8】TCP:单向通信Socket之客户端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.net.InetAddress;

import java.net.Socket;

/**

 * 最简单的Socket客户端

 * @author Administrator

 */

public class BasicSocketClient {

    public static void main(String[] args) {

        Socket socket = null;

        BufferedReader br = null;

        try {

            /*

             * 创建Scoket对象:指定要连接的服务器的IP和端口而不是自己机器的

             * 端口。发送端口是随机的。

             */

            socket = new Socket(InetAddress.getLocalHost(), 8888);

            //获取scoket的输入流,并使用缓冲流进行包装

            br = new BufferedReader(new

                                   InputStreamReader(socket.getInputStream()));

            //接收服务器端发送的信息

            System.out.println(br.readLine());

        catch (Exception e) {

            e.printStackTrace();

        finally {

            // 关闭流及socket连接

            if (br != null) {

                try {

                    br.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if (socket != null) {

                try {

                    socket.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

    }

}

【示例12-9】TCP:双向通信Socket之服务器端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStreamWriter;

import java.net.ServerSocket;

import java.net.Socket;

public class Server {

    public static void main(String[] args){

        Socket socket = null;

        BufferedReader in = null;

        BufferedWriter out = null;

        BufferedReader br = null;

        try {

            //创建服务器端套接字:指定监听端口

            ServerSocket server = new ServerSocket(8888);

            //监听客户端的连接

            socket = server.accept();

            //获取socket的输入输出流接收和发送信息

            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            out = new BufferedWriter(new

                                   OutputStreamWriter(socket.getOutputStream()));

            br = new BufferedReader(new InputStreamReader(System.in));

            while (true) {

                //接收客户端发送的信息

                String str = in.readLine();

                System.out.println("客户端说:" + str);

                String str2 = "";

                //如果客户端发送的是“end”则终止连接 

                if (str.equals("end")){

                    break;

                }

                //否则,发送反馈信息

                str2 = br.readLine(); // 读到\n为止,因此一定要输入换行符!

                out.write(str2 + "\n");

                out.flush();

            }

        catch (IOException e) {

            e.printStackTrace();

        finally {

            //关闭资源

            if(in != null){

                try {

                    in.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if(out != null){

                try {

                    out.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if(br != null){

                try {

                    br.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if(socket != null){

                try {

                    socket.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

    }

}

【示例12-10】TCP:双向通信Socket之客户端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStreamWriter;

import java.net.InetAddress;

import java.net.Socket;

import java.net.UnknownHostException;

public class Client {

    public static void main(String[] args) {

        Socket socket = null;

        BufferedReader in = null;

        BufferedWriter out = null;

        BufferedReader wt = null;

        try {

            //创建Socket对象,指定服务器端的IP与端口

            socket = new Socket(InetAddress.getLocalHost(), 8888);

            //获取scoket的输入输出流接收和发送信息

            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            out = new BufferedWriter(new

                                   OutputStreamWriter(socket.getOutputStream()));

            wt = new BufferedReader(new InputStreamReader(System.in));

            while (true) {

                //发送信息

                String str = wt.readLine();

                out.write(str + "\n");

                out.flush();

                //如果输入的信息为“end”则终止连接

                if (str.equals("end")) {

                    break;

                }

                //否则,接收并输出服务器端信息

                System.out.println("服务器端说:" + in.readLine());

            }

        catch (UnknownHostException e) {

            e.printStackTrace();

        catch (IOException e) {

            e.printStackTrace();

        finally {

            // 关闭资源

            if (out != null) {

                try {

                    out.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if (in != null) {

                try {

                    in.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if (wt != null) {

                try {

                    wt.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

            if (socket != null) {

                try {

                    socket.close();

                catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

    }

}

执行结果如图12-7与图12-8所示

图12-7示例12-9与12-10运行效果图—服务器端

图12-8示例12-9与12-10运行效果图—客户端

菜鸟雷区

运行时,要先启动服务器端,再启动客户端,才能得到正常的运行效果。

但是,上面这个程序,必须按照安排好的顺序,服务器和客户端一问一答!不够灵活!!可以使用多线程实现更加灵活的双向通讯!!

服务器端:一个线程专门发送消息,一个线程专门接收消息。

客户端:一个线程专门发送消息,一个线程专门接收消息。

【示例12-11】TCP:聊天室之服务器端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStreamWriter;

import java.net.ServerSocket;

import java.net.Socket;

public class ChatServer {

    public static void main(String[] args) {

        ServerSocket server = null;

        Socket socket = null;

        BufferedReader in = null;

        try {

            server = new ServerSocket(8888);

            socket = server.accept();

            //创建向客户端发送消息的线程,并启动

            new ServerThread(socket).start();

            // main线程负责读取客户端发来的信息

            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            while (true) {

                String str = in.readLine();

                System.out.println("客户端说:" + str);

            }

        catch (IOException e) {

            e.printStackTrace();

        finally {

            try {

                if (in != null) {

                    in.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (socket != null) {

                    socket.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

/**

 * 专门向客户端发送消息的线程

 

 * @author Administrator

 *

 */

class ServerThread extends Thread {

    Socket ss;

    BufferedWriter out;

    BufferedReader br;

    public ServerThread(Socket ss) {

        this.ss = ss;

        try {

            out = new BufferedWriter(new OutputStreamWriter(ss.getOutputStream()));

            br = new BufferedReader(new InputStreamReader(System.in));

        catch (IOException e) {

            e.printStackTrace();

        }

    }

    public void run() {

        try {

            while (true) {

                String str2 = br.readLine();

                out.write(str2 + "\n");

                out.flush();

            }

        catch (IOException e) {

            e.printStackTrace();

        finally {

            try {

                if(out != null){

                out.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if(br != null){

                    br.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

【示例12-12】TCP:聊天室之客户端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStreamWriter;

import java.net.InetAddress;

import java.net.Socket;

import java.net.UnknownHostException;

public class ChatClient {

    public static void main(String[] args) {

        Socket socket = null;

        BufferedReader in = null;

        try {

            socket = new Socket(InetAddress.getByName("127.0.1.1"), 8888);

            // 创建向服务器端发送信息的线程,并启动

            new ClientThread(socket).start();

            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            // main线程负责接收服务器发来的信息

            while (true) {

                System.out.println("服务器说:" + in.readLine());

            }

        catch (UnknownHostException e) {

            e.printStackTrace();

        catch (IOException e) {

            e.printStackTrace();

        finally {

            try {

                if (socket != null) {

                    socket.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (in != null) {

                    in.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

/**

 * 用于向服务器发送消息

 

 * @author Administrator

 *

 */

class ClientThread extends Thread {

    Socket s;

    BufferedWriter out;

    BufferedReader wt;

    public ClientThread(Socket s) {

        this.s = s;

        try {

            out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));

            wt = new BufferedReader(new InputStreamReader(System.in));

        catch (IOException e) {

            e.printStackTrace();

        }

    }

    public void run() {

        try {

            while (true) {

                String str = wt.readLine();

                out.write(str + "\n");

                out.flush();

            }

        catch (IOException e) {

            e.printStackTrace();

        finally {

            try {

                if (wt != null) {

                    wt.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

            try {

                if (out != null) {

                    out.close();

                }

            catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

执行结果如图12-9与图12-10所示:

图12-9 示例12-11与12-12运行效果图—服务器端

图12-10 示例12-11与12-12运行效果图—客户端

12.3.5 UDP通讯的实现

UDP协议与上节讲的TCP协议不同,它是面向无连接的,双方不需要进行连接就可以通讯。UDP通讯所发送的数据需要进行封包操作(使用DatagramPacket类),然后才能发送或者接收(使用DatagrampSocket类)。

▪ DatagramSocket:用于发送或接收数据报包

当服务器要向客户端发送数据时,需要在服务器端产生一个DatagramSocket对象,在客户端产生一个DatagramSocket对象。服务器端的DatagramSocket将DatagramPacket发送到网络上,然后被客户端的DatagramSocket接收。

DatagramSocket有两种常用的构造函数。一种是无需任何参数的,常用于客户端;另一种需要指定端口,常用于服务器端。如下所示:

DatagramSocket() :构造数据报套接字并将其绑定到本地主机上任何可用的端口。

DatagramSocket(int port) :创建数据报套接字并将其绑定到本地主机上的指定端口。

常用方法:

Ø send(DatagramPacket p) :从此套接字发送数据报包。

Ø receive(DatagramPacket p) :从此套接字接收数据报包。

Ø close() :关闭此数据报套接字。

▪ DatagramPacket:数据容器(封包)的作用

此类表示数据报包。 数据报包用来实现封包的功能。

常用方法:

Ø DatagramPacket(byte[] buf, int length) :构造数据报包,用来接收长度为 length 的数据包。

Ø DatagramPacket(byte[] buf, int length, InetAddress address, int port) :构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。

Ø getAddress() :获取发送或接收方计算机的IP地址,此数据报将要发往该机器或者是从该机器接收到的。

Ø getData() :获取发送或接收的数据。

Ø setData(byte[] buf) :设置发送的数据。

UDP通信编程基本步骤:

1. 创建客户端的DatagramSocket,创建时,定义客户端的监听端口。

2. 创建服务器端的DatagramSocket,创建时,定义服务器端的监听端口。

3. 在服务器端定义DatagramPacket对象,封装待发送的数据包。

4. 客户端将数据报包发送出去。

5. 服务器端接收数据报包。

【示例12-13】UDP:单向通信之客户端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetSocketAddress;

public class Client {

    public static void main(String[] args) throws Exception {

        byte[] b = "北京尚学堂".getBytes();

        //必须告诉数据报包要发到哪台计算机的哪个端口,发送的数据以及数据的长度

        DatagramPacket dp = new DatagramPacket(b,b.length,new

InetSocketAddress("localhost",8999));

        //创建数据报套接字:指定发送信息的端口

        DatagramSocket ds = new DatagramSocket(9000);

        //发送数据报包

        ds.send(dp);

        //关闭资源

        ds.close();

    }

}

【示例12-14】UDP:单向通信之服务器端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

import java.net.DatagramPacket;

import java.net.DatagramSocket;

public class Server {

    public static void main(String[] args) throws Exception {

        //创建数据报套接字:指定接收信息的端口

        DatagramSocket ds = new DatagramSocket(8999);

        byte[] b = new byte[1024];

        //创建数据报包,指定要接收的数据的缓存位置和长度

        DatagramPacket dp = new DatagramPacket(b, b.length);

        //接收客户端发送的数据报

        ds.receive(dp); // 阻塞式方法

        //dp.getLength()返回实际收到的数据的字节数

        String string = new String(dp.getData(), 0, dp.getLength());

        System.out.println(string);

        //关闭资源

        ds.close();

    }

}

执行结果如图12-11所示:

图12-11 示例12-13与12-14运行效果图

通过字节数组流ByteArrayInputStream、ByteArrayOutputStream与数据流DataInputStream、DataOutputStream联合使用可以传递基本数据类型。

【示例12-15】UDP:基本数据类型的传递之客户端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

import java.io.ByteArrayOutputStream;

import java.io.DataOutputStream;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetSocketAddress;

public class Client {

    public static void main(String[] args) throws Exception {

        long n = 2000L;

        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        DataOutputStream dos = new DataOutputStream(bos);

        dos.writeLong(n);

        //获取字节数组流中的字节数组(我们要发送的数据)

        byte[] b = bos.toByteArray();

        //必须告诉数据报包要发到哪台计算机的哪个端口,发送的数据以及数据的长度

        DatagramPacket dp = new DatagramPacket(b,b.length,new

                                             InetSocketAddress("localhost",8999));

        //创建数据报套接字:指定发送信息的端口

        DatagramSocket ds = new DatagramSocket(9000);

        //发送数据报包

        ds.send(dp);

        //关闭资源

        dos.close();

        bos.close();

        ds.close();

    }

}

【示例12-16】UDP:基本数据类型的传递之服务器端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

import java.io.ByteArrayInputStream;

import java.io.DataInputStream;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

public class Server {

    public static void main(String[] args) throws Exception {

        //创建数据报套接字:指定接收信息的端口

        DatagramSocket ds = new DatagramSocket(8999);

        byte[] b = new byte[1024];

        //创建数据报包,指定要接收的数据的缓存位置和长度

        DatagramPacket dp = new DatagramPacket(b, b.length);

        //接收客户端发送的数据报

        ds.receive(dp); // 阻塞式方法

        //dp.getData():获取客户端发送的数据,返回值是一个字节数组

        ByteArrayInputStream bis = new ByteArrayInputStream(dp.getData());

        DataInputStream dis = new DataInputStream(bis);

        System.out.println(dis.readLong());

        //关闭资源

        dis.close();

        bis.close();

        ds.close();

    }

}

执行结果如图12-12所示:

图12-12 示例12-15与12-16运行效果图

通过字节数组流ByteArrayInputStream、ByteArrayOutputStream与数据流ObjectInputStream、ObjectOutputStream联合使用可以传递对象。

【示例12-17】UDP:对象的传递之Person类

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import java.io.Serializable;

public class Person implements Serializable{

    private static final long serialVersionUID = 1L;

    int age;

    String name;

    public Person(int age, String name) {

        super();

        this.age = age;

        this.name = name;

    }

    @Override

    public String toString() {

        return "Person [age=" + age + ", name=" + name + "]";

    }

}

【示例12-18】UDP:对象的传递之客户端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

import java.io.ByteArrayOutputStream;

import java.io.ObjectOutputStream;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetSocketAddress;

public class Client {

    public static void main(String[] args) throws Exception {

        //创建要发送的对象

        Person person = new Person(18"高淇");

        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        ObjectOutputStream oos = new ObjectOutputStream(bos);

        oos.writeObject(person);

        //获取字节数组流中的字节数组(我们要发送的数据)

        byte[] b = bos.toByteArray();

        //必须告诉数据报包要发到哪台计算机的哪个端口,发送的数据以及数据的长度

        DatagramPacket dp = new DatagramPacket(b,b.length,new

                                             InetSocketAddress("localhost",8999));

        //创建数据报套接字:指定发送信息的端口

        DatagramSocket ds = new DatagramSocket(9000);

        //发送数据报包

        ds.send(dp);

        //关闭资源

        oos.close();

        bos.close();

        ds.close();

    }

}  

【示例12-19】UDP:对象的传递之服务器端

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

import java.io.ByteArrayInputStream;

import java.io.ObjectInputStream;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

public class Server {

    public static void main(String[] args) throws Exception {

        //创建数据报套接字:指定接收信息的端口

        DatagramSocket ds = new DatagramSocket(8999);

        byte[] b = new byte[1024];

        //创建数据报包,指定要接收的数据的缓存位置和长度

        DatagramPacket dp = new DatagramPacket(b, b.length);

        //接收客户端发送的数据报

        ds.receive(dp); // 阻塞式方法

        //dp.getData():获取客户端发送的数据,返回值是一个字节数组

        ByteArrayInputStream bis = new ByteArrayInputStream(dp.getData());

        ObjectInputStream ois = new ObjectInputStream(bis);

        System.out.println(ois.readObject());

        //关闭资源

        ois.close();

        bis.close();

        ds.close();

    }

}

执行结果如图12-13所示:

图12-13 示例12-17~示例12-19运行效果图

第十二章 总结

1. 端口是虚拟的概念,并不是说在主机上真的有若干个端口。

2. 在www上,每一信息资源都有统一且唯一的地址,该地址就叫URL(Uniform Resource Locator),它是www的统一资源定位符。

3. TCP与UDP的区别

1)TCP是面向连接的,传输数据安全,稳定,效率相对较低。

2)UDP是面向无连接的,传输数据不安全,效率较高。

4. Socket通信是一种基于TCP协议,建立稳定连接的点对点的通信。

5. 网络编程是由java.net包来提供网络功能。

1)InetAddress:封装计算机的IP地址和DNS(没有端口信息!)。

2)InetSocketAddress:包含IP和端口,常用于Socket通信。

3)URL:以使用它的各种方法来对URL对象进行分割、合并等处理。

6. 基于TCP协议的Socket编程和通信

1)“请求-响应”模式:

--Socket类:发送TCP消息。

--ServerSocket类:创建服务器。

7. UDP通讯的实现

1)DatagramSocket:用于发送或接收数据报包。

2)常用方法:send()、receive()、 close()。

8. DatagramPacket:数据容器(封包)的作用

1)常用方法:构造方法、getAddrress(获取发送或接收方计算机的IP地址)、getData(获取发送或接收的数据)、setData(设置发送的数据)。

JAVA程序设计实战(10-13章)相关推荐

  1. JAVA程序设计实战(1-9章)

    目录 1.Java入门 1.1 Java发展简史 1.2 Java的核心优势 1.3 Java各版本的含义 1.4 Java的特性和优势 1.5 Java应用程序的运行机制 1.6 JDK.JAR和J ...

  2. 尚学堂(java)1到13章所有练习题答案详解

    尚学堂1到13章所有练习题答案详解 未完待续- 文章目录 尚学堂1到13章所有练习题答案详解 B站网课链接 第一章 一.选择题 二.简答题 三.上机操作 第二章 第三章 第四章 一.选择题 二.简答题 ...

  3. 尚学堂 实战java程序设计 第1,2章课后题答案

    第1章 一.选择题 1.C 2.AD 3.D 4.B 5.A 二.简答题 1.答:计算机语言总的来说分为机器语言,汇编语言,高级语言三大类.这三种语言是计算机语言发展历史的三个阶段. 2.答:java ...

  4. Java并发编程实战 第13章 显式锁

    接口Lock的实现类: ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock Reentra ...

  5. 201521123070 《JAVA程序设计》第13周学习总结

    1. 本章学习总结 以你喜欢的方式(思维导图.OneNote或其他)归纳总结多网络相关内容. 2. 书面作业 Q1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jm ...

  6. 201621123031 《Java程序设计》第13周学习总结

    作业13-网络 1.本周学习总结 以你喜欢的方式(思维导图.OneNote或其他)归纳总结多网络相关内容. 2. 为你的系统增加网络功能(购物车.图书馆管理.斗地主等)-分组完成 为了让你的系统可以被 ...

  7. 201521123013 《Java程序设计》第13周学习总结

    1. 本周学习总结 2. 书面作业 Q1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu.edu.cn,分析返回结果有何不同?为什么会有这样的不同? ping值 ...

  8. 201521123007《Java程序设计》第13周学习总结

    1. 本周学习总结 以你喜欢的方式(思维导图.OneNote或其他)归纳总结多网络相关内容. 2. 书面作业 1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu ...

  9. 201521123023《Java程序设计》第13周学习总结

    1. 本周学习总结 (1)网络中为了进行数据交换(通信)而建立的规则.标准或约定(=语义+语法+规则)称之为协议(常用http/ftp) (2)大致熟悉了TCP协议,但是UDP怎么辣么蓝,好理解却用不 ...

最新文章

  1. iOS学习之路十三(动态调整UITableViewCell的高度)
  2. 一些非常酷的GAN的应用
  3. HDU1576 A/B 费马小定理
  4. 程序员每天到底可以写几行代码?
  5. 【程序性能分析利器】Google Perf Tool 和 Valgrind 工具包简介
  6. linux维护rpm 数据库,Linux运维知识之linux rpm命令详细介绍
  7. 不好的女人只能消耗你,好的女人不仅可以给你一个家和孩子
  8. AD中按Y使器件竖直方向上镜像翻转
  9. futuretask java_Java并发编程一(FutureTask)
  10. 苹果Mac全能视频播放器:Playr
  11. qt解决中文乱码问题。总结一下
  12. MDK Pack安装包下载算法BUG
  13. 浅谈SQL语言的特点
  14. Python_乌龟和鱼游戏
  15. 解决在HD Audio模式下Windows系统前置面板耳机无声音的问题
  16. 服务器安装版u盘如何制作,U盘怎么安装原版Windows server 2016?
  17. linux底层播放器,[转载]媒体播放器三大底层架构:MPC、MPLAYER、VLC
  18. 24速算c语言实训报告ppt,原创:C语言速算24数据结构课程设计最终版
  19. 解决css样式中first-child和last-child不生效的问题
  20. kali linux网络扫描~无线网络扫描

热门文章

  1. 【Python游戏】在这款程序员游戏新作《现代空战—战机游戏》里,你可以体验一把紧张的空战感觉、刺激鸭~打飞机游戏都能有那么多骚操作……
  2. C++在linux上读写文件
  3. spring多例的销毁
  4. python爬虫爬取华硕笔记本信息
  5. 【NOI模拟赛】最小生成树(kruskal算法,线段树合并)
  6. 模式识别作业-线性分类器设计总结
  7. jsp火车时刻查询服务系统ssh
  8. 揭秘“白帽黑客”特训 入职可获年薪20万以上
  9. 微信小程序Cannot read property ‘data‘ of undefined;
  10. 「Python数据分析系列」6. 概率论基础介绍