串讲补充


串讲补充

单例模式

单例模式的写法

请参考单例模式,内容包括单例的概念、用途、实现方式、如何防止被序列化破坏等。

单例的实际应用

数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因此用单例模式来维护,就可以大大降低这种损耗。

工厂模式

工厂模式分为3种,我们这里以简单工厂模式来讲解,大家知道这个就行了:

155764447104263

举例如下:(我们举一个发送邮件和短信的例子)
首先,创建二者的共同接口:

public interface Sender {
    public void Send();
}

其次,创建实现类:

public class MailSender implements Sender {
    @Override
    public void Send() {
        System.out.println("this is mailsender!");
    }
}
public class SmsSender implements Sender {
    @Override
    public void Send() {
        System.out.println("this is sms sender!");
    }
}

最后,建工厂类:

public class SendFactory {
    public Sender produce(String type) {
        if ("mail".equals(type)) {
            return new MailSender();
        } else if ("sms".equals(type)) {
            return new SmsSender();
        } else {
            System.out.println("请输入正确的类型!");
            return null;
        }
    }
}

我们来测试下:

public class FactoryTest {
    public static void main(String[] args) {
        SendFactory factory = new SendFactory();
        Sender sender = factory.produce("sms");
        sender.Send();
    }
}

输出:this is sms sender!

枚举

枚举作用

请参考 枚举作用

枚举实现

Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。

要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举:

public enum t {
    SPRING,SUMMER;
}

然后我们使用反编译,看看这段代码到底是怎么实现的,反编译后代码内容如下:

public final class T extends Enum
{
    private T(String s, int i)
    {
        super(s, i);
    }
    public static T[] values()
    {
        T at[];
        int i;
        T at1[];
        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
        return at1;
    }
    public static T valueOf(String s)
    {
        return (T)Enum.valueOf(demo/T, s);
    }
    public static final T SPRING;
    public static final T SUMMER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER
        });
    }
}

通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。

当我们使用enum来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。

枚举与单例

Joshua Bloch大神在《Effective Java》中明确表达过的观点:

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

