JavaSE二周目计划

基础不能落下,习惯使用框架后基本功都忘得差不多,是时候复习一波了,跳着看的,重点放在多线程、IO、Socket 上;
这一篇是个开头,也正好以前学 SE 的时候还没搭博客所以也没 md 笔记,这次就顺便补上;
开头算是个补充,泛型、动态代理、悲观锁/乐观锁的小补充,后面是复习系列的多线程和 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
// 用在类上,使用时最好是显式指定
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}

// 用在方法上
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}

在 JDK1.7+ 后会自动根据 type inference 进行推导,所以说在调用方法是是可以不写泛型的
泛型的定义一般是在类名的后面,或者方法返回值之前,有了泛型,省去了很多的强转,在编译阶段就能发现错误
然后说一下比较重要的,泛型的上限和下限:

  • 上限(<? extends E>
    说明传入的可以是 E 或者 E 的子类
  • 下限(<? super E>
    说明传入的值可以是 E 或者 E 的父类

实现原理

在 C++ 中采用的方案是当实例化一个泛型会生成一个新类,例如模板类是 List ,然后你用 int ,double,string, Employee 分别去实例化, 那编译的时候,我们就会生成四个新类出来,例如List_int和List_double,List_string, List_Employee。

而在 Java 中采用的是擦除法,简单来说就是一个参数化的类型经过擦除后会去除参数, 例如 ArrayList<T> 会被擦除为 ArrayList ,既然这样类中使用的泛型怎么办,擦除了岂不是消失了?
最终其实泛型都会被替换为 Object,然后还有一个问题,比如 Integer i = list1.get(0); 现在类型被擦除,返回值变成 Object 了, 怎么处理啊?很简单,只需要在编译的时候做点手脚来个强转就行了,因为可以保证类型一致,否则也不会通过编译检测:Integer i = (Integer)list1.get(0);

数据类型

Java 中的两大类型:基本数据类型和引用数据类型,这个很熟悉都知道,在之前的 Java复习之内存 也说的比较清晰了,但是还有一些小的知识点漏掉了,现在就来补上
都知道字符串、数组、自定对象之类的都是引用数据类型,会存储在堆内存中,默认会进行初始化,能够赋为 null,我想要说的是,基本数据类型也有可能放在堆中,也会进行初始化,那就是当它被定义为全局变量的时候,所以你能在定义全局变量的时候写:int a; 默认为 0,而在方法内定义时必须要这样写:int a = 0; 给它一个初始值,但是不能设置为 null

动态代理&ASM

关于什么是动态代理、如何使用在前面的 Spring 文章和 Java 知识补充中已经说的很详细了,这里来补充一波没提到(写的好分散,忍忍吧,有时间再整理到一起
官方的动态代理基于接口实现(通过反射)
CGLib(Code Generation Library)采用的是继承的方式实现,CGLIb 为了提高性能,还用了一种叫做FastClass 的方式来直接调用一个对象的方法,而不是通过反射。


ASM 虽然很低调,但它确实很厉害,Spring , hibernate 的核心都是基于它来实现的,更不要说 AOP 了,以及上面的 CGLib 也是基于 ASM,那么它到底是什么呢?

它可以动态的修改已经编译过的 class , 还可以动态的生成新的 java class, 注意我说的动态这个词, 那可以是完全在运行时, 在内存中完成的, 这是一件非常厉害的本事。

并不是仅仅像 jsp 那样, 使用 JavaComplier 接口在运行时动态的编译一个 java 源代码

名字的由来:
C语言中的 __asm__ 这个关键字, 可以允许你们在 C 语言中写点汇编, 他就把 ASM 这个关键字挪用了
需要注意的是,想使用 ASM 需要非常透彻的理解 Java 虚拟机指令和 Java 虚拟机内部结构,我们常用的 Spring、CGLib 都是进行了高级的封装使用起来更加友好。
有很多的语言是利用 ASM 来动态的生成字节码(解释性语言),跑在 JVM 的虚拟机上

官方提供的文档:http://download.forge.objectweb.org/asm/asm4-guide.pdf

悲观锁和乐观锁

应该是和数据库相关了,但是多少有点联系,也找不到合适的地方就写在这里吧
悲观锁:
总认为数据会被别人修改,所以就总是给数据加锁,如果持有锁的时间过长,那么用户等待的时间也就越长
乐观锁:
本质其实是不加锁,但是会在字段的旁边加一个版本记录,读取时获取数值和版本,写入时先检查版本是否一致,如果不一致就重新获取重新计算;
这样的话如果冲突很多数据库争用激烈会导致不断的进行重试,反而降低了性能

Java 中的 Sychronized 可以看作是悲观,CAS(Compare and Swap) 操作可以看作是乐观。java.util.concurrent 包中借助 CAS 实现了区别于 synchronouse 同步锁的一种乐观锁
关于 CAS 其实还是蛮复杂的,不在深究,等以后如果用到了再看吧,这里给个网址和一点介绍吧

目前的处理器基本都支持 CAS,只不过不同的厂家的实现不一样罢了。
CAS 有三个操作数:内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做并返回 false。

http://ifeve.com/compare-and-swap/

匿名内部类&继承

要使用匿名内部类需要有一个前提条件:继承某个类或者实现某个接口
形式就是在需要某个类对象的时候直接 new 它的父类或者接口,然后去实现或者覆盖某些方法,并且一般需要实现的方法不会超过三个,否则阅读性极差,在回调机制中的用的较多,Android 上应该也有大量使用。
特殊的,因为所有的类都继承 object,所以甚至可以这么写:

1
2
3
new Object(){
public void show(){System.out.println("haha");}
}.show();

但是不能定义一个 Obj 类型的变量去接收(这就相当于隐式的向上转型了),这样里面的方法就没法调了


现在我知道了 JVM 在创建子类的时候首先会创建一个父类对象,并且放在子类对象的堆空间里(就是说是子对象的一部分)
然后在内存的方法区(分为静态方法区和非静态方法区),将 this 和 super 放在子对象拥有的空间里(和方法在一起),super 指向的就是父对象。
父类的 private 方法不能称之为覆盖,因为子类压根不知道有这个方法,称为覆盖不是很合适,就当做是子类的一个普通方法就行了,还有就是构造方法不能覆盖(构造方法和本类名一样,怎么可能覆盖 XD)
因为子类在创建时需要先创建父类,如果父类没有空的构造函数,需要在子类的构造函数中的第一行手动用 super 指定调用那个构造函数,也能看出一个规律: this() 和 super() 必须放在第一行

线程

先来说说线程的几种状态(还可细分,那就需要 JVM 的知识了):创建 、 运行 、 阻塞 、 冻结(睡眠、等待)、 消亡
说明一下,阻塞状态是拥有执行资格但是没有执行权;冻结是没有执行资格,也自然没有执行权;运行状态是有执行资格也有执行权(当前正在运行)。

然后就是重要的线程安全问题了,同步函数(函数上加 synchronized)默认使用的锁是 this,但是当函数被静态修饰的时候使用的不再是 this(这时内存中没有本类对象只有本类的字节码对象),用的是 className.class 字节码对象(相当于是某个类的 class 对象,也就是字节码对象),当其他地方需要用这把锁的时候就需要传入 XX.class 了。
在单例模式中的懒汉式,如果使用多线程访问就会存在安全性,但是如果将 get 方法加锁会变的低效,比较好的做法就是:

1
2
3
4
5
6
7
8
9
10
11
12
// 单例对象必须是 volatile 修饰的,保证在多核处理器安全运行
// 详细的原因可参考 二周目计划(二)
public XX getXX(){
if(xxx == null){
synchronized(XX.class){
if(xxx == null){
xxx = new XX();
}
}
}
return xxx;
}

因为 get 方法一般是静态的,所以我们使用本类的字节码对象作为锁,并且做双重判断来解决线程安全和同步函数低效的问题,一般还是用饿汉式吧,简单方便;一般来说如果你确定这个对象一定会用到那就肯定用饿汉式

死锁的出现一般是同步的嵌套,但是用的锁(顺序)不同的情况下,并且死锁并不一定会出现,万一和谐了呢….
只要是多个线程在处理同一个资源一般就需要进行加锁,不管这几个线程是不是在一个类中,并且还要保证锁是相同的,这才是线程安全的前提。
多个线程在操作共享数据的时候(并且是执行同一个方法)可以采用一个标志位来做等待唤醒机制,达到生产者生产一个,消费者消费一个,即使生产者和消费者有多个线程。

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
class Resource{
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name){
while(flag)
try{this.wait();}catch(InterruptedException e){}

this.name = name + count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
flag = true;
notifyAll();
}

public synchronized void out(){
while(!flag)
try{this.wait();}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);
flag = false;
notifyAll();
}
}

// 生产者
class Producer implements Runnable {
private Resource r;
Producer(Resource r){
this.r = r;
}
public void run(){
while(true){
r.set("烤鸭");
}
}
}

// 消费者
class Consumer implements Runnable {
private Resource r;
Consumer(Resource r){
this.r = r;
}
public void run(){
while(true){
r.out();
}
}
}

class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);

Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}

