20145107 《Java程序设计》 第六周学习总结

20145107 《Java程序设计》第六周学习总结

教材学习内容总结

本周进行了第十章,第十一章的学习。本周的学习目标是:
1.理解流与IO
2.理解InputStream/OutPutStream的继承架构
3.理解Reader/Writer继承架构
4.会使用装饰类
5.会使用多线程进行并发程序设计
本周的学习与第五周类似,多是采用一些新的方法来实现新的功能等内容。

第十章:

  • 串流设计的概念:
    在java中输入与输出被抽象化为串流,用通俗的例子来说,数据就好比是水,而串流就是水管,通过水管的链接,水由一端流向另外一端。如果要将数据从来源处取出,可以使用输入串流,要将数据写入目的地,则可以使用输出串流。在Java中,输入串流的代表对象为java.io.InputStream实例,输出串流的代表对象为java.io.OutputStream。无论数据源或目的地为何,只要设法取得两者的实例,那
    么接下来操作输入输出的方式都是一样的,不用去理会来源或目的地的真正形式。 在上述的前提下书中有一个通用的dump()的方法:
package Stream;

import java.io.*;

public class IO {
    public static void dump(InputStream src, OutputStream dest)
            throws IOException {
        try (InputStream input = src; OutputStream output = dest) {
            byte[] data = new byte[1024];
            int length;
            while ((length = input.read(data)) != -1) {
                output.write(data, 0, length);
            }
        }
    }
}

此方法接受了InputStream与OutStream实例,分别代表了读取数据的来源与输出数据的目的地。当两者实例相关操作发生错误时会抛出java.io.IoExpection异常,系统并不会做特别的处理,而是在dump()方法上声明throw,由调用dump()方法的客户端进行处理。但是,我们对dump()方法的来源与目的地并不清楚,也不知道,其来源于目的地的真正形式,而是依赖于InputStream,OutputStream。如果要将某个文档读入并另存为另一个文档,书中给出的程序实例如下:

package Stream;

import java.io.*;

public class Copy {
    public static void main(String[] args) throws IOException {
        IO.dump(
                new FileInputStream(args[0]),
                new FileOutputStream(args[1])
        );
    }
}

此程序可以由命令行自变量指定读取的文档来源与写出的目的地。若是要从HTTP服务器读取某个网页,并另存为文档,还可以使用如下方法:

package Stream;

import java.io.*;
import java.net.URL;

public class Download {
    public static void main(String[] args) throws IOException {
        URL url = new URL(args[0]);
        InputStream src = url.openStream();
        OutputStream dest = new FileOutputStream(args[1]);
        IO.dump(src, dest);
    }
}

  • 串流继承架构:
    我们在Java API中可以发现:System.in与System.out分别是InputStream与PrintStream的实例,分别对应了标准输入与标准输出。在本章的学习中,可以使用System的Setin()方法指定InputStream实例,重新指定标准输入来源,在下面的Standardln范例中故意将标准输入指定为FileInputStream,可以读取指定文档并显示在文本模式:
package Stream;

import java.io.*;
import java.util.*;

public class StanddardIn {
    public static void main(String[] args) throws IOException {
        System.setIn(new FileInputStream(args[0]));
        try (Scanner file = new Scanner(System.in)) {
            while (file.hasNextLine()) {
                System.out.println(file.nextLine());
            }
        }
    }
}

同理,故意将标准输出值指定文档的程序为:

package cc.openhome;

import java.io.*;

