学习内容
多线程
进程
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和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() {
synchronized ("锁") {
System.out.println("t1 线程开始");
try {
"锁".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 线程结束");
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized ("lock1") {
System.out.println("t2 线程开始");
try {
"lock1".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 线程结束");
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
synchronized ("锁") {
System.out.println("t3 线程开始");
try {
"锁".notify();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("t3 线程结束");
}
}
});
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
synchronized ("lock1") {
System.out.println("t4 线程开始");
try {
"lock1".notify();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("t4 线程结束");
}
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
执行结果并不唯一,有多种情况
//第一次执行
t1 线程开始
t2 线程开始
t3 线程开始
t4 线程开始
t3 线程结束
t4 线程结束
t1 线程结束
t2 线程结束
//第二次执行
t1 线程开始
t2 线程开始
t3 线程开始
t3 线程结束
t1 线程结束
t4 线程开始
t4 线程结束
t2 线程结束
//第三次执行
t2 线程开始
t1 线程开始
t4 线程开始
t3 线程开始
t4 线程结束
t3 线程结束
t1 线程结束
t2 线程结束
原因:
线程的执行顺序是不确定的。调用Thread的start()方法启动线程时,线程的执行顺序是不确定的。
也就是说,在同一个方法中,连续创建多个线程后,调用线程的start()方法的顺序并不能决定线程的执行顺序。
但是利用wait()和notify()方法后,可以保证一定的顺序。
所以在上面的结果中, t1,t2 结束一定不会都在 t3、t4 开始的上面。即:只有当调用了线程 t3 或 t4 中的notify()方法,线程 t1 和 t2 中wait()方法后的语句才会被执行。
1-2
在 main 函数中开启一个子线程,如果想让 main 主线程在子线程执行完之后才继续执行,代码该怎么写?
任务2
使用 synchronized 实现抢票程序:某商场做活动,有 100 部 iPhone 可以抽奖兑换。
现在在三个柜台同时兑换,要求所有柜台已兑换的 iPhone 数量加起来刚好是 100,既不能多换,也不能少换。
要求:
用 Thread 类实现;
用 Runnable 接口实现。
Runnable 类
package com.xxm.advanced_camp.mission10_multithreading;
/*
使用 synchronized 实现抢票程序:某商场做活动,有 100 部 iPhone 可以抽奖兑换。
现在在三个柜台同时兑换,要求所有柜台已兑换的 iPhone 数量加起来刚好是 100,既不能多换,也不能少换。要求:
用 Thread 类实现;
用 Runnable 接口实现。
*/
public class SellPhones {
private static int iphones = 100;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized ("lock") {
if (iphones > 0) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "柜台正在兑换第 " + iphones + " 台 iPhone");
iphones--;
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized ("lock") {
if (iphones > 0) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "柜台正在兑换第 " + iphones + " 台 iPhone");
iphones--;
}
}
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized ("lock") {
if (iphones > 0) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "柜台正在兑换第 " + iphones + " 台 iPhone");
iphones--;
}
}
}
}
});
t1.start();
t2.start();
t3.start();
}
}
Thread 类:
1、先实现 Thread 接口,覆写run()方法
package com.xxm.advanced_camp.mission10_multithreading.task2;
import com.xxm.advanced_camp.mission10_multithreading.task2.SellPhones2;
public class MyThread extends Thread{
@Override
public void run(){
while (true) {
synchronized ("lock") {
if (SellPhones2.iphones > 0) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "柜台正在兑换第 " + SellPhones2.iphones + " 台 iPhone");
SellPhones2.iphones--;
}
}
}
}
}
2、创建MyThread类的实例对象,直接start,完成。
package com.xxm.advanced_camp.mission10_multithreading.task2;
/*
使用 synchronized 实现抢票程序:某商场做活动,有 100 部 iPhone 可以抽奖兑换。
现在在三个柜台同时兑换,要求所有柜台已兑换的 iPhone 数量加起来刚好是 100,既不能多换,也不能少换。要求:
用 Thread 类实现;
*/
public class SellPhones2 {
public static int iphones = 100;
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
任务3
使用 newScheduledThreadPool 线程池实现每隔 1 分钟打印一条消息。
答:代码如下:
package com.xxm.advanced_camp.mission10_multithreading.task3;
/*
使用 newScheduledThreadPool 线程池实现每隔 1 分钟打印一条消息。
*/
import java.util.concurrent.*;
public class Task3 {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("一分钟过去了");
}
}, 0, 1, TimeUnit.MINUTES);
}
}
任务4
(1)在 Windows 系统中,以递归方式读取 C 盘中所有的目录和文件,并打印出每个文件的大小和每个目中文件的数量。
答:思路:
1、创建File 对象,
2、创建一个打印的方法printAll(File f)。该方法中,使用listFiles方法创建File[]数组,遍历获取f对象下的所有文件及文件夹。
3、对数组中的所有对象,进行判断:是文件,则打印其绝对路径;是文件夹,则打印其绝对路径,并让这个文件夹调用printAll方法,实现递归。
package com.xxm.advanced_camp.Mission11_IO;
import java.io.File;
/*
1、创建File 对象,
2、创建一个打印的方法printAll(File f)。该方法中,使用listFiles方法创建File[]数组,遍历获取f对象下的所有文件及文件夹。
3、对数组中的所有对象,进行判断:是文件,则打印其绝对路径;是文件夹,则打印其绝对路径,并让这个文件夹调用printAll方法,实现递归。
*/
public class Task1 {
public static void main(String[] args) {
//1、创建File对象
File f = new File("C:");
printAll(f);
}
//2、创建遍历打印的方法
public static void printAll(File file) {
File[] files = file.listFiles();
for (File f : files) {
//判断是否为文件或文件夹
if (f.isFile()) {
System.out.println(f.getAbsolutePath());
} else if (f.isDirectory()) {
System.out.println(f.getAbsolutePath());
printAll(f);
}
}
}
}package com.xxm.advanced_camp.Mission11_IO;
import java.io.File;
/*
1、创建File 对象,
2、创建一个打印的方法printAll(File f)。该方法中,使用listFiles方法创建File[]数组,遍历获取f对象下的所有文件及文件夹。
3、对数组中的所有对象,进行判断:是文件,则打印其绝对路径;是文件夹,则打印其绝对路径,并让这个文件夹调用printAll方法,实现递归。
*/
public class Task1 {
public static void main(String[] args) {
//1、创建File对象
File f = new File("C:");
printAll(f);
}
//2、创建遍历打印的方法
public static void printAll(File file) {
File[] files = file.listFiles();
for (File f : files) {
//判断是否为文件或文件夹
if (f.isFile()) {
System.out.println(f.getAbsolutePath());
} else if (f.isDirectory()) {
System.out.println(f.getAbsolutePath());
printAll(f);
}
}
}
}
任务5
在网络上下载一个大文件(任何文件都可以,可以是 exe 文件,也可以是日志、电影或其它类型的大文件),然后使用 Java 标准 I/O 模型读取并复制成另一份文件:
用字节流实现;
答:
package com.xxm.advanced_camp.Mission11_IO;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
/*
在网络上下载一个大文件(任何文件都可以,可以是 exe 文件,也可以是日志、电影或其它类型的大文件),然后使用 Java 标准 I/O 模型读取并复制成另一份文件:
用字节流实现;
*/
public class Task2 {
public static void main(String[] args) throws Exception {
File file = new File("E:\\xxm_C1\\计算机通识-1.mp4");
//1.指定数据源
FileInputStream fis = new FileInputStream(file);
//2.指定输出位置
FileOutputStream fos = new FileOutputStream("E:\\xxm_C1\\计算机通识的分身1号.mp4");
//3.读取数据
byte[] b = new byte[1024];
int len;
while ((len = fis.read(b)) != -1) {
fos.write(b, 0, len);
}
//4.释放资源
fis.close();
fos.close();
}
}
用字符流实现:
package com.xxm.advanced_camp.Mission11_IO;
import java.io.FileReader;
import java.io.FileWriter;
public class Task2_2 {
public static void main(String[] args) throws Exception {
//1.指定输入源
FileReader fr = new FileReader("C:\\Users\\yyq\\Desktop\\日报\\10月25日.md");
//2.指定输出位置
FileWriter fw = new FileWriter("C:\\Users\\yyq\\Desktop\\日报\\10月25日的日报的分身.md");
//3.读取数据
char[] c = new char[1024];
int len;
while ((len = fr.read(c)) != -1) {
fw.write(c, 0, len);
fw.flush();
}
//4.释放资源
fr.close();
fw.close();
}
}
任务6
针对练习 2,用 NIO 重新实现,然后比较一下执行效率。
答:
package com.xxm.advanced_camp.Mission11_IO;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
public class Task3 {
public static void main(String[] args) throws Exception {
FileChannel in = new FileInputStream("C:\\Users\\yyq\\Desktop\\日报\\微型 ORM 框架.md").getChannel(),
out = new FileOutputStream("C:\\Users\\yyq\\Desktop\\日报\\微型 ORM 框架的 NIO 分身.md").getChannel();
in.transferTo(0, in.size(), out);
}
}
复制同一个文本文件时,字符流和 NIO 执行时间差不多,但有可能是因为文本文件太小,导致产生了天花板效应。
复制同一个视频文件(1.58 G)时,字节流耗时约为 37520 毫秒;NIO 方法约为 36902 毫秒,好像差不多的样子。。
这篇博客详细介绍了Java中的多线程概念,包括进程、线程的区别,线程调度,以及创建新线程的三种方式。还讨论了同步机制,如同步代码块和同步方法,以及Lock锁。此外,博主分享了Java中的File类及其相关方法,讲解了字节流和字符流的使用,最后给出了几个关于线程和I/O操作的编程任务。
&spm=1001.2101.3001.5002&articleId=121089378&d=1&t=3&u=1827eb9db6bb4a63ab62f0a90765d93e)
365

被折叠的 条评论
为什么被折叠?