如果你真的深入理解了单例的用法以及一些可能存在的坑的话,那么你也许也能得到相同的结论,那就是:使用枚举实现单例是一种很好的方法。

  • 枚举单例写法简单

    如果你看过单例模式中的实现单例的所有方式的代码,那就会发现,各种方式实现单例的代码都比较复杂。主要原因是在考虑线程安全问题。

    我们简单对比下“双重校验锁”方式和枚举方式实现单例的代码。

    “双重校验锁”实现单例:

    public class Singleton {  
        private volatile static Singleton singleton;  
        private Singleton (){}  
        public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
            if (singleton == null) {  
                singleton = new Singleton();  
            }  
            }  
        }  
        return singleton;  
        }  
    }
    

    枚举实现单例:

    public enum Singleton {  
        INSTANCE;  
        public void whateverMethod() {  
        }  
    }
    

    相比之下,你就会发现,枚举实现单例的代码会精简很多。

    上面的双重锁校验的代码之所以很臃肿,是因为大部分代码都是在保证线程安全。为了在保证线程安全和锁粒度之间做权衡,代码难免会写的复杂些。但是,这段代码还是有问题的,因为他无法解决反序列化会破坏单例的问题。

  • 枚举可解决线程安全问题

    上面提到过。使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?

    其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。

    那么,“底层”到底指的是什么?

    这就要说到关于枚举的实现了。这部分内容可以参考我的另外一篇博文深度分析Java的枚举类型—-枚举的线程安全性及序列化问题,这里我简单说明一下:

    定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。

    通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。

    而且,枚举中的各个枚举项同事通过static来定义的。如:

    public enum T {
        SPRING,SUMMER,AUTUMN,WINTER;
    }
    

    反编译后代码为:

    public final class T extends Enum
    {
        //省略部分内容
        public static final T SPRING;
        public static final T SUMMER;
        public static final T AUTUMN;
        public static final T WINTER;
        private static final T ENUM$VALUES[];
        static
        {
            SPRING = new T("SPRING", 0);
            SUMMER = new T("SUMMER", 1);
            AUTUMN = new T("AUTUMN", 2);
            WINTER = new T("WINTER", 3);
            ENUM$VALUES = (new T[] {
                SPRING, SUMMER, AUTUMN, WINTER
            });
        }
    }
    

    了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,我们在深度分析Java的ClassLoader机制(源码级别)Java类的加载、链接和初始化两个文章中分别介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

    也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。

    所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

  • 枚举可解决反序列化会破坏单例的问题

    前面我们提到过,就是使用双重校验锁实现的单例其实是存在一定问题的,就是这种单例有可能被序列化锁破坏,关于这种破坏及解决办法,参看单例与序列化的那些事儿,这里不做更加详细的说明了。

    那么,对于序列化这件事情,为什么枚举又有先天的优势了呢?答案可以在Java Object Serialization Specification 中找到答案。其中专门对枚举的序列化做了如下规定:

    687474703a2f2f7777772e686f6c6c6973636875616e672e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031382f30362f73657269616c697a6174696f6e2e706e67

    大概意思就是:在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.EnumvalueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve等方法。

    普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。

    但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。这部分内容在深度分析Java的枚举类型—-枚举的线程安全性及序列化问题中也有更加详细的介绍,还展示了部分代码,感兴趣的朋友可以前往阅读。

  • 总结

    在所有的单例实现方式中,枚举是一种在代码写法上最简单的方式,之所以代码十分简洁,是因为Java给我们提供了enum关键字,我们便可以很方便的声明一个枚举类型,而不需要关心其初始化过程中的线程安全问题,因为枚举类在被虚拟机加载的时候会保证线程安全的被初始化。

    除此之外,在序列化方面,Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。

枚举的序列化

我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。原文如下:

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored—all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

大概意思就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf方法:

	public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {  
            T result = enumType.enumConstantDirectory().get(name);  
            if (result != null)  
                return result;  
            if (name == null)  
                throw new NullPointerException("Name is null");  
            throw new IllegalArgumentException(  
                "No enum const " + enumType +"." + name);  
     }	

从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。

所以,JVM对序列化有保证。

枚举的线程安全

当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的

字节流和字符流有什么区别,如何相互转换呢?

  • 字节与字符

    Bit最小的二进制单位 ,是计算机的操作部分。取值0或者1

    Byte(字节)是计算机操作数据的最小单位由8位bit组成 取值(-128-127)

    Char(字符)是用户的可读写的最小单位,在Java里面由16位bit组成 取值(0-65535)

  • 字节流

    操作byte类型数据,主要操作类是OutputStream、InputStream的子类;不用缓冲区,直接对文件本身操作。

  • 字符流

    操作字符类型数据,主要操作类是Reader、Writer的子类;使用缓冲区缓冲字符,不关闭流就不会输出任何内容。

  • 互相转换

    整个IO包实际上分为字节流和字符流,但是除了这两个流之外,还存在一组字节流-字符流的转换类。

    OutputStreamWriter:是Writer的子类,将输出的字符流变为字节流,即将一个字符流的输出对象变为字节流输出对象。

    InputStreamReader:是Reader的子类,将输入的字节流变为字符流,即将一个字节流的输入对象变为字符流的输入对象。

项目的环境为UTF-8,需要读取文件step4_a.txt(编码为GBK)的内容,中文如何正确处理呢

这道题主要考核流里面要带编码,具体如下:
【答案解析】
这道题主要是考核字符流或者字节流,因为涉及编码转换,输入流要有编码,核心代码如下,请参看:
字节流方式

    File file = new File(path);  
    BufferedReader reader = new BufferedReader(new InputStreamReader(  
                    new FileInputStream(file), "GBK"));  
    String line = null;  
    String content = null;
    while ((line = reader.readLine()) != null) { 
        System.out.println(line);
        content += line + "/n"; ///n表示换行符
    }
    reader.close();