public class StandardOut {
    public static void main(String[] args) throws IOException {
        try (PrintStream file = new PrintStream(
                new FileOutputStream(args[0]))) {
            System.setOut(file);
            System.out.println("HelloWorld");
        }
    }
}

  • 串流处理装饰器:
    InputStream与OutputStream提供串流的基本操作,若想对其输入输出的数据进行操作,我们可以使用相应的打包器,常用的输入输出打包器有:BufferedInputStream和BufferedoutputStream,具备数据转换处理作用的:DataInputStream与DataOutputStream,具备对象串行化能力的objectInputStream与objectOutputStream。上述类并没有改变输入输出类的行为,只不过对得到的数据进行一些加工处理,因此又称他们为装饰器。
    1.BufferedInputStream和BufferedoutputStream:
    BufferedInputStream和BufferedoutputStream主要在内部提供缓冲区的功能,在操作上与InputStream和OutputStream并没有太大的差别书中给定了一个队IO进行改进的程序范例:
package Stream;

import java.io.*;

public class BUfferedIO {
    public static void dump(InputStream src, OutputStream dest)
            throws IOException {
        try(InputStream input = new BufferedInputStream(src);
            OutputStream output = new BufferedOutputStream(dest)) {
            byte[] data = new byte[1024];
            int length;
            while ((length = input.read(data)) != -1) {
                output.write(data, 0, length);
            }
        }
    }
}

2.DataInputStream与DataOutputStream:
DataInputStream,DataOutputStream用来装饰对InputStream,OutStream提供读取,写入Java基本类型的方法像是读写int,double等方法。书中有一个使用此类打包器的member的例子:

package Stream;

import java.io.IOException;
import static java.lang.System.out;

public class MemberDemo {
    public static void main(String[] args) throws IOException {
        Member[] members = {
                new Member("B1234", "Justin", 90),
                new Member("B5678", "Monica", 95),
                new Member("B9876", "Irene", 88)
        };
        for(Member member : members) {
            member.save();
        }
        out.println(Member.load("B1234"));
        out.println(Member.load("B5678"));
        out.println(Member.load("B9876"));
    }
}

相应的程序范例如图所示:

书中还有objectInputStream与objectOutputStream相应范例,因比较相似,不在此一一列举。

  • 字符处理装饰器:
    在Java中,对串流可以有装饰器,那么对字符串也有装饰器书中介绍了InputStreamREader与OutputStreamWriter,BufferedReader与BufferedWriter和PrintWriter这三种打包器。
    1.InputStreamREader与OutputStreamWriter:
    在建立InputStreamREader与OutputStreamWriter时可以指定编码,对字节数据转化为相应的编码字符。
    2.BufferedReader与BufferedWriter:
    BufferedReader与BufferedWriter可以将转换后的数据做缓冲处理,以增加读取效率。
    3.PrintWriter:
    PrintWriter与PrintStreame十分相似既可以对OutputStream打包,也可以对Writer打包。
  • Reader与Writer继承架构:
    针对字符数据的读取,Javase提供了java.io.Reader类,它抽象化了字符数据读入的来源。针对字符数据的写入,则提供了java.io.Writer类,其抽象化了数据写出的目的地。在教材中,若想从来源读入字符数据,或将字符数据写至目的地都可以使用下面的CharUtil.dump()方法:
package Stream;

import java.io.*;

public class CharUtil {
    public static void dump(Reader src, Writer dest) throws IOException {
        try(Reader input = src; Writer output = dest) {
            char[] data = new char[1024];
            int length;
            while((length = input.read(data)) != -1) {
                output.write(data, 0, length);
            }
        }
    }
}

在本程序中,dump()方法接受Reader与Writer的实例,分别代表了读取数据的来源与输出数据的目的地,当发生错误时,其处理办法与上面的public class IO程序中处理的方法类似。
书中有关于Reader与Writer的继承架构图,从图中得知FileReader是一种Reader,用于读取文档并将读到的数据转化为字符;StringWriter是一种Writer,可以将字符数据写至StringWriter,最后使用toString()方法取得字符串,代表所有写入的字符数据。所以,若要使用CharUtil.dump(),读如文档转为字符串并显示在文本模式中,可以如下:

package Stream;

import java.io.*;