在 set 和 out 方法上加 synchronized 是为了保证不会出现生产一个消费两次,或者生产两个消费一次的情况。
就是说多个线程要参与同一件事就需要在 wait 之前循环判断标记(where,不能用 if),防止某个线程唤醒后直接往下走(此时另一个线程已经重新修改了标记)。
但是循环判断标记带来的另一个问题就是可能所有的线程都会一直处于 wait 状态(消费者唤醒的是消费者这种情况),就会出现和死锁差不多的情况,所以要使用 notifyAll。

补充:wait()notify() 这两个方法定义在 Obj 中,只有锁才能调用这两个方法,因为任何对象都可以是锁,所以这两个方法就只能定义在 object 中了吧

使用Locks

上面的是曾经的写法,在 JDK1.5+ 后出现了新的工具,所以说 1.5 真是里程碑式的升级,新的工具在 java.util.concurrent.locks 包下,最重要的就是 Lock 替代了 synchronized 的使用,Condition 替代了 Object 监视器(wait 、notufy)的使用.

JDK1.5 以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作
Lock 接口: 替代了同步代码块或者同步函数。将同步的隐式锁操作变成现实锁操作。同时更为灵活。可以一个锁上加上多组监视器
lock(): 获取锁。
unlock(): 释放锁,通常需要定义 finally 代码块中。
Condition接口:出现替代了 Object 中的 wait 、 notify 、 notifyAll 方法。将这些监视器方法单独进行了封装,变成 Condition 监视器对象。可以与任意锁进行组合。
await();
signal();
signalAll();