字符流读取文件

    InputStreamReader inrr = new InputStreamReader(new FileInputStream(file),"UTF-8");
    char bb [] = new char [(int) file.length()];
    int count =0;
    int temp=0;
    while((temp=inrr.read())!=(-1)){
      bb[count++]=(char)temp;
    }
    inrr.close();
    content = new String(bb);
    System.out.println("char方式二全部读取:/r/n"+content);

什么是序列化和反序列化?

序列化是将对象转换为可传输格式的过程。 是一种数据的持久化手段。一般广泛应用于网络传输,RMI和RPC等场景中。

反序列化是序列化的逆操作。

序列化是将对象的状态信息转换为可存储或传输的形式的过程。一般是以字节码或XML格式传输。而字节码或XML编码格式可以还原为完全相等的对象。这个相反的过程称为反序列化。

常见的序列化方式?

Java原生序列化

Java类通过实现Serializable接口来实现该类对象的序列化,这个接口非常特殊,没有任何方法,只起标识作用.Java序列化保留了对象类的元数据(如类、成员变量、继承类信息等),以及对象数据等,兼容性最好,但不支持跨语言,而且性能一般。

实现Serializable接口的类建议设置serialVersionUID字段值,如果不设置,那么每次运行时,编译器会根据类的内部实现,包括类名、接口名、方法和属性等来自动生成serialVersionUID。如果类的源代码有修改,那么重新编译后serial VersionUID的取值可能会发生变化。因此实现Serializable接口的类一定要显式地定义serialVersionUID属性值。修改类时需要根据兼容性决定是否修改serialVersionUID值:
1.如果是兼容升级,请不要修改serialVersionUID字段,避免反序列化失败。

2.如果是不兼容升级,需要修改serialVersionUID值,避免反序列化混乱。

使用Java原生序列化需注意,Java反序列化时不会调用类的无参构造方法,而是调用native方法将成员变量赋值为对应类型的初始值。基于性能及兼容性考虑,不推荐使用Java 原生序列化。

157535616348936

Hessian 序列化

Hessian 序列化是一种支持动态类型、跨语言、基于对象
传输的网络协议。 Java 对象序列化的二进制流可以被其他语言 ( 如 C++、 Python )反序列化。 Hessian 协议具有如下特性:
自描述序列化类型。不依赖外部描述文件或接口定义 , 用一个字节表示常用基础类型 , 极大缩短二进制流。

  • 自描述序列化类型。不依赖外部描述文件或接口定义 , 用一个字节表示常用基础类型 , 极大缩短二进制流。
  • 语言无关,支持脚本语言。
  • 协议简单,比 Java 原生序列化高效。

相比 Hessian 1.0, Hess ian 2.0 中增加了压缩编码,其序列化二进制流大小是 Java序列化的 50% , 序列化耗时是 Java 序列化的 30% ,反序列化耗时是 Java 反序列化的20% 。
Hessian 会把复杂对象所有属性存储在一个 Map 申 进行序列化。所以在父类、子类存在同名成员变量的情况下, Hessian 序列化时,先序列化子类 ,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖。

157535611710832

Json序列化

JSON ( JavaScript O同 ect Notation )是一种轻量级的数据交
换格式。 JSON 序列化就是将数据对象转换为 JSON 字符串。在序列化过程中抛弃了类型信息,所以反序列化时只有提供类型信息才能准确地反序列化。相比前两种方式,JSON 可读性比较好,方便调试。

IO模型

IO

什么是IO? 它是指计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。

在 Java 编程中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。

BIO

Java BIO即Block I/O , 同步并阻塞的IO。

BIO就是传统的java.io包下面的代码实现。

NIO

