Day18
网络编程
java.net.Socket是Java提供的用于进行网络编程的API
Socket编程可以让软件与软件之间以计算机为载体,以网络作为信息流动的通道进行远端数据传输
Socket
Socket中封装了TCP协议的通讯细节,使用它可以和远端计算机建立网络连接,并基于两条流(一条输入一条输出)的读写与对方进行数据交换
- 网络通信标准API;
- 进行可靠的网络通讯
Socket通讯
两端只有两个插口,中间实现过程不关注,只关注两端的插座
IP地址
IP地址用于标识网络上的计算机·
端口号
计算机内部对各个程序进行唯一标识,端口号就是每个程序的唯一标识
[!NOTE]
通讯时使用 ”IP+端口号“ 做到计算机之间的精确访问
客户端与服务端(c/s)
- 谁发起请求谁就是客户端,谁接收请求就是服务端
- 客户端与服务端之间为多对一的关系
客户端与服务端为不对等的,服务端的总机有许多Socket,用于满足客户端的访问需求
ServerSocket
java.net.ServerSocket
- 运行在服务端,相当于客户中心的总机,上面有若干插座(Socket),客户端与服务端连接就是与总机的插座连接,总机分配一个插座与之连接,保持双方通讯
ServerSocket的主要工作
- 创建时向系统申请服务端口,以便客户端可以通过端口找到
- 监听该端口,一旦一个客户端连接,便创建一个Socket,通过它与客户端通讯
常用方法
accept()
- 该方法为一个阻塞方法,调用后程序会暂停等待客户端使用Socket与之连接,此时accept会立即返回一个Socket通过返回的Socket就可以与连接的客户端双向通讯了
- 多次调用accept方法可以连接多个客户端
示例
try {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端已连接");
} catch (IOException e) {
e.printStackTrace();
System.out.println("连接建立失败");
}
客户端发送消息给服务端
原理
实现
try {
OutputStream outputStream = socket.getOutputStream();
//构建四层输出流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream,StandardCharsets.UTF_8);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
PrintWriter printWriter = new PrintWriter(bufferedWriter,true);
printWriter.println("你好!");
} catch (IOException e) {
e.printStackTrace();
System.out.println("获取输出流失败");
}
服务端接收客户端消息
原理
实现
try {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端已连接");
//构建三层文件接收流
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line = bufferedReader.readLine();
System.out.println("客户端发来的消息是:" + line);
int d = inputStream.read();
System.out.println(d);
} catch (IOException e) {
e.printStackTrace();
System.out.println("连接建立失败");
}
TCP和UDP
TCP和UDP的区别
- TCP丢包会重发,UDP丢包不会重发
- TCP传输会继续多次验证,丢包率低
- UDP单向传输,只发送一次
- TCP常用于少量数据传输,例如网站请求发送
- UDP常用于极大量数据发送,例如视频信号
TCP三次握手
三次握手用于基于TCP进行连接时使用的建立可靠连接方式,通过客户端于服务端之间的三次通讯可以建立较为稳定的网络连接用于通讯
原理
代码实现
代码中新建客户端Socket后会自动与服务端建立三次握手,保证可靠连接
new Socket(String host, int port)
TCP四次挥手
四次挥手用于基于TCP的网络连接在断开连接时使用的方式,通过客户端与服务端之间的四次通讯保证服务端的数据已完整的传输给客户端,避免了因结束通讯而导致数据丢包,提高了连接的稳定性
原理
代码实现
在客户端finally中添加close四次挥手代码,保证不管何时断开连接都会进行
try {
socket.close();//TCP四次挥手,释放资源
System.out.println("客户端已退出");
} catch (IOException e) {
e.printStackTrace();
}
Day19
多线程
线程
程序中一个单一的顺序执行流程
顺序执行
弊端:代码之间耦合太高,一块执行失败则全局失败
并发执行
原理:
- CPU一次只能执行一条指令
- 微观上走走停停,宏观上都在运行,视为并发,不是绝对意义上的“同时运行”
- 操作系统中的线程调度器将时间划分成很多时间块,尽可能均分给各个线程
多线程使用场景
- 多线程用于在一个程序中需要同时处理多个互不干扰的任务
- 多线程可以加快程序运行速度
多核并发
一块cpu上集成多个核心,即多个cpu放在一个主板上,即可真正实现多线程并行运行
创建并启动线程
方式一
- 定义一个类继承Thread
- 重写run方法来定义线程要执行的任务代码
- 调用线程的start方法启动线程(不可主动调run方法)
public class TheadDemo1 {
public static void main(String[] args) {
new MyThread0().start();
new MyThread1().start();
}
}
class MyThread0 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程名:"+Thread.currentThread().getName()+"\ti="+(i+1));
}
}
}
class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程名:"+Thread.currentThread().getName()+"\ti="+(i+1));
}
}
}
方式一弊端:
- 继承冲突问题:Java中为单继承规则,继承了线程Thread则不能继承其他类了
- 线程和线程任务之间存在耦合:会导致线程的重用性变低
方式二
该方式可以避免直接继承Thread类导致的继承冲突问题
- 类实现Runnable接口,并重写run方法
- 实例化要并发执行的任务
- 使用任务实例化线程
public class ThreadDemo2 {
public static void main(String[] args) {
/*
实例化线程要并发执行的任务
*/
MyRunnable0 runnable0 = new MyRunnable0();
MyRunnable1 runnable1 = new MyRunnable1();
/*
实例化线程
*/
new Thread(runnable0).start();
new Thread(runnable1).start();
}
}
class MyRunnable0 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程1:\t"+i);
}
}
}
class MyRunnable1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("*****线程2:\t"+i);
}
}
}
简化写法
Thread t3 = new Thread(() -> b2.doSome());
Thread t2 = new Thread(b2::doSome);
[!NOTE]
@FunctionalInterface
Java中标注该注解的类均可以使用lambda表达式创建
主线程
static Thread currentThread()
静态方法,使用Thread类名直接调用,返回对当前正在执行的线程对象的引用。
- java中所有代码都是线程运行的,main方法也不例外
- JVM启动后会自动创建一个名为main的线程,main方法就是在main线程中执行的,称为主线程
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println("主线程名称:" + thread.getName());//主线程名称:main
}
在方法中调用currentThread方法时无法直接指定线程,在运行时谁调用了该方法,则指向的就是哪个线程
public static void main(String[] args) {
showInfo();//指向main主线程,输出——>当前线程名称:main
}
private static void showInfo() {
Thread currentThread = Thread.currentThread();
System.out.println("当前线程名称:" + currentThread.getName());
}
线程常见命令
//获取线程基本信息
String threadName = thread.getName();//获取线程名
long threadId = thread.getId();//获取线程id
int threadPriority = thread.getPriority();//获取线程优先级,线程默认优先级为5,优先级范围为1~10
//判断线程状态
boolean alive = thread.isAlive();//获取线程是否存活,线程run方法执行完线程就会被杀死
boolean daemon = thread.isDaemon();//获取线程是否为守护线程
boolean interrupted = thread.isInterrupted();//获取线程是否被中断
补充——从A类向B类的方法中传参
- 通过在B类中添加B类的构造方法(含所需参数),
- 在A类中实例化B类对象时,将参数传给B类
设置线程优先级
调用setPriority方法设置线程的优先级,优先级越高执行次数越多
thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.NORM_PRIORITY);
thread3.setPriority(Thread.MAX_PRIORITY);
-
优先级最小值MIN_PRIORITY:1
-
优先级最大值MAX_PRIORITY:10
-
优先级默认值NORM_PRIORITY(线程创建默认):5
线程和进程的区别
-
线程是进程内的执行单元,一个线程中可以有多个进程,多个线程共享同一进程的资源
-
线程是一个动态概念,是程序中一个单一的顺序执行流程
进程时一个静态概念,是操作系统分配资源的基本单位
-
创建、切换和销毁进程的开销比较大,需要分配和回收整个进程自身的资源
创建、切换和销毁线程的开销相对较小,线程共享进程的资源,只需分配和回收栈和寄存器等线程私有的资源
-
多个进程可以同时进行,每个进程都拥有自己的的地址空间,互不干扰
多个线程共享同一进程的资源,可以并发执行,但可能需要同步和互斥来确保数据一致性
守护线程
-
守护线程的特点是:当进程中只剩下守护线程时,所有守护线程强制终止
-
GC就是运行在一个守护线程上的
-
正常常见线程时,默认都是用户线程。
-
通过setDaemon方法将线程设置为守护线程,设置守护线程的操作必须在线程执行前进行
jack.setDaemon(true); jack.start();
-
守护线程通常用于执行后台任务,比如垃圾回收线程。
-
守护线程与用户线程的区别在于,当进程中不存在任何用户线程时,守护线程会自动结束。
-
主线程是用户线程,在main方法中线程启动后主线程立即死亡
-
守护线程没有针对性,只有进程中的所有用户线程全部死亡,守护线程才会死亡,只要有用户线程还在,就不会杀死守护线程
线程阻塞
调用静态方法sleep对线程进行固定时间阻塞
该方法声明抛出一个InterruptedException异常,需要对该异常进行捕获并处理
方法格式
public static native void sleep(long millis) throws InterruptedException;
- 线程休眠sleep阻塞
- 让线程休眠指定的毫秒数,进入阻塞状态。
- 阻塞时间结束,线程自动苏醒进入就绪状态,等待CPU调度。
//倒计时,每隔1秒输出一个数字
for (int i = 5; i > 0; i--){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
线程唤醒
-
一个线程中使用sleep方法进行长时间的线程阻塞,并捕获InterruptedException异常
-
在另一个线程中添加interrupt方法将其唤醒,使用interrupt方法可以终端一条正在sleep阻塞的线程,此时sleep方法会立即抛出中断异常(唤醒),并执行中断处理代码
-
注:被唤醒的线程应该在前,否则后者无法访问到被唤醒线程
Thread thread1 = new Thread(){ try{ //... sleep(10000000000); } catch(InterruptedException e){ //唤醒后的操作 } }; Thread thread2 = new Thread(){ try{ //... } catch(InterruptedException e){ throw new RuntimeException(e); } thread1.interrupt };
线程的生命周期
调用yield方法可以让线程将剩余时间片直接释放
Thread.yield();
线程并发安全问题
- 当多个线程并发操作同一资源,由于线程切换的时机不确定,导致执行顺序出现混乱产生不良后果
- 临界资源:操作该资源的完成过程同一时刻只能被单个线程进行
- 常见临界资源:多线程共享实例变量、多线程共享静态公共变量
解决并发问题
synchronized锁的时对应的对象
方法1
弊端:对于方法中不需要同步执行的操作,会导致运行速度大大降低,浪费cpu资源
- 将异步运行转换为同步运行,即排队运行
- 在方法上加synchronized关键字,将该方法转变为同步方法
public synchronized boolean getMoney(int money){
if (money<=getAccount()){
this.account -= money;
Thread.yield();
save(account);
return true;
}
return false;
}
方法2
- 通过精确化同步范围,将固定的操作代码转为同步,可以避免非必要同步代码也同步执行
- 在需要同步的操作代码上加synchronized代码块
方法格式
参数可以传this或字符串直接量(“123”)
参数有效条件
- 引用类型
- 需要排队执行该代码的线程看到的都是一个对象(字符串直接量创建时放在常量池中,所以所有线程看到的是同一个)
- 在成员方法上使用synchronized时只能使用this
- 静态方法使用synchronized时同步监视器对象依然只能用this
- 当使用synchronized关键字修饰一个静态方法时,它将锁定整个类。这意味着当一个线程调用该方法时,其他线程将无法访问该类的任何静态方法,直到第一个线程完成对该方法的调用。即当一个线程调用静态方法时,它一定是同步的
- 静态方法中也可以使用synchronized代码块
synchronized(this){ //this指的是buy方法的所属对象
...
}
示例
void buy(){
try {
Thread thread = Thread.currentThread();
String name = thread.getName();
System.out.println(name+" 正在挑衣服...");
Thread.sleep(5000);
//使用同步代码块缩小同步范围
synchronized (this) { //this指的是buy方法的所属对象
System.out.println(name+" 正在试衣服...");
Thread.sleep(5000);
}
System.out.println(name+" 结账离开...");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
StringBuild和StringBuffer
- StringBuffer是线程安全的,而StringBuilder不是线程安全的。
- StringBuffer的方法中加入了同步锁synchronized,而StringBuilder的方法中则没有加入同步锁。
- StringBuffer是线程安全的,在多线程环境下,可以用来保证线程安全,而StringBuilder不是线程安全的,在多线程环境下,不能保证线程安全。
public StringBuilder append(String str) {
super.append(str);
return this;
}
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
集合在并发场景的操作
- ArrayList、LinkedList、HashSet均不是线程安全的,在多线程场景下均无法正常使用
- Collections类中有synchronizedXxx方法可以将对应的集合转为并发集合,需要将一个对应的普通集合作为参数传入方法中
HashSet<Object> set = new HashSet<>();
Set<Object> synchronizedSet = Collections.synchronizedSet(set);
互斥锁
- 当使用多个synchronized方法时,但是指定的同步监视器对象时同一个类,此时虽然对象调的时不同的方法,也需要同步排队执行
- 保证多个线程调用的是同一个同步监视器对象,否则无法保证互斥性。
public class SyncDemo6 {
public static void main(String[] args) {
Foo foo = new Foo();
Thread t1 = new Thread(foo::Method1);
Thread t2 = new Thread(foo::Method2);
t1.start();
t2.start();
}
}
class Foo{
public synchronized void Method1(){
Thread thread = Thread.currentThread();
String threadName = thread.getName();
System.out.println(threadName + "开始执行Method1");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(threadName + "执行Method1完毕");
}
public synchronized void Method2(){
Thread thread = Thread.currentThread();
String threadName = thread.getName();
System.out.println(threadName + "开始执行Method2");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(threadName + "执行Method2完毕");
}
}
Day20
Map接口
java.util.Map查找表
- Map是Java集合框架的成员
- Map是一种把键对象和值对象进行关联的对象。
- Map中不能包含重复的键;每个键都对应一个值。
- Map集合没有迭代器,但是可以通过entrySet()方法提取出key/value组成的Set集合中使用迭代器
- Map集合判断两个对象相等的标准是:两个对象通过equals()方法返回true。
结构
key | value |
---|---|
… | … |
特点
- key不允许重复
- 以key-value成对保存数据
- 根据key获取对应的value,key可以看成的value的索引
分类
根据内部数据结构不同,Map接口有很多实现类
- 内部为hash表实现为HashMap
- 内部使用链表维护顺序的HashMap是LinkedHashMap
- 内部为排序二叉树实现的是TreeMap
常用方法
- 使用put方法向已存在的key中添加value,会覆盖原value,并返回旧value
- 要使用包装类接收,避免空指针异常,因为若使用基本类型接收时,返回时会自动拆箱,若key不存在,则value为null,使用null拆箱会抛出异常
V put(K key, V value); //增
V get(Object key); //查
int size(); //获取map的大小
boolean isEmpty(); //判空
V remove(Object key); //删除键值对,返回被删元素对应的value
boolean containsKey(Object key); //判断指定key是否存在
boolean containsValue(Object value); //判断指定值value是否存在
map的遍历
-
只遍历key
Set<String> keySet = hashMap.keySet(); keySet.stream() .map(x->"key:"+x).forEach(System.out::println);
-
只遍历value
hashMap.values().stream() .map(x->"value:"+x) .forEach(System.out::println);
-
遍历key和value
hashMap.entrySet().stream() .map(x->"key:"+x.getKey()+" value:"+x.getValue()) .forEach(System.out::println);
-
JDK8之后可以使用Lambda表达式对Map进行遍历
hashMap.forEach((x, y) -> System.out.println("key:" + x + " value:" + y));
-
使用迭代器
Map集合没有迭代器,但是可以通过entrySet()方法提取出key/value组成的Set集合中使用迭代器
Iterator<HashMap.Entry<String, Integer>> iterator = hashMap.entrySet().iterator();
-
使用增强for循环
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) { System.out.println("key:" + entry.getKey() + " value:" + entry.getValue()); }
反射
- 反射是Java的动态机制
- 反射机制允许程序在运行期间确定对象实例化,方法调用,属性操作
- 反射机制可提高代码的灵活度,但运行效率慢,开销大,不能过度依赖反射
- Java大部分的框架中都使用了反射机制
Class类
- JVM中每个被加载的类都有且只有一个Class实例
- Class类的构造器是私有的,开发者不能主动实例化Class类的对象
- Class类的对象仅能由JVM创建
常用方法
-
获取类全限定名(包名.类名)
stringClass.getName();
-
仅获取类名
stringClass.getSimpleName();
-
获取包信息
Package aPackage = stringClass.getPackage(); String packageName = aPackage.getName();
-
获取类中所有公开方法
Method[] methods = stringClass.getMethods(); System.out.println("String类中所有公开方法有 " + methods.length+" 个"); for (Method method : methods) { System.out.println(method.getName()); }
-
使用forName创建反射
注:使用forName创建反射时需要输入字符串格式的类全限定名(包名.类名)
Scanner sc = new Scanner(System.in); String className = sc.nextLine(); System.out.println("请输入要创建对象的全限定名"); Class<?> aClass = Class.forName(className);