泛型
1.概述
- 泛型提供编译时类型安全检测机制
- 好处是把运行时的可能出现的错误提前到编译期间
- 泛型分类:泛型类、泛型方法、泛型接口
2.自定义泛型类
- 格式
修饰符 class 类名<标识>{}
- 范例
public class Generic<T>
- 上述T是标识,其他的还有如T、E、K、V等标识
3.自定义泛型方法
- 格式
修饰符 <标识> 返回值类型 方法名(类型 变量){}
- 范例
public <T> void show(T t){}
4.自定义泛型接口
- 格式
修饰符 interface 接口名<标识>{}
- 范例
public interface Generic<T>{}
- 与泛型类相似
- 泛型接口有两种使用方式,一种是在实现类给出确定的T,一种是写成泛型类(即不给出具体的T)
5.类型通配符
- 类型通配符为:<?>
- 例如ArrayList:表示元素类型未知的ArrayList,它的元素可以匹配任何类型
- 通配符上限:
<? extends 某个类型>
表示匹配的类型应该是某个类型或者某个类型的子类 - 通配符下限:
<? super 某个类型>
表示匹配的类型应该是某个类型或者某个类型的父类
文件和IO流
1.File类
- 文件和目录可以通过File封装成对象
- Flie封装的仅仅是一个路径名,可以是存在的也可以是不存在的
- 构造方法
方法名 |
说明 |
File(String pathname) |
通过路径名字符串创建一个File对象 |
File(String parent,String child) |
通过父路径和子路径拼接创建一个File对象 |
File(File parent,String child) |
通过父路径File对象和子路径拼接创建一个File对象 |
- 相对路径和绝对路径
- 绝对路径是从盘符开始给出文件的路径
- 相对路径是相对当前项目下的文件路径
- 常用方法
方法名 |
说明 |
public boolean createNewFile() |
创建一个文件,如果该文件已存在返回false,如果不存在则创建该文件并且返回true,注意该文件所在文件夹必须存在 |
public boolean mkdir() |
创建一个单级文件夹,如果文件夹已存在则返回false,如果不存在则创建文件夹并且返回true |
public boolean mkdirs() |
创建一个单级或多级文件夹,如果文件夹已存在则返回false,如果不存在则创建文件夹并且返回true(常用,既能创建单级也能创建多级) |
public boolean delete() |
删除一个文件或者一个空文件夹,不能删除含有文件的文件夹 |
public boolean isDirectory() |
判断该路径表示的File对象是不是目录 |
public boolean isFile() |
判断该路径表示的File对象是不是文件 |
public boolean exists() |
判断该路径表示的File对象是否存在 |
public String getName() |
返回该路径表示的File对象的文件名(含后缀)或者文件夹名 |
- 特殊方法
方法名 |
说明 |
public File[] listFiles() |
返回一个File数组,里面装有该路径下的文件和目录 |
- 当调用者不存在时,返回null
- 当调用者是一个文件是,返回null
- 当调用者是一个空文件夹时,返回长度为0的数组
- 当调用者是一个非空的文件夹时,返回一个装有该目录下所有的文件(含隐藏文件)和文件夹的File数组
- 当调用者是一个需要权限的文件夹时,返回null
- 范例,如何删除含有文件和文件夹的文件夹?
public static void deleteDir(File src){
//先删除文件夹里面的内容,再删除文件夹
//使用递归思路
File[] files = src.listFiles(); for(File file:files){ if(file.isFile()){ //如果是文件则删除 file.delete(); }else{ //如果是文件夹则递归删除 deleteDir(file); } } src.delete();//最后删除文件夹 }
2.IO流的分类
- 按流向分
graph TB;
IO流-->输入流
IO流-->输出流
- 按数据类型分
graph TB;
IO流-->字节流
IO流-->字符流
字节流-->操作所有类型的文件
字符流-->只能操作纯文本文件
3.字节流
- 字节输出流FileOutputStream
- 常用构造方法
方法名 |
说明 |
public FileOutputStream(String finepath) |
根据文件路径创建字节输出流对象 |
public FileOutputStream(File file) |
根据文件对象创建字节输出流对象 |
public FileOutputStream(String filepath,boolean append) |
append指定是否允许在文件追加数据 |
public FileOutputStream(File file,boolean append) |
append指定是否允许在文件追加数据 |
- 写数据的三种方式
方法名 |
说明 |
void write(int b) |
一次写一个字节数据,这里的int值对应成ASCII中的字符 |
void write(byte[] b) |
一次写一个字节数组数据 |
void write(byte[] b,int off,int len) |
一次写一个字节数组某长度的数据 |
- 如果在写数据时需要换行,可以使用
write("\r\n".getBytes())
在windows系统中写入一个换行符,linux使用\n,mac使用\r - 默认情况下每次打开文件写数据时会将文件内容清空,如果需要实现追加的功能,可以使用带有append参数的构造方法来创建FileOutputStream对象
- 字节输出流写文件的步骤
- 创建字节输出流对象
文件不存在会自动创建,文件存在如果不使用含有append参数的构造方法则会清空文件 - 写数据
- 释放资源
- 异常处理,使用finally块
//try..catch..finally块处理异常的模板 FileOutputStream fos = null; try { fos = new FileOutputStream("a.txt"); fos.write(98); } catch (IOException e) { e.printStackTrace(); } finally { //finally块的代码无论是否产生异常,都会执行 if(fos != null){ try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } }
- 字节输入流FileInputStream
- 常用构造方法
方法名 |
说明 |
public FileInputStream(String filepath) |
根据文件路径创建字节输入流对象 |
public FileInputStream(File file) |
根据文件对象创建字节输入流对象 |
- 读数据的方法
方法名 |
说明 |
int read() |
从输入流中读一个字节,返回的是该字符的ASCII值 |
int read(byte[] b) |
从输入流中最多将b.length个字节读取到数组中,返回的是实际读到的字节数 |
int read(byte[] b,off,len) |
从输入流中最多将len个字节读取到数组中,返回的是实际读到的字节数 |
- 当使用读出来的数据是-1时,表明文件已经读到底部
- 连续读取多个数据范例
FileInputStream fis = null; int b; try { fis = new FileInputStream("a.txt"); while ((b = fis.read()) != -1) { System.out.print((char)b);//由于读出来的数据是字节,需要强转 } } catch (IOException e) { e.printStackTrace(); } finally { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } }
- 文件拷贝范例1(单个字节读写)
FileInputStream fis = null; FileOutputStream fos = null; int b; try { fis = new FileInputStream("D:\\Code\\HTML\\project1.html"); fos = new FileOutputStream("a.txt"); while((b = fis.read()) != -1){ fos.write(b); } } catch (IOException e) { e.printStackTrace(); } finally { try { fis.close(); fos.close(); } catch (IOException e) { e.printStackTrace(); } }
- 文件拷贝范例2(多个字节读写)
FileInputStream fis = null; FileOutputStream fos = null; int len; byte[] temp = new byte[1024]; try { fis = new FileInputStream("D:\\Code\\HTML\\project1.html"); fos = new FileOutputStream("a.txt"); while((len = fis.read(temp)) != -1){ fos.write(temp,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { try { fis.close(); fos.close(); } catch (IOException e) { e.printStackTrace(); } }
4.字节缓冲流
- 字节缓冲流是将数据从硬盘读/写到缓冲区中,待缓冲区满了再进行读/写,不用每次都到硬盘中操作,提高了读写的效率
- 字节缓冲流仅仅提供缓冲区,而真正读写数据还得依靠基本的字节流对象进行操作
- 字节缓冲输出流BufferedOutputStream
- 构造方法:
public BufferedOutputStream(OutputStream out);
默认缓冲区大小public BufferedOutputStream(OutputStream out,int size);
指定缓冲区大小
- 构造的参数是OutputStream类型的对象
- 字节缓冲输入流BufferedInputStream
- 构造方法:
public BufferedInputStream(InputStream in);
默认缓冲区大小public BufferedInputStream(InputStream in,int size);
指定缓冲区大小
- 构造的参数是InputStream类型的对象
- 使用缓冲流拷贝文件范例1(单个字节读写)
BufferedInputStream bis = null; BufferedOutputStream bos = null; int b; try { bis = new BufferedInputStream(new FileInputStream("D:\\Code\\HTML\\project1.html")); bos = new BufferedOutputStream(new FileOutputStream("a.txt")); while((b = bis.read()) != -1){ bos.write(b); } } catch (IOException e) { e.printStackTrace(); } finally { try { bis.close(); bos.close(); } catch (IOException e) { e.printStackTrace(); } }
- 使用缓冲流拷贝文件范例2(多个字节读写)
BufferedInputStream bis = null; BufferedOutputStream bos = null; int len; byte[] b = new byte[1024]; try { bis = new BufferedInputStream(new FileInputStream("D:\\Code\\HTML\\project1.html")); bos = new BufferedOutputStream(new FileOutputStream("a.txt")); while((len = bis.read(b)) != -1){ bos.write(b,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { try { bis.close(); bos.close(); } catch (IOException e) { e.printStackTrace(); } }
5.字符流
- 由于编码和解码的方式不同,计算机中可能会出现乱码的问题
- Windows默认使用的码表是GBK,一个汉字占两个字节,IDEA和以后工作默认使用Unicode的UTF-8编解码格式,一个汉字占三个字节
- 字符串中的编码和解码
byte[] getBytes()
,使用平台默认字符集将该String编码为一系列字节,将结果存储到字节数组中返回byte[] getBytes(String charsetName)
,使用指定的字符集将该String编码为一系列字节,将结果存储到字节数组中返回public String(byte[] bytes)
,String类的一个构造方法,使用平台默认的字符集对给定的字节数组进行解码(创建字符串对象)public String(byte[] bytes,String charsetName)
,String类的一个构造方法,使用指定的字符集对给定的字节数组进行解码(创建字符串对象)
- 如果需要将文本文件数据读取到内存或者将内存数据写入文本文件时,建议使用字符流,而文件拷贝使用字节流
- 字符输出流FileWriter
- 构造方法
方法名 |
说明 |
public FileWriter(String filepath) |
通过文件路径创建字符输出流对象 |
public FileWriter(File file) |
通过文件对象创建字符输出流对象 |
public FileWriter(String filepath,boolean append) |
append指定是否允许在文件追加数据 |
public FileWriter(File file,boolean append) |
append指定是否允许在文件追加数据 |
- 字符流底层使用到了字节流
- 字符流写数据
方法名 |
说明 |
void write(int c) |
写一个字符,int值是字符的ASCII值 |
void write(char[] cbuf) |
写一个字符数组 |
void write(char[] cbuf,int off,int len) |
写一个字符数组的指定长度 |
void write(String str) |
写一个字符串 |
void write(String str,int off,int len) |
写一个字符串的指定长度 |
- flush()方法和close()方法
void flush()
:刷新流,后续仍然能写数据void close()
:关闭流,关闭前会刷新一次,后续不能再写数据 - 写文件范例
FileWriter fw = null; char[] c = new char[]{'廖','向','前'}; try { fw = new FileWriter("a.txt"); fw.write(c); } catch (IOException e) { e.printStackTrace(); }finally { try { fw.close(); } catch (IOException e) { e.printStackTrace(); } }
- 字符输入流FileReader
- 构造方法
方法名 |
说明 |
public FileReader(String filepath) |
通过文件路径创建字符输入流对象 |
public FileReader(File file) |
通过文件对象创建字符输入流对象 |
- 底层用到字节流
- 字符流读数据
方法名 |
说明 |
int read() |
读一个字符,返回的int是字符的整数表示 |
int read(char[] cbuf) |
读最多cbuf.leng个字符到字符数组中,返回实际读到的字符数 |
int read(char[] cbuf,int off,int len) |
读最多len个字符到字符数组中,返回实际读到的字符数 |
- 读文件范例
FileReader fr = null; char[] chars = new char[1024]; int len; try { fr = new FileReader("a.txt"); while((len = fr.read(chars)) != -1){ System.out.println(new String(chars,0,len)); } } catch (IOException e) { e.printStackTrace(); }finally { try { fr.close(); } catch (IOException e) { e.printStackTrace(); } }
6.字符缓冲流
- 字符缓冲流也是用于提高效率的
- 字符缓冲输出流BufferedWriter
- 构造方法
public BufferedWriter(Writer out);
默认缓冲区大小public BufferedWriter(Writer out,int size)
,指定缓冲区大小 - 构造的参数是Writer
- 特殊方法
public void newLine();
写入一个换行符
- 字符缓冲输入流BufferedReader
- 构造方法
public BufferedReader(Reader in);
默认缓冲区大小public BufferedReader(Reader in,int size)
,指定缓冲区大小 - 构造的参数是Reader
- 特殊方法
public String readLine();
读一行数据并返回,如果是文件末尾返回null
- 使用字符缓冲输出流范例
BufferedWriter bw = null; try { bw = new BufferedWriter(new FileWriter("a.txt")); bw.write("我是大帅逼"); } catch (IOException e) { e.printStackTrace(); }finally { try { bw.close(); } catch (IOException e) { e.printStackTrace(); } }
- 使用字符缓冲输入流范例
BufferedReader br = null; char[] chars = new char[1024]; int len; try { br = new BufferedReader(new FileReader("a.txt")); while((len = br.read(chars)) != -1){ System.out.println(new String(chars,0,len)); } } catch (IOException e) { e.printStackTrace(); }finally { try { br.close(); } catch (IOException e) { e.printStackTrace(); } }
7.转换流
- InputStreamReader,字节流转为字符流,硬盘-->内存
- InputStreamReader是FileReader的父类,构造FileReader对象底层是用到了InputStreamReader的构造方法
- 作用是将硬盘中的字节码读取出来,经过解码从而转换为字符,所以使用字符流使用到了转换流
- OutputStreamWriter,字符流转为字节流,内存-->硬盘
- OutputStreamWriter是FileWriter的父类,构造FileWriter对象底层是用到了OutputStreamWriter的构造方法
- 作用是将内存中的字符经过编码转换为字节码,然后写入硬盘,所以使用字符流使用到了转换流
- 转换流的构造方法可以指定字符集,在JDK11以前字符流不能指定字符集,而JDK11后字符流新增了可以指定字符集的构造方法,从而可以直接使用字符流来完成读写而无需使用转换流
8.对象流
- 将对象以流的形式传输,这种流称为对象流
- 分类
graph TB;
对象流-->对象输入流
对象流-->对象输出流
对象输入流-->ObjectInputStream
对象输出流-->ObjectOutputStream
- 对象输出流又称为对象序列化流,用于将对象写入文件或在网络中传输,使用writeObject()方法写对象
- 构造方法:
public ObjectOutputStream(OutputStream out);
- 常用方法:
void writeObject(Object o);
- 对象输入流又称为对象反序列化流,用于将对象从文件中读到内存或接收网络中的对象,使用readObject()方法读对象,注意类型转换
- 构造方法:
public ObjectInputStream(InputStream in);
- 常用方法:
Object readObject();
- 对象需要序列化必须实现Serializable接口,该接口是标记接口,没有任何抽象方法,只要是实现该接口的类,它的对象就能序列化
- 注意事项1
- 用对象序列化流序列化一个对象后,如果修改了对象所属的类,再次读取该对象时会抛出InvalidClassException异常
- 这是因为serialVersionUID是由虚拟机自动生成,写入文件时与读取文件时的serialVersionUID不一致造成的
- 解决办法是在序列化的类中手动给出serialVersionUID,并且这个值固定不变,格式是:
private static final long serialVersionUID = long值;
- 注意事项2
- 如果类中某个成员变量的值不想要被序列化,可以给该成员变量加上transient关键字修饰,表示该成员变量不参与序列化过程
- 读取多个对象时难以分辨循环中是否读取到文件末尾,所以在存对象时可以将对象先存放在集合中再写入文件,这样读取对象时可以只使用一次读操作将该集合读取出来
9.Properties类
- 是Map集合中的一个类,存放的是双列数据
- 含有跟IO相关的方法
- 键值对的数据类型一般使用String
- 由于是Map集合体系中的类,所以一般的增删改查方法与Map集合中的相同
- 其他方法
方法名 |
说明 |
Object setProperty(String key,String value) |
设置集合的键和值,都是String类型,底层调用HashTable的put()方法 |
String getProperty(String key) |
根据键获取值 |
Set stringPropertyNames() |
返回一个装有键的Set集合,其中键值都是String类型 |
- load()和store()方法
方法名 |
说明 |
void load(InputStream in) |
从字节输入流中读取键值对到Peoperties集合 |
void load(Reader reader) |
从字符输入流中读取键值对到Peoperties集合 |
void store(OutputStream out,String comments) |
将Properties集合中的键值对写入字节输出流,保存到文件中 |
void store(Writer writer,String comments) |
将Properties集合中的键值对写入字符输出流,保存到文件中 |
- comments参数作用是在配置文件中写注释
- 一般存放Properties集合的文件后缀是.properties,一般用于配置文件
- 范例
//store()方法使用 Properties properties = new Properties(); properties.setProperty("user","root"); properties.setProperty("password","root"); FileWriter fw = null; try { fw = new FileWriter("prop.properties"); properties.store(fw,null); } catch (IOException e) { e.printStackTrace(); } finally { try { fw.close(); } catch (IOException e) { e.printStackTrace(); } } //load()方法使用 Properties newProperties = new Properties(); FileReader fr = null; try { fr = new FileReader("prop.properties"); newProperties.load(fr); System.out.println(newProperties); } catch (IOException e) { e.printStackTrace(); } finally { try { fr.close(); } catch (IOException e) { e.printStackTrace(); } }
- 由于Properties集合没有关闭流的操作,所以不建议使用匿名内部类的形式来使用store()和load()方法
多线程
1.概述
- 多线程是指从硬件或软件上实现多个线程并发执行的技术
- 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能
2.并行和并发
- 并行:在同一时刻,有多个指令在多个CPU上同时执行
- 并发:在同一时刻,有多个指令在单个CPU上交替执行
3.进程和线程
- 进程:正在运行的软件
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以和其他进程一起并发执行
- 线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,称为单线程程序
- 多线程:一个进程如果有多条执行路径,称为多线程程序
4.多线程实现方式
- 继承Thread类方式进行
- 自定义类继承Thread类
- 在自定义类中重写run()方法
- 创建自定义类的对象
- 启动线程
- 范例
public class Test { public static void main(String[] args){ //创建线程1 MyThread thread1 = new MyThread(); //创建线程2 MyThread thread2 = new MyThread(); //启动线程1 thread1.start(); //启动线程2 thread2.start(); } } class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i); } } }
- 注意事项
- run()方法用来封装被线程执行的代码,如果直接调用该方法不能实现多线程,而是单纯的方法调用
- start()方法用来启动一个线程,然后JVM调用该线程的run()方法
- 实现Runnable接口方式进行
- 自定义类实现Runnable接口
- 在自定义类中重写run()方法
- 创建自定义类对象
- 创建Thread类对象,把自定义类对象作为构造方法的参数
- 启动线程
- 范例
public class Test { public static void main(String[] args){ //创建线程1 MyRunnable myRunnable1 = new MyRunnable(); Thread thread1 = new Thread(myRunnable1); //创建线程2 MyRunnable myRunnable2 = new Runnable(); Thread thread2 = new Thread(myRunnable2); //启动线程1 thread1.start(); //启动线程2 thread2.start(); } } class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i); } } }
- 利用Callable接口和FutureTask类进行
- 自定义类实现Callable接口
- 在自定义类中重写call()方法
- 创建自定义类的对象
- 创建FutureTask类的对象,把自定义类的对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- FutrueTask中的get()方法可以获取线程执行完后的返回值,如果在线程开启前调用该方法,那么该程序会一直停留在该代码处
- 范例
public class Test { public static void main(String[] args){ //创建线程1 MyCallable myCallable1 = new MyCallable(); FutureTask<String> futureTask1 = new FutureTask<>(myCallable1); Thread thread1 = new Thread(futureTask1); //创建线程2 MyCallable myCallable2 = new MyCallable(); FutureTask<String> futureTask2 = new FutureTask<>(myCallable2); Thread thread2 = new Thread(futureTask2); //启动线程1 thread1.start(); //启动线程2 thread2.start(); //获取执行完的结果 System.out.println(futureTask1.get()); System.out.println(futureTask2.get()); } } class MyCallable implements Callable<String> { @Override public String call() throws Exception { for(int i = 0;i < 100;i++){ System.out.println(i); } //返回值表示线程执行完返回的结果 return "该线程执行完啦"; } }
- 三种方式对比
方式 |
优点 |
缺点 |
继承Thread类 |
编程简单,run()方法中能直接使用Thread类中的方法 |
扩展性差,不能继承其他类 |
实现Runnable接口或实现Callable接口 |
扩展性强,实现接口的同时还能继承其它类 |
编程复杂,run()方法或者call()方法中不能直接使用Thread类中的方法 |
5.Thread类常用方法
方法名 |
说明 |
String getName() |
获取线程名称 |
void setName(String name) |
设置线程名称 |
public static Thread currentThread() |
返回当前线程对象的引用 |
public static void sleep(long time) |
让线程休眠指定时间,单位为毫秒 |
public final void setPriority(int priority) |
设置线程优先级(优先级范围是1~10,默认是5) |
public final int getPriority() |
获取线程优先级 |
public final void setDaemon(boolean on) |
设置线程为守护线程 |
- 线程调度
- 多线程的并发运行:计算机中的CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行代码,各个线程轮流获得CPU的使用权,分别执行各自的任务
- 两种调度模型:
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程优先级相同,则随机执行,优先级高的线程获得的时间片的概率较高
- java使用第二种调度模型
- 守护线程:守护线程是为了守护普通线程而存在的,普通线程执行完毕后,守护线程也会停止运行
6.线程安全问题
- 多线程操作共享数据时,会出现多线程的数据安全问题,例如卖票的案例
- 解决方式:把操作共享数据的多条代码锁起来,让任意时刻只能有一个线程去执行,Java中提供同步代码块的方式来解决
- 同步代码块
- 格式:
synchronized(obj){共享数据的代码}
- obj表示任意一个对象,每一个对象都有一个锁
- 一个线程要想执行代码块中的代码必须获得这个锁,任何时刻只能有一个线程可以获得这个代码块的锁,多个线程想要实现同步则必须共用一个锁
- 当代码块执行完毕时或者代码块中抛出异常都会释放锁
- 好处是解决了多线程数据安全问题,弊端是降低程序执行效率
- 同步方法
- 格式:
修饰符 synchronized 返回值类型 方法名(方法参数){}
- 同步方法的锁对象是this
- 如果同步方法是静态方法,则锁对象是类名.class
- 同步代码块和同步方法区别
- 同步代码块是锁住指定代码,同步方法是锁住方法中全部代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
- Lock锁
- 为了更加清晰表达如何加锁和释放锁,JDK5以后提供了一个锁对象Lock
- 由于Lock是接口,所以实际使用它的实现类ReentrantLock类来实例化
- Lock提供了两个方法
void lock();
和void unlock();
来分别加锁和释放锁,需要同步的代码则处于这两个方法之间 - 为了避免在同步代码中出现异常导致程序中断而没能执行unlock()方法,所以unlock()方法一般放在finally块中处理
- 死锁:线程死锁是由于两个或多个线程持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。比如在锁嵌套时会出现死锁
7.生产者消费者模式
- 等待唤醒机制
- 多线程协作的模式,加深多线程编程的理解
- Object类中的等待和唤醒方法
方法名 |
说明 |
void wait() |
使线程处于等待状态,直到另一个线程调用该对象的notify()方法或notifyAll()方法 |
void notify() |
唤醒正在等待的单个线程 |
void notifyAll() |
唤醒正在等待的所有线程 |
- 生产者消费者模式范例
public class Test { public static void main(String[] args){ Desk desk = new Desk(); Foodie foodie = new Foodie(desk); Cooker cooker = new Cooker(desk); foodie.start(); cooker.start(); } } class Desk{ //定义标记 //true表示桌上还有汉堡 //false表示桌上没有汉堡 private boolean flag; //汉堡包总数量 private int count; //锁对象 private final Object lock = new Object(); public Desk() { this(false,10); } public Desk(boolean flag, int count) { this.flag = flag; this.count = count; } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } public int getCount() { return count; } public void setCount(int count) { this.count = count; } public Object getLock() { return lock; } @Override public String toString() { return "Desk{" + "flag=" + flag + ", count=" + count + ", lock=" + lock + '}'; } } class Foodie extends Thread{ //消费者步骤: //判断桌子是否含有汉堡 //没有就等待 //有就开吃 //吃完之后,唤醒生产者继续生产 //汉堡数量-1 private Desk desk; public Foodie(Desk desk) { this.desk = desk; } @Override public void run() { while(true){ synchronized (desk.getLock()){ if(desk.getCount() == 0){ break; }else{ if(desk.isFlag()){ //有,开吃 System.out.println("消费者吃汉堡....."); desk.setFlag(false); desk.getLock().notifyAll(); desk.setCount(desk.getCount()-1); }else{ //桌子上没有,等待 //使用什么锁对象,就用那个锁对象去唤醒或等待 try { desk.getLock().wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } } class Cooker extends Thread{ //生产者步骤: //判断桌子是否有汉堡 //如果有则等待 //如果没有则生产汉堡,放在桌子上 //唤醒等待的消费者 private Desk desk; public Cooker(Desk desk) { this.desk = desk; } @Override public void run() { while(true){ synchronized (desk.getLock()) { if(desk.getCount() == 0){ break; }else{ if(desk.isFlag()){ //有,等待 try { desk.getLock().wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ //没有,开始生产 //放在桌上 //唤醒消费者 System.out.println("生产者正在做汉堡....."); desk.setFlag(true); desk.getLock().notifyAll(); } } } } } }
- 阻塞队列
- ArrayBlockingQueue类,底层是数组,有界,没有无参构造方法
- LinkedBlockingQueue类,底层是链表,无界但最多能存放int的最大值,无参构造方法默认容量就是最大值
- 常用方法
put(Object o);
,将参数放入队列,如果放不进去会阻塞take();
,取出第一个数据,取不到会阻塞
- 使用阻塞队列实现生产者消费者模式
import java.util.concurrent.ArrayBlockingQueue; public class Test { public static void main(String[] args) throws InterruptedException { ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1); Foodie foodie = new Foodie(arrayBlockingQueue); Cooker cooker = new Cooker(arrayBlockingQueue); foodie.start(); cooker.start(); } } class Foodie extends Thread{ private ArrayBlockingQueue<String> arrayBlockingQueue; public Foodie(ArrayBlockingQueue<String> arrayBlockingQueue){ this.arrayBlockingQueue = arrayBlockingQueue; } @Override public void run() { while (true) { try { String take = arrayBlockingQueue.take(); System.out.println("生产者消费了一个"+take); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Cooker extends Thread{ private ArrayBlockingQueue<String> arrayBlockingQueue; public Cooker(ArrayBlockingQueue<String> arrayBlockingQueue){ this.arrayBlockingQueue = arrayBlockingQueue; } @Override public void run() { while (true) { try { arrayBlockingQueue.put("汉堡包"); System.out.println("生产者放了一个汉堡包"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
- 由于锁是在put()和take()里边,而输出的语句在外边,所以控制台输出时会出现重复