【网络】
① 要建立网络连接,需要知道两项关于服务器的信息:它在哪儿;它用哪个端口收发数据
即IP地址和TCP端口号
② 关于TCP(传输控制协议)端口 ↓
TCP端口号是个16位的值,用来指定服务器上特定的应用程序;
每个服务器都有65536个端口(0~65535),其中0~1023保留给特定的服务器,1024~65535是我们可挑选的;
一些众所周知的服务的端口号:HTTP:80;HTTPS:443;FTP:23
③ Socket是用于网络连接的对象
以下分别是:
使用BufferedReader从Socket读取数据和使用PrintWriter写数据到Socket
根据这个串流图,也能对串流连接有更进一步的理解
Java的好处在于大部分的输入/输出工作并不在乎链接串流的上游实际是什么。比如上图,可以放心使用BufferedReader,不用去在乎上游串流来自文件还是Socket
代码 ↓
// 读
try {
Socket chatSocket = new Socket("127.0.0.1", 5000); // 127.0.0.1代表本机
InputStreamReader stream = new InputStreamReader(chatSocket.getInputStream());
BufferedReader reader = new BufferedReader(stream);
String msg = reader.readLine();
System.out.println(msg);
reader.close(); // 不要忘记关闭 >_<
} catch(Exception e) {
e.printStackTrace();
}
// 写
try {
Socket chatSocket = new Socket("127.0.0.1", 5000);
PrintWriter writer = new PrintWriter(chatSocket.getOutputStream());
writer.println("Loli suki");
writer.println("FBI!OPen the door!");
writer.close();
} catch(Exception e) {
e.printStackTrace();
}
【多线程(multithreading)】
① Thread是个表示线程的类。ta有启动线程、连接线程、闲置线程等方法
② Java中每个线程都有独立的执行空间
③ 如何启动自定义的新线程?
❶ 写一个实现Runnable的类(Thread()需要一个任务,这个任务是一个Runnable对象)
❷ 重写run()方法(Runnable是一个接口,且只有一个run()方法,run()就是那个具体的任务;而且run()是抽象方法,必须被重写)
❸ 启动Thread()
④ 直接看一个典型的demo,立马就能明白上面的文字 ↓
class MyRunnable implements Runnable{ // 实现Runnable接口(根据is-A测试,就可以说这是一个Runnable类)
public void run() { // 必须重写覆盖的抽象方法:run()
go();
}
public void go() {
doMore();
}
public void doMore() {
System.out.println("Loli saikou!!!");
}
}
public class test {
public static void main(String[] args) {
Runnable threadJob = new MyRunnable(); // Runnable类作为"任务"
Thread myThread = new Thread(threadJob); // 创建Thread对象时,传入"任务"
myThread.start(); // 启动新线程
System.out.println("Loli suki!!!"); // 这是主线程中的语句
}
}
图解更清晰 :
注意:
❶ 输出结果是随机的:我们不能确定是"Loli saikou!!!"还是"Loli suki!!!"会先输出
因为新线程启动后,主线程和新线程便开始反复横跳
❷ 这是由调度器(scheduler)控制的;但调度器不能保证执行的时间和顺序,也没有任何API可以调用调度器;
甚至,调度器在同一个JVM中执行同一个程序也会有不同的做法
⑤ Thread对象可以重复使用吗?再start()一次?
不行。一旦线程的run()方法完成后,该线程就不能再重新启动。因为该线程结束一次后,作为一个线程,它彻底死了。
Thread对象可能还呆在堆上,如同活着的对象一般还能接受某些方法的调用,但已经永远失去了线程的执行性,只剩下对象本身。
【并发(concurrency)与同步化(synchronized)】
❶ 并发性(并行性)问题是多线程的典型问题。并发性问题会引发竞争状态,竞争状态会引发数据的损毁
❷ 两个经典的并发性问题:
❸ 怎样解决上面的问题呢?
他们需要对账户存取上一道锁(术语为monitor,即监视器)
❹ 因此引入了同步化(Synchronized)
使用Synchronized关键词修饰符可以防止两个线程同时进入同一对象的同一方法;基本原理如下:
同步化锁住的是方法而不是数据,当一个线程进入该方法后,取得钥匙并将该方法锁上;
另一个线程企图进入该方法时,会因为没有钥匙而一直处于等待状态;
❺ 锁有对象锁和类锁;上面说的是对象锁
注意,对象锁就是方法锁,不要理解为“这个对象被锁了”,被锁的是对象内部的同步方法(★)
❻ 方法锁让这个方法具有了原子性;事实上,当一个对象有多个同步化方法时,一个线程在访问其中一个时,另一个线程也无法访问这个对象的其他同步化方法(但可以被访问非同步方法)
也就说,在这个层面上,这多个同步化方法也获得了原子性(实质上锁多个同步方法,只用了一个锁)
因此,锁一个同步化方法,则这个对象的所有同步化方法都被锁,这就是“对象锁”名称的由来;
但不要从对象的层面去理解,要看清本质:锁的是方法,不是对象
❼ 类锁是锁住了多个实例对象
仔细想想,如果同步化的是一个静态方法,这个锁不就是类层面的了吗?
和对象锁一样,在层面上锁的是类层面,实质上锁的还是方法
❽ 非静态方法和静态方法的同步化是两个层面的(对象层面、类层面),且它们是彼此独立的。
也就说,在两个线程中,对同一个对象,我们是可以调用一个同步化的非静态普通方法和一个同步化的静态方法的
❾ 可以看出,当同步化一个方法后,不仅这个方法的内容成了原子,这个对象的所有被同步化的方法也成了原子
这种双重含义有时我们并不需要,有时候同步化代码块是个更好的选择:
synchronized(this){
int i = count;
count = i + 1;
}
❾ 同步化是有代价的。
- 同步化的方法会有额外的成本。比如查询“钥匙”对性能的损耗
- 同步化的方法会让你的程序因为要同步并行,而慢下来。话句话说,同步化会强制线程排队等待
- 最可怕的是死锁(deadlock)现象
关于死锁产生的过程:
线程A进入foo对象的同步化方法
↓
调度到线程B,线程B进入bar对象的同步化方法
↓
bar对象的方法需要调用foo对象的同步化方法,但线程A把foo的对象锁钥匙拿走了,线程B只能等待
↓
调度到线程A,foo对象的方法也需要调用线程B的方法,也没钥匙,也只能等待
↓
这样,不管此时调度器决定执行哪个线程,A或B都只能等待、僵持…
❿ 一个经典的考试(面试)题:
Question:当一个线程进入了对象的一个方法后,其他线程可否进入该对象的其他方法?
Answer:
总的来说分两种情况:
- 如果进入的这个方法被同步了(synchronized),则其他线程可以进入该对象的非同步的普通方法,不可以进入该对象的其他同步化方法;
- 如果进入的这个方法未被同步,在其他线程可以进入
再补充说明一下:
- 就算是其他方法是加了关键词的同步方法,若内部有wait,则其他线程可以进入;
- 静态方法和非静态方法的同步化是彼此独立的。上面的回答都建立在方法均为非静态普通方法的基础上。
☀ 《Head First Java》Kathy Sierra & Bert Bates