文章

Java杂项

Java杂项

运行一串 java 代码时,系统怎么处理的?

题目等价于 Java 代码怎么运行的?JVM 是怎样运行 Java 代码的呢?

  1. 通过 javac 命令将 .java 源文件编译为字节码(文件后缀名为 .class)
  2. 通过 java 启动一个 JVM 将其运行起来
  3. JVM 运行字节码文件之前,有一个类加载器,将编译的字节码文件加载到 JVM 中,加载到方法区
  4. 通过字节码执行引擎,来按需执行那些加载到内存中的类

我们编写的Java代码到底是如何运行起来的? 你知道 Java 代码是如何运行的吗?

Java 事务

什么是事务?

事务(Transaction)是并发控制单位,是用户定义的一个操作序列,这些操作要么都做,要么都不做,是一个不可分割的工作单位。

事务的特性?

事务必须服从 ISO/IEC 所制定的 ACID 原则。ACID 是原子性(atomicity)、一致性(consistency)、隔离性 (isolation)和持久性(durability)的缩写。
1、原子性(Atomicity)
事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。
2、一致性(Consistency)
当事务执行失败时,所有被该事务影响的数据都应该恢复到事务执行前的状态。
3、隔离性(Isolation)
对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。
4、持久性 (Durability)
事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的持久性。

通俗的理解,事务是一组原子操作单元,从数据库角度说,就是一组 SQL 指令,要么全部执行成功,若因为某个原因其中一条指令执行有错误,则撤销先前执行过的所有指令。更简答的说就是:要么全部执行成功,要么撤销不执行。

Socket

什么是 Socket?

网络协议是分层次的,从 HTTP ->TCP/UDP ->IP ->MAC 层,实现了对数据的封装和分发。而套接字 Socket,实际上是以门面模式实现对 TCP/IP 协议的封装。

Socket 怎么验证安全性?

SSLSocket 和 SSLServerSocket

SSLContext 这个类是对安全套接字协议的实现,并扮演了一个安全套接字工厂的角色。

WebSocket?

随机数 Random

随机 1-10 的值

1
2
3
4
5
6
public static void main(String[] args) {
    Random random = new Random();
    for (int i = 0; i < 20; i++) {
        System.out.println(random.nextInt(10)+1);
    }
}

随机 0-10 的值

1
2
3
4
5
6
public static void main(String[] args) {
    Random random = new Random();
    for (int i = 0; i < 20; i++) {
        System.out.println(random.nextInt(11));
    }
}

Java 序列化

序列化和反序列化

序列化

将数据结构或对象转换成二进制串的过程

反序列化

将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

几种常见的序列化和反序列化协议

  1. XML&SOAP
  2. JSON
  3. Protobuf

Serializable 接口

是 Java 提供的序列化接口,它是一个空接口

1
2
public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列
化。

Serializable 特点

  1. 可序列化类中,未实现 Serializable 的属性状态无法被序列化/反序列化
  2. 要有个空参数构造函数
  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
30
31
32
33
34
35
36
37
public class Student implements Serializable {
    // serialVersionUID唯一标识了一个可序列化的类
    private static final long serialVersionUID = -2100492893943893602L;
    
    private String name;
    private String sax;
    private Integer age;
    // Course也需要实现Serializable接口
    private List<Course> courses;
    
    // 用transient关键字标记的成员变量不参与序列化(在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null)
    private transient Date createTime;
    
    // 静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,也
    就是它的成员变量因此序列化不会关注静态变量)
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
   
    public Student() {
        System.out.println("Student: empty");
    }
    public Student(String name, String sax, Integer age) {
        System.out.println("Student: " + name + " " + sax + " " + age);
        this.name = name;
        this.sax = sax;
        this.age = age;
        courses = new ArrayList<>();
        createTime = new Date();
    }
    //...
}
// Course也需要实现Serializable接口
public class Course implements Serializable {
    private static final long serialVersionUID = 667279791530738499L;
    private String name;
    private float score;
    // ...
}

