第25天多线程、网络编程
多线程、网络编程
-
JDK5的锁和等待唤醒
-
Lock接口介绍
在JDK5之前,同步和锁绑定在一起,同时锁和等待唤醒也绑定在一起。在前学习同步代码块或同步方法的时候,线程要进入同步首先需要隐式的获取同步上的锁对象。只有持有锁对象的线程才能进入到同步中。线程出同步的时候会隐式释放锁。锁的获取和释放我们并无法操作。
到JDK5之后,将同步上的锁的获取和释放修改为由编程人员自己控制。升级后将锁单独的封装在Lock接口中。
JDK5中的Lock接口,它其实是在代替JDK5之前的同步方法或同步代码块。
将JDK5之前的隐式获取锁,修改为手动获取:
同时将隐式释放锁,也修改为手动操作:
如果使用JDK5的Lock接口,要求必须使用下面的模版代码:
手动调用lock方法获取锁
try{
书写的被同步的代码
}finally{
手动的调用unlock方法释放锁
}
-
Lock接口使用
/*
* 演示使用JDK5的Lock接口完成线程的同步
*/
class Ticket implements Runnable{
private int num = 100;
// 定义Lock接口的对象
private Lock loc = new ReentrantLock();
public void run(){
while(true){
// 手动获取锁
loc.lock(); // 从这里开始的代码就会被同步
try{
if( num > 0 ){
System.out.println(Thread.currentThread().getName()+"....."+num);
num--;
}
}finally{
// 手动释放锁
loc.unlock(); // 同步结束
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Ticket task= new Ticket();
Thread t = new Thread( task );
Thread t2 = new Thread( task );
t.start();
t2.start();
}
}
-
Condition接口介绍
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
wait方法被下面方法代替
notify 被下面方法代替
notifyAll:
-
Condition接口使用
/*
* 单生产单消费 使用JDK5中的Lock接口和Condition接口替换
*/
// 专门定义一个负责描述保存和取出线程的操作的资源类
class Resource {
// 定义成员变量,充当保存和取出的容器
private Object[] objs = new Object[1];
// 定义JDK5中的Lock接口对象
private Lock loc = new ReentrantLock();
// 定义监视保存线程的等待和唤醒对象
private Condition pro_con = loc.newCondition();
// 定义监视取消线程的等待和唤醒对象
private Condition con_con = loc.newCondition();
// 提供一个保存数据的方法
public void save( Object obj ) {
loc.lock();
try{
// 判断能否操作容器
if( objs[0] != null ){
// 判断成立 说明保存的线程不能保存,需要等待
try { pro_con.await(); } catch( InterruptedException e ){ }
}
// 将数据保存到容器中
objs[0] = obj;
System.out.println(Thread.currentThread().getName()+" 正在保存的数据是:"+objs[0]);
con_con.signal();
}finally{
loc.unlock();
}
}
// 提供一个取出数据的方法
public void get(){
loc.lock();
try{
if( objs[0] == null ){
// 判断成立,说明没有数据,不能取出,需要等待
try { con_con.await(); } catch( InterruptedException e ){ }
}
// 打印取出的数据
System.out.println(Thread.currentThread().getName() + " 正在取出的数据::::" +objs[0]);
objs[0] = null;
// 消费结束,需要通知生产者线程
pro_con.signal();
}finally{
loc.unlock();
}
}
}
// 书写保存数据线程的任务
class Productor implements Runnable {
private Resource r ;
// 书写构造方法的目的是在明确线程的任务的时候,
// 可以通过构造方法告诉线程的任务它们操作的资源对象
public Productor( Resource r){
this.r = r ;
}
public void run() {
// 给资源中保存数据
while(true){
r.save( "苹果" );
}
}
}
// 书写取出数据的线程任务
class Consumer implements Runnable {
private Resource r ;
// 书写构造方法的目的是在明确线程的任务的时候,
// 可以通过构造方法告诉线程的任务它们操作的资源对象
public Consumer( Resource r){
this.r = r ;
}
public void run() {
// 从资源中取出数据
while(true){
r.get();
}
}
}
// 测试类
public class ThreadTest {
public static void main(String[] args) {
// 先要创建一个保存和取出操作的共同的资源对象
Resource r = new Resource();
// 创建线程的任务对象
Productor pro = new Productor( r );
Consumer con = new Consumer( r );
// 创建线程对象 ,这个线程负责保存数据
Thread t = new Thread( pro );
// 创建线程对象 ,这个线程负责取出数据
Thread t2 = new Thread( con );
// 开启线程
t.start();
t2.start();
}
}
-
多线程的细节
-
wait和sleep区别
wait:需要被唤醒,sleep时间到自然醒
wait:它只能放在同步中,sleep可以在同步中,也可以不在。
wait:可以指定时间,但大部分情况我们是不指定时间,而sleep必须指定时间
wait:它让线程等待之后,线程会将锁是否,而sleep如果在同步中,它让线程休眠,这时线程是不会放锁。
-
同步能不能添加在run方法上
按照语法规则,是可以在run方法上书写同步的。但是实际开发中是不可能在run方法上添加同步的。
同步方法在线程执行的时候,每个线程要进入这个方法都需要获取同步锁,如果获取不到锁,线程就无法去执行这个方法。而我们知道run方法是线程要执行的任务方法,如果线程都进不去run方法,相当于线程根本就没有拿到自己的任务。
-
线程组
线程组:将多个操作行为相同的线程,可以划分到一个组中,然后我们不要去面对每个线程,而只要通过这个组操作组里面的所有线程。
如何将线程添加到线程组中:
在创建Thread对象的时候,可以根据Thread的构造方法将线程添加到对应的线程组中。
-
线程优先级
Thread类中的API:每个线程都有一个优先级,高优先级线程的执行优先于低优先级线程。
我们在创建线程的时候,可以指定每个线程的优先级,如果没有指定的话,这个线程优先级默认和执行创建线程时所在的线程优先级相同。主线程的优先级默认为5.
线程的优先级: 1 ~ 10 ,一般建议如果需要设计线程的优先级设置为 1 或 5 或 10。
修改线程的优先级:
获取线程的优先级:
注意:高优先级的线程被CPU执行的概率会被低优先级的高,但是并不意味着低优先级的线程CPU不执行。
-
守护线程
守护线程:每个线程都可以或不可以标记为一个守护程序。当且仅当创建线程是守护线程时,新线程才是守护程序。
前台线程:从主程序开始执行的那个线程一定是前台线程。主线程属于前台线程(非守护线程)。
守护线程也被称为后台线程,或者被称为用户线程,这些线程它们依然可以去运行线程的任务,但是如果程序中的前台线程全部结束,这时不管后台(守护)线程的任务是否结束,守护线程会自动停止运行。
/*
* 演示守护线程
*/
class Demo implements Runnable{
public void run(){
for( int i = 0 ; ; i++){
System.out.println(Thread.currentThread().getName()+"...."+i);
}
}
}
public class TheradDemo {
public static void main(String[] args) {
// 执行main方法的线程,一定是前台线程
Demo d = new Demo();
// 下面创建的线程和主线程都属于前台线程
Thread t = new Thread(d);
Thread t2 = new Thread(d);
// 人为的将创建出来的线程修改为后台(守护线程),一定要在开启线程之前修改
t.setDaemon(true);
t2.setDaemon(true);
t.start();
t2.start();
for( int i = 0 ; i < 20 ; i++){
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
-
定时器介绍
定时器:让设备可以定时的去(重复)执行某个功能。
定时器类:
构造方法:
下面的方法是给Timer(定时器)对象绑定任务
/*
* 演示定时器
*/
public class TimerDemo {
public static void main(String[] args) {
// 创建定时器对象
Timer t = new Timer();
/*
* 给定时器指定任务
* schedule(TimerTask task, long delay, long period)
* TimerTask task : 定时器要执行的任务
* long delay : 当前时间往后延时多久,开始执行任务
* long period : 到执行任务的时间点之后,每隔多久时间重复执行一次任务
*/
final Random r = new Random();
t.schedule( new TimerTask(){
@Override
public void run() {
// 在run方法中书写具体Java代码完成定时器需要做事情
System.out.println("你好");
File file = new File("e:/abc/"+r.nextInt()+".txt");
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}, 1000, 1000);
}
}
-
网络介绍
-
网络介绍(了解)
网络:通过一些中间的设备,可以将终端设备连接在一起,并且这些终端设备之间可以进行数据的交互。
中间设备:网线、交换机、路由器、无线设备、卫星等。
终端设备:电脑、服务器、手机、智能家电等。
网络分成:局域网、城域网、广域网。
-
网络模型介绍(了解)
由于网络中的设备太多,于是将工作在网络中的不同设备功能(职责)进行划分,将网络化成七层(网络模型):
OSI(Open System Interconnection开放系统互连)参考模型 。
1.层物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。 2.层数据链路层:主要将从物理层接收的数据进行MAC地址(网卡的地址)的封装与解封装。常把这一层的数据叫做帧。在这一层工作的设备是交换机,数据通过交换机来传输。 3.层网络层:主要将从下层接收到的数据进行IP地址(例192.168.0.1)的封装与解封装。在这一层工作的设备是路由器,常把这一层的数据叫做数据包。 4.层传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。 5.会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名) 6.表示层:主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等(也就是把计算机能够识别的东西转换成人能够能识别的东西(如图片、声音等)。 7.应用层: 主要是一些终端的应用,比如说FTP(各种文件下载),WEB(IE浏览),QQ之类的(可以把它理解成我们在电脑屏幕上可以看到的东西.就是终端应用)。
-
网络三要素(重点掌握)
-
IP介绍(重点)
IP:它本质是连接在网络中那个终端设备的一个数字标识。如果我们需要进行网络通信,这时我们就必须知道通信对方的IP地址是多少,才能和对方进行通信。
IP地址:
它被分成5类:A、B、C、D、E。
平时我们上网的时候,从来不手动设置IP地址,这时IP地址由宽带提供商提供。
如何查询自己电脑的IP地址:
IP地址:它是连接在网络中终端设备的唯一的数字标识。
MAC地址:每个网卡都有一个网卡地址,这个地址也被称为物理地址。每个网卡在生产的时候就已经固定死(全球唯一)。
网卡是设备的物理标识,它不容易记忆,也不容器操作。但是我们的确可以通过网卡地址找到需要通信的设备。但是实际应用中,程序不会根据网卡地址通信。
IP地址,逻辑地址:它可以分配给任何一个连接在网络中的设备。
网卡地址和IP地址:
网卡地址:可以理解成手机的唯一编号(*#06#)。
IP地址:可以理解成手机号码。
了解的内容:其实每个网卡在出厂的时候已经分配了一个IP地址。
这个地址:本地回环地址。127.0.0.1
-
域名解析(了解)
IP地址是我们唯一访问网络中设备的路径。平时我们上网的时候根本没有使用IP地址访问任何网络中的信息。
平时在浏览器的地址栏中输入域名(www.baidu.com),通过域名访问网络中的资源数据。
当我们在浏览器的地址栏中输入域名之后,其实背后对应的将域名解析成对应的IP地址,最后还是在使用IP地址访问。
关于域名解析成IP地址:域名解析
1、先通过操作系统本地的hosts文件中查询当前的域名有没有对应的IP地址,如果有就会使用hosts文件中的IP,作为当前域名的对应的IP地址访问。
C:\Windows\System32\drivers\etc\hosts
2、如果第一步解析IP地址失败,这时系统会自动到DNS服务器上解析。DNS服务器上会有全球所有的网站对应的IP地址。查询到域名对应的ip地址,就会使用这个ip地址访问。如果DNS服务器解析失败,这时说明域名有问题。
-
端口介绍(重点)
通过IP地址,可以访问到连接在网络中的某个终端设备。设备中肯定会运行很多的应用程序(软件),最终我们需要和设备中的某个应用软件进行数据交互。
运行在终端设备中的任何应用软件,它们在这个设备中都有一个唯一的标识,这个标识我们称为软件在此设备中的端口号。
端口号:从0~65535之间,一般0~1024之间的端口号,是给系统软件使用的时候,因此我们自己编程中,开发的程序需要绑定端口的时候,建议使用1025~65535之间的端口号。
总结:
IP:它本质是网络中的电脑的标识。
端口:电脑中运行的软件的标识。
-
协议介绍(重点)
协议:双方需要共同遵守的规则。在网络编程中,协议是在规范通信双方需要遵守的数据交互格式。
我们学习的网络编程中的协议,主要是OSI模型中传输层协议(底层协议):
UDP协议:用户数据报文包协议。
通信双方不同关心对方是否在接收数据。发送方只管发送数据,接收方如果在,就可以接收到数据,如果不在,数据就会被丢弃。
UDP协议,传输快,效率高,但不安全。不能发送大数据。
UDP协议,一般用在实时聊天等程序底层。
TCP协议:传输控制协议。
通信的双方,必须经常三次握手,建立数据交互的通道。然后双方在这个通道中进行数据传递。如果有一方断开通道,这时通道就被破坏,无法在继续通信。
TCP协议:它主要用在对数据安全性要求较高的软件底层。传递慢,效率低,但安全。
总结:最终我们通过网络通信,需要上门介绍的三个要素:
协议:双方通信的规则
ip:对方的设备标识
端口:对方设备中运行的软件标识
-
网络编程
-
-
IP对象(理解+编码)
连接在网络中的任何设备都有一个IP地址。因此Java也将IP地址封装成一个对象,如果我们在编程中需要操作IP地址,可以直接使用InetAddress对象完成。
Java中的网络编程相关的类和接口都在java.net包下:
Ipv4:它采用的4个数字作为ip地址。每个数字范围0~255之间。它也是IP的第四个版本。
Ipv6:它是IP地址的第六个版本。它采用的是八个数字作为ip地址。首选形式为 x:x:x:x:x:x:x:x,其中这些 'x' 是八个 16 位地址段的十六进制值。
InetAddress类它没有对外提供构造方法,只能通过其中的静态方法获取对象:
/*
* 演示InetAddress对象
*/
public class IPDemo {
public static void main(String[] args) throws UnknownHostException {
method2();
}
public static void method2() throws UnknownHostException {
InetAddress[] ints = InetAddress.getAllByName("www.sina.com.cn");
for (InetAddress inet : ints) {
System.out.println(inet.getHostAddress());
}
}
public static void method1() throws UnknownHostException {
/*
* 使用静态方法获取IP对象
* getByName(String host)
* String host: 表示是需要封装的IP地址,
* 字符串可以书写:IP地址、 主机名、域名
*/
InetAddress inet = InetAddress.getByName("www.baidu.com");
// 获取IP地址 getHostAddress()
String ip = inet.getHostAddress();
/*
* 获取主机名
* getHostName() 有时无法解析出对方的主机名称时,这个值就会变成IP地址
*/
String name = inet.getHostName();
System.out.println(ip+"...."+name);
}
}
-
Socket对象(理解)
Socket : 网络套接字。
-
UDP协议编程
-
DatagramSocket对象(理解)
-
DatagramSocketL:它是用来发送和接收数据报包的通信端点(终端设备)对象。
-
DatagramPacket对象(理解)
DatagramPacket:它相当于打包或拆包的对象。
-
UDP发送端(重点理解+编码)
/*
* 简单演示基于UDP协议发送方实现
*/
public class UdpSendDemo {
public static void main(String[] args) throws Exception {
// 创建发送方对象
DatagramSocket ds = new DatagramSocket(9191);
/*
* 创建打包对象
* DatagramPacket(byte[] buf, int length, InetAddress address, int port)
* byte[] buf : 被发送的数据
* int length : 发送的数据的长度
* InetAddress address : 接收方的IP地址对象
* int port : 接收方的端口
*/
byte[] buf = "有人犯困啦!UDP肯定挂掉!!!".getBytes();
int length = buf.length;
InetAddress address = InetAddress.getByName("192.168.49.40");
int port = 9292;
DatagramPacket dp = new DatagramPacket( buf , length , address , port);
// 发送数据
ds.send(dp);
// 关闭发送方
ds.close();
}
}
-
UDP接收端(重点理解+编码)
/*
* 简单实现基于UDP协议的接收方
*/
public class UdpReceDemo {
public static void main(String[] args) throws Exception {
// 创建接收方对象
DatagramSocket ds = new DatagramSocket(9292);
/*
* 创建拆包对象
* DatagramPacket(byte[] buf, int length)
* byte[] buf : 临时用于存储接收到的数据的容器
* int length : 临时用于存储数据的场地大小
*/
DatagramPacket dp = new DatagramPacket( new byte[1024] , 1024 );
// 接收数据
ds.receive(dp);
// 获取接收到的数据
byte[] buf = dp.getData();
// 接收到的数据长度
int length = dp.getLength();
// 获取发送方的ip
String ip = dp.getAddress().getHostAddress();
// 获取发送方的端口号
int port = dp.getPort();
System.out.println(ip+".."+port + ", 数据是:"+ new String( buf , 0 , length ));
// 关闭接收方
ds.close();
}
}
-
UDP练习
-
需求:
发送方键盘录入字符串数据,将字符串数据发送给接收方,接收方接收到之后,统计出其中字母字符的个数,然后将结果发送给发送方。
-
-
发送端(编码实战)
/*
* 发送方:
* 1、键盘录入字符串数据,
* 2、将数据发送给接收方
* 3、接收 接收方发来的结果数据
*/
public class UdpSendTest {
public static void main(String[] args) throws IOException {
// 创建发送方对象
DatagramSocket ds = new DatagramSocket(9393);
// 创建Scanner对象
Scanner sc = new Scanner( System.in );
System.out.println("请输入数据:");
String line = sc.nextLine();
// 创建打包对象
DatagramPacket send_dp = new DatagramPacket(
line.getBytes(),
line.getBytes().length,
InetAddress.getByName("192.168.49.40"),
9494
);
// 发送数据
ds.send(send_dp);
///////////////////数据已经发送给接收方,需要接收数据//////////////////////
// 创建拆包对象
DatagramPacket rece_dp = new DatagramPacket( new byte[1024] , 1024 );
// 接收数据
ds.receive(rece_dp);
// 获取数据
String s = new String( rece_dp.getData() , 0 , rece_dp.getLength() );
System.out.println(s);
// 关闭
ds.close();
}
}
-
接收端(编码实战)
/*
* 接收方:
* 1、接收数据
* 2、统计出其中的字母字符个数
* 3、将结果发送给发送方
*/
public class UdpReceTest {
public static void main(String[] args) throws Exception {
// 创建接收方对象
DatagramSocket ds = new DatagramSocket( 9494 );
// 创建拆包对象
DatagramPacket rece_dp = new DatagramPacket( new byte[1024] , 1024 );
// 接收数据
ds.receive(rece_dp);
// 获取数据
String data = new String( rece_dp.getData() , 0 , rece_dp.getLength() ) ;
System.out.println(rece_dp.getAddress().getHostAddress()+"接收方接收到的数据是:"+data);
// 处理:统计字母字符个数
int count = 0;
// 遍历字符串
for( int i = 0 ; i < data.length() ; i++ ){
// 取出字符串中的每个字符
char ch = data.charAt(i);
// 判断是否是字母字符
if( (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ){
count++;
}
}
// 统计结束之后,需要将结果发送给发送方
String s = "统计的结果是:"+count;
// 创建打包对象
DatagramPacket send_dp = new DatagramPacket(
s.getBytes(),
s.getBytes().length,
// 这里的IP地址是发送方的IP地址,可以通过上面的拆包对象获取
rece_dp.getAddress(),
// 接收方的端口号
rece_dp.getPort()
);
/// 发送数据
ds.send(send_dp);
// 关闭
ds.close();
}
}
-
TCP编程
-
回顾IO流
-
-
TCP编程模型介绍(理解)
-
TCP客户端(理解)
-
TCP服务端(理解)
-
TCP客户端实现(重点+编码)
-
TCP服务端实现(重点+编码)
-
TCP练习
-
需求:
-
需求:客户端键盘录入数据,发送给服务器,服务端将数据保存到磁盘中,服务端告诉客户端"微博发送成功"。
-
客户端实现
-
服务端实现
-
服务端开启多线程