什么是NIO? NIO 与原来的 I/O 有同样的作用和目的, 他们之间最重要的区别是数据打包和传输的方式。原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。

面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。

一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

AIO

Java AIO即Async非阻塞,是异步非阻塞的IO。

区别和联系

BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。

NIO (New I/O):同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。

AIO ( Asynchronous I/O):异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。

各自适用的场景

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

使用方式

  • 使用BIO实现文件的读取和写入

            User1 user = new User1();
            user.setName("hollis");
            user.setAge(23);
            System.out.println(user);
            //Write Obj to File
            ObjectOutputStream oos = null;
            try {
                oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
                oos.writeObject(user);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                IOUtils.closeQuietly(oos);
            }
            //Read Obj from File
            File file = new File("tempFile");
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(new FileInputStream(file));
                User1 newUser = (User1) ois.readObject();
                System.out.println(newUser);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } finally {
                IOUtils.closeQuietly(ois);
                try {
                    FileUtils.forceDelete(file);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
           //Initializes The Object
            User1 user = new User1();
            user.setName("hollis");
            user.setAge(23);
            System.out.println(user);
            //Write Obj to File
            ObjectOutputStream oos = null;
            try {
                oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
                oos.writeObject(user);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                IOUtils.closeQuietly(oos);
            }
            //Read Obj from File
            File file = new File("tempFile");
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(new FileInputStream(file));
                User1 newUser = (User1) ois.readObject();
                System.out.println(newUser);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } finally {
                IOUtils.closeQuietly(ois);
                try {
                    FileUtils.forceDelete(file);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
  • 使用NIO实现文件的读取和写入。

    	static void readNIO() {
            String pathname = "C://Users//adew//Desktop//jd-gui.cfg";
            FileInputStream fin = null;
            try {
                fin = new FileInputStream(new File(pathname));
                FileChannel channel = fin.getChannel();
                int capacity = 100;// 字节
                ByteBuffer bf = ByteBuffer.allocate(capacity);
                System.out.println("限制是:" + bf.limit() + "容量是:" + bf.capacity()
                        + "位置是:" + bf.position());
                int length = -1;
                while ((length = channel.read(bf)) != -1) {
                    /*
                     * 注意,读取后,将位置置为0,将limit置为容量, 以备下次读入到字节缓冲中,从0开始存储
                     */
                    bf.clear();
                    byte[] bytes = bf.array();
                    System.out.write(bytes, 0, length);
                    System.out.println();
                    System.out.println("限制是:" + bf.limit() + "容量是:" + bf.capacity()
                            + "位置是:" + bf.position());
                }
                channel.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fin != null) {
                    try {
                        fin.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        static void writeNIO() {
            String filename = "out.txt";
            FileOutputStream fos = null;
            try {
                fos = new FileOutputStream(new File(filename));
                FileChannel channel = fos.getChannel();
                ByteBuffer src = Charset.forName("utf8").encode("你好你好你好你好你好");
                // 字节缓冲的容量和limit会随着数据长度变化,不是固定不变的
                System.out.println("初始化容量和limit:" + src.capacity() + ","
                        + src.limit());
                int length = 0;
                while ((length = channel.write(src)) != 0) {
                    /*
                     * 注意,这里不需要clear,将缓冲中的数据写入到通道中后 第二次接着上一次的顺序往下读
                     */
                    System.out.println("写入长度:" + length);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
  • 使用AIO实现文件的读取和写入

    public class ReadFromFile {
      public static void main(String[] args) throws Exception {
        Path file = Paths.get("/usr/a.txt");
        AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);
        ByteBuffer buffer = ByteBuffer.allocate(100_000);
        Future<Integer> result = channel.read(buffer, 0);
        while (!result.isDone()) {
          ProfitCalculator.calculateTax();
        }
        Integer bytesRead = result.get();
        System.out.println("Bytes read [" + bytesRead + "]");
      }
    }
    class ProfitCalculator {
      public ProfitCalculator() {
      }
      public static void calculateTax() {
      }
    }
    public class WriteToFile {
      public static void main(String[] args) throws Exception {
        AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
            Paths.get("/asynchronous.txt"), StandardOpenOption.READ,
            StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        CompletionHandler<Integer, Object> handler = new CompletionHandler<Integer, Object>() {
          @Override
          public void completed(Integer result, Object attachment) {
            System.out.println("Attachment: " + attachment + " " + result
                + " bytes written");
            System.out.println("CompletionHandler Thread ID: "
                + Thread.currentThread().getId());
          }
          @Override
          public void failed(Throwable e, Object attachment) {
            System.err.println("Attachment: " + attachment + " failed with:");
            e.printStackTrace();
          }
        };
        System.out.println("Main Thread ID: " + Thread.currentThread().getId());
        fileChannel.write(ByteBuffer.wrap("Sample".getBytes()), 0, "First Write",
            handler);
        fileChannel.write(ByteBuffer.wrap("Box".getBytes()), 0, "Second Write",
            handler);
      }
    }
    

测试环境,代码问题快速解决方法

1.可以用catalina.bat jpda start直接用debug模式启动tomcat,在tomcat的后台可以看到tomcat已经在8000端口进行监听。

157535597764913

2.然后用Eclipse进行远程调试即可

157535596674688

【备注】可以参照这个附件报里面的内容自己操作一遍,实际我们也会演示一下
项目和如何远程调试可以参照这个地址://down.bimowo.com/kejian/java1/sxxm/tsxg.zip

如果连接不上远程tomcat该如何判断问题出在什么地方呢:
一般是未能正确debug打开,要不就是本地有防火墙,不能通过端口访问
首先在本地判断debug端口是否打开 telnet 127.0.0.1 8000
然后首先ping 远程ip看看服务器能ping通么,然后再telnet 远程ip 端口看看能调试端口通么,一般到这步就没有问题了

Java生产环境下问题排查

在生产环境中,我们无法通过断点调试、新增log、可视化工具去立马查看当前的运行状态和拿到错误信息,此时,借助Java自带的命令行工具以及相关dump分析工具以及一些小技巧,可以大大提升我们排查问题的效率

  • 运行参数

    下面会列出一些常用且非常有效的命令以及参数来查看运行时Java程序的信息,从而辅助你了解程序运行状态。还有大量可用的功能由其他参数提供,自行参阅oracle文档

  • 查看JVM参数

    jps -l 查看所有正在运行的Java程序,同时显示启动类类名,获取到PID

    4706 org.apache.catalina.startup.Bootstrap
    5023 sun.tools.jps.Jps
    

    jinfo -flags PID 查看运行时进程参数与JVM参数

    Attaching to process ID 28987, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.171-b11
    Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=132120576 -XX:MaxHeapSize=2092957696 -XX:MaxNewSize=697303040 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=44040192 -XX:OldSize=88080384 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
    Command line:  -Dspring.config.location=application.properties -Dspring.profiles.active=staging
    

    java -XX:+PrintFlagsFinal -version 查看当前虚拟机默认JVM参数

  • 查看即时GC状态

    jstat -gc PID 1000 10 每秒查看一次gc信息,共10次

    输出比较多的参数,每个字段的解释参看 https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

     S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
    512.0  512.0   15.3   0.0    4416.0   1055.2   11372.0     7572.5   14720.0 14322.5 1664.0 1522.8     40    0.137   8      0.039    0.176
    

    期间可能碰到提示sun.jvm.hotspot.runtime.VMVersionMismatchException: Supported versions are 24.181-b01. Target VM is 25.171-b11的问题,原因在于安装了多个版本,使用whichls -l可简介定位到与当前执行Java程序相同的Java版本

  • 错误排查

    • 内存问题

      内存泄露导致OOM?内存占用异常的高?这是生产环境常常出现的问题,Java提供dump文件供我们对内存里发生过的事情进行了记录,我们需要借助一些工具从中获取有价值的信息。

    • 导出Dump文件

      如果Dump文件不太大的话,可以传到 http://heaphero.io/index.jsp 来分析

      文件比较大,且想进行更加系统的分析,推荐使用MAT分析,有如下几种常用查看方式

      1. 首页中的【Leak Suspects】能推测出问题所在
      2. 点击【Create a histogram from an arbitrary set of objects】查到所有对象的数量
      3. 右键点击某个对象【Merge Shortest Paths to GC Roots】-> 【exclude all phantom/weak/soft etc. references】能查询到大量数量的某个对象是哪个GC ROOT引用的
  • 线程问题

    任务长时间不退出?CPU 负载过高?很可能因为死循环或者死锁,导致某些线程一直执行不被中断,但是不报错是最烦人的,所以日志里看不到错误信息,并且又不能用dump文件分析,因为跟内存无关。这个时候就需要用线程分析工具来帮我们了。

    • 导出jstack文件

      使用jstack PID > 文件,如果失败请加-F参数,如果还失败请使用Java程序启动时使用的用户执行jstack,下面是jstack的部分输出格式

            线程名                                                              PID的16进制
      "http-nio-8080-Acceptor-0" #17 daemon prio=5 os_prio=0 tid=0x00007fac2c4bd000 nid=0x29f4 runnable [0x00007fac192f6000]
         java.lang.Thread.State: RUNNABLE(tomcat的工作线程正在运行,有NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WATING/TERMINATED状态)
          at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
          at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422)
          at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250)
          - locked <0x00000000faf845a8> (a java.lang.Object)
          at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:682)
          at java.lang.Thread.run(Thread.java:748)
      

      jstack的输出可以看到所有的线程以及他们的状态,我们就可以看有哪些我们自己创建的正在运行的线程,那很可能就是那个一直在执行的线程了,此时线程名就格外重要了,所以建议创建新线程时指定有意义的线程名。当然,通过PID查找也非常方便。

    • 排查步骤

      1. top 查看到哪个java程序负载高
      2. top -p PID -H 查看该进程所有进程的运行状态
      3. 记录下高负载的线程ID,printf "&x" PID转换成16进制
      4. jstack PID > 文件
      5. 在jstack文件中用转换成16进制之后的线程ID查询线程运行堆栈
      6. 从堆栈中了解到线程在执行什么任务,并结合业务与代码判断问题所在

泛型

泛型是什么,简单举例

泛型,即“参数化类型”,简单来说泛型就是函数的参数类型可以变化。

接口、类和方法也都可以使用泛型定义以及相应的使用。在具体使用时,可以分为泛型接口、泛型类和泛型方法,例如:
class Box { private T data; //get和set方法}
由于接收来自外部使用时候传入的类型实参。那么对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?参照如下范例:

	public static void main(String[] args) {
        // TODO Auto-generated method stub
        Box<String> name = new Box<String>("corn");
        Box<Integer> age = new Box<Integer>(712);
        System.out.println("name class:" + name.getClass());      // com.bimowu.showstudy.java3.Step3fan1$Box
        System.out.println("age class:" + age.getClass());        // com.bimowu.showstudy.java3.Step3fan1$Box
        System.out.println(name.getClass() == age.getClass());    // true
    }
    static class Box<T> { 
        private T data; 
         public Box() {
         }
         public Box(T data) {
             this.data = data;
         }
         public T getData() {
            return data;
        }
    }

输出为:

name class:class com.bimowu.showstudy.java3.Step3Fan1$Box
age class:class com.bimowu.showstudy.java3.Step3Fan1$Box
true

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

泛型举例

  1. 限定通配符,指的泛型类型必须用限定内的类型来初始化,否则会导致编译错误。
    1. 通配符上限,格式为<? extends T>,即类型必须为T类型或者T子类
    2. 通配符上限,格式为:<? super T>,即类型必须为T类型或者T的父类
  2. 非限定通配符,表示可以用任意类型来替代。例如类型为<?>

面试题:List<? extends T>和List <? super T>之间有什么区别 ?

回答:这两个List的声明都是 限定通配符的例子,List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List或List

反射

反射场景,反射作用

  • 什么是反射机制:

    反射是java中一种比较特别的机制,指的是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。

  • 什么时候用到反射机制?
    用于某些模块集成场合。当你不能在开发时即得到其目标类完整接口定义,只能根据命名规则去进行集成时。并可以延伸到包装、动态代理等模式的应用中。有时候也干些hack的事情,比如绕过private保护机制啥的。
    例如:Class.forName(“com.mysql.jdbc.Driver.class”).newInstance()就是反射。现在很多开发框架都用到反射机制,spring,struts,很多地方都是用反射机制实现的。

  • 利用反射机制能获得什么信息:
    一句话,类中有什么信息,它就可以获得什么信息,不过前提是得知道类的名字,要不就没有后文了。可以如下:
    在运行时判断任意一个对象所属的类;
    在运行时构造任意一个类的对象;
    在运行时判断任意一个类所具有的成员变量和方法;
    在运行时调用任意一个对象的方法;
    生成动态代理。

  • 【反射举例如下】

    public class TestReflect {
        public static final String KEY1 = "123";
        private String priData;
        public String pubData;
        public static void main(String[] args) throws Exception{
            // TODO Auto-generated method stub
            //取得本类的全部属性
            dealProperty();
            //取得本类的全部属性
            doMethod();
        }
        public static void doMethod() throws Exception{
            System.out.println("-----------------------");
            Class<?> clazz = Class.forName("com.bimowu.showstudy.java3.TestReflect");
            // 调用TestReflect类中的reflect1方法
            Method method = clazz.getMethod("reflect1");
            method.invoke(clazz.newInstance());
            // Java 反射机制 - 调用某个类的方法1.
            // 调用TestReflect的reflect2方法
            method = clazz.getMethod("reflect2", int.class, String.class);
            method.invoke(clazz.newInstance(), 20, "张三");
            // Java 反射机制 - 调用某个类的方法2.
            // age -> 20. name -> 张三
            //错误的调用方法reflect2
            method = clazz.getMethod("reflect2");//此处会报java.lang.NoSuchMethodException: com.bimowu.showstudy.java3.TestReflect.reflect2()
            method.invoke(clazz.newInstance(), 20, "张三");
            System.out.println("-------------------------");
        }
        public static void dealProperty() throws Exception{
            System.out.println("++++++++++++++++++++++++++");
            Class<?> clazz = Class.forName("com.bimowu.showstudy.java3.TestReflect");
            System.out.println("===============本类属性===============");
            // 取得本类的全部属性
            Field[] field = clazz.getDeclaredFields();
            //Field[] filed1 = clazz.getFields(); 如果是类似的用法,显示的是public的属性
            for (int i = 0; i < field.length; i++) {
                // 权限修饰符
                int mo = field[i].getModifiers();
                String priv = Modifier.toString(mo);
                // 属性类型
                Class<?> type = field[i].getType();
                System.out.println(priv + " : " + type.getName() + " : " + field[i].getName() + ";");
            }
            System.out.println("++++++++++++++++++++++++++");
        }
        public void reflect1() {
            System.out.println("Java 反射机制 - 调用某个类的方法1.");
        }
        public void reflect2(int age, String name) {
            System.out.println("Java 反射机制 - 调用某个类的方法2.");
            System.out.println("age -> " + age + ". name -> " + name);
        }
    }
    

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/278753.html

(0)
上一篇 2022年8月3日
下一篇 2022年8月3日

相关推荐

发表回复

登录后才能评论