serialVersionUID 与兼容性

  • serialVersionUID 的作用

serialVersionUID 用来表明类的不同版本间的兼容性。如果你修改了此类,要修改此值。否则以前用老版本的类序列化的类恢复时会报错:InvalidClassException

  • 设置方式

在 JDK 中,可以利用 JDK 的 bin 目录下的 serialver.exe 工具产生这个 serialVersionUID,对于 Test.class,执行命令:serialver Test

  • 兼容性问题

为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入 private static final long serialVersionUID 这个属性,具体数值自己定义。这样,即使某个类在与之对应的对象 已经序列化出去后做了修改,该对象依然可以被正确反序列化。否则,如果不显式定义该属性,这个属性值将由 JVM 根据类的相关信息计算,而修改后的类的计算 结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。不显式定义这个属性值的另一个坏处是,不利于程序在不同的 JVM 之间的移植。因为不同的编译器实现该属性值的计算策略可能不同,从而造成虽然类没有改变,但是因为 JVM 不同,出现因类版本不兼容而无法正确反序列化的现象出现

Externalizable 接口

1
2
3
4
public interface Externalizable extends Serializable {
    void writeExternal(ObjectOutput var1) throws IOException;
    void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException;
}

简单使用:

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
public class Course1 implements Externalizable {
    private static final long serialVersionUID = 667279791530738499L;
    private String name;
    private float score;
    // ...
    @Override
    public void writeExternal(ObjectOutput objectOutput) throws IOException {
        System.out.println("writeExternal");
        objectOutput.writeObject(name);
        objectOutput.writeFloat(score);
    }
    @Override
    public void readExternal(ObjectInput objectInput) throws IOException, ClassNotFoundException {
        System.out.println("readExternal");
        name = (String)objectInput.readObject();
        score = objectInput.readFloat();
    }
    // ...
    public static void main(String... args) throws Exception {
        Course1 course = new Course1("英语", 12f);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(out);
        oos.writeObject(course);
        course.setScore(78f);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bs));
        Course1 course1 = (Course1) ois.readObject();
        System.out.println("course1: " + course1);
    }
}

杂症

1、i++ 是线程安全的吗?

  1. 如果是方法里定义的,一定是线程安全的,因为每个方法栈是线程私有的,JVM 的栈是线程私有的,所以每个栈帧上定义的局部变量也是线程私有的,意味着是线程安全的。
  2. 如果是类的成员变量,i++ 则不是线程安全的,因为 i++ 会被编译成几句字节码语句执行
    1. 不是原子的,因为这个是分为三步,读值,+1,写值。在这三步任何之间都可能会有 CPU 调度产生,造成 i 的值被修改,造成脏读脏写
    2. volatile 不能解决这个线程安全问题。因为 volatile 只能保证可见性,不能保证原子性。
  3. 解决
    1. 锁:用 synchronized 或者 ReentrantLock 都可以解决这个问题。这里还可以比较下这两种方式的优劣。教科书式的比较结束后,来一句 “ 我认为一般使用 synchronized 更好,因为 JVM 团队一直以来都在优先改进这个机制,可以尽早获得更好的性能,并且 synchronized 对大多数开发人员来说更加熟悉,方便代码的阅读 “。讲讲 JVM 对 synchronized 的优化。
    2. AtomicInteger 为什么 AtomicInteger 使用 CAS 完成?因为传统的锁机制需要陷入内核态,造成上下文切换,但是一般持有锁的时间很短,频繁的陷入内核开销太大,所以随着机器硬件支持 CAS 后,JAVA 推出基于 compare and set 机制的 AtomicInteger,实际上就是一个 CPU 循环忙等待。因为持有锁时间一般较短,所以大部分情况 CAS 比锁性能更优。(最初是没有 CAS,只有陷入内核态的锁,这种锁当然也需要硬件的支持。后来硬件发展了,有了 CAS 锁,把 compare 和 set 在硬件层次上做成原子的,才有了 CAS 锁。)
本文由作者按照 CC BY 4.0 进行授权