Head First Java 和 AcWing Java课程做的总结8。
所有网络运作的低层细节都已经由java.net
函数库处理掉了。
传送与接收网络上的数据只不过是链接上使用不同的链接串流的输入输出。如果有BufferedWriter
和BufferedReader
就可以读取或写出。
用socket
来链接外面的世界,用多线程``multithread`来一边发消息,一边接收。
- 客户端的
socket
- 服务器的
socket
8.1 网络socket
连接
客户端建立Socket
连接:
-
Socket
是个代表两台机器之间网络连接的对象。(java.net.Socket)
-
网络设备:一种让运行在Java虚拟机上的程序能够找到方法去通过实际的硬件(比如网卡)在机器之间传送数据的机制。有人会负责这些低层的工作,比如操作系统的特定部分与Java的网络API。
-
//创建举例,参数是IP地址和端口号 Socket chatSocket = new Socket("192.164.1.103", 5000); //连接的建立代表两台机器之间存有对方的信息
-
TCP端口
- 一个16位宽,用于识别服务器上特定程序的数字;
- 0—1023已留给已知的特定服务;1024—65535自用;
- 逻辑上用来表示应用程序的数字;
- 举例:
HTTP:80;Telnet:23;POP3:110;SMTP:25
; - 不同程序不可以共享一个端口,绑定。
服务器端建立Socket
连接:
-
服务器端会有一对
Socket
- 一个等待用户请求的
ServerSocket
- 一个用户通信用的
Socket
- 一个等待用户请求的
-
工作方式
-
//服务器应用程序对特定端口创建出ServerSocket ServerSocket serverSocket = new ServerSocket(5000); /*服务器应用程序开始监听来自5000端口的客户端请求*/ //客户端对服务器应用程序建立Socket连接 Socket sock = new Socket("192.164.1.103", 5000); /*客户端得知道IP地址和端口号,在客户端代码中*/ //服务器创建出与客户端通信的新Socket Socket sock = serverSocket.accept();
-
accept()
方法会在等待用户Socket
连接时闲置着。当用户连上来时,此方法会返回一个Socket
(在不同的端口上)以便于客户端通信。Socket
与ServerSocket
的端口不相同,因此ServerSocket
可以空出来等待其他的用户。
-
读取Socket
:
-
使用
BufferedReader
从socket
上读取数据; -
Java的好处在于,大部分输入、输出工作不在乎链接串流的上游实际是什么。
-
//建立对服务器的Socket连接 Socket chatSocket = new Socket("127.0.0.1", 5000); //建立连接到Scocket上低层输入串流的InputStreamReader,将字节->字符 InputStreamReader stream = new InputStreamReader(chatSocket.getInputStream()); //建立BufferedReader来读取 BufferedReader reader = new BufferedReader(stream); //将BufferedReader链接到InputStreamReader String message = reader.readLine(); //关闭所有串流 reader.close();
写数据到Socket
:
-
使用
PrintWriter
写数据到Socket
上; -
每次都被写入一个
String
,所以PrintWriter
是最标准的做法 -
//对服务器建立Socket连接 Socket chatSocket = new Socket("127.0.0.1", 5000); //建立链接到Socket的PrintWriter PrintWriter writer = new PrintWriter(chatSocket.getOutputStream()); //PrintWriter是字符数据和字节间的转换桥梁,可以衔接String和Socket两端 //写入数据 writer.println("message to send:"); writer.print("another message");
8.2 客户端程序举例
//客户端代码
import java.io.*;
import java.net.*;
public class DailyAdviceClient{
public void go(){
try{
Socket s = new Socket("127.0.0.1", 5000);
InputSreamReader streamReader = new InputStreamReader(s.getInputStream());
//链接数据串流
BufferedReader reader = new BufferedReader(streamReader);
String advice = reader.readLine();
System.out.println("Today you should:" + advice);
reader.close();
}catch(IOException ex){
ex.printStackTrace();
}
}
public static void main(String[] args){
DailyAdviceClient client = new DailyAdviceClient();
client.go();
}
}
8.2 服务器程序举例
//服务器端代码
import java.io.*;
import java.net.*;
public class DailyAdviceServer{
String[] adviceList = {/*一堆字符串*/};
public void go(){
try{
ServerSocket serverSock = new ServerSocket(5000);
//服务器进入无穷循环等待服务客户端的请求
while(true){
Socket sock = serverSock.accept();
//卡在这知道请求到达
PrintWriter writer = new PrintWriter(sock.getOutputStream());
String advice = getAdvice();
writer.println(advice);
writer.close();
System.out.println(advice);
}
}catch(IOException ex){
ex.printStackTrace();
}
}
private String getAdvice(){
//返回一条建议
}
public static void main(String[] args){
DailyAdviceServer server = new DailyAdviceServer();
server.go();
}
}
8.4 多线程
上面服务器代码有一个非常严重的问题——它一次只能服务一个用户。
- 在没有完成目前用户的响应程序循环之前,它无法回到循环的开始出来处理下一个请求(无法进入
accept()
的等待来建立Socket
给新的用户) - 想要服务器能够同时处理多个用户——需要使用多个线程。
线程说明:
-
线程
(thread)
是独立的线程,代表一个独立的执行空间(stack)
; -
每个Java应用程序会启动一个主线程——将
main()
放在它自己执行空间的最开始处。Java虚拟机会负责主线程的启动(以及比如垃圾收集所需的系统用线程)。 -
除非计算机有多个处理器,否则Java上新的线程其实不会是运行在操作系统上独立的进程。
-
Java的
multithreading
-
//建立新的线程来执行 Thread t = new Thread(); t.start();
-
但是该线程并没执行任何程序,没有线程的任务——也就是独立线程要跑的程序代码。
-
-
java.lang.Thread
类java.lang
是默认就被import
的,它是包括String、System
等语言本身的基础的。Thread
是个表示线程的类,有启动start
、连接join
和让线程闲置sleep
的方法等。
-
多个执行空间
- 看起来像是有好几件事同时发生;
- 执行动作可以在执行空间非常快速的来回交换;
- Java也只是在低层操作系统上执行的进程;
- 轮到Java执行时,Java虚拟机中目前执行空间最上面的会被执行。
- 线程要记录的一项事物就是目前线程执行空间做到哪里。
线程的任务:
-
Runnable
对象(线程的任务)Runnable
是个接口;- 在创建
Thread
对象时,传入Runnable
对象; - 启动
Thread
,当调用线程的start()
方法后,新线程会把Runnale
对象的方法摆到新的执行空间中。 Runnable
带有会放在执行空间的第一项方法run()
。
-
Thread
对象需要任务,任务是线程启动时去执行的工作。该任务是新线程空间上的第一个方法,长得如下:-
public void run(){ //会被新线程执行的代码 }
-
Runnable
定义了一个协议,它是一个接口,只有上面所述的一个方法。 -
线程的任务可以被定义在任何实现
Runnable
的类上。线程只在乎传入给Thread
的构造函数的参数是否为实现Runnable
的类。 -
把
Runnable
传给Thread
的构造函数时,实际上就是再给Thread
取得run()
的方法。
-
-
实现
Runnable
接口举例:-
public class MyRunnable implements Runnable{ public void run(){ //线程要实现的任务 } } class ThreadTester{ public static void main(String[] args){ Runnable threadJob = new MyRunnable(); Thread myThread = new Thread(threadJob); myThread.start(); //调用start才会让线程开始执行,在此之前,他只是个Thread的实例,并不是真正的线程 System.out.println("back in main"); } }
-
-
Thread
对象不能重复使用,即一旦线程的run()
方法完成之后,该线程就不能再重新启动。过了改点后,线程就凉凉。Thread对象可能还待在堆上,还能接受某些方法的调用,但已经永远地失去了线程的可执行性,只剩下对象本身。 -
Thread
对象是可以命名的:setName()
和getName()
;Thread.currentThread()
用来表示当前正在执行的线程。
线程的状态:
-
新建
Thread t = new Thread(r);
Thread
的实例已经创建,但还没有启动,即有Thread
对象,没有执行中线程。
-
可执行
t.start();
- 当启动线程时,它会变成可执行状态,此时该线程已经布置好执行空间。
-
执行中
- 依靠Java虚拟机中的线程调度机制来决定。
-
一旦线程进入可执行状态,它会在可执行和执行中两种状态中来来去去。但也存在另外一种状态:暂时不可执行(又称为被堵塞状态)
-
线程调度器(scheduler)
- 没有API可以调用调度器,调度无法确定;
- 不能让程序依靠调度的特定行为来保持执行的正确性;
sleep
,让线程去睡个几毫秒,才能让所有线程都有机会被执行。sleep()
,传入ms指定的时间,可以让程序更加可预测;- 但这个方法可能会抛出
InterruptedException
异常,必须包含在try-catch
块中。 - 这个异常其实是API用来支持线程间通信的机制,但实际没人这么做。
- 单处理器的机器只能有一个执行中的线程。
另一种创建线程的方法:
- 用
Thread
的子类来覆盖掉run()
这个方法,然后调用Thread的无参构造函数来创建出新的线程。Thread t = new Thread();
- 这是另外一种创建线程的方法,是以面向对象的观点来建立的。将
Thread
做个子类来覆盖掉run()
是完全合法的。
线程的常用API:
-
start():开启一个线程、
-
Thread.sleep(): 休眠一个线程
-
join():等待线程执行结束
worker1.join()
:主线程会等待worker1执行完成后执行- join可以加最大等待时间作参数
-
interrupt():从休眠中中断线程
- 只能中断睡眠的线程
-
setDaemon():将线程设置为守护线程。当只剩下守护线程时,程序自动退出
8.5 并发性
线程会造成并发性的问题。
并发性(concurrency)问题会引发竞争状态,竞争状态会引发数据的损毁。
这一切都来自于一种状况:两个或以上线程存取单一对象的数据,也就是说两个不同执行空间上的方法都在堆上对同一个对象执行getter
或setter
。
锁机制:
-
确保线程一旦进入某方法后,就必须要能够在其他线程进入之前把任务执行完毕,让该方法跑起来像个原子;
-
使用
synchronized
这个关键词来修饰方法使它每次只能被单一线程存取。synchronized
关键词代表线程需要一把钥匙来存取被同步化(synchronized
)过的线程。- 要保护数据,就把作用在数据上的方法给同步化。
-
对象的锁:
- 每个Java对象都有一个锁,每个锁只有一把钥匙;通常对象没上锁。
- 对象的锁只会在有同步化的方法上起作用;当对象有一个或多个同步化方法时,线程只有在取得对象锁的钥匙时,才能进入同步化的方法。
- 锁住的是存取数据的方法;
- 当线程开始执行并遇上有同步化的方法时,会发生:
- 线程会认识到它需要对象的钥匙才能进入该方法。它会取得钥匙(这是由Java虚拟机处理的)。
- 当线程持有钥匙时,没有其他线程可以进入该对象的同步化方法,因为每个对象只有一个钥匙。
-
不把全部东西同步化的原因:
-
同步化的方法有额外的成本;
-
同步化方法会因为要同步并行的问题而慢下来;
-
可能导致死锁。
-
原则上,最好只做最少量的同步化。理论上,同步化规模可以小于方法。
-
public void go(){ doStuff(); synchronized(this){ criticalStuff(); moreCriticalStuff(); } }
-
-
-
死锁:
- 两个线程互相持有对方正在等待的东西;
- Java没有数据库的事务回滚机制;
- Java没有处理死锁的机制,甚至不知道死锁的发生。
-
静态变量:
- 每个被载入的类也有锁;
- 当要对静态的方法做同步化是,Java会使用类本身的锁;
- 如果同一个类有两个被同步化过的静态方法,则线程需要取得类的锁才能进入这些方法。
线程之间存在优先级,用来控制调度。
Synchronized
是一个语法糖。
-
将Synchronized加到代码块上
-
继承线程的写法
-
class Count { public int cnt = 0; } class Worker extends Thread { public final Count count; public Worker(Count count) { this.count = count; } @Override public void run() { synchronized (count) { for (int i = 0; i < 100000; i ++ ) { count.cnt ++ ; } } } } public class Main { public static void main(String[] args) throws InterruptedException { Count count = new Count(); Worker worker1 = new Worker(count); Worker worker2 = new Worker(count); worker1.start(); worker2.start(); worker1.join(); worker2.join(); System.out.println(count.cnt); } }
-
-
将Synchronized加到函数上(锁加到了this对象上)
-
实现Runnable接口的方法
-
class Worker implements Runnable { public static int cnt = 0; private synchronized void work() { for (int i = 0; i < 100000; i ++ ) { cnt ++ ; } } @Override public void run() { work(); } } public class Main { public static void main(String[] args) throws InterruptedException { Worker worker = new Worker(); Thread worker1 = new Thread(worker); Thread worker2 = new Thread(worker); worker1.start(); worker2.start(); worker1.join(); worker2.join(); System.out.println(Worker.cnt); } }
-
8.6 锁
- lock:获取锁,如果锁已经被其他线程获取,则阻塞
- unlock:释放锁,并唤醒被该锁阻塞的其他线程
import java.util.concurrent.locks.ReentrantLock;
class Worker extends Thread {
public static int cnt = 0;
private static final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 100000; i ++ ) {
lock.lock();
try {
cnt ++ ;
} finally {
lock.unlock();
}
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Worker worker1 = new Worker();
Worker worker2 = new Worker();
worker1.start();
worker2.start();
worker1.join();
worker2.join();
System.out.println(Worker.cnt);
}
}
wait
与notify
:
- 只能用在
synchronized
的代码中;
package org.yxc;
class Worker extends Thread {
private final Object object;
private final boolean needWait;
public Worker(Object object, boolean needWait) {
this.object = object;
this.needWait = needWait;
}
@Override
public void run() {
synchronized (object) {
try {
if (needWait) {
object.wait();
//另外线程执行结束后,被唤醒
System.out.println(this.getName() + ": 被唤醒啦!");
} else {
object.notifyAll();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
for (int i = 0; i < 5; i ++ ) {
Worker worker = new Worker(object, true);
worker.setName("thread-" + i);
worker.start();
}
Worker worker = new Worker(object, false);
worker.setName("thread-" + 5);
Thread.sleep(1000);
worker.start();
}
}
标签:Socket,Thread,Worker,网络,线程,new,多线程,public
From: https://www.cnblogs.com/whxky/p/16964947.html