Java笔记(四)
多线程基础
一、线程相关概念
- 程序:是为完成特定任务、用某种语言编写的一组指令的集合。简单的说:就是我们写的代码。
- 进程:指运行中的程序
- 比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间;
- 进程是程序的一次执行过程,或是正在运行的一个程序;
- 进程是动态过程:有它自身的产生、存在和消亡的过程。
- 线程
- 线程有进程创建,是进程的一个实体;
- 一个进程可以有多个线程。
- 单线程:同一时刻,只允许执行一个线程。
- 多线程:同一个时刻,可以执行多个线程,比如:一个QQ进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件。
- 并发:同一时刻,多个任务交替执行,造成一种“貌似同时”的错觉,单核CPU的多任务就是并发。
- 并行:同一时刻,多个任务同时执行,多核CPU可以实现并行。
二、创建线程的两种方法
在Java中,创建线程有两种方法:
- 继承Thread类,重写run方法(Thread类也实现了Runnable接口);
- 实现Runnable接口,重写run方法。
继承Thread类
- 当一个类继承了 Thread 类,该类就可以当做线程使用;
- 我们会重写 run 方法,写上自己的业务代码;
- Thread 类 实现了 Runnable 接口的 run 方法。
- 关于
run()
方法:run()
方法只是一个普通方法,没有真正的启动一个线程,真正启动线程的方法是start()
方法;- 当调用线程对象的
start()
方法后会启动线程,无需等待run()
方法中的业务代码执行完毕,此时线程进入就绪态; - 当CPU调度使当前线程为运行线程时,该线程就进入了运行态,此时开始执行
run()
方法中的语句; start()
方法会调用start0()
方法,start0()
是本地方法,是 JVM 调用, 底层是 c/c++实现。
实现Runnable接口
- Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能了;
- java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程;
- 实现
Runnable
接口的线程对象不能直接调用start()
方法,需要创建Thread对象,把线程对象(实现 Runnable),放入Thread才可以调用线程相关方法。
代码演示:
1 | package Thread; |
两种方法的区别
- 从Java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从JDK帮助文档中我们可以看到Thread类本身就实现了Runnable接口;
- 实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制,建议使用Runnable。
线程终止
可以通过为线程对象设置变量来通知线程终止。
如下代码演示:
1 | package Thread; |
三、线程常用方法
第一组
setName
:设置线程名称,使之与参数name相同getName
:返回该线程的名称start
:使该线程开始执行,Java虚拟机底层调用该线程的start0()
方法run
:调用线程对象run方法;setPriority
:更改线程的优先级getPriority
:获取线程的优先级sleep
:线程的静态方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)interrupt
:中断线程,但并没有真正的结束线程,所以一般用于中断正在休眠线程getState()
:获取线程的状态
第二组
yield
:线程的礼让。让出CPU,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功join
:线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
用户线程和守护线程
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束;
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束,常见的守护线程:垃圾回收机制。
- 将线程设置为守护线程:用
setDaemon(true)
设置。 - 代码演示:
1 | package Thread; |
四、线程的状态
JDK 中用
Thread.State
枚举了表示线程的几种状态NEW
:尚未启动的线程处于此状态。RUNNABLE
在Java虚拟机中执行的线程处于此状态。BLOCKED
被阻塞等待监视器锁定的线程处于此状态。WAITING
正在等待另一个线程执行特定动作的线程处于此状态。TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。TERMINATED
已退出的线程处于此状态。
用
getState()
来获取当前线程的状态。线程状态转化图
五、线程同步机制
Synchronized
线程同步
- 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
- 也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
具体同步方法:
Synchronized
同步代码块:得到对象的锁才能操作同步代码
synchronized(对象){
// 需要被同步的代码
}
同步方法
public synchronized void m(String name){
// 需要被同步的代码
}
互斥锁
基本介绍
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性;
- 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象;
- 关键字
synchronized
与对象的互斥锁联系。当某个对象用synchronized
修饰时,表明该对象在任一时刻只能由一个线程访问; - 同步的局限性:导致程序的执行效率要降低;
- 同步方法(非静态的)的锁可以是this(当前类对象),也可以是其他对象(要求是同一个对象);
- 同步方法(静态的)的锁为当前类本身,因为静态类加载时可能无对象。
注意事项
- 当修饰静态方法时,锁定的是当前类的
Class 对象
(类锁) - 当修饰非静态方法时,锁定的是当前实例对象的
this
(实例锁)
- 当修饰静态方法时,锁定的是当前类的
实现的步骤
- 先分析上锁的代码;
- 选择同步代码块或同步方法;
- 要求多个线程的锁的对象为同一个即可。
互斥锁的几种实现方法
主函数
1 | public class test { |
- 同步方法
1 | class tickets1 implements Runnable { |
- 实例锁
1 | class tickets2 implements Runnable { |
- 类锁
1 | class tickets3 implements Runnable { |
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
- 产生的必要条件
- 互斥使用,即当资源被一个线程占用时,别的线程不能使用;
- 不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放;
- 请求和保持,当资源请求者在请求其他资源的同时保持对原因资源的占有;
- 循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路。
- 代码模拟死锁
1 | package Thread; |
释放锁
- 下面操作会释放锁
- 当前线程的同步方法、同步代码块执行结束;
- 当前线程在同步代码块、同步方法中遇到break、return;
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束;
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释
放锁。
- 下面操作不会释放锁
- 线程执行同步代码块或同步方法时,程序调用
Thread.sleep()
、Thread.yield()
方法暂停当前线程的执行,不会释放锁; - 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。提示:应尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用。
- 线程执行同步代码块或同步方法时,程序调用
Lock
- 基本介绍
- 从JDK 5.0开始,Java提供了更强大的线程同步机制—通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock
类实现了Lock,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁、释放锁,- 注意:如果同步代码有异常,要将unlock()写入finally语句块
- 使用方法
1 | class A{ |
- 与
synchronized
比较- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放;
- Lock只有代码块锁,
synchronized
有代码块锁和方法锁 - 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
IO流
IO流体系图
一、File类
- 获取文件的相关信息
file.getName()
:文件名字file.getAbsolutePath()
:文件绝对路径file.getParent()
:文件父级目录file.length()
:文件大小(字节)file.exists()
:文件是否存在file.isFile()
:判断是不是一个文件file.isDirectory()
:判断是不是一个目录
- 新建和删除文件
public boolean createNewFile()
:创建文件。若文件存在,则不创建,返回falsepublic boolean mkdir()
:创建一级目录public boolean mkdirs()
:创建多级目录public boolean delete()
:删除空目录或文件(Java中的删除不走回收站)
二、IO流
流的分类
按操作数据单位不同分为:字节流(8 bit)二进制文件,字符流(按字符)文本文件
按数据流的流向不同分为:输入流,输出流
按流的角色的不同分为:节点流,处理流/包装流
字节流和字符流
| (抽象基类) | 字节流 | 字符流 |
| ————— | —————— | ——— |
| 输入流 | InputStream | Reader |
| 输出流 | OutputStream | Writer |- Java的IO流共涉及40多个类,实际上非常规则,都是从如上4个抽象基类派生的。
- 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
IO流的原理即流的分类
InputStream & Reader
InputStream 和 Reader 是所有输入流的基类。
程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源
- InputStream
int read()
:从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因
为已经到达流末尾而没有可用的字节,则返回值 -1。int read(byte[] b)
:从此输入流中将最多b.length
个字节的数据读入一个 byte 数组中。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。int read(byte[] b, int off,int len)
:将输入流中最多len
个数据字节读入 byte 数组。尝试读取len
个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值 -1。public void close() throws IOException
:关闭此输入流并释放与该流关联的所有系统资源。
- Reader
int read()
:读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个字节的Unicode码),如果已到达流的末尾,则返回 -1int read(char[] cbuf)
:将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。int read(char[] cbuf,int off,int len)
:将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。public void close() throws IOException
:关闭此输入流并释放与该流关联的所有系统资源。
OutputStream & Writer
- OutputStream 和 Writer 是所有输出流的基类。
- OutputStream
void write(int b)
:将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。void write(byte[] b)
:将b.length
个字节从指定的 byte 数组写入此输出流。write(b) 的常规协定是:应该与调用write(b, 0, b.length)
的效果完全相同。void write(byte[] b,int off,int len)
:将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。public void flush()throws IOException
:刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标。public void close() throws IOException
:关闭此输出流并释放与该流关联的所有系统资源。
- Writer
void write(int c)
:写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码。void write(char[] cbuf)
:写入字符数组。void write(char[] cbuf,int off,int len)
:写入字符数组的某一部分。从off开始,写入len个字符void write(String str)
:写入字符串。void write(String str,int off,int len)
:写入字符串的某一部分。void flush()
:刷新该流的缓冲,则立即将它们写入预期目标。public void close() throws IOException
:关闭此输出流并释放与该流关联的所有系统资源。
节点流(文件流)
- 读取文件
- 创建一个对象流,将已存在的文件加载进流:
FileReader fr = new FileReader(new File(“Test.txt”));
- 创建一个临时存放数据的数组:
char[] ch = new char[1024];
- 调用流对象的读取方法将流中的数据读入到数组中:
fr.read(ch);
- 关闭资源:
fr.close();
- 代码演示:
- 创建一个对象流,将已存在的文件加载进流:
1 | FileReader fr = null; |
- 写入文件
- 创建流对象,建立数据存放文件:
FileWriter fw = new FileWriter(new File(“Test.txt”));
- 调用流对象的写入方法,将数据写入流:
fw.write("HelloWorld!");
- 关闭流资源,并将流中的数据清空到文件中:
fw.close();
- 代码演示
- 创建流对象,建立数据存放文件:
1 | FileWriter fw = null; |
- 注意事项
- 定义文件路径时,可以用”
/
“或者”\\
“。 - 在写入一个文件时,如果使用构造器
FileOutputStream(file)
,则目录下有同名文件将被覆盖。 - 如果使用构造器
FileOutputStream(file,true)
,则目录下的同名文件不会被覆盖,在文件内容末尾追加内容。 - 在读取文件时,必须保证该文件已存在,否则报异常。
- 字节流操作字节,比如:
.mp3, .avi, .rmvb, mp4, .jpg, .doc, .ppt
- 字符流操作字符,只能操作普通文本文件。最常见的文本文件:
.txt, .java, .c,.cpp
等语言的源代码。尤其注意.doc,excel,ppt这些不是文本文件。
- 定义文件路径时,可以用”
缓冲流
基本介绍
为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类
时,会创建一个内部缓冲区数组,缺省使用8192个字节(8Kb)的缓冲区。缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:
BufferedInputStream
和BufferedOutputStream
BufferedReader
和BufferedWriter
注意事项
- 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区;
- 当使用
BufferedInputStream
读取字节文件时,BufferedInputStream
会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区用完了,才重新从文件中读取下一个8192个字节数组; - 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,
BufferedOutputStream
才会把缓冲区中的数据一次性写到文件里。使用方法flush()
可以强制将缓冲区的内容全部写入输出流; - 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流;
flush()
方法的使用:手动将buffer中内容写入文件;- 如果是带缓冲区的流对象的
close()
方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出。
代码演示
1 | BufferedReader br = null; |
转换流
- 转换流提供了在字节流和字符流之间的转换
- Java API提供了两个转换流:
InputStreamReader
:(Reader的子类)InputStream转换为ReaderOutputStreamWriter
:(Writer的子类)OutputStream转换为Writer- 字节流中的数据都是字符时,转成字符流操作更高效。
- 很多时候我们使用转换流来处理文件乱码问题,实现编码和解码的功能。
InputStreamReader
实现将字节的输入流按指定字符集转换为字符的输入流,若不指定则默认。
需要和InputStream“套接”。
构造器:
public InputStreamReader(InputStream in)
public InputSreamReader(InputStream in,String charsetName)
如:
Reader isr = new InputStreamReader(System.in,"gbk");
OutputStreamWriter
- 实现将字符的输出流按指定字符集转换为字节的输出流。
- 需要和OutputStream“套接”。
- 构造器
public OutputStreamWriter(OutputStream out)
public OutputSreamWriter(OutputStream out,String charsetName)
代码演示
1 | public void testMyInput () throws Exception |
对象流
引入:
有如下需求:
将int num= 100 这个int数据保存到文件中,注意不是100数字,而是int 100,并且,能够从文件中直接恢复int 100。
将Dog dog = new Dog(”小黄”,3) 这个 dog对象 保存到 文件中,并且能够从文件恢复。
上面的要求,就是能够将 基本数据类型或者对象进行序列化和反序列化操作
- 序列化与反序列化示意图
- 基本介绍
- 功能:提供了对基本类型或对象类型的序列化和反序列化的方法
ObjectOutputStream
提供序列化功能ObjectInputStream
提供反序列化功能
- 使用细节
- 读写顺序要一致;
- 要求序列化或反序列化对象,需要实现
Serializable
; - 序列化的类中建议添加
SerialVersionUID
,为了提高版本的兼容性; - 序列化对象时,默认将里面所有属性都进行序列化,但除了static或transient修饰的成员;
- 序列化对象时,要求里面属性的类型也需要实现序列化接口;
- 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化。
- 序列化代码演示
1 | /* |
- 反序列化代码演示
1 | /* |
*标准输入输出流
System.in
和System.out
分别代表了系统标准的输入和输出设备- 默认输入设备是:键盘,输出设备是:显示器
- System.in的类型是InputStream
- System.out的类型是PrintStream,其是FilterOutputStream 和OutputStream的子类
- 重定向:通过System类的setIn,setOut方法对默认设备进行改变。
public static void setIn(InputStream in)
public static void setOut(PrintStream out)
Properties类
引入:
有如下配置文件
mysql.properties
,内容为:
1
2
3 ip=192.168.0.13
user=root
pwd=12345设计程序读取相关值。
- 基本介绍
- 专门用于读写配置文件的集合类;
- 配置文件的格式:
键=值
- 键值对无空格,值不需要用引号一起来,默认类型是String
- 常见方法
load
:加载配置文件的键值对到Properties对象list
:将数据显示到指定设备getProperty(key)
:根据键获取值setProperty(key,value)
:设置键值对到Properties对象store
:将Properties中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为unicode码
- 配置文件读取代码
1 | package demo; |
- 配置文件设置代码
1 | package demo; |