使用新特性改造代码,关键是可以绑定多个监视器,就可以指定唤醒对方的某个线程而不是全部唤醒了:

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.util.concurrent.locks.*;

class Resource{
private String name;
private int count = 1;
private boolean flag = false;

Lock lock = new ReentrantLock(); // 获取锁
//通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
Condition producer_con = lock.newCondition();
Condition consumer_con = lock.newCondition();

public void set(String name){
lock.lock();
try{
while(flag)
try{producer_con.await();}catch(InterruptedException e){}

this.name = name + count++;
System.out.println(Thread.currentThread().getName()+"...生产者5.0..."+this.name);
flag = true;
// 唤醒对方
consumer_con.signal();
}finally{
lock.unlock();
}
}

public void out(){
lock.lock();
try{
while(!flag)
try{cousumer_con.await();}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"...消费者.5.0......."+this.name);
flag = false;
// 唤醒对方
producer_con.signal();
}finally{
lock.unlock();
}
}
}

虽然可能感觉写法上麻烦了一些,但是这样确实是更清晰了

停止线程

真正的让线程停下来只有一种方法,那就是 run 方法结束,我们写的 run 方法一般都是循环结构,所以只要控制住循环就能让线程停下来。
特殊情况下,线程处于冻结状态(wait 或 sleep)然后没人唤醒的情况下怎么办,不能一直挂在这啊,Thread 类有个方法叫 interrupt 方法,作用就是强制唤醒冻结状态的线程,毕竟是强制,所以同时会抛出一个 InterruptedException 异常.
可以认为,只要出现了这个异常就说名有人在调用 interrupt 方法,也就是有人想让它停下来,所以直接在 catch 里让自己停下来就行了,比如修改标志

守护线程

简单介绍下什么是守护线程,守护线程在一般情况和用户线程没啥区别,都是在和主线程争夺 CPU ,关键是在结束的时候, 当主线程结束时,守护线程会自动结束,不管有没有执行完 ,有点像依赖关系。
设置一个线程为守护线程的方式为(必须要在 start 方法调用之前执行):t.setDaemon(true) 然后 start 就可以了,然后当程序的线程全是守护线程时,JVM 会自动退出。

其他

介绍些其他的方法,线程方法还有个 join 方法,作用可以理解为抢夺 CPU 的执行权,例如在主线程执行到了 t1.join(); 那么主线程会把执行权让给 t1 ,自己进入冻结状态,当 t1 执行完后再恢复执行。
设置优先级是:t.setPriority(Thread.MAX_PRIORITY); 优先级就 1-10,其中最明显的 1、5、10 都有常量定义,阅读性好些。
当线程执行到 Thread.yield(); 时,会自动的交出执行权。

IO流

JavaSE 中重要的一环,现在来复习一下,总体上可分为两类,字节流和字符流
字节流的两个基类:InputStream 和 OutputStream
字符流的两个基类:Reader 和 Writer
它们都是抽象的,然后一个一个来看。


然后除了上面的两大类,还有个桥梁,转换流,如果只有字节流的话就不需要转换了,所以转换流的体系是在字符流中,主要是:InputStreamReader 和 OutputStreamWriter;使用非常的简单,一般直接是扔进字符流的缓冲区包装类就行了,Bufr/Bufw 就都会用了。
最佳的测试方法就是用标准输入输出流了,System.in / System.out 它们都属于字节流,为了便于操作一般都将其转换为字符流,这样就用上上面的两个类了。
转换流还有一个好用的功能就是可以指定码表,这也许才是本体 2333

字符流

