学习内容
多线程
进程
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
线程
某些进程内部还需要同时执行多个子任务,这些子任务就是线程。
例如:用播放器播放视频时,程序输出视频画面是一个进程、音频是一个进程、字幕是一个进程、显示视频进度是一个进程。
线程是操作系统调度的最小任务单位。
线程与进程
进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。
具体采用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程的缺点在于:
- 创建进程比创建线程开销大,尤其是在Windows系统上;
- 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:
- 多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
并发和并行
并行:在同一时刻,在多个指令在多个cpu同时进行。
并发:在同一时刻,在多个指令在单个cpu交替执行。
多线程编程
Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()
方法,在main()
方法内部,我们又可以启动多个线程。对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。
和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。
例如,播放视频时时,就必须由三个线程分别播放视频、播放音频、呈现字母,三个线程需要协调运行,否则就会出现如音画不同步、字母不同步等情况。因此,多线程编程的复杂度高,调度更困难。
Java多线程编程的特点又在于:
- 多线程模型是Java程序最基本的并发模型;
- 后续读写网络、数据库、Web开发等都依赖Java多线程模型。
因此,必须掌握Java多线程编程才能继续深入学习其他内容。
java 中的线程
线程调度
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程所占用的时间片。
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对较多一些。
创建新线程
Java用Thread
对象表示一个线程。
第一种方式,利用 Thread
类
-
创建
Thread
类的子类 -
在这个子类中重写
Thread
类的run()
方法,设置线程任务。 -
创建子类的实例对象。
-
调用
Thread
类中的start()
方法,开启这个线程,调用run()
方法。
注意:一个线程对象只能调用一次 `start()` 方法;
线程的执行代码写在 `run()` 方法中;
线程调度由操作系统决定,程序本身无法决定调度顺序;
第二种方式,实现 Runnable
接口
- 定义
Runnable
接口的实现类,并重写该接口的run()
方法,该run()
方法的方法体同样是该线程的线程执行体。 - 创建
Runnable
实现类的实例,并以此实例作为Thread
的target
来创建Thread
对象,该Thread
对象才是真正
的线程对象。 - 调用线程对象的
start()
方法来启动线程。
Thread.sleep()
可以把当前线程暂停一段时间,传入的参数是毫秒。可以通过传参来调整暂停时间的大小。
第三种方式:匿名内部类
线程的优先级
可以对线程设定优先级,设定优先级的方法是:
Thread.setPriority(int n) // 1~10, 默认值5
优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
同步
同步代码块
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
- 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程进入阻塞状态等待(BLOCKED)。
同步方法
使用 synchronized
修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着,进入阻塞状态。
格式:
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步方法中的同步锁:
- 对于非static方法,同步锁就是this。
- 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
Lock 锁机制
java.util.concurrent.locks.Lock
机制提供了比synchronized
代码块和synchronized
方法更广泛的锁定操作,同步代码块 / 同步方法具有的功能Lock
都有,还有更强大的功能。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock() :加同步锁。
public void unlock() :释放同步锁。
使用步骤:
-
先创建
Lock
对象,Lock lock =new ReentrantLock();
-
在可能出现同步安全问题的代码前调用
Lock
接口中的方法lock()
获取锁 -
在可能出现同步安全问题的代码后调用
Lock
接口中的方法unlock()
释放锁
线程状态
-
新建(NEW):新创建了一个线程,但还没调用线程的 start 方法;
-
运行(RUNNABLE),又包括:
就绪(ready):运行线程的 start 方法启动后,线程位于可运行线程池中,等待被调度;
运行中(RUNNING):就绪的线程获得 CPU 的时间片就变为运行中。
-
阻塞(BLOCKED):线程等待获得锁;
-
等待(WAITING):接收事件通知后或系统中断后进入等待,进入这个状态后是不能自动唤醒的,必须等待另一个线程调用
notify
或者notifyAll
方法才能够唤醒。 -
超时(TIMED_WAITING):等待指定时间后会自行返回;
-
终止(TERMINATED):线程已执行完毕;
注意:阻塞(BLOCKED)和等待(WAITING)不用刻意区分,这两者都会暂停线程。
I/0
File 类
概述
文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
File 类中的静态成员变量
static String pathSeparator
:与系统有关的路径分隔符,用来分隔多个路径,在 windows 中是分号 ;
。
static String separator :与系统有关的默认名称分隔符,在 widows 中是反斜杠 \
。
路径
绝对路径:从盘符开始的路径,这是一个完整的路径。
相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。
构造方法
public File(String pathname)
:通过将给定的路径名字符串转换为抽象路径名来创建新的 File
实例。
public File(String parent, String child)
:从父路径名字符串和子路径名字符串创建新的 File
实例。
public File(File parent, String child)
:从父抽象路径名和子路径名字符串创建新的 File
实例。
常用方法
public String getAbsolutePath()
:返回此File
的绝对路径名字符串。
public String getPath()
:将此File
转换为路径名字符串。
public String getName()
:返回由此File
表示的文件或目录的名称。
public long length()
:返回由此File
表示的文件的长度。
判断功能的方法
public boolean exists()
:此File
表示的文件或目录是否实际存在。
public boolean isDirectory()
:此File
表示的是否为目录。
public boolean isFile()
:此File
表示的是否为文件。
增删的方法
public boolean createNewFile()
:当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
public boolean delete()
:删除由此File表示的文件或目录。
public boolean mkdir()
:创建由此File表示的目录。
public boolean mkdirs()
:创建由此File表示的目录,包括任何必需但不存在的父目录。
目录的遍历
public String[] list()
:返回一个String数组,表示该File目录中的所有子文件或目录。
public File[] listFiles()
:返回一个File数组,表示该File目录中的所有的子文件或目录。
字节流
字节输出流
java.io.OutputStream
类是一个抽象类,表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字
节输出流的基本共性功能方法。
常用方法
public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。当完成流的操作时,必须调用此方法,释放系统资源。
public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。
public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。
public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len
字节,从偏移量 off
开始输出到此输出流。
public abstract void write(int b)
:将指定的字节输出流。
FileOutputStream 类
FileOutputStream
类是 OutputStream
类的子类,它是文件输出流,用于将数据输出到文件。
常用方法:
public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。
public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。
public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。
public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
public abstract void write(int b)
:将指定的字节输出流。
字节输入流 InputStream
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。当完成流的操作时,必须调用此方法,释放系统资源。
public abstract int read()
: 从输入流读取数据的下一个字节。
public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。
FileInputStream 类
该类是文件输入流,从文件中读取字节。
读取字节数据
- 读取字节:
read
方法,每次可以读取一个字节的数据,提升为int
类型,读取到文件末尾,返回-1
。 - 使用字节数组读取:
read(byte[] b)
,每次读取b
的长度个字节到数组中,返回读取到的有效字节个数,读
取到末尾时,返回-1
。
字符流
内容跟字节流基本差不多,就不复制粘贴了。
要点:输入流的抽象类是Reader
,其读取字符文件的子类是FileReader
。
输出流的抽象类是Writer
,其读取字符文件的子类是FileWrite
。
读取、输出的时候,把字节流里的byte[]
换成char[]
即可。
与字节流区别
字符流,只能操作文本文件,不能操作图片,视频等非文本文件。
当我们单纯读或者写文本文件时 使用字符流 其他情况使用字节流
任务内容
任务0
运行后可看看效果,然后尝试着解释为什么结果是这样的。
package com.xxm.advanced_camp.mission10_multithreading;
public class test1 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized ("锁") {
System.out.println("t1 start");
try {
// t1 释放锁
"锁".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 end");
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized ("锁") {
System.out.println("t2 start");
try {
// 通知 t1 进入等待队列
"锁".notify();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("t2 end");
}
}
});
t1.start();
t2.start();
}
}
答:运行结果
t1 start
t2 start
t2 end
t1 end
答:
尝试解释:
JVM先调用主方法 → 调用 t1 线程的 run() 方法 → 创造一个“同步锁” → 执行输出语句,输出 “t1 start” → 进入等待状态,JVM继续执行主方法 → 调用 t2 线程的 run() 方法 → 执行输出语句,输出 “t2 start” → 通知所有“同步锁”的成员,等待中的线程可以继续执行了 → 执行输出语句,输出 “t2 end” →继续执行“同步锁”成员中的等待成员后面的语句,也就是 t1 线程中后面的语句,也就是执行输出语句,输出"t1 end"。
任务1 熟悉线程生命周期方法
1-1
开启四个线程,两个线程调用锁的 wait 方法,另外两个调用锁的 notify 方法,观察执行结果并解释原因。
答:代码如下 :
package com.xxm.advanced_camp.mission10_multithreading.task1;
/*
开启四个线程,两个线程调用锁的 wait 方法,另外两个调用锁的 notify 方法,观察执行结果并解释原因。
*/
public class Task1 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronize