HeadFirst Java
本人有C语言基础,通过阅读Java廖雪峰网站,简单速成了java,但对其中一些入门概念有所疏漏,阅读本书以弥补。
第一章 Java入门
第二章 面向对象
第三章 变量
第四章 方法操作实例变量
第五章 程序实战
第六章 Java函数库
第七章 继承与多态
第八章 深入多态
第九章 构造器与垃圾收集器
第十章 数字与静态
第十一章 异常处理
第十二章 GUI(内部类)
第十三章 Swing
第十四章 保存对象
第十五章 网络与线程
前言
终于来到一个超大头章节,网络与线程,后端开发的重要章节!这是让我们的程序连接到互联网世界的第一步!这一章以实现一个实时聊天程序为例子,介绍相关的内容~
连接、传送、接受
客户端的工作需要这三件事
1.与服务器建立连接
2.传送信息到服务器
3.从服务器接受信息
这之中涉及计算机网络的底层原理,但java.net包帮我们解决了,但还是应该深入去研究计算机网络相关知识。
建立Socket连接
Socket(java.net.Socket):是代表两个机器之间网络连接的对象
创建Socket连接需要知道机器在哪里,用哪个端口收发数据
Socket chatSocket = new Socket("196.164.1.103",5000);
TCP端口
网页服务器(HTTP)的端口号是80
TCP端口:16位宽的帮助客户端识别服务器特定服务程序的标志
不同程序不可共享一个端口
BufferedReader从Socket上读取数据
使用串流,Java的大部分I/O不在乎链接串流的上游
用PrintWriter写数据到Socket
相比读取,没有用缓冲区,原文说是每次都写入一个String,PrintWriter更标准
编写简单的服务器
需要一对Socket,一个等待用户请求(当用户创建Socket)的ServerSocket,一个与用户通信的Socket
对于服务器来说,这里1,3步的Socket是指一对Socket;第2步属于客户端做的事情
public void go() {try {ServerSocket serverSock = new ServerSocket(4242);while () {Socket sock = serverSock.accept();//停下来直到接收客户端请求//服务器创建出与客户端Socket通信的Socket,进行数据传送/*具体服务代码*/}}catch ()
}
具体代码 P486-
编写聊天客户端程序
第一版:可发送
客户端建立Socket连接到服务器,PrintWriter流获取Socket输出流
第二版:可发送和可接收
从服务器获得信息——与服务器Socket连接,BufferReader获取输入流
何时从服务器获得?
1.间隔查询
2.用户送出信息时,顺便读取信息
3.信息送到服务器上时读取
这里选择第三种方法,程序如何写呢?照前文提到的accept(),需要有个循环等待服务器信息,但GUI启动之后,只有事件触发才会有程序运转,循环应该放在哪里?
显然,我们这里需要同时执行两件事的程序,一个负责用户与GUI的交互,一个用于持续读取服务器的数据。这意味着我们需要新的线程与执行空间。
但现实我们并没有多个处理器CPU,Java的新线程并不会让OS多开一个进程,只是感觉像。这是一种用户态的多线程。具体的细节得学习操作系统,这里不多讲解。
线程与Thread
线程独立,对应独立的执行空间
Thread是Java中线程的类
多个执行空间?
真正的多线程(多空间)是多处理器并行执行多件事情。
实际:Java只是在底层OS上的执行的进程(即非并行),Java的线程实现模拟的并行(不同模拟空间的来回快速切换)
轮到Java进程时,JVM执行什么?——目前栈空间的最上层
如何启动新线程
Runnable job = new MyRunnable();//新建工作,runnable是接口
Thread thread = new Thread(job);//给工人安排工作,传入runnable(的run方法)
thread.start();//启动,执行工作(新空间第一个方法)
Thread是工人,Runnable是他的工作
Thread对象不可复用,线程的run()方法执行后,线程死亡。Thread对象还在堆上,一般方法可调用。
Runnable接口只有一个方法void run();
回忆:Runnable,为什么要作为接口?——便于多态,一种合约,不同的任务有不同实现方式的run()方法
不使用Runnable,用Thread子类实现run方法?——可以,但不够面向对象。从目的角度,理解为有特殊的工人,只执行特殊的任务。
线程的状态
新建——》可执行——》执行中
新建Thread对象
thread.start
JVM调度机制决定何时进入执行中状态
一、可执行与执行中交替
二、暂时不可执行(阻塞)
某些特殊原因,调转机制将进程 变为不可执行
线程调度器
调度器决定线程何时被执行,何时等待被执行,何时去阻塞。但它的机制是模糊的,没有API可以控制调度,就算同一台机器,其结果都是不可预测的。
经典的错误:在单一机器测试多线程程序,且认为其他机器的调度器有相同行为
图2第三部分,新建线程是被搁置的,这里写错了。
使用sleep()
让程序更好预测,强制当前进程进入一定时间阻塞。
可让所有线程有机会运行
会抛出InterruptException异常,需要try/catch块
多线程以及并发性问题
两个线程存取同一对象的数据,即两个执行空间对堆上同一对象执行getter,setter。这会引发严重的问题(如数据损毁)
关于数据的损毁,原书中举了一个有趣的例子,一对夫妻有公共资产,取钱,约定会先查询余额。丈夫查询后进入了睡眠,妻子在丈夫睡眠途中查询取钱。丈夫醒来不再查询,认为余额充足而取钱。最后导致直接负债。。。
相关代码P507,分析后的结论是:妻子或丈夫应该学会在另一方取钱前不进行查询操作,而这需要的是一个锁
对象的锁与同步化
synchronized(同步化),对方法声明修饰,让其像原子一样
原子(atomic):古典说法,不可分割之意
每个对象都有锁,可假设配有虚拟钥匙随时待命。大部分时间对象都没有被锁上。
锁只在同步化的方法上起作用,线程只有取得对象的锁(的钥匙),才能进入同步化的方法
具体的过程:
线程 调用 同步化方法
线程尝试取得 对象的锁(的钥匙)
若取得,其他线程不可进入 该对象的 任何同步化方法
完成方法后,解锁并释放钥匙
说是对象的锁,但其实锁住的是存取对象实例变量的方法(不让线程并发冲突)
经典并发问题:丢失更新问题
多个线程对余额进行+1操作
public class test implements Runnable {int balance;public void run() {inc();}public void inc() {//这里进行同步化修饰即可解决问题int i = balance;balance = i+1;}
}
同步化的代价
JVM查询钥匙的性能损耗
强制线程排队
死锁
局部同步化:
public void go() {do();//this.do();synchronized(this) {//参数写入对象,表示需要取用该对象的锁,this即当前对象(执行该行为的实例)//else}
}
死锁
两个线程互相持有对方需要的钥匙
处理死锁
数据库的方法:超时释放 + 事务回滚复原
Java:无处理机制,自行合理设计,学习《Java Thread》
其他
静态的方法可否同步化呢?
可以的,对应静态变量的保护,类也有锁(与钥匙)。线程需要取得类的锁,才能进入同步化的静态方法。
线程优先级
对调度器有一定影响,但依旧并不能确保准确性
为什么不把getter与setter同步化?
可以,但我们为什么同步化?是为了查询余额与提款 这两个不可分割的动作进行原子化。仅仅对getter与setter同步化,依旧会出现负债问题。
Singleton模式(单例模式)
class Accum {private static Accum a = new Accum();//静态实例private Accum() {}//私有构造函数
}
小结
本章很长!耐心看下来,很有收获,该书总能从为什么触发,一步一步引导你的思考。