public class CharUtilDemo {
    public static void main(String[] args) throws IOException {
        FileReader reader = new FileReader(args[0]);
        StringWriter writer = new StringWriter();
        CharUtil.dump(reader, writer);
        System.out.println(writer.toString());
    }
}

第十一章:线程与并行

在本章学习之前,我们所编写的所有程序都是单线程的程序即程序开始运行时从起点到终点只有一个流程,而在本章,告诉了我们如何进行多个流程的程序设计,也就是多线程程序。
在程序编写时,若是遇到了书中范例的龟兔赛跑的问题,我们在一开始可能会只编写一个单线程的程序,这个程序设计出来大致是这样的:

package Thread;

import static java.lang.System.out;

public class TortoiseHareRace {
    public static void main(String[] args) {
        boolean[] flags = {true, false};
        int totalStep = 10;
        int tortoiseStep = 0;
        int hareStep = 0;
        out.println("龟兔赛跑开始...");
        while(tortoiseStep < totalStep && hareStep < totalStep) {
            tortoiseStep++;
            out.printf("乌龟跑了 %d 步...%n", tortoiseStep);
            boolean isHareSleep = flags[((int) (Math.random() * 10)) % 2];
            if(isHareSleep) {
                out.println("兔子睡着了zzzz");
            } else {
                hareStep += 2;
                out.printf("兔子跑了 %d 步...%n", hareStep);
            }
        }
    }
}

就这个程序而言,它只有一个流程,它的循环控制为乌龟或兔子走完十步,比赛结束。按照程序由上而下的运行原则,每次都是乌龟先递增步数,然后再是兔子随机睡觉或走两步,两者好像并不是同步的,这样对兔子来说并不公平,所以,为了设计一个更加公平的比赛程序,我们希望两者可以不受程序运行过程中先后运行顺序的干扰,同时进行步数的移动。这里就用到了书中介绍到的多线程运行程序。
在Java的操作中,如果想在main以外独自设计流程,可以撰写类操作接口java.lang.Runnable。流程的进入点是操作在run()方法中,我们可以分别进行乌龟和兔子的流程操作设计乌龟的流程操作设计如下:

package Thread;

public class Tortoise implements Runnable {
    private int totalStep;
    private int step;

    public Tortoise(int totalStep) {
        this.totalStep = totalStep;
    }

    @Override
    public void run() {
        while (step < totalStep) {
            step++;
            System.out.printf("乌龟跑了 %d 步...%n", step);
        }
    }
}

在上面的程序中,乌龟的流程会从run()开始,乌龟在要专心负责走自己的步数就可以了。而相对于兔子来说,程序设计如下:

package Thread;

public class Hare implements Runnable {
    private boolean[] flags = {true, false};
    private int totalStep;
    private int step;

    public Hare(int totalStep) {
        this.totalStep = totalStep;
    }

    @Override
    public void run() {
        while (step < totalStep) {
            boolean isHareSleep = flags[((int) (Math.random() * 10)) % 2];
            if (isHareSleep) {
                System.out.println("兔子睡著了zzzz");
            } else {
                step += 2;
                System.out.printf("兔子跑了 %d 步...%n", step);
            }
        }
    }
}

在上面的程序中,兔子的程序运行流程会从run()开始,与乌龟不会相互影响。
为了使上面的程序中Tortoise与Hare,以及run()方法得到执行,我们还可以穿见下面的实例来执行Runnable定义的run()方法:

package Thread;

public class TortoiseHareRace2 {
    public static void main(String[] args) {
        Tortoise tortoise = new Tortoise(10);
        Hare hare = new Hare(10);
        Thread tortoiseThread = new Thread(tortoise);
        Thread hareThread = new Thread(hare);
        tortoiseThread.start();
        hareThread.start();
    }
}

程序范例的执行如下图所示:

在上面的程序中,主程序执行Main定义的流程,main()定义的流程中建立了tortoiseThread,hareThread两个线程,这两个线程又会分别执行上面两个程序中run()定义的流程,在这里注意,要想调用指定流程,则必须要用Thread实例的start()方法,程序才能运行。

  • synchronized与volatile:
    在6.25节中,我们有一个ArrayList的类,在没有学习到多线程的时候,如果单单在主线程中执行ArrayList,它没有什么问题,就如书中所说,如果有两个以上的线程同时使你用它就会有如下的效果:

在本程序中出现的问题就是线程存取同一个对象相同资源时所引发的竞速情况,就上面那个程序而言,t1,t2同时存取next,从而使得next脱离了原本管控的条件,这样的ArrayList类就是不具备线程安全的类。为了解决这个问题,我们可以在add()方法前面加上synchronized关键字,程序改进后如下:

package Thread;

import java.util.Arrays;

public class ArrayList {
    private Object[] elems;
    private int next;

    public ArrayList(int capacity) {
        elems = new Object[capacity];
    }

    public ArrayList() {
        this(16);
    }

    public synchronized void add(Object o) {
        if(next == elems.length) {
            elems = Arrays.copyOf(elems, elems.length * 2);
        }
        elems[next++] = o;
    }

    public Object get(int index) {
        return elems[index];
    }

    public int size() {
        return next;
    }
}

在运行相应的程序就不会出错了。

还有一个问题就是死结问题,如果某些资源在多线程下彼此交叉使用就有可能造成死结,书中有如下的范例:

package cc.openhome;

class Resource {
    private String name;
    private int resource;

    Resource(String name, int resource) {
        this.name = name;
        this.resource = resource;
    }

    String getName() {
        return name;
    }

    synchronized int doSome() {
        return ++resource;
    }

    synchronized void cooperate(Resource resource) {
        resource.doSome();
        System.out.printf("%s 整合 %s 的资源%n",
                this.name, resource.getName());
    }
}

public class DeadLockDemo {
    public static void main(String[] args) {
        Resource resource1 = new Resource("resource1", 10);
        Resource resource2 = new Resource("resource2", 20);
        
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                resource1.cooperate(resource2);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                resource2.cooperate(resource1);
            }
        });

        thread1.start();
        thread2.start();
    }
}

在idea中运行,程序运行如下:

教材学习中的问题和解决过程

在后面书中有关于并行API的知识,在里面的使用lock分治里解决了上面的程序出现的死结的问题,改进后的程序如下:

package Concurrency;

import java.util.concurrent.locks.*;

class Resource {
    private ReentrantLock lock = new ReentrantLock();
    private String name;

    Resource(String name) {
        this.name = name;
    }

    void cooperate(Resource res) {
        while (true) {
            try {
                if (lockMeAnd(res)) {
                    System.out.printf("%s 整合 %s 的资源%n", this.name, res.name);
                    break;
                }
            } finally {
                unLockMeAnd(res);
            }
        }
    }

    private boolean lockMeAnd(Resource res) {
        return this.lock.tryLock() && res.lock.tryLock();
    }

    private void unLockMeAnd(Resource res) {
        if (this.lock.isHeldByCurrentThread()) {
            this.lock.unlock();
        }
        if (res.lock.isHeldByCurrentThread()) {
            res.lock.unlock();
        }
    }
}

public class NoDeadLockDemo {

    public static void main(String[] args) {
        Resource res1 = new Resource("resource1");
        Resource res2 = new Resource("resource2");

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                res1.cooperate(res2);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                res2.cooperate(res1);
            }
        });

        thread1.start();
        thread2.start();
    }
}

相应的程序运行如下所示:

本周代码托管截图

本周代码合集

代码合集

本周代码统计

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 200/200 2/2 20/20
第二周 250/300 1/3 18/38
第三周 300/500 1/4 22/60
第四周 500/750 1/5 30/90
第五周 750/1200 1/6 20/110
第六周 1200/1600 2/8 30/140

参考资料

posted @ 2016-04-10 13:03  20145107lcd  阅读(201)  评论(2编辑  收藏  举报