一、多线程的常用方法及API
java.lang.Thread 类中提供了大量的相关的方法:
new Thread();
new Thread(name);
new Thread(Runnable,name);
new Thread(Runnable)
常用方法:
getId() :获取线程的唯一标识
getName() :获取线程名
getPriority():获取线程的优先级: 优先级从1-10 , min-priority:1 max-priority:10 norm- priority:5 注意说明优先级高的获取到cpu资源的概率越大,并不是一定会优先执行完成。
currentThread():获取当前线程的对象引用
getState():获取线程的状态 (这是返回线程状态的枚举, NEW:未启动,RUNABLE:可运行 BLOCK:阻塞状态, WAITING:等待状态TIMED-WAITIN: 等待另一个线程执行完)
interrupt():中断这个线程
isInterrupted(): 返回boolean 测试当前线程是否中断
isAlive():该线程是否处于活动状态
isDaemon():判断该线程是否是守护线程
setDaemon():设置该线程是否是守护线程
join() :合并线程,使它变为单线程
sleep(ms) :让当前线程休眠 ,休眠时间到了,自动唤醒
yield(): 让出cpu资源,使当前线程处理可运行状态(可运行状态也随时可以获取cpu资源)
案例1: 测试线程的基本属性
System.out.println("当前主线程:"+Thread.currentThread().getName());
System.out.println("主线程id:"+Thread.currentThread().getId());
//设置主线程的线程级别
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
System.out.println("主线程的线程级别:"+Thread.currentThread().getPriority());
// 创建线程对象
MyThread my = new MyThread();
//设置线程名
my.setName("线程A");
//设置优先级
my.setPriority(10);
my.start();
MyThread my1 = new MyThread();
my1.setName("线程B");
//设置优先级 线程A 获取到资源的概率 大于线程B (大概率线程A优先执行完)
my1.setPriority(1);
//新生态
System.out.println("线程"+my1.getName()+"状态-----"+my1.getState());
my1.start();
//可运行状态(就绪)
System.out.println("线程"+my1.getName()+"状态-----"+my1.getState());
for(int i = 0;i<100;i++){
System.out.println("主线程------"+i);
}
守护线程
案例2: 守护线程
线程类型分为两种 一种是用户线程一种是守护线程,用户线程是执行某一个任务的独立代码 ,守护线程是用于守护用户线程的线程, 它的特点是 当用户线程执行完毕后守护现在自动结束,当用户线程没有执行完, 守护线程也不会停止
操作系统中有守护进程 ,用于操作系统的运行,只有关机进程自动结束,这里守护线程和守护进程类似。
//创建线程对象
DaemonThread daemonThread = new DaemonThread();
//设置该线程为守护线程 守护的是与它并行的线程类 ,当主线程或其他线程执行完毕,守护线程自动结束
// daemonThread.setDaemon(true);
System.out.println("是否是守护线程:"+daemonThread.isDaemon());
daemonThread.start();
for(int i=0;i<100;i++){
System.out.println("主线程i------"+i);
}
活动的线程总数: Thread.activeCount()
线程中断
案例3: 关于终止线程
线程中止就是当线程运行时由于满足特定的条件需要停止运行,此时我们需要考虑如何安全的中止线程这里中止线程提供几个方法
方法1 : 打标记中断法
线程运行1000,当程序达到500时,中止程序
public class ThreadEnd extends Thread {
@Override
public void run() {
boolean isOver=false;
for(int i = 0 ;i<1000;i++){
if(i>=500){
isOver= true;
return ;
}
System.out.println("线程结果i-----------"+i);
}
System.out.println("正常结束");
}
public static void main(String[] args) {
ThreadEnd th = new ThreadEnd();
th.start();
}
}
方法2: 异常中断法
- interrupt() :给线程打一个中断标记,不会立马中断
- interrupted() : 检测线程是否中断,并清除中断标记,返回boolean ,如果线程打标记了,就返回true
- isInterrupted() : 检测线程是否中断,但不清除中断标记, 返回boolean
注意用法: interrupted() : 它所处于的位置,对应于它作用的位置 ,通过线程类名调用
interrupt() 和 isInterrupted() : 使用线程对象调用。
public class Thread1 extends Thread {
@Override
public void run() {
int i =0;
while(true){
System.out.println("线程--------------"+i);
//判断当前线程是否有中断标记 ,但是不清除中断标记
if(this.isInterrupted()){
// 通过抛出异常或 break
System.out.println("当前线程打中断标记,可以停止了");
break;
}
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 th = new Thread1();
th.start();
// 休眠一会儿
Thread.sleep(2000);
//给th打中断标记
System.out.println("打标记");
th.interrupt(); //给th打标记
}
3个方法的用法
// Thread.currentThread().interrupt();
// System.out.println("判断当前线程是否打标记 (清除标记):"+ Thread.interrupted());
System.out.println("判断线程是否打标记(不清除标记)"+ Thread.currentThread().isInterrupted());
System.out.println("判断当前线程是否打标记 (清除标记):"+ Thread.interrupted()); // 静态方法
join用法
案例四: join的用法: 合并当前线程 ,使其变为单线程 ,哪个线程调用join方法,就立即将该线程剩下的部分执行完成,再执行其他线程
public class ThreadJoin extends Thread {
@Override
public void run() {
ThreadJoin2 th = new ThreadJoin2();
th.setName("线程C");
th.start();
for(int i=0;i<100;i++){
try {
th.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadJoin threadJoin = new ThreadJoin();
threadJoin.setName("线程A");
threadJoin.start();
// ThreadJoin threadJoin2 = new ThreadJoin();
// threadJoin2.setName("线程B");
// threadJoin2.start();
for(int i=0;i<100 ;i++){
if(i==50){
// 合并线程 (threadJoin线程的所有代码合并到 主线程中,先执行threadJoin线程)
threadJoin.join();
}
// if(i==70){
// threadJoin2.join();
// }
System.out.println("main---"+i);
}
}
sleep用法
案例五: sleep的用法: 用于休闲当前线程 ,休眠时间结束后自动唤醒继续执行,如果同时有多个线程执行 ,如果线程没有同步的情况下,相互休眠不影响,资源被公用。
public static void main(String[] args) {
for(int i =0;i<10;i++){
try {
//让当前线程休眠200毫秒 200毫秒后自动唤醒线程 继续执行
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}
class ThreadSleep implements Runnable{
@Override
public void run() {
for(int i =0;i<100;i++){
try {
Thread.sleep(1000); // 当前线程休眠时 不影响其他线程执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"----"+i);
}
}
}
ThreadSleep obj = new ThreadSleep();
Thread th1 = new Thread(obj , "线程A");
Thread th2 = new Thread(obj , "线程B");
th1.start();
th2.start();
yield用法
案例六:yield的用法 : 出让cpu, 让当先线程变为可运行状态 ,并也可以继续抢占cpu资源
public static void main(String[] args) {
ThreadYiled th = new ThreadYiled();
th.start();
// yield 让出cpu资源
for(int i = 0;i<100;i++){
if(i==50){
//主线程让cpu
System.out.println("让出cpu");
Thread.currentThread().yield();
}
System.out.println("主线程----"+i);
}
}
二、线程同步
线程并发场景:
在实际开发中,很多时候会出现多个线程同时访问同一个内存空间(变量)的场景,当他们同时对数据进行更新时,可能会出现数据不安全问题 ,例如经典的银行取钱案例
假设有一个账户(Account) ,余额是1100元,有小明和小明的爸爸同时取钱,小明拿着银行卡取ATM机取钱,小明的爸爸拿着存折取柜台取钱 ,他们都需要取1000块,小明在取钱时 系统会判断是否余额大于1000,如果大于,可以取钱,由于取钱需要一个过程,此时正好小明的爸爸也对该账户取1000,由于小明没有完成取钱的操作,卡里的钱还没有及时更新提交,所以小明的爸爸也可以通过系统判断的验证, 余额也大于1000,小明的爸爸也可以取钱,所以现在可能会出现他们两个人都取出1000元,导致账户数据不完整,这就是线程编发导致的问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3RiDqgG3-1605449529568)(assets/1604913153658.png)]
使用代码模拟场景
1、先有一个账户 (卡号,余额)
2、取钱的任务 ,由于需要使用同一个账户 ,这里的任务中有一个相同的账户 。
同步的解决办法:
1、将需要操作公共资源的代码增加 “同步锁” (也叫互斥锁)
语法:
synchronized(对象锁){
代码块
}
注意这里的对象锁必须满足 两个线程是同一个对象(同一把锁)
public void run() {
System.out.println("开始取钱了");
// 增加互斥锁,协同步伐 ,这个锁必须是公有的对象
synchronized(account) {
//先判断账户余额是否足够
if (account.getMoney() >= 1000) {
System.out.println(Thread.currentThread().getName() + "可以取钱");
System.out.println(Thread.currentThread().getName() + "正在取钱");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新账户余额
account.setMoney(account.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() +
"取到了钱,卡里余额还剩:" + account.getMoney());
} else {
System.out.println("抱歉,卡里余额不足,不能取1000元");
}
}
// 以上代码需要将操作通过资源的代码块 增加同步关键字
}
2、 同步方法
在方法的返回值前面增加 “synchronize” , 此时的锁代表的是当前this对象
public synchronized void get(){
//先判断账户余额是否足够
if (account.getMoney() >= 1000) {
System.out.println(Thread.currentThread().getName() + "可以取钱");
System.out.println(Thread.currentThread().getName() + "正在取钱");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新账户余额
account.setMoney(account.getMoney() - 1000);
System.out.println(Thread.currentThread().getName() +
"取到了钱,卡里余额还剩:" + account.getMoney());
} else {
System.out.println("抱歉,卡里余额不足,不能取1000元");
}
}
同步代码块和同步方法的区别:
1、语法不同,同步代码块更灵活,可以自定义锁对象 ,而同步方法不可以指定锁对象
二、线程死锁
线程死锁的产生
线程同步可以帮助我们解决多个线程操作同一个资源而导致数据不安全的问题,但线程同步也有可能产生隐患,假如一个线程中出现多个锁对象时,可能出现锁使用不当,导致锁与锁之前相互等待对方释放资源,从而形成一种 “相互等待”的僵局,这就是线程死锁。 例如哲学家吃饭
模拟线程死锁
public class DeadThread implements Runnable {
Object obj1 = new Object();
Object obj2 = new Object();
@Override
public void run() {
// 模拟线程死锁
// 如果当前线程为线程A 先拿到obj1锁 ,等待obj2锁资源
// 如果当前线程为线程B 先拿到obj2锁 ,等待obj1锁的资源
if(Thread.currentThread().getName().equals("线程A")){
synchronized (obj1){
System.out.println(Thread.currentThread().getName()+"拿到了obj1的锁");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我在等待obj2锁。。。。。");
synchronized (obj2){
System.out.println("我已经拿到了obj2锁。。。。");
}
}
}else{
//线程B
synchronized (obj2){
System.out.println(Thread.currentThread().getName()+"拿到了obj2的锁");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我在等待obj1锁。。。。");
synchronized (obj1){
System.out.println("我已经拿到了obj1锁,我和开心");
}
}
}
}
}
public static void main(String[] args) {
DeadThread dead = new DeadThread();
Thread th1 = new Thread(dead,"线程A");
Thread th2 = new Thread(dead,"线程B");
th1.start();
th2.start();
}
之所以会发生死锁,因为对象锁直接没有良好的“沟通”,导致互相获取对方的锁 ,进入等待中 ,可以通过线程类的几个方法 解决线程之间通信问题
三、线程通信的几个方法
wait() : 让当前线程处于等待中,会释放对象锁 ,但是不会自动唤醒,需要其他线程唤醒
notify() : 唤醒等待中的一个线程
notifyAll: 唤醒所有等待中的线程
他们都属性Object的方法,需要相同的对象 ,使用时 通过Object的对象调用
注意: 以上方法的调用必须满足两个条件: a、他们必须在同步代码块中执行, b、调用该方法的对象是锁对象
案例1:模拟3个人,张飞、李逵和刘备,来买电影票,售票员只有一张5元的钱,电影票5元钱一张。
-
张飞拿20元一张的人民币排在李逵和刘备的前面,李逵和刘备各拿了一张5元的人民币买票。
package com.j2008.waitnotify; /** * ClassName: TicketThread * Description: * date: 2020/11/10 11:27 * * @author wuyafeng * @version 1.0 softeem.com */ public class TicketThread implements Runnable{ //公共资源: int fiveCount=1; int twentyCount =0; @Override public void run() { //开始买票 如果是张飞,他是20元面值,其他都是5元 if(Thread.currentThread().getName().equals("张飞")){ //20元面值买票 takeTicket(20); }else{ // 5元面值买票 takeTicket(5); } } /** * 买票过程 给方法加同步 ,锁对象默认是 方法 * @param money */ public synchronized void takeTicket(int money){ if(money ==20){ // 验证 当前公共资源 中是否有3张5元 while(fiveCount<3){ //等待 System.out.println(Thread.currentThread().getName()+"不能买到票,要继续等待"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 程序执行这里,说明 fiveCount >=3 System.out.println(Thread.currentThread().getName()+"正在买票。。"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } fiveCount-=3; twentyCount++; System.out.println(Thread.currentThread().getName()+"已经买到了票。。"); }else{ System.out.println(Thread.currentThread().getName()+"正在买票。。"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } fiveCount++; System.out.println(Thread.currentThread().getName()+"已经买到了票。。"); // 唤醒等待的线程 this.notify(); } } }
public static void main(String[] args) { TicketThread ticketThread = new TicketThread(); Thread th1 = new Thread(ticketThread,"张飞"); Thread th2 = new Thread(ticketThread,"李逵"); Thread th3 = new Thread(ticketThread,"刘备"); //开启线程 th1.start(); th2.start(); th3.start(); }
四、线程的生产者和消费者模式
多个线程同时运行时,会产生线程并发可使用同步操作确保数据的安全性,如果需要各线程之间交互,可是使用线程等待和唤醒模式,在这里常用的等待唤醒中经典的模式为“生产者和消费者模式”
生产者和消费者由两类线程组成: 若干个生产者线程 负责提交用户的请求,若干个消费者线程负责处理生成出来的任务。 他们操作一块共享内存区进行数据通信。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-He0Q87BA-1605449576018)(assets/1604992093615.png)]
生成/消费的产品(数据): Mobile (手机编号)
生成者线程类: Provider : 无限制的生成手机
消费者线程类:Customer : 无限制的消费手机
共享存储区: Storage ( push 、pop) 存储手机的对象数组
测试类
public class Mobile {
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Mobile(int id) {
this.id = id;
}
}
存储区
package com.j2008.provider_customer;
/**
* ClassName: Storage
* Description:
* date: 2020/11/10 15:32
* 存储区,它是生产者和消费者共享的空间 ( 生产者和消费者将该对象作为公有锁)
* @author wuyafeng
* @version 1.0 softeem.com
*/
public class Storage {
// 定义存储手机的对象数据
Mobile [] mobiles = new Mobile[10];
int index=0; // 个数
static int n=1000;
/**
* 存放手机
* @param mobile
*/
public synchronized void push(Mobile mobile){
//考虑容器上限已满,必须等待
while(index == mobiles.length){
System.out.println("容器已满,需等待");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知消费者取消费 ,将其他线程全部唤醒
this.notifyAll();
mobiles[index]=mobile;
index++;
}
/**
* 取出手机 1 2 3 4
* 1 2 3
* index--;
* mobile[index] =null
* @return 取出的手机对象
*/
public synchronized Mobile pop(){
Mobile m = null;
// 判断index是否小于0
while(index<=0){
//等待
System.out.println("容器中没有手机,需要等待");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
index--;
m = mobiles[index];
//将容器中的这个位置 设置为空
mobiles[index]=null;
// 通知生产者去生产
this.notifyAll();
return m;
}
public synchronized int getSize(){
return index;
}
}
生产者:
package com.j2008.provider_customer;
/**
* ClassName: Provider
* Description:
* date: 2020/11/10 15:54
*
* @author wuyafeng
* @version 1.0 softeem.com
*/
public class Provider implements Runnable {
//共享存储区
Storage storage =null;
public Provider(Storage storage){
this.storage = storage;
}
@Override
public void run() {
//手机编号
int n=1000;
//一直生产
while(true){
Mobile m = new Mobile(n);
storage.push(m);
System.out.println(Thread.currentThread().getName()+
"生产了一部手机,其编号:"+m.getId()+" 其库存:"+storage.getSize());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
n++;
}
}
}
消费者
package com.j2008.provider_customer;
/**
* ClassName: Customer
* Description:
* date: 2020/11/10 15:58
*
* @author wuyafeng
* @version 1.0 softeem.com
*/
public class Customer implements Runnable {
Storage storage=null;
public Customer(Storage storage){
this.storage = storage;
}
@Override
public void run() {
while(true){
Mobile mobile = storage.pop();
System.out.println(Thread.currentThread().getName()+
"消费了一部手机,编号》》"+mobile.getId()+" 库存:"+storage.getSize());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类
package com.j2008.provider_customer;
/**
* ClassName: TestProviderCustomer
* Description:
* date: 2020/11/10 16:01
*
* @author wuyafeng
* @version 1.0 softeem.com
*/
public class TestProviderCustomer {
public static void main(String[] args) {
//创建公有的存储空间
Storage storage = new Storage();
Provider provider1 = new Provider(storage);
Provider provider2 = new Provider(storage);
Provider provider3 = new Provider(storage);
Thread th1 = new Thread(provider1,"张飞");
Thread th2 = new Thread(provider2,"刘备");
Thread th3 = new Thread(provider3,"关羽");
th1.start();
th2.start();
th3.start();
//消费者上场
Customer customer1 = new Customer(storage);
Customer customer2 = new Customer(storage);
Customer customer3 = new Customer(storage);
Thread th4 = new Thread(customer1,"张飞的老婆");
Thread th5 = new Thread(customer2,"刘备的老婆");
Thread th6 = new Thread(customer3,"关羽的老婆");
th4.start();
th5.start();
th6.start();
}
}
测试结果
关羽生产了一部手机,其编号:1000 其库存:1
张飞的老婆消费了一部手机,编号》》1000 库存:0
张飞生产了一部手机,其编号:1000 其库存:1
刘备生产了一部手机,其编号:1000 其库存:2
刘备的老婆消费了一部手机,编号》》1000 库存:1
关羽的老婆消费了一部手机,编号》》1000 库存:0
容器中没有手机,需要等待
关羽生产了一部手机,其编号:1001 其库存:0
张飞的老婆消费了一部手机,编号》》1001 库存:0
张飞生产了一部手机,其编号:1001 其库存:2
刘备生产了一部手机,其编号:1001 其库存:2
关羽的老婆消费了一部手机,编号》》1001 库存:0
刘备的老婆消费了一部手机,编号》》1001 库存:0
容器中没有手机,需要等待
关羽生产了一部手机,其编号:1002 其库存:0
张飞的老婆消费了一部手机,编号》》1002 库存:0
刘备生产了一部手机,其编号:1002 其库存:2
张飞生产了一部手机,其编号:1002 其库存:2
刘备的老婆消费了一部手机,编号》》1002 库存:1
关羽的老婆消费了一部手机,编号》》1002 库存:0
五、线程池
1、定义
用于创建和管理线程的容器就是线程池 (Thread Pool) ,在线程池中的线程执行完任务后不会立马进入销毁状态,而是重置到线程池中变为“空闲线程” 。 有利于避免频繁创建线程消耗资源,提供线程复用率,有限管理该线程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PHbFViHk-1605449576020)(assets/1604998934958.png)]
2、使用线程池的原因:
在多线程环境下,对于不断创建和销毁效率非常消耗系统资源,对于多线程之间的切换存在线程安全问题, 这是使用统一的管理类管理一些线程是比较好的解决办法
3、线程的运行机制:
- 在线程池模式下,任务是提交给线程池,由线程池根据当前空闲线程进行分配任务,如果没有空闲线程,由管理类创建线程或者进入任务等待队列中。
- 一个线程同时只能执行一个任务,但多个任务可以同时提交给这个线程池。
线程池的常用类 (ExecutedService)
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
参数1: corePoolSize:核心线程数
参数2:maximumPoolSize :最大线程数
参数3: keepAliveTime : 线程活动时长
参数4: 对于参数3的单位
1、可缓存的线程池 newCacheThreadPool(n);如果线程池中没有空闲线程,则创建新线程并放入线程池中,无上限线程数,如果有空闲线程则直接使用该线程
public static void main(String[] args) {
// 创建可缓存线程
ExecutorService service =Executors.newCachedThreadPool();
// 创建10个线程
int n=0;
for(int i = 0 ;i < 10;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.execute(new Runnable() {
@Override //你们内部类
public void run() {
//任务
System.out.println(Thread.currentThread().getName()+"---"+n);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//由于以上的线程 每次执行完成只需要500毫秒 ,会释放线程对象到线程池中
// 1000毫秒后创建的线程 就会复用上一次的线程对象
}
//关闭线程池
service.shutdown();
}
}
2、可重用的固定线程池: newFixedThreadPool(n) ,线程数量固定,如果没有空闲线程,则存放无界队列中等待
public static void main(String[] args) {
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(3);
// 连续创建10个线程, 由于只有3个线程,所有线程只能等待
// 2秒后再执行3个
for(int i =0;i<10;i++){
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
// 结果: 每2秒执行3个线程
}
3、 固定长度的可执行定时任务的线程池 newScheduledThreadPool , 类似于定时器线程
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println("延迟1秒后每3秒执行一次");
}
}, 1, 3, TimeUnit.SECONDS);
4、单线程的线程池 newSingleThreadExecutor : 线程池中只有一个线程数,所有的任务都通过该线程执行,它可以保证所有的任务是FIFO模式, 满足队列结构
线程池的几种方式掌握, 写一个自定义的线程池
一、计算机网络基础
1、计算机网络定义
把分布在不同区域的计算机与专门的外部设备通过通信线路连接成复杂的网络系统, 众多计算机之间可以方便的互相传输信息,数据共享。
2、计算机网络主要功能:
资源共享
信息传输与集中处理
均衡负荷与分布处理
综合信息服务
计算机之间需要数据传输,离不开网络通信协议,网络通信协议就是 双方在传输数据时的约定
3、网络通信协议定义:
计算机在数据传输时的通用标准 。约定了他们的传输速率,传输代码、代码结构,出错控制等标准。
根据国际约束的协议分为网络通信协议的七层协议,按照实际应用也可分为四层协议
七层从下往上 : 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
四层协议从下往上: 物理+数据链路层 、网络层(IP层)、传输层、应用层
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aY9Y50wp-1605449799230)(assets/1605066241136.png)]
由于网络传输本身是比较复杂的过程,Java 对每一层进行封装,对每一层提供对应的API ,使我们在进行网络传输时不需要跟踪底层协议,只需对每一层提供的API掌握,同时数据传输的过程和 文件流一样操作,从而简化该过程。 所以网络传输会使用流、多线程的概念。
二、Java的网络编程
Java的网络编程包 java.net.*
网络层: 掌握网络IP 和 端口号 java.net.InetAddress \Inet4Address\Inet6Address
IP的定义: IP(Internet Protocol) 互联网协议, 在全球互联网范围内每一个IP地址表示一台独立的计算机(广域网内) ,IPv4由四个段组成,每一个段的数都从0-255
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FDcjBxBV-1605449799233)(assets/1605066922649.png)]
通过一个IP地址可以路由到唯一的计算机。
端口号(PORT): 一台计算机上可运行多个应用程序,通过端口号区分该计算机的指定应用程序, 同一个计算机下,不同应用程序,端口号一定不同。 端口号的范围(0-65535)
常用端口号 :
端口号 | 对应服务 | 端口号 | 对应服务 |
---|---|---|---|
7 | Echo服务器 | 80(http)/443(https) | 浏览器默认端口 |
21 | ftp | 8080 | Tomcat默认端口 |
23 | telnet远程连接 | 3306 | MySql |
25 | SMTP(邮件服务) | 110 | POP3 |
2425 | 内网通端口/飞秋 | 1521 | Oracle |
每一个通信程序都有一个端口号
IP层的类:
java.net. InetAddress
常用方法
getLocalHost() : 获取本地IP对象
getHostAddress():获取IP地址
getHostName():获取计算机名
getAddress():获取ip地址的数组表现形式
static getByName(参数) :通过ip地址或计算机名 返回一个InetAddress对象
//确定主机名称的IP地址。
// 获取本机 LocalHost 表示本机ip
InetAddress inet = InetAddress.getLocalHost();
System.out.println("主机地址:"+inet.getHostAddress());
System.out.println("主机名:"+inet.getHostName());
System.out.println("主机名IP数组:"+
Arrays.toString( inet.getAddress()));
// 也可以通过ip地址获取InetAddress对象
InetAddress inet2= InetAddress.getByName("DESKTOP-346CK63");
//获取ip地址
System.out.println(inet2.getHostAddress());
InetAddress inet3 = InetAddress.getByName("192.168.7.189");
//获取计算机名
System.out.println(inet3.getHostName());
InetAddress inet4 = InetAddress.getByName("192.168.7.103");
System.out.println("对方的计算机名:"+ inet4.getHostName());
URL类:
URL全称 统一资源定位符,用于访问互联网上的资源地址,也称为 网址,
完成的URL地址包括以下部分
协议名://ip地址:端口号/文件路径/文件名
例如: http://47.100.182.246:8080/robot/
协议名: http 、https、ftp协议
URL : url的路由对象
URLConnection : url的连接对象, 可获取输入流
常用方法:
getConnect() : 获取连接对象的内容
-
getConnectTimeout()
URL url = new URL("https://www.baidu.com/");
// 获取根据ip + 端口 + 协议 + 文件组成URL
// URL url2 = new URL("http","47.100.182.246",8080,"/robot");
//获取连接 (与该地址的连接)
URLConnection conn = url.openConnection();
// 获取这里的资源,首先需要获取输入流 ,下载远程资源相当于IO流操作
InputStream is = conn.getInputStream();
// 包装成一个字符流
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String str=null;
while((str =br.readLine()) !=null){
System.out.println(str);
}
br.close();
下载文件 案例
public static void main(String[] args) throws IOException {
//https://zhuanlan.zhihu.com/p/285529011
// 1、创建连接
URL url = new URL("https://zhuanlan.zhihu.com/p/285529011");
System.out.println(url);
// 2、打开连接
InputStream is = url.openStream();
// 定义文件输出流
FileOutputStream fos = new FileOutputStream("d:/zhihu.html");
byte [] b = new byte[1024];
int len = 0;
while( (len =is.read(b)) !=-1){
fos.write(b,0,len);
}
System.out.println("文件下载 成功");
fos.close();
is.close();
}
传输层(TCP/UDP)
传输层用于数据的传输,在数据传输过程中根据数据的可靠性可分为两类,
1、基于TCP的传输
TCP特点: 两台计算机之间建立可靠连接,基于Socket的通道一旦建立,则数据可通过字节流的方式传输到另一方, 安全的可靠协议 ,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qyUv7Pnh-1605449799235)(assets/1605080331247.png)]
2、基于UDP的传输
UDP特点: 它面向无连接的协议,不保证数据的可靠性,传播速度比较快
TCP和UDP的区别
TCP协议 | UDP协议 | |
---|---|---|
是否面向连接 | 面向连接 | 面向无连接 |
可靠性 | 可靠 | 不可靠 |
应用场合 | 适用于数据量较大的文件 | 数据量较少 |
速度 | 较慢 | 较快 |
在Java中如何实现TCP传输
核心类: java.net.ServerSocket : 用于接收数据的套接字
java.net.Socket :用于发送数据的套接字
套接字是两台计算机建立通信连接的端点 ,在Java对应Socket类
a、实现数据的单行发送和接收
以“客户端”发送数据给“服务端” : 客户端是发送方, 服务端是接收方
//定义数据的接收方
ServerSocket server = new ServerSocket(8888);
// 监听返回套接字的 对象 线程会阻塞在这里,只要有人发送消息,就获取消息对应的端点Socket
Socket socket = server.accept();
System.out.println(socket.getInetAddress().getHostAddress()+"发送了消息");
// 获取接收对象的输入流
InputStream is = socket.getInputStream();
byte [] b = new byte[100];
// 实际读取的长度
int len = is.read(b);
String result = new String(b,0,len);
System.out.println(
socket.getInetAddress().getHostAddress()+"发送的数据:"+result);
public static void main(String[] args) throws IOException {
// 客户端用于发送数据
//创建Socket对象 并根据ip地址和端口号8888
Socket socket = new Socket(InetAddress.getByName("192.168.7.189"),8888);
System.out.println("请输入你要发送的内容:");
Scanner sc = new Scanner(System.in);
String msg = sc.next();
OutputStream os = socket.getOutputStream();
os.write(msg.getBytes());
System.out.println("发送成功。");
}