多线程

线程的创建方式

继承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流都可以使用高级流包装