多线程的学习重点:
- 线程的创建方式
- 线程的周期(线程五种状态的转换)
- 线程同步
- 线程通信
1 线程控制
void | join() 等待这个线程死亡 |
---|---|
void | setDaemon(boolean on) 将此线程标记为 [daemon]线程或用户线程。 |
static void | sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 |
2 线程的分类
分类:守护线程 用户线程
- 守护线程和用户线程基本相同,唯一区别就是判断 JVM 何时离开
- 守护线程是用来服务用户线程的,会随用户线程的结束而结束。通过在 start 方法前调用 thread.setDaemon(true)可将一个用户线程变为守护线程
- java中的垃圾回收是一个典型的守护线程
- JVM 中如果都是守护线程,则 JVM 会直接退出
3 线程的生命周期
JDK 中用 Thread State 定义了线程的状态
线程状态可分为
- NEW 尚未启动的线程状态
- RUNNABLE 在JVM种执行的线程状态
- BLOCKED 被阻塞等待监视器锁定的线程状态
- WAITING / TIMED_WAITING 等待另一个线程执行特殊动作/执行特殊动作达到指定时间的线程状态
- TERMINATED 已退出的线程状态
线程的状态分有5种:
新建:Thread 类及其子类的对象被声明并创建时
就绪:线程启动( start() )但还未获得CPU执行权时
(有执行资格,没有执行权)
运行:线程获得CPU执行权,进行执行时
(有执行资格,有执行权)
阻塞:在某种特殊情况下(如被人为挂起或进行输入输出操作),让出CPU并临时终止运行时
(没有执行资格,没有执行权)
死亡:完成全部工作( run() 结束)或被强制提前终止( stop() )或出现异常导致线程异常结束时
线程状态之间的相互转换
死亡线程不可再启动
4 线程同步
4.1 解决数据安全问题——同步代码块
出现问题的条件:
- 多线程环境
- 有共享数据
- 有多条语句操作共享数据
如何解决线程安全问题:
基本思想:让程序没有安全问题的环境
实现方法:把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行
java 提供了给代码加锁的方法 synchronized,可用在代码块和方法上,分别称为同步代码块和同步方法
在同步代码块中,任意对象都可以充当对象,一般情况用 this
4.2 解决数据安全问题——同步方法
在方法的声明上加上 synchronized 修饰符,该方法称为同步方法,方法的锁对象为 this
静态的同步方法或静态方法中的同步代码块锁的对象是 类名.class
单例设计模式的懒汉式有线程安全问题:多线程在判断对象是否存在后令其休眠,若对象不存在,可能会创建出多个对象
4.3线程安全的类
StringBuffer 线程安全的可变字符序列
Vector 线程同步
HashTable 线程同步,是一个哈希表的实现
在实际使用时,如果不需要线程安全的实现,推荐使用与之功能相同但线程不同步的实现
5 线程的死锁
死锁是需要规避的问题
线程死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源
死锁不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
死锁问题的出现是一个概率事件
死锁问题的解决:
- 减少同步代码块或同步方法的嵌套
- 尽量减少同步资源的定义
- 使用专门的算法
6 线程通信
6.1 什么时候需要线程通信
多个线程并发执行时,希望线程有规律的执行(在默认情况下CPU随机切换线程)
6.2 先创建如何通信
用 wait() 使线程等待,用 notify() / notifyAll() 唤醒等待线程
void | wait() 导致当前线程等待,直到另一个线程调用该对象的 [notify() ]方法或 [notifyAll() ]方法。 |
---|---|
void | notify() 唤醒正在等待对象监视器的单个线程。 |
void | notifyAll() 唤醒正在等待对象监视器的所有线程。 |
void | wait(long timeout) 导致当前线程等待,直到另一个线程调用 [notify() ]方法或该对象的 [notifyAll() ]方法,或者指定的时间已过。 |
若想让多个线程按顺序执行
唤醒线程,执行一次后使该线程等待,唤醒下一个线程
不能同时使用 wait 和 notifyAll
6.3 互斥锁
锁是用于通过多个线程控制对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁
void | lock() 获得锁。 |
---|---|
void | unlock() 释放锁。 |
Interface Lock 可以使用的实现类
ReentrantLock
一个可重入互斥[Lock ]具有与使用synchronized 方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。 |
---|
Condition
void | await() 导致当前线程等到发信号或 [interrupted]。 |
---|---|
boolean | await(long time, TimeUnit unit) 使当前线程等待直到发出信号或中断,或指定的等待时间过去。 |
void | signal() 唤醒一个等待线程。 |
void | signalAll() 唤醒所有等待线程。 |
使用不同的 Condition 可以区分唤醒的线程
6.4 生产者消费者案例
案例需求:
生产者和消费者案例中包含几个对象:
1 奶箱(box) 定义一个变量 表示奶箱中奶的数量 提供存储牛奶和获取牛奶的方法
2 生产者(product) 线程 重写run 调用存储牛奶的方法
3消费者(Customer) 线程 重写run 调用获取牛奶的方法
测试:
1 创建奶箱对象 这是共享数据区
2生产者对象,把奶箱作为参数传递给生产者。 调用存储的方法
3创建消费者 把奶箱作为参数传递给消费者。 调用获取的方法
4 创建线程 启动线程
public class Box {
private int milk;//表示奶箱中牛奶的数量
private boolean state = false; //表示奶箱的状态 false 表示空 true 满
public synchronized void put(int milk){
if(state){// 表示奶箱中有牛奶
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有牛奶 就生产牛奶
this.milk = milk;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产者将第" + milk + "瓶牛奶放入奶箱");
//生产完毕之后 修改奶箱的状态
this.state = true;
// 唤醒消费者来消费
notifyAll();
}
public synchronized void get(){
// 如果没有牛奶 就等待生产
if(!state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 有牛奶 就消费
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("用户消费了第" + milk +"瓶牛奶");
state = false;
//唤醒生产者生产
notifyAll();
}
}
package cn.lanqiao;
public class Product implements Runnable{
private Box box;
public Product(Box box){
this.box = box;
}
@Override
public void run() {
for(int i = 0; i <=30 ; i++){
box.put(i);
}
}
}
package cn.lanqiao;
public class Customer implements Runnable{
private Box box;
public Customer(Box box){
this.box = box;
}
@Override
public void run() {
while(true){
box.get();
}
}
}
public class BoxTest {
public static void main(String[] args) {
Box box = new Box();//这是生产者和消费者的共享数据
//创建生产者
Product p = new Product(box);
// 创建消费者
Customer c = new Customer(box);
//创建线程
Thread t1= new Thread(p);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
作用:
- 通过平衡生产者的生产能力和消费者消费能力来提升整个系统的运行效率
- 解耦
7 网络编程
7.1 计算机网络
网络 是将地理位置不同的具有地理功能的计算机及其外部设备,通过通信线路链接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
网络编程 在网络通信协议下,实现网络互联的不同的计算机上运行的程序间可以进行数据交换
7.2 网络编程的三要素
IP地址: 网络中每台计算机的唯一标识号
端口: 可以锁定设备上应用程序的唯一标识
协议: 对数据传输做的统一规定,将这种规则称为通信协议。常见通信协议:UDP和TCP
7.3 InetAddress
所有与网络编程有关的类都位于 java.net 包下
static InetAddress | getByName(String host) 确定主机名称的IP地址。 |
---|---|
String | getHostName() 获取此IP地址的主机名。 |
String | getHostAddress() 返回文本显示中的IP地址字符串 |
static InetAddress | getLocalHost() 返回本地主机的地址。 |
7.4 端口
是程序在设备上的标识号
端口号:使用两个字节表示的整数,取值范围为0 ~ 65535,其中0 ~ 1023的端口号主要用于一些网络服务和应用,因此自己编写网络程序时,尽量使用1024以上的端口
7.5 协议
UDP协议:用户数据报协议
UDP协议是无连接的协议,在数据传输时,数据的发送端和接收端不建立逻辑联系
优点:
UDP协议消耗资源少,通信效率高
TCP协议:传输控制协议
TCP是面向连接的通信协议,传输数据时,必须在发送端和接收端建立逻辑连接,然后才能传输数据,可以提供两台设备之间的可靠无差错通信
TCP协议链接建立需要经过“三次握手“,断开链接需要经过”四次挥手“
7.6 UDP协议通信程序
UDP属于一种不可靠的协议,在通信两端需要建立 Socket(网络套接字) 对象。Socket 对象只发送和接收数据,因此UDP协议没有客户端和服务器的概念
DataGramSocket 基于UDP协议的Socket
- 此类表示用于发送和接收数据报数据包的套接字。
- 数据报套接字是分组传送服务的发送或接收点。
构造方法:
更多操作DatagramSocket() 构造数据报套接字并将其绑定到本地主机上的任何可用端口。 | |
---|---|
protected | DatagramSocket(DatagramSocketImpl impl) 使用指定的DatagramSocketImpl创建一个未绑定的数据报套接字。 |
DatagramSocket(int port) 构造数据报套接字并将其绑定到本地主机上的指定端口。 | |
DatagramSocket(int port, InetAddress laddr) 创建一个数据报套接字,绑定到指定的本地地址。 | |
| DatagramSocket(SocketAddress bindaddr) 创建一个数据报套接字,绑定到指定的本地套接字地址。 |
void | receive(DatagramPacket p) 从此套接字接收数据报包。 |
void | send(DatagramPacket p) 从此套接字发送数据报包。 |
DatagramPacket:
该类表示数据报包。数据报包用于实现无连接分组传送服务。
DatagramPacket(byte[] buf, int length) 构造一个 DatagramPacket 用于接收长度的数据包 length 。 |
---|
DatagramPacket(byte[] buf, int length, InetAddress address, int port) 构造用于发送长度的分组的数据报包 length 指定主机上到指定的端口号。 |
DatagramPacket(byte[] buf, int offset, int length) 构造一个 DatagramPacket 用于接收长度的分组 length ,指定偏移到缓冲器中。 |
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) 构造用于发送长度的分组数据报包 length 具有偏移 ioffset 指定主机上到指定的端口号。 |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造用于发送长度的分组数据报包 length 具有偏移 ioffset 指定主机上到指定的端口号。 |
DatagramPacket(byte[] buf, int length, SocketAddress address) 构造用于发送长度的分组的数据报包 length 指定主机上到指定的端口号。 |
发送数据的步骤:
- 创建发送端的Socket对象 DataGramSocket
- 创建数据 并将数据打包 DatagramPacket
- 调用DataGramSocket 的send方法 发送数据
- 关闭发送端 close
接收数据的步骤:
- 创建接收端的Socket对象:DataGramSocket
- 创建一个数据报包 用于接收数据DatagramPacket
- 调用DataGramSocket的reveive接收
- 解析数据报包,把信息输出
- 关闭接收端