就以它的一个简单的孩子来看:FileWriter 对象,当它被 new 时会关联一个文件,此时文件会自动创建,如果存在会被覆盖,可通过其他的重载形式传入一个 Boolean(true) 来使用追加模式。
为了提高效率,一般都是用缓冲区,Java 提供了两个类来支持缓冲:BufferedWriter、BufferedReader
命名也是很有意思的,一般前面是功能描述,后面是所属的类别,比如上面的就是缓冲+字符流,大体就能知道是做什么的了。bufr 最爽的就是能一次读一行了(返回结果中不包含换行符 ,原理是用数组做缓存,还是一个个读,都存到数组中,遇到回车标记返回整个数组的数据),用的很频繁。
调用缓冲区的 close 方法就不需要调用 FileWriter 的 close ,本质上它们是一样的,真正操作文件的还是 fw。
缓冲功能的设计其实就是使用了装饰模式,对已有对象的功能进行增强。

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
public void WriterFile() {
String data = "xxxxxxxxxxxxx我是数据";
FileWriter fw = null;
try {
// true 代表以追加的形式写入,也可以使用一个参数的构造方法(不使用追加)
// 如果文件不存在此方法会自动创建文件
fw = new FileWriter("test.txt",true);
fw.write(data);

} catch (IOException e) {
e.printStackTrace();
}finally {
if (fw != null)
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public void ReadFile() {
FileReader fr = null;
try {
fr = new FileReader("test.txt");

// 单个读取
int ch = 0;
while ((ch = fr.read()) != -1){
// 自动查码表
System.out.print((char)ch);
}

// 使用缓冲区
char[] buf = new char[1024];
int len = 0;
while ((len = fr.read(buf)) != -1){
System.out.print(new String(buf,0,len));
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

// 使用JAVA提供的缓冲区
public void WriterFileBufr() {
FileWriter fw = null;
try {
fw = new FileWriter("test.txt");
BufferedWriter bufw = new BufferedWriter(fw);

for (int i = 0; i < 4; i++) {
bufw.write("test + " + i);
// 根据系统插入相应的换行
bufw.newLine();
bufw.flush(); // 使用缓冲区,记得刷新
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (fw != null)
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public void ReadFileBufr() {
FileReader fr = null;
try {
fr = new FileReader("tesst.txt");
BufferedReader bufr = new BufferedReader(fr);
String len;
while ((len = bufr.readLine()) != null) {
// 并不会读取换行符,所以需要手动换行
System.out.println(len);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

上面的仅供参考,最基本的 IO 读写文件的写法吧,使用字符流最后记得调用 flush 刷进去,因为它内部必定是有一小块缓存的,保证至少读完一个字符才写(不同的编码所占的空间不同,底层还是使用字节流来读取),并不是都一个写一个,大体就是这么个意思。

字节流

看过字符流后字节流也基本一致,API 高度相似,最大的区别就是字节流的缓冲数组是 byte 数组,字符流是 char 数组,原因也很好理解
单纯使用字节流写入并不需要刷新,字符流中一个中文字符是两个字节(编码不同而不同),所以他必须得先存起来(一个非常小的缓存吧),所以最后需要刷新,字节流没这个问题,可以直接写,毕竟字符流的底层也是用的字节流。
下面主要说说字节流缓冲区的使用,其他的和上面基本一致:

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
public static void copy() {
BufferedInputStream bufis = null;
BufferedOutputStream bufos = null;
try {
bufis = new BufferedInputStream(new FileInputStream("test.txt"));
bufos = new BufferedOutputStream(new FileOutputStream("test.txt"));

int len;
// 先把文件的部分内容读到缓冲区(字节数组),再从缓冲区一个一个的读
while ((len = bufis.read()) != -1) {
bufos.write(len);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if (bufis != null) {
try {
bufis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufos != null) {
try {
bufos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

至于缓冲区的实现原理,大概的代码可以参考:我的Github

其他

class 前面没有加任何的访问修饰符,通常称为“默认访问模式”(default ),在该模式下,这个类只能被同一个包中的类访问或引用,这一访问特性又称包访问性。
一个 .java 文件可以有多个类,但是只能有一个 public 类, 而且如果有 public 类的话,这个文件的名字要和这个类的名字一样,编译的时候会分开生成多个 class 文件

关于 IO,Java 中有一个 Properties 对象,利用它可以获取系统的“环境变量”,我想表达的就是可以看平台的默认文件编码集设置的是什么,一般中文系统默认是 GBK,所以要想输出其他编码格式的文件不能用 FileWriter,用转换流进行转换才行:

1
2
3
Properties prop = System.getProperties();
prop.list(System.out);
// new PrintStream("a.txt");

在处理异常的时候的那个对象也和 IO 有点关系,可以看看它的 API,封装的还是挺方便的。

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~