多线程
线程的创建方式
继承Thread
继承Thread
后重写run
方法,之后只需要创建子类对象,使用子类对象调用start()
方法来开启线程
这种方式可以直接使用Thread
的方法,但可扩展性差
实现runable接口
通过实现runable
接口并重写run
方法,之后需要创建实现类对象并将其作为参数传递给Thread(实现类)
构造函数来创建一个线程,之后使用Thread
对象调用Start()
方法来开启线程
这种方式实现类无法直接使用Thread
方法,必须依靠Thread
对象,实现类作为任务对象存在
实现类内部依靠Thread.currentThread()
方法来获取当前线程的对象之后才可以在实现类内使用Thread
方法了
实现callable接口和Future接口
上述两种方法都是没有返回值的,当需要线程的返回值时需要使用这种方法
实现Callable<返回值类型>
接口并创建实现类对象,然后将实现类对象作为参数传递给构造函数来创建FutureTask<返回值类型>
对象来管理返回值,之后将FutureTask<返回值类型>
对象作为参数传递给Thread(实现类)
构造函数来创建一个线程,之后使用Thread
对象调用Start()
方法来开启线程
可以使用FutureTask<返回值类型>
对象的方法get()
来获取返回值
缺点也是无法直接使用Thread
方法
Thread常用方法
void setName(String name)
,给线程设置名字,也可以在创建线程时直接设置名字
String getName()
,获取线程名字,如果没设置名字,则默认thread-序号
Thread currentThread()
,获取当前线程
static void sleep(long millis)
,线程休眠,单位毫秒
final int getPriority()
,获取线程优先级
final void setPriority(int newPriority)
,设置线程优先级,默认为5,最小1,最大10
void setDaemon(boolean on)
,设置为守护线程,当非守护线程结束时,守护线程会跟着陆续结束(不是立即结束)
static void yield()
,礼让线程
static void join()
,插入线程,将线程插入到当前线程之前,当前线程就是join
执行的线程
生命周期
- 创建
- 就绪,此时线程开始抢夺CPU执行权但未执行
- 运行,抢到CPU执行权后开始执行代码,如果此时被抢走CPU执行权会回到就绪状态
- 阻塞,遇到sleep等阻塞方法,阻塞结束后会回到就绪状态
- 死亡,线程代码执行完毕
同步代码块
一个线程在执行同步代码块的时候,其他线程无法访问同一个锁对象看管的同步代码块,确保静态资源不会出现重复
格式synchronized(锁对象){代码}
,其中锁对象可以是任意对象,只需要确保需要锁住的同步代码块之间的锁对象是唯一的即可,也就是静态共享的,确保锁是一样的否则无法阻止其他线程进入
一般可以使用当前类的字节码文件对象,因为一个文件夹的字节码文件是确定唯一不可能有重复的,xxxx.class
同步方法
格式修饰符 synchronized 返回值 方法名(){}
,锁对象无法自己指定,如果方法不是静态的则默认this,如果方法是静态的则为字节码文件对象
lock锁
比同步代码块灵活,可以自己控制上锁开锁的时机
lock无法实例化,因此创建其实现类ReentrantLock
,同时需要注意创建的锁对象要唯一,最好静态
方法有lock.lock()
,lock.unlock()
,需要注意程序完成一定要释放锁,否则会导致其他线程无法访问导致程序无法结束,可以使用finally作为扫尾
等待唤醒机制
程序分为生产者和消费者,生产者判断队列是否有内容,有则等待,没有则发布,发布完叫醒消费者
消费者先判断队列有没有内容,没有则等待,有则取出,并叫醒生产者
常用方法
wait()
,线程等待,直到被唤醒,这个方法会释放锁
notify()
,随机唤醒单个线程,这个方法会将锁给包含wait方法的线程,让其继续进行
notifyAll()
,唤醒所有线程,这个方法会将锁给包含wait方法的线程,让其继续进行
书写时必须使用锁对象去调用方法,让线程和锁对象绑定,因为唤醒的时候不能唤醒java中所有线程,因此使用锁对象调用表示只唤醒锁对象绑定的线程
为什么三个方法必须要在同步代码块中执行
因为以上三个方法都包含释放锁或者转交锁这个步骤,而释放锁的前提是自己有锁
阻塞队列实现等待唤醒机制
实现了生产者和消费者之间的队列,队列满则生产者阻塞,队列空则消费者阻塞,不需要自己书写同步代码块,提供了队列方法
阻塞队列实现
是个接口,只能实现其实现类,有两个
ArrayBlockingQueue(队列长度)
,底层是数组,有界
LinkedBlockingQueue()
,底层是链表,无界,上限是int边界
同时注意,生产者和消费者的队列必须是同一个队列对象,因此最好单独创建队列对象,在创建生产者和消费者时将其作为参数传入,保证其唯一性
常用方法
put()
,生产者将内容放到队列,满则自动阻塞
take()
,消费者从队列中取内容,队列空则自动阻塞
以上两种方法不需要自己加锁,因为方法已经调用了锁
线程的完整六状态
- 创建
- 就绪,此时线程开始抢夺CPU执行权但未执行
- 计时状态,遇到sleep,时间到会自动醒来
- 等待状态,遇到wait,等待notify唤醒
- 阻塞状态,无法获得锁对象,得到锁醒来
- 死亡,线程代码执行完毕
java实际上没有运行状态,因为线程获得cpu执行权后虚拟机会将其交给操作系统,虚拟机不会管理
线程池
在每次执行线程时我们都创建一个新的线程并在执行完销毁,导致资源浪费,因此线程池可以将空闲线程存放并在下次需要使用时重复利用
线程池会在有任务时看看池内有没有空闲线程,没有则创建一个新的线程,线程池可以设置上限,如果同时执行的任务超过上限,线程池会让超过上限的任务进行排队等待其他任务归还线程
常用方法
Executors.newCachedThreadPool()
,创建一个无上限的线程池
Executors.newFixedThreadPool(int)
,创建一个有上限的线程池
submit()
,提交任务,获取线程
shutdown()
,销毁线程池
自定义线程池
java提供的线程池不够灵活,因此我们可以使用自定义线程池
我们可以使用ThreadPoolExecutor
类来创建一个自定义线程池,它的构造方法最多要七个参数,分别是
- 核心线程的数量,不小于0
- 线程池中的最大数量,据此可以计算出临时线程数,不能小于核心线程数
- 临时线程空闲多长时间被销毁的值
- 临时线程空闲多长时间被销毁的单位,使用
TimeUnit
类中的常量表示 - 阻塞队列,存放等待核心线程的任务的,
new ArrayBlockingQueue(队列长度)
就可以 - 创建线程的方式,使用
Executors.defaultThreadFactory()
创建 - 当任务过多时采取的策略,是静态内部类,因此创建格式为
new ThreadPoolExecutor.AbortPolicy()
,还有其他三种策略,了解即可
示例new ThreadPoolExecutor(3, 6, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
自定义线程池执行顺序为,先是创建核心线程,当任务超过核心线程让其排队等待,当队列满了之后开始创建临时线程为超出队列的任务服务,如果连临时线程都满了则执行拒绝策略
最大并行数
就是电脑cpu的线程数,或者java可以使用的cpu线程数
线程池多大合适
cpu密集型运算
计算较多,文件读取较少,线程池大小为最大并行数+1
I/O密集型运算
读取文件较多,则线程池大小为:最大并行数*
期望CPU利用率*
((CPU计算时间+等待时间)/CPU计算时间)
网络编程
网络三要素
ip
ip是确定一台电脑的地址
使用InetAddress
类来创建一个ip对象,InetAddress
不能直接new出来,只能使用静态方法getByName(主机名)
来获取
可以看做一个电脑的对象,可以获取到主机的名字getHostName()
,主机的ipgetHostAddress()
端口号
端口可以唯一确定一个应用
取值范围0~65535,0~1023之间的端口号用于知名网络服务不能使用
一个端口号只能一个应用程序使用
协议
UDP协议:
- 用户数据报协议
- 面向无连接通信
- 速度快,有大小限制64k,数据不安全,易丢失
TCP协议
- 传输控制协议
- 面向连接
- 速度慢,数据安全,没有大小限制
UDP协议
发送数据
使用DatagramSocket
类创建一个数据传输对象,
使用DatagramPacket
类打包数据,他接受一个字符数组、数组长度、ip地址InetAddress
、端口号
使用send(DatagramPacket对象)
方法发送数据
使用close()
方法释放资源
接收数据
使用DatagramSocket
类创建一个数据传输对象,此时创建时需要绑定端口号,且必须是发送端指定的端口号
使用DatagramPacket
类作为接收数据的载体,接受一个字符数组和长度
使用DatagramSocket对象.receive(DatagramPacket对象)
方法接收数据
使用DatagramPacket对象.getData()
、getLength()
、getAddress()
、getPost()
来解析数据
三种方式
- 单播,一对一
- 组播,一对多,组播地址:
224.0.0.0
~239.255.255.255
,如果是我们自己使用则只能使用预留地址224.0.0.0
~224.0.0.255
,创建的数据传输对象变成MulticastSocket
,发送IP设为预留地址,在接收端需要将本机加入组内才能收到数据 - 广播,一对多,广播地址:
255.255.255.255
,只需要发送地址改成255.255.255.255
即可
TCP协议
发送数据
使用Socket
类配合I/O输出流来输出数据
接收数据
使用ServerSocket
类配合I/O输入流来接收数据
端口一定要对上
两者I/O流都可以使用高级流包装