文件操作-- File
创建File对象
-
File(String pathname)
-
java.io.File用于表示文件(目录),也就是说程序员可以通过File类在程序中操作硬盘上的文件和目录
-
File类只用于表示文件(目录)的信息(名称,大小等等),不能对象文件的内容进行访问
-
构造方法
-
File(Stirng pathName)
-
通过将给定路径名字符串转换成抽象路径名来创建一个新File实例
-
抽象路径应尽量使用相对路径,并且目录的层级分离不要直接写"/“或”\",应使用File.separator这个常量表示,以避免不同系统带来的差异
@Test public void testFile(){ //File对象只是指向某个文件/目录,无所谓它是否存在 //相对于当前项目在本地的src目录 File file = new File("demo" + File.separator + "HelloWorld.txt" ); try{ byte[] bytes = new byte[10]; FileInputStream fis = new FileInputStream(file); fis.read(bytes); }catch (FileNotFoundException e){ e.printStacktrace(); }catch (IOException e){ e.printStacktrace(); } System.out.println(file); }
-
File(File parent, String child)
-
根据parent抽象路径名和child路径名字符串创建一个新File实例
@Test public void testFile2(0){ File parent = new File("demo"); File file = new File(parent,"HelloWorld.txt"); System.out.println(file); }
-
-
-
isFile()方法
- File的isFile方法用于判断当前File对象所表示的是不是一个文件
- boolean isFile()
@Test
public void testIsFile(){
File file = new File("demo" + File.separator + "HelloWorld.txt" );
System.out.println(file.isFile());//true
file = new File("demo");
System.out.println(file.isFile());//false
}
length()方法
- File的length方法用于返回此抽象路径名表示的文件的长度(占用的字节量 byte或B)
- long length();
- 返回值 : 当前File对象所表示的文件所占用的字节量
@Test
public void testLength(){
File file = new File("demo" + File.separator + "HelloWorld.txt" );
System.out.println(file.length());
}
exists方法
- File的exists方法用于测试此抽象路径名表示的文件或目录是否存在
- boolean exists();
- 返回值 : 若该File表示的文件或目录存在则返回true,否则false
@Test
public void testExsits(){
File file = new File("demo" + File.separator + "HelloWorld.txt" );
System.out.println(file.exists());
}
createNewFile()方法
- File的createNewFile()方法用于当前仅当不存在具有此抽象路径名指定的名称的文件时,原子的创建由此抽象路径名指定的一个新的空文件
- boolean createNewFile();
- 返回值 : 如果指定的文件不存在并且创建成功则返回true,如果指点的文件已经存在,则返回false.
@Test
public void testCreateNewFile(){
File file = new File("demo" + File.separator + "HelloWorld1.txt" );
System.out.println(file.exists());
try{
System.out.println(file.createNewFile());
}catch(IOException e){
e.printStackTrack();
}
}
delete()方法
- File 的 delete()用于删除此抽象路径名表示的文件或目录
- boolean delete();
- 返回值 : 当且仅当成功删除文件或目录时,返回true,否则返回false
- 需要注意的是,若此File对象所表示的是一个目录时,在删除时需要保证此为空目录才可以成功删除(目录中不能含有任何子项)
@Test
public void testDelete(){
File file = new File("demo" + File.separator + "HelloWorld1.txt" );
System.out.println(file.delete());//true
file = new File("demo");
System.out.println(file.delete());//false
}
isDirectory()方法
- File 的 isDirectory() 方法用于判断当前File表示的是否为一个目录
- boolean isDirectory();
- 返回值 : 当前File对象表示的是一个目录是返回true,否则返回false
@Test
public void testIsDirectory(){
File file = new File("demo" + File.separator + "HelloWorld.txt" );
System.out.println(file.isDirectory());//false
file = new File("demo");
System.out.println(file.isDirectory());//true
}
mkdir()方法
- File的mkdir()方法用于创建此抽象路径名的指定的目录
- boolean mkdir();
- 返回值 : 当且仅当已创建目录时,返回true 否则返回false
@Test
public void testMkDir(){
File file = new File("myDir" );
System.out.print(file.mkdir());
}
mkdirs()方法
- File的mkdirs()方法用于创建此抽象路径名的指定的目录,包括所有必须存在且不存在的父目录
- boolean mkdirs();
- 返回值 : 当且仅当已创建目录时及所有必须的父目录时,返回true 否则返回false
- 注意,此操作失败时,也有可能创建一部分的父目录
@Test
public void testMkDirs(){
File file = new File("myDir" + File.separator + "myDirs");
System.out.print(file.mkdirs());
}
ListFiles()方法
- File的ListFiles方法用于返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的子项(文件或目录)
- File[] ListFiles();
- 返回值 : 抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件和目录,如果目录为空,那么数组也将为空,如果抽象路径名不表示一个目录或者发生I/O错误,则返回null
@Test
public void testListFiles(){
File file = new File("demo");
File[] files = file.listFiles();
for(File file : files)
{
System.out.println(file.getName());
}
}
FileFilter接口
- FileFilter 用于抽象路径名的过滤器
- 此接口的实例可传递给File类的listFiles(FileFilter)方法,用于返回满足该过滤器要求的子项
- File[] listFiles(FileFilter)
@Test
public void testFileFilter(){
File file = new File("demo");
File[] subFiles = file.listFiles(new FileFilter(){
@Override
public boolean accept(File pathName){
return pathName.getName().startsWith("H");
}
});
for(File file : subFiles)
{
System.out.println(file.getName());
}
}
RandomAccessFile
创建对象
- 简介:
- Java提供了一个可以对文件随机访问的操作,访问包括读和写操作,该类名为RandomAccessFile,该类的读写是基于指针的操作.
- RandomAccessFile在对文件进行随机访问操作时有两个模式,分别为只读模式(只读取文件数据),和读写模式(对文件数据进行读写)
访问模式
- 在创建RandomAccessFile时,其提供的构造方法要求我们传入访问模式
- RandomAccessFile(File file, String mode);
- RandomAccessFile(String filename, String mode);
- 其中构造方法的第一个参数是需要访问的文件,而第二个参数是访问模式:
- “r” : 字符串"r"表示对该文件的访问是只读的.
- “rw” : 字符串"rw"表示对该文件的访问是读写模式.
字节数据读写操作
write()方法
- RandomAccessFile 提供了一个可以向文件中写出字节的方法
- void write(int d)
- 该方法会根据当前指针所在位置写入一个字节,是将参数int的"低八位"写出
read()方法
- RandomAccessFile 提供了一个可以向文件中读取字节的方法
- int read()
- 该方法会从文件中读取一个byte(8位)填充到int的低八位,高24位为0,返回值范围正数0-255,如果返回-1表示读取到了文件的末尾,每次读取后自动移动指针,准备下次读取.
写入文件为什么会乱码?
- RandomAccessFile 写的时候用的是ISO-8859-1(编码),写入到文件的时候(文件绝大多数时候使用的是gb2312解码格式
@Test
public void testRandomAccessFile() throws IOException {
File file = new File( "src"+File.separator +"demo" + File.separator + "HelloWorld.txt" );
RandomAccessFile randomAccessFile = new RandomAccessFile(file,"r");
int read = randomAccessFile.read();
System.out.println(read);
}
@Test
public void testRandomAccessFileWrite() throws IOException {
File file = new File( "src"+File.separator +"demo" + File.separator + "HelloWorld.txt" );
RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw");
// randomAccessFile.write(633333);
//randomAccessFile.seek(0);//重置指针位置
System.out.println(new String(randomAccessFile.readLine().getBytes("ISO-8859-1"),"UTF-8"));
}
write(byte[] d)方法
-
RandomAccessFile提供了一个可以向文件中写出一组字节的方法
-
void write(byte[] d)
-
该方法会根据当前指针所在位置处连续写出给定数组中的所有字节
-
与该方法相似的还有一个常用方法
-
void writr(byte[] d. int offset int len)
-
该方法会根据当前指针所在位置处连续写出给定数组中的部分字节,这个部分是从数组的offset处开始,连续len个字节
@Test
public void testWriteByteArray() throws Exception{
RandomAccessFile raf = new RandomAccessFile("raf.dat","rw");
//将字符串的按照默认编码转换为字节数组
byte[] buf = "你好".getBytes();
//将字节数组一次性写出
raf.write(buf);
raf.close();
}
read(byte[] b)
- RandomAccessFile 提供了一个可以从文件中批量读取字节的方法
- int read(byte[] b)
- 该方法会从指针位置处尝试最多读取的给定数组的总长度的字节量,并从给定的字节数组第一个位置开始,将读取的到的字节顺序存放至数组中,返回值为实际读取到的字节量
@Test
public void testReadByteArray() throws Exception{
RandomAccessFile raf = new RandomAccessFile("raf.dat","r");
//创建长度为10的字节数组
byte[] buf = new byte[10];
//尝试读取10个字节存入数组,返回值为读取的字节量
int len = raf.read(buf);
System.out.println("读取到了" + len + "个字节");
System.out.println(Arrays.toString(buf));
System.out.println(new String(buf));
}
close()方法
- RandomAccessFiles在对文件访问的操作全部结束之后,要调用close方法来释放与其相关的所用系统资源
- void close()
文件指针操作
getFilePointer()方法
- RandomAccessFile的读写操作是都是基于指针的,也就是说总是在指针当前位置进行读写操作
- long getFilePointer()
- 该方法用于获取当前RandomAccessFile的指针位置
skipBytes方法
- RandomAccessFile提供了一个方法可以尝试跳过输入的n个字节可以丢弃跳过的字节
- int skipBytes(int n)
- 该方法可能跳过一些较小的字节,这可能由任意数量的条件引起,在跳过n个字节之前,已达到文件的末尾只是其中一种可能
- 此方法不抛出EOFException异常,返回跳过的实际字节数,如果n为负数,则不跳过任何字节
seek方法
- RandomAccessFile提供了一个方法可以将指针移动到指定位置
- void seek(long pos)
- 该方法用于移动当前RandomAccessFile的指针位置
@Test
public void testPoint() throws IOException {
RandomAccessFile raf = new RandomAccessFile("raf.dat","r");
//输出指针位置
System.out.println("指针位置:" + raf.getFilePointer());//0
//读取World,需要将Hello跳过
raf.skipBytes(5);
System.out.println("指针位置:" + raf.getFilePointer());//5
//读取
byte[] bytes = new byte[5];
raf.read(bytes);
System.out.println(new String(bytes));
System.out.println("指针位置:" + raf.getFilePointer());//10
//移动指针到开始的位置
raf.seek(0);
System.out.println("指针位置:" + raf.getFilePointer());//0
raf.close();
}
基本IO操作
1.1IS和OS
输入与输出
我们编写的程序除了自身会定义一些数据信息外,经常还会引用外界的数据,或是将自身的数据发送到外界,比如:我们编写的程序想要读取一个文本文件,又或者我们想要将程序中的某些数据写入到一个文件中,这时我们就要使用输入与输出
- 什么是输入
- 输入是一个从外界进入到程序中的方向,通常我们需要"读取"外界的数据时,使用输入,所以输入是用来读取数据的
- 什么是输出
- 输出是一个程序发送到外界的方向,通常我们需要"写出"数据到外界时,使用输出,所以输出是用来写出数据的
节点流和处理流
-
按照流是否直接与特定的地方(如磁盘,内存,设备等)相连,分为节点流和处理流两类
-
节点流 : 可以从或向一个特定的地方(节点)读写数据
-
处理流 : 是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写
-
处理流的构造方法总是要带一个其他的流的对象做参数,一个流对象经过其他流的多次包装,称为流的链接
-
通常节点流也称为低级流
-
通常处理流也称为高级流或过滤流
IS和OS处理方法
-
InputStream是所有字节输入流的父类,其定义了基础的读取方法,常用方法如下
- int read()
- 读取一个字节,以int形式返回,该int值的"低八位"有效,若返回值为-1,则不表示EOF(end of file)
- int read(byte[] d)
- 尝试最多读取给定数组的length个字节并存入数组,返回值为实际读取到的字节量
- int read()
-
OutputStream是所有字节输出流的父类,其定义类基础的写出方法,常用方法如下:
- void write(int d)
- 写出一个字节,写出给定的int的"低八位"
- void write(byte[] d)
- 将给定的字节数组中所有的字节全部写出
- void write(int d)
文件流
创建FOS对象
-
FileOutputStream是文件的字节输出流,我们使用该流可以以字节为单位将数据写入文件
-
构造方法
-
FileOutputStream(File file)
- 创建一个向指定File对象表示的文件中写出的数据的文件输出流
-
FileOutputStream(String filename)
- 创建一个向具有指定名称的文件中写出数据的文件输出流
-
PS: 若指定文件已经包含内容,那么当使用FOS对其写入数据时,会将该文件中原有的数据全部清除
public void testFos() throws Exception {
//创建文件字节输出流
// FileOutputStream fos = new FileOutputStream(new File("fos.dat"));
FileOutputStream fos = new FileOutputStream("fos.dat");
fos.write("helloworld".getBytes());
fos.close();
}
创建FOS对象(追加模式)
-
构造方法
-
FileOutputStream(File file,boolean append)
- 创建一个向指定File对象表示的文件中写出的数据的文件输出流
-
FileOutputStream(String filename,boolean append)
- 创建一个向具有指定名称的文件中写出数据的文件输出流
-
PS : 以上两构造方法中,第二个参数若为true,那么通过该FOS写出的数据都是在文件末尾追加的
@Test
public void testFosByAppend() throws Exception {
//创建文件字节输出流
// FileOutputStream fos = new FileOutputStream(new File("fos.dat"),true);
FileOutputStream fos = new FileOutputStream("fos.dat",true);
fos.write("testFosByAppend".getBytes());
fos.close();
}
}
创建FIS对象
- FileInputStream是文件的字节输入流,我们使用该流可以以字节为单位从文件中读取数据
- 构造方法
- FileInputStream(File file)
- 创建一个向指定File对象表示的文件中读取数据的文件输入流
- FileInputStream(String name)
- 创建一个用于读取给定的文件系统中的路径名name所指定的文件的文件输入流
- FileInputStream(File file)
read()和write()方法
-
FileInputStream继承自InputStream,其提供了,以字节为单位读取文件数据的方法read()
-
int read()
- 从此输入流中读取一个数据字节,若返回-1,则表示EOF(end of file)
-
FIleOutputStream继承自OutputStream,其提供了以字节为单位向文件写入数据的方法write
-
void write(int d)
- 将指定字节写入此文件输出流,这里只写给定的int值的"低八位"
@Test
public void testCopyFile1() throws Exception{
FileInputStream fis = new FileInputStream("fos.dat");
FileOutputStream fos = new FileOutputStream("fos_copy.dat");
//读取数据
int d = -1;
while((d = fis.read()) != -1 ){
//实现文件复制
fos.write(d);
}
System.out.println("复制完毕");
fis.close();
fos.close();
}
}
read(byte[] d) 和write(byte[] d)方法
-
FileInputStream也支持批量读取字节数据的方法
-
int read(byte[] b)
- 从此输入流中将最多b.length个字节的数据读入到字节数组b中
-
FileInputStream也支持批量写出字节数据的方法
-
void write(byte[] b)
- 将b.length个字节从指定byte数组写出此文件输出流中
-
void write(byte[] b,int offset,int len)
- 将指定byte数组中从偏移量offset开始的len个字节写入此文件输出流
简述RandomAccessFile和FileInputStream和FileOutputStream在使用中的区别
相同点:
- 二者在进行读写操作时使用的方法write(int d)/write(byte[])/read()/read(byte[])都是相同的原理,都参考自InputStream/OutputStream
不同点
- RandomAccessFile基于指针的随机访问方式,通过改变指针可以任意的访问文件中的内容
- FileInputStream及FileOutputStream没有指针的概念,通过"覆盖"和"追加"两种模式完成
缓冲流
BOS基本工作原理
- 在向硬件设置做写出操作时,增大写出次数无疑会降低写出效率,为此我们可以使用缓冲输出流来一次性批量写出若干数据减少写出次数来提高写出效率
- BufferedOutputStream缓冲输出流,内部维护这一个缓冲区,每当我们向该流写出数据时,都会先将数据存入缓冲区,当缓冲区已满时,缓冲流会将数据一次性全部写出
@Test
public void testBos() throws Exception{
FileOutputStream fos = new FileOutputStream("fos.dat");
//创建缓冲字节输出流
BufferedOutputStream bos = new BufferedOutputStream(fos);
//所有字节被存入缓冲区等待被一次性写入
bos.write("helloworld".getBytes());
//关闭流之前,缓冲输出流会将缓冲区内容一次性写出
bos.close();
}
BOS的flush方法
-
使用缓冲输出流可以提高写的效率,但这也存在一个问题,就是写出数据缺乏即时性,有时我们需要在执行完某些操作后,就希望将这些数据确实写出,而非在缓冲区保存直到缓冲区满后才写出,这时我们可以使用缓冲流的一个方法flush
-
void flush()
-
清空缓冲区,将缓冲区中的数据强制写出
BIS基本工作原理
- 在读取数据时若以字节为单位读取数据,会导致读取次数过于频繁,从而大大的降低读取效率,为此我们可以通过提高一次读取的字节数量减少读写次数,来提高读取的效率
- BufferedInputStream是缓冲字节输入流,其内部维护着一个缓冲区(字节数组).使用该流在读取一个字节时该流会尽可能多的一次性读取若干字节并存入缓冲区,然后逐一的将字节返回,直到缓冲区中的数据被全部读取完毕,会再次读取若干字节从而反复,这样就减少了读取的次数,从而提高了读取效率
- BIS是一个处理流,该流为我们提供了缓冲功能
@Test
public void testBis() throws Exception{
FileInputStream fis = new FileInputStream("fos.dat");
//创建缓冲字节输入流
BufferedInputStream bis = new BufferedInputStream(fis);
int d = -1;
while((d = bis.read())!= -1){
System.out.println(d+" ");
}
bis.close();
}
@Test
public void testCopyByBuffered() throws IOException {
FileInputStream fileInputStream = new FileInputStream("fos.dat");
FileOutputStream fileOutputStream = new FileOutputStream("fos_copy.dat");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
int d = -1;
while ((d = bufferedInputStream.read())!= -1) {
bufferedOutputStream.write(d);
}
bufferedInputStream.close();
bufferedOutputStream.close();
}
序列化
对象序列化的概念
- 对象是存在内存中,有时候我们需要将对象保持到硬盘上,又有时,我们需要将对象传输到另一台计算机上等等这样的操作,这时我们需要将对象转化为一个字节序列,而这个过程又称为对象序列化。相反,我i们由一个字节序列文件需要将其转换为对应的对象,这个过程就称为对象的反序列化
使用OOS实现对象序列化
- objectOutputStream是用来对对象进行序列化的输出流
- void writeObject(Object o)
- 该方法可以将给定的对象转换为一个字节序列后输出.
Serializable接口
- objectOutputStream在对对象进行序列化时有一个要求,就是需要序列化的对象所属的类需要去实现Serializable接口
- 实现了该接口不需要重写任何方法,其只是作为可序列化的标志
- 通常实现该接口的类需要提供一个常量serialVersionUID(long类型),表名该类版本,若不显示的声明,在对象序列化时也会根据当前类的各个方面计算该类的默认serialVersionUID,但不同的平
- 台编译器实现不同,所以若想跨平台,都应该显示的声明版本号
PS:默认的序列化号在1.8版本及以后不支持兼容
- 如果声明的类型的对象的序列化存到硬盘上,之后随着需求的变化更改了类的属性(增加或减少或改名),那么当反序列时就会出现InvalidClassException异常,这样就会造成不兼容性问题
- 但当serialVersionUID相同时,他就会将不一样的filed以type的预设值(默认值),进行反序列化,可避开不兼容问题
@Test
public void testOOS() throws IOException {
Emp emp = new Emp("张三",18, '男',3000);
File file = new File("emp.obj");
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(emp);
System.out.println("序列化完毕");
objectOutputStream.close();
//如果出现NotSerializableException异常就说明没有实现Serializable接口
}
反序列化
@Test
public void testOIS() throws IOException, ClassNotFoundException {
Emp emp = new Emp("张三",18, '男',3000);
File file = new File("emp.obj");
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
System.out.println(objectInputStream.readObject());
System.out.println("反序列化完毕");
objectInputStream.close();
}
transient关键字
-
对象进行序列化后得到的字节序列往往比较大,有时我们在对一个对象进行序列化时可以忽略某些不必要的属性,从而对你序列化后的字节序列进行"瘦身"
-
transient关键字
-
被关键字修饰的属性在序列化时该值将被忽略
字符流原理
- Reader是字符输入流的父类
- Write是字符输出流 的父类
- 字符流是以字符(char)为读取单位,一次性处理一个Unicode
- 字符流底层依旧是字节流
常用方法
Reader的常用方法
- int read() : 读取一个字符,返回的int值为"低16位"有效
- int read(char[] chs) : 从该流中读取一个字符数组的length字符并存入该数组,返回值位实际读取到的字符量
Write常用方法
- void write(int c)写出一个字符,写出给定int值的"低16位"为表示的字符
- void write(char[] chs) : 将给定字符数组中的所有字符写出
- void write(String str) : 将给定的字符串写出
- void write(char[] chs,int offset,int len) : 将给定的字符数组中从offset开始练习len个字符写出
转换流
字符转换流原理
-
InputStreamReader字符输入流
-
使用该流可以设置字符集,并按照指定字符集从流中按照该编码格式将字符字节数据转换为字符并读取
-
OutputStreamWriter字符输出流
-
使用该流可以设置字符集,并按照指定的字符集将字符转换为对应字节后通过该流写出
指定字符编码
-
InputStreamReader的构造方法允许我们设置的字符集
-
InputStreamReader(InputStream,String charsetName)
-
基于给定的字节输入流以及字符编码创建ISR
-
InputStreamReader(InputStream)
-
该构造方法会根据系统默认字符集创建ISR
-
OutputStreamWriter构造方法
-
OutputStreamWriter(OutputStream out,String charsetName)
-
基于给定的字节输入流以及字符编码创建OSR
-
OutputStreamWriter(OutputStream out);
-
该构造方法会根据系统默认字符集创建OSR
@Test
public void testOSW() throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("osw.txt");
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream,"UTF-8");
outputStreamWriter.write("hello,张三");
outputStreamWriter.close();
}
@Test
public void testISR() throws IOException {
FileInputStream fileInputStream = new FileInputStream("osw.txt");
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
int chs = -1;
while((chs = inputStreamReader.read())!= -1){
System.out.print((char) chs);
}
inputStreamReader.close();
}
名词解释:ISO-8859-1,GBK,UTF-8
-
ISO-8859-1:是Java的一种编码格式,是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。此字符集支持部分于欧洲使用的语言
-
GBK:是在GB2312-80标准基础上的内码扩展规范,使用了双字节编码方案,
其编码范围从8140至FEFE(剔除xx7F),共23940个码位,共收录了21003个汉字,完全兼容GB2312-80标准,支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字,并包含了BIG5编码中的所有汉字。 -
UTF-8:是针对Unicode的一种可变长度字符编码。它可以用来表示Unicode标准中的任何字符,范围为字节0x00到0x7F(ASCII兼容)
文件数据的IO操作
PrintWriter
- 创建PrintWriter对象
- PrintWriter是具有自动行刷新的缓冲字符输出流,其提供了比较丰富的构造方法
- PrintWriter(File file)
- PrintWriter(String fileName)
- PrintWriter(OutputStream out)
- PrintWriter(OutputStream out,boolean autoFlush)
- PrintWriter(Writer writer)
- PrintWriter(Writer writer, boolean autoFlush)
- 其中参数为OutputStream与Writer的构造方法提供了一个可以传入boolean值参数,该参数用于表示PrinterWriter是否具有自动行刷新
print和println方法
PrintWriter提供了丰富的重载print和println方法,其中println方法在于输出目标数据后,自动输出一个系统支持的换行符,若该流是具有自动行刷新的,那么每当通过println方法写出的内容都会被实际写出,而不是进行缓存
- 常用方法
- void print(int i) : 打印整数
- void print(char c) : 打印字符
- void print(double d) : 打印double值
- void print(boolean b) : 打印boolean值
- void print(char[] c) : 打印字符数组
- void print(float f) : 打印float值
- void print(long l) : 打印long值
- void print(String s) : 打印字符串
@Test
public void testPrinterWriter() throws IOException {
//创建PrinterWriter对象
//创建字节流对象
FileOutputStream fileOutputStream = new FileOutputStream("demo.txt");
//创建字符流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "UTF-8");
//创建缓冲字符输出流
PrintWriter printWriter = new PrintWriter(outputStreamWriter,true);
printWriter.print("大家好,我爱李连杰");
printWriter.close();//关闭时,printWriter也会进行flush
}
BufferedReader
-
BufferedReader缓冲字符输入流,其内部提供缓冲区,可以提高读取效率
-
BufferedReader常用构造方法
- BufferedReader(Reader reader)
-
使用BufferedReader读取字符串
- BufferedReader提供了一个可以便于读取一行字符串的方法
- Stirng readLine();
- 该方法连续读取一行字符串,直到取到换行符为止,返回的字符串中不包含换行符
- 如果已经达到流末尾就会返回null
@Test
public void testBufferedReader() throws IOException {
File file = new File("pw.txt");
file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
PrintWriter printWriter = new PrintWriter(outputStreamWriter);
printWriter.println(Calendar.getInstance().getTime() + "张三" + "删除了数据库20条数据");
printWriter.println(Calendar.getInstance().getTime() + "李四" + "更新了emp表中id为33的员工salary");
printWriter.flush();
printWriter.close();
FileInputStream fileInputStream = new FileInputStream(file);
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine())!= null)
System.out.println(str);
bufferedReader.close();
}
异常处理
使用返回值状态表示异常
-
在Java语言出现以前,传统和异常处理方式,多采用返回值来表示程序出现的异常情况,这种方式虽然为程序员所熟悉,但却用多个坏处
- 首先,一个API可以返回任意 的返回值,而这些返回值本身并不能解释该返回值是否代表一个异常发生了和该异常的具体情况,需要调用API的程序自己判断并解释返回值的含义
- 其次,并没有一种机制来保证异常情况一定会得到处理,调用程序可以简单的忽略该返回值,需用调用API的程序员记住去检测返回值并处理异常情况,这种方式还使得程序变得冗长,尤其是当进行IO操作等容易出现异常的处理时,代码的很大一部分用于处理异常情况的switch分支,程序代码的可读性变得很差
异常处理机制
当程序中抛出一个异常后,程序从程序中断导致异常的代码跳出,java虚拟机检测寻找和try关键字匹配处理该异常的catch,如果找到,将控制权交到catch块中的代码,然后继续往下执行程序,try块中发生异常的代码不会被重新执行,如果没有找到处理该异常的catch块.在所有的finally块代码被执行和当前线程所属的ThreadGoup的uncaughtException方法被调用后,遇到异常的当前线程被被终止.
异常的捕获和处理
-
Throwable 和 Error 和 Exception
-
Java异常结构中定义有Throwable类,Error和Exception是其派生的两个子类,其中Exception表示由于网络故障,文件损坏,设备错误,用户输入非法等情况导致的异常(代码错了就是Exception),而Error表示Java运行时环境出现异常,例如 : JVM内存资源耗尽
try-catch
- try{…}语句指定了一段代码,改代码就是一次捕获并处理例外的范围
- 在执行过程中,该段代码中可能会产生并抛出一种或几种类型的异常的异常对象,它后面的 catch语句分别对这些异常做了相应的处理
- 如果没有异常发生,所有的catch代码段都被略过不执行
- 在catch中的语句是对异常进行处理的代码
- 在catch中声明的异常对象封装了异常事件发生的信息,在catch语句块中可以使用这个对象的一些方法获取这些信息
@Test
public void testCopy(){
FileInputStream fis = null;
FileOutputStream fos = null;
try{
fis = new FileInputStream("fos.dat");
fos = new FileOutputStream("fos_copy.dat");
int d= -1;
while ((d = fis.read())!=-1){
fos.write(d);
}
}catch (FileNotFoundException e){ //catch是互斥的只会执行一个
System.out.println("文件没有找到");
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println("文件读取失败");
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
}catch (IOException e){
System.out.println("流关闭失败");
}
}
}
多个catch
- 每个try语句块可以伴随一个或者多个catch语句,用于处理可能产生的不同类型的异常
- catch捕获的异常类型由上至下的捕获异常类型的顺序应该是子类到父类
finally的作用
- finally语句为异常处理提供一个统一的出口,使得在控制流程转到程序的其他部分之前,能够对程序的状态统一管理
- 无论try所指定的程序块中是否抛出异常,finally所指定的代码都要执行
- 通常在finally语句中可以进行资源释放的工作,如关闭打开的文件,删除临时文件等等
- finally后面也能够写代码,在finally执行完之后执行
throw 关键字
- 当程序发生错误而无法处理的时候,会抛出对应的异常对象,除此之外,在某些时候,我们可能会想要自行抛出一个异常,例如在异常处理结束后,将异常抛出,让下一层异常处理块捕获,若想要自行抛出异常,我们可以使用throw关键字,并生成指定的异常对象后抛出.
try {
System.out.println(5/0);
}catch(ArithmeticException e){
System.out.println("1.发生了异常");
}
try{
throw new ArithmeticException();
}catch (ArithmeticException e){
System.out.println("2.发生了异常");
}
同一个try所对应的catch是互斥的,只会执行其中一个,如果想要抛出的异常被catch捕捉,则在程序中抛出不要写在catch中
throws关键字
- 在程序中会声明许多方法(Method).这些方法中可能会因为某些错误而引发异常,当我们不希望在这个方法中处理这些异常,而希望调用这个方法的方法来统一处理,这个时候我们可以使用throws关键字来声明这个方法将会抛出异常
// public void testThrows() throws FileNotFoundException(){
// File file = new File("fos.dat");
// FileInputStream fis = new FileInputStream(file);
// }
publiv void testThrows() throws NullPointerException{
System.out.println("123")
}
public void testOperate(){
testThrows();
}
PS : 编译器可以识别的异常抛出,则调用方法一定要处理,编译器无法识别的异常抛出,则可以不处理,则更多的是开发者之间的约定,建议也处理
重写方法时的throws
- 如果使用继承时,在父类别的某个方法上宣告了throws某些异常,而在子类中重新定义了该方法时,我们可以:
- 不处理异常(重新定义时不设定throws)
- 可仅throws父类别中声明的部分异常
- 可throws父类别方法中抛出异常的子类异常
- 但不可以是:
- throws出额外的异常
- throws父类方法中抛出异常的父类异常
小结 : 以上五个概念是正确的,当使用时要注意下列的事项:
FileNotFoundException,IOException是检查时异常
NullPointerException,ArithmeticException等都属于运行时异常
重写方法一定不能抛出新的检查时异常,或者抛出比被重写方法声明更加宽泛(父类)的检查时异常,但可以随便抛出非检查异常(运行时异常)
Error 和 RuntiimeException及其子类称为非检查异常(运行时异常),其他异常为检查时异常
RuntimeException
-
Java异常可以分为两类:检查时异常,非检查异常
-
可检查异常 : 可检查异常编译器验证,对于声明抛出异常的任何方法,编译器将强制执行或处理或声明规则,不捕捉这个异常,编译器就不通果,不允许编译
-
非检查异常 : 非检查异常不遵循处理或声明规则,在产生此类异常时,不一定非要采取任何适当操作,编译器不会检测是否已经解决了这样一个异常
-
RuntimeException类属于非检查异常,因为 JVM 操作引起的运行时异常随时有可能发生,此类异常一般是由特定的操作引起的,但这些操作在Java应用程序中会频繁出现,因此也不受编译器检查与处理或声明特定规则和限制
常见的RuntimeException
-
IllegalArgumentException : 抛出的异常表明向方法传递的一个不合法或不正确的参数
-
NullPointerException,ArrayIndexOutOgBoundsException,ClassCastException,NumberFormatException,ArithmeticException
Exception 的常用API
printStackTrace
- Throwable中定义了一个方法可以输出错误信息,用来跟踪异常事件发生时执行的堆栈内容,
- void printStackTrace();
getMessage
- Throwable中定义的一个方法可以得到有关异常事件的信息
- String getMessage
getCause
-
很多时候,当一个异常由另一个异常导致异常而被抛出的时候,Java库和开发源代码会将一种异常包装成另外一种异常,这时,日志记录和打印根异常就变得非常重要
-
Java异常类提供getCause()方法来检索导致异常的原因,这些可以对异常根层次的原因提供更多的信息.对Java实际对代码的调试或故障排除有很大的帮助
-
Throwable getCause()
- 获取该异常出现的原因
多线程基础
进程与线程
什么是进程
- 进程是操作系统中运行的一个任务(一般而言,一个应用程序运行在一个进程中)
- 进程(process)是一块包含了某些资源的内存区域,操作系统利用进程把它的工作划分为一些功能单元
- 进程中所包含的一个或多个执行单元称为线程(thread).进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问.
- 线程只能归属于一个进程并且它只能访问该进程所拥有的资源,但操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程
什么是线程
-
一个线程是进程的一个顺序执行流
-
同类的多个线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行时的堆栈(空间).线程在切换时负荷小,因此,线程被称为轻负荷进程,一个线程中可以包含多个线程.
进程与线程的区别?
- 一个进程至少有一个线程.
- 线程的划分尺度小于进程,使得多线程程序并发性高,另外,进程在执行过程中拥有独立的内存单元,而多线程共享内存,从而极大的提高了程序的运行效率.
- 线程在执行过程中与进程的区别在于每个独立的线程有一个程序运行的入口,顺序执行序列和程序出口,但是线程是不能够独立运行的,必须依存在应用程序中,由应用程序提供多个线程的执行控制
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行,但操作系统并没有将多个线程看做多个独立的应用来实现进程的调度和管理以及资源分配
创建线程
使用Thread创建并启动线程
- Thread类是线程类,其中每一个实例表示一个可以并发运行的线程,我们可以通过继承该类并重写run方法来定义一个具体的线程,其中重写run方法的目的是定义该线程要执行的逻辑,启动线程是调用线程的start()方法而非直接调用run()方法,start()方法会将当前线程纳入线程调度,使当前可以开始并发运行,当线程获取时间片段后自动开始执行run()方法中的逻辑
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
MyThread myThread1 = new MyThread();
//此时的线程出于new状态,处于该状态的线程无法获取CPU分配的时间片资源,不会运行
//将new状态 => 可执行状态
myThread.start();
myThread1.start();
}
}
class MyThread extends Thread {
@Override//当前线程要执行的方法
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(i + this.getName());
}
}
}
使用Runnable创建并启动线程
- 实现Runnable接口并重写run方法来定义线程体,然后在创建线程的时候将 Runnable 的实例传入并启动线程
- 这样的好处在于可以将线程与线程要执行的任务分离开减少耦合,同时 Java 是单继承的,定义一个类实现Runnable接口这样的做法可以更好的去实现其他父类或接口,因为接口是多实现关系.
public class TestRunnable extends JFrame implements Runnable {
public static void main(String[] args) {
TestRunnable testRunnable = new TestRunnable();
testRunnable.setSize(300,300);
testRunnable.setVisible(true);
Thread thread = new Thread(testRunnable);
thread.start();
}
@Override
public void run() {
//改变窗体颜色
JPanel jPanel = new JPanel();
jPanel.setSize(300,300);
//将面板放入窗体中
this.setContentPane(jPanel);
int i = 0;
while(true){
i = i==0?1:0;
if(i==0){
jPanel.setBackground(Color.BLACK);
}else{
jPanel.setBackground(Color.WHITE);
}
}
}
}
使用内部类创建线程
- 通常我们可以通过匿名内部类的方式创建线程,使用该方式可以简化编写代码的复杂度,当一个线程仅需要一个实例时我们通常使用这样的方式来创建.
public class TestAnonymousRunnable {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}).start();
}
}
线程的使用场合
- 线程通常用于一个程序中需要完成多个任务的情况,我们可以将每个任务单行为一个线程,使得他们得以一同工作
并发原理
- 多个线程"同时"运行只是文我们感官上的一种表现,事实上我们线程是并发运行的,OS将时间划分为很多时间片段(时间片),尽可能均匀的分配给每一个线程,获取时间片段的线程被CPU运行,而其他的线程全部等待,所以微观上是走走停停的,宏观上都在运行,这种现象叫并发,但不是绝对意义上的"同时发生".
线程操作API
- Thread.currentThread方法
- Thread的静态方法currentThread方法可以用于获取运行当前代码片段的线程
- Thread current = Thread.currentThread();
public void TestCurrentThread{
public static void main(String[] args){
System.out.println("运行main方法的线程为" + Thread.currentThread());
//调用
testCurrent();
Thread t = new Thread(){
@Override
public void run(){
System.out.println("线程t:" + Thread.currentThread());
testCurrent();
}
};
//启动线程
t.start();
}
public static void testCurrent(){
System.out.println("运行testCurrent方法的线程为:" + Thread.currentThread());
}
}
获取线程信息
- Thread提供了获取线程信息的相关方法
- long getId() : 返回该线程的标识符
- String getName() : 返回该线程的名称
- int getPriority() : 返回线程的优先级
- Thread.state getState() : 获取线程的状态
- boolean isAlive() : 测试线程是否处于活动状态
- boolean isDaemorn() : 测试线程是否为守护线程
- boolean isInterrupted ; 测试线程是否已经中断
Thread t = new Thread("地瓜花");
System.out.println(t.getName());
System.out.println(t.getId());
线程的优先级
-
线程的切换是由线程调用控制的,我们无法通过代码来进行干涉,但是我们可以通过提高线程的优先级来最大程度的改善线程获取时间片的记录
-
线程的优先级来最大程度的改善线程获取时间片的记录
-
线程的优先级被划分为10级,值分别是1-10,其中1最低,10最高,线程提供3个常量来表示最低,最高,以及默认的优先级.
- Thread.MIN_PRIORITY
- Thread.MAX_PRIORITY
- Thread.NORM_PRIORITY
-
void setPriority(int priority) : 设置线程的优先级
-
void getPriority() : 获取线程的优先级
守护线程
- 守护线程与普通线程在表现上没有什么区别,我们只需要通过Thread提供的方法来设定即可:
- void setDaemon(boolean)
- 当参数为true时,该线程为守护线程
- 守护线程的特点是,当进程中只剩下守护线程时,所有的守护线程强制终止
- GC就是运行在一个守护线程上
sleep()方法
- Thread的静态方法sleep用于使用当前线程进入阻塞状态
- static void sleep(long ms)或者使用该方法TimeUnit.SECONDS.sleep(1);
- 该方法会使当前线程进入阻塞状态指定毫秒,当阻塞指定毫秒后,当线程会重新进入Runnable状态,等待分配时间片.
- 该方法会声明抛出InterruptedException,所以在使用该方法需要捕获这个异常
PS : 让一个线程提前结束阻塞状态进入可运行状态,可以让其他线程调用阻塞线程的interrupt()方法,会让该线程提前结束阻塞
该方法是以抛出异常InterruptedException的方法让线程提前结束阻塞
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("线程开始");
try {
this.sleep(1000000000);
} catch (InterruptedException e) {
System.out.println("出现了InterruptedException异常");
}
System.out.println("线程结束...");
}
};
//启动线程
thread.start();
System.out.println("测试开始");
//调用线程的interrupt方法,提前结束阻塞状态
thread.interrupt();
System.out.println("测试结束");
yiled方法
- Thread的静态方法yield
- static void yield()
- 该方法用于使当前线程主动让出当次CPU时间片回到Runnable状态,等待分配时间片
join方法
- Thread的方法join
- void join()
- 该方法用于等待当前线程结束
- 该方法声明抛出InterruptedException
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
System.out.println("t1 : 正在下载图片: " + i*10 +"%");
//延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1:图片下载完成");
}
};
Thread t2 = new Thread(){
@Override
public void run() {
System.out.println("t2: 等待图片下载完成");
//使用join方法使得当前线程结束
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 : 显示图片 ");
}
};
t1.start();
t2.start();
线程同步
- synchronized关键字
- 多个线程并发读写同一个临界资源时会发生"线程并发安全问题"
- 常见的临界资源
- 多线程共享实例变量
- 多线程静态公共变量
- 若想解决多线程安全问题,需要将异步操作变为同步操作
- 异步操作 : 多线程并发操作,相当于各干各的
- 同步操作 : 有先后顺序的操作,相当于你干完,我再干
- synchronized关键字是java中的同步锁
锁机制
-
Java提供了一种内置的锁机制来支持原子性
- 原子性 : 不可再分割 --> 一段代码不可再分割 --> 这段代码执行完毕之后,别的线程才能进来访问
-
同步代码块(synchronized关键字),同步代码块包含两部分:
- 一个作为锁对象的引用,一个作为由这个锁保护的代码块
synchronized(同步监视器--锁对象引用){ //代码块 //调用方法 }
- 若方法所有代码都需要同步也可以给方法直接加锁
- 每个Java对象都可以用作一个实现同步的锁,线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而且无论是通过正常途径退出还是通过抛出异常退出都一样,获得内置锁的唯一途径就是进入这个锁保护的同步代码块或者方法
public class TestSynchronized extends Thread {
public static void main(String[] args) {
//创建临界资源
Object lock = new Object();
//获取当前时间
long startTime = System.currentTimeMillis();
Runnable t1 = new Runnable() {
@Override
public void run() {
System.out.println("t1启动" + (System.currentTimeMillis()-startTime));
//加同步锁
synchronized (lock){
System.out.println("t1拿到了锁" + (System.currentTimeMillis()-startTime));
System.out.println("t1开始睡眠" + (System.currentTimeMillis()-startTime));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1释放了锁" + (System.currentTimeMillis()-startTime));
}
};
Runnable t2 = new Runnable(){
@Override
public void run() {
System.out.println("t2启动" + (System.currentTimeMillis()-startTime));
synchronized (lock){
System.out.println("t2拿到了锁" + (System.currentTimeMillis()-startTime));
}
}
};
Thread T1 = new Thread(t1);
Thread T2 = new Thread(t2);
T1.start();
T2.start();
}
}
选择合适的锁对象
- 使用synchronized需要对象上锁以保证线程的同步,那么这个锁对象应该注意:
- 多个同步锁的线程在访问该同步块时,看到的应该是同一个锁的对象引用,否则达不到同步效果
- 通常我们会使用this来作为锁对象
选择合适的锁范围
- 在使用同步块时,应该尽量在允许的情况下减少同步范围,以提高并发的执行效率.
方法锁和静态方法锁
- 方法锁 : 对象锁(实例锁)
- synchronized是对类的当前实例(当前对象)进行加锁,防止其他线程同时访问该类的实例的所有的synchronized块,每个对象都有一个锁,且是唯一的
- 静态方法锁:
- public synchronized static void xxx(){…}
- 那么该方法的对象是类对象,每个类都有唯一个的一个类对象,获取类对象的方式 : 类名.class
- 静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的,原因在于,静态方法,锁的是类对象,而非静态方法,锁的是当前方法所属对象.
public class TestSynchronizedStatic {
public synchronized void sum(){
int count = 0;
count++;
System.out.println(Thread.currentThread()+","+count + "进入方法");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"运行结束");
}
public static void main(String[] args) {
TestSynchronizedStatic sync = new TestSynchronizedStatic();
Object lock = new Object();
long startTime = System.currentTimeMillis();
Runnable t1 = new Runnable() {
@Override
public void run() {
sync.sum();
}
};
Runnable t2 = new Runnable() {
@Override
public void run() {
sync.sum();
}
};
Thread thread1 = new Thread(t1);
thread1.start();
Thread thread2 = new Thread(t2);
thread2.start();
}
}
PS :
- 指的是"类的当前实例",类的两个不同实例就没有这种约束了
- 对静态方法加锁
- static synchronized又称为类锁/全局锁
- 该锁针对的是类,无论实例出多少个对象,那么线程依然共享
- static synchronized 是限制多个线程中该类的所用实例同时访问该类所对应的代码块
PS :
- static synchronized 并不是关键字,只是代表给静态方法加锁
- 锁住的只是static synchronized 块,synchronized块锁不住,而不加锁的方法更加锁不住
wait和notify
- 多线程之间需要协调工作
- 如果条件不满足,则等待,当条件满足时,等待该条件的线程将被唤醒,在java中,这个机制的实现依赖于wait/notify.等待机制与锁机制是密切关联的.
notify和notifyAll的区别
- notify和notifyAll()都是用来唤醒调用wait()方法进入等待锁资源队列的线程,区别在于:
- notify()唤醒正在等待此对象监视器的单个线程,如果有多个线程在等待,则其中一个随机唤醒(由JVM调度器决定),唤醒的线程享有公平的竞争资源的权力
- notifyAll(): 唤醒正在等待此对象监视器的所有线程,唤醒的所有线程公平竞争资源.
wait
- wait是object类的实例方法,调用wait会使得当前线程进入等待状态(getState==WAITTING),(进入该状态不会获取时间片,解除该状态回到Runnable之后才有可能获取时间片),只有获取到锁才能调用
- wait(在同步块内使用),未获得锁时wait会抛出异常,等待状态会释放执行wait的锁的资源(仅限于执行wait的锁,其他资源并不会释放).
- wait可以设置等待时间,到达时间自动唤醒而不需要(notify,notifyAll).调用需要捕获InterruptedException(避免数据期间设置中断标志)
PS : 线程如果调用wait方法失去锁资源,而后被其他线程唤醒,如果此时线程没有获取对应的锁资源,即使唤醒也依旧不会执行.
public class TestWaitAndNotify {
public static void main(String[] args) {
//创建两个临界资源
Object o = new Object();
Object o1 = new Object();
long startTime = System.currentTimeMillis();
//2.创建线程
Runnable rw1 = new Runnable(){
@Override
public void run() {
System.out.println("rw1启动:" + (System.currentTimeMillis()-startTime));
synchronized (o){
System.out.println("rw1拿到了锁1:" +(System.currentTimeMillis()-startTime));
synchronized (o1){
System.out.println("rw1拿到了锁2:" +(System.currentTimeMillis()-startTime));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒全部线程
o1.notifyAll();//谁调用就将把等待谁的全部线程唤醒
System.out.println("rw1开始等待:"+(System.currentTimeMillis()-startTime));
try {
//rw1锁持有的o锁释放
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("rw1等待结束:"+(System.currentTimeMillis()-startTime));
o.notifyAll();
}
System.out.println("rw1释放了锁2:"+(System.currentTimeMillis()-startTime));
try {
System.out.println("释放了锁二,但没有释放锁一");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("这里才是释放了锁1");
}
System.out.println("rw1释放了锁1:"+(System.currentTimeMillis()-startTime));
}
};
Runnable rw2 = new Runnable() {
@Override
public void run() {
System.out.println("rw2启动"+(System.currentTimeMillis()-startTime));
synchronized (o){
System.out.println("rw2拿到了锁1"+(System.currentTimeMillis()-startTime));
System.out.println("rw2睡眠1秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
o.notifyAll();
System.out.println("rw2开始等待" + (System.currentTimeMillis()-startTime));
try {
o.wait(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("rw2等待结束"+(System.currentTimeMillis()-startTime));
}
}
};
Runnable rw3 = new Runnable() {
@Override
public void run() {
System.out.println("rw3启动"+(System.currentTimeMillis()-startTime));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("rw3拿到了锁2:"+(System.currentTimeMillis()-startTime));
}
System.out.println("rw3释放了锁2:"+(System.currentTimeMillis()-startTime));
}
};
Thread RW1 = new Thread(rw1);
Thread RW2 = new Thread(rw2);
Thread RW3 = new Thread(rw3);
RW1.start();
RW2.start();
RW3.start();
}
}
volatile
- Java提供了一种稍弱的同步机制,即volatile修饰的变量,用来确保将变量更新的操作实时通知到其他线程
package api;
public class TestVolatileVariable {
public static int found = 0;
public static void main(String[] args) {
//创建线程
new Thread(){
@Override
public void run() {
System.out.println("老板上菜有点慢");
while (0==found){//第一次的时候取了found,后面一直都是线程副本中的found
//加了volatile修饰,使得线程每次获取时间片运行的时候都先去主内存中重新获取
}
System.out.println("菜做的不好吃");
}
}.start();
new Thread(){
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("催老板上菜");
change();
}
}.start();
}
public static void change(){
found = 1;
}
}
PS:
- found 变量是多线程之间共享的,每次修改都会同步到主内存中
- 但是每个线程运行的时候都只会读取一次
- 加了volatile之后会每次运行的时候都从主内存读取一次
volatile和synchronized的区别
- volatile是轻量级,只能修饰变量, synchronized是重量级的,还可以修饰方法
- volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会发生阻塞,synchronized既可以保证可见性,同时可以保证原子性,因为只有获取了锁的线程才能够进入临界区,从而保证临界区所有的语句都全部执行,多个线程争夺synchronized锁对象时,会出现线程阻塞.所以zaisidangdeshihoushiyongvolatile可以提高线程执行的效率.
Java公平锁&非公平锁
- 公平锁 : 每个线程获取锁的机会是平等的,常见的公平锁对象 : ReetrantLock(true)
- 非公平锁 : 每个线程获取锁的机会是不平等的,常见的非公平锁 : synchronized,ReetrantLock(false)
论证synchronized是非公平锁
package api;
public class SynchronizedDemo {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task,"线程一");
Thread thread1 = new Thread(task,"线程二");
Thread thread2 = new Thread(task,"线程三");
thread.start();
thread1.start();
thread2.start();
}
}
class Task implements Runnable{
@Override
public void run() {
while(true){
synchronized (this){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
}
//结果全是线程一
//或者全是线程二
//或者全是线程三
小结:在一段时间内较大概率输出线程一的信息,说明被"线程一"长时间持有,体现了非公平性
论证ReetrantLock(true)是公平锁
package api;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SynchronizedDemo {
public static void main(String[] args) {
Task task = new Task();
Thread thread = new Thread(task,"线程一");
Thread thread1 = new Thread(task,"线程二");
Thread thread2 = new Thread(task,"线程三");
thread.start();
thread1.start();
thread2.start();
}
}
class Task implements Runnable{
Lock lock = new ReentrantLock(true);
@Override
public void run() {
while (true){
lock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println(Thread.currentThread().getName());
}
}
}
读写锁
-
独占锁(写锁)/共享锁(读锁)
- 独占锁 : 指该锁一次只能被一个线程锁持有,对于ReentrantLock和Synchronized而言都是独占锁
- 共享锁 : 指该锁可以被多个线程持有
- 读读可共存
- 读写不可共存
- 写写不能共存
-
为什么会有读锁和写锁
- 原理我们使用ReentrantLock和Synchronized创建锁的时候,均是是独占锁,也就是说一次只能一个线程访问,但是如果我们业务有一个读写分离场景,读的时候可以让多人同时进行,这样的话原先的独占锁并发性就显的比较差了
- 而我们读业务并不会造成数据不一致问题,所以应该建议多人可以一起读.
-
多个线程同时读一个资源类是没有任何问题的,所以为了满足并发量,读取共享资源应该同时进行,但是,如果一个线程想要去写共享资源,就不应该再有其他线程可以对该资源进行读或写
package api;
import java.util.HashMap;
import java.util.Map;
public class MyCacheDemo {
Map<String,Object> map = new HashMap<>();
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+":"+"正在写入"+key);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key,value);
System.out.println(Thread.currentThread().getName()+":"+"写入完成");
}
public void get(String key){
System.out.println(Thread.currentThread().getName()+":"+"读取"+key);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.get(key);
System.out.println(Thread.currentThread().getName()+":"+"读取完成");
}
}
class ReadWriteLock{
public static void main(String[] args) {
MyCacheDemo myCacheDemo = new MyCacheDemo();
for (int i = 0; i < 5; i++) {
//线程start并不带表完成了.i在循环运行结束之后就等待回收了
//这个时候线程不一定完成了,所以要接一下
//也可以再用final修饰一下,防止多核cpu共同操作,改变了数据
int tempInt = i;
new Thread(){
@Override
public void run() {
myCacheDemo.put(tempInt + "",tempInt + "");
}
}.start();
}
for (int i = 0; i < 5; i++) {
int tempInt = i;
new Thread(){
@Override
public void run() {
myCacheDemo.get(tempInt+"");
}
}.start();
}
}
}
/*
Thread-0:正在写入0
Thread-1:正在写入1
Thread-2:正在写入2
Thread-3:正在写入3
Thread-4:正在写入4
Thread-6:读取1
Thread-5:读取0
Thread-7:读取2
Thread-8:读取3
Thread-9:读取4
Thread-2:写入完成
Thread-3:写入完成
Thread-1:写入完成
Thread-0:写入完成
Thread-4:写入完成
Thread-7:读取完成
Thread-6:读取完成
Thread-5:读取完成
Thread-9:读取完成
Thread-8:读取完成
*/
小结:
从目前的结果看到,写入的时候,写操作可以被其他线程打断,这就造成了,写操作还没完,读操作又过来了,数据量一旦变大就有可能造成读取null值的现象,这种情况应该避免
解决方案:为上述代码添加读写锁
创建读写锁
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
创建一个写锁
readWriteLock.writeLock().lock();
创建一个读锁
readWriteLock.readLock().lock();
读锁 释放
readWriteLock.readLock().unlock();
package api;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyCacheDemo {
Map<String,Object> map = new HashMap<>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+":"+"正在写入"+key);
Thread.sleep(300);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+":"+"写入完成");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+":"+"读取"+key);
Thread.sleep(300);
map.get(key);
System.out.println(Thread.currentThread().getName()+":"+"读取完成");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
}
class ReadWriteLock{
public static void main(String[] args) {
MyCacheDemo myCacheDemo = new MyCacheDemo();
for (int i = 0; i < 5; i++) {
//线程start并不带表完成了.i在循环运行结束之后就等待回收了
//这个时候线程不一定完成了,所以要接一下
//也可以再用final修饰一下,防止多核cpu共同操作,改变了数据
int tempInt = i;
new Thread(){
@Override
public void run() {
myCacheDemo.put(tempInt + "",tempInt + "");
}
}.start();
}
for (int i = 0; i < 5; i++) {
int tempInt = i;
new Thread(){
@Override
public void run() {
myCacheDemo.get(tempInt+"");
}
}.start();
}
}
}
/*
Thread-0:正在写入0
Thread-0:写入完成
Thread-1:正在写入1
Thread-1:写入完成
Thread-2:正在写入2
Thread-2:写入完成
Thread-3:正在写入3
Thread-3:写入完成
Thread-4:正在写入4
Thread-4:写入完成
Thread-6:读取1
Thread-5:读取0
Thread-7:读取2
Thread-8:读取3
Thread-9:读取4
Thread-5:读取完成
Thread-6:读取完成
Thread-9:读取完成
Thread-8:读取完成
Thread-7:读取完成
*/
从运行结果我们可以看出,写入操作是一个一个的线程进行执行的,并且中间不会被打断,而读操作操作的时候是5个线程同时进行的,然后并发读取
线程进入读锁的前提条件
- 没有其他线程的写锁
- 没有写请求或有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件
- 没有其他线程的读锁
- 没有其他线程的写锁
TCP通信
Socket简介
- socket通常称作"套接字",用于描述IP地址和端口,是一个通信链句柄,在Internet上的主机一般运行了多个服务软件,同时提供几种服务,每种服务都打开了一个socket,并绑定到一个端口上,不同的端口对应不同的服务
- 应用程序通常通过"套接字"向网络发送请求,或者应答网络请求,Socket和ServerSocket类位于java.net包中,ServerSocket用于服务端,Socket是建立网络连接时使用的,在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需要会话
获取本地地址和端口号
- Java.net.Socket为套接字,其提供了很多方法,其中,我们可以通过Socket获取本地的地址以及端口号
- Int getLocalPort()
- 该方法用于获取本地使用的端口号
- IntAddress getLocalAddress();
- 该方法用于获取套接字绑定的本地地址
- 使用InetAddress获取本地的地址方法
- String getCanonicalHostName()
- 获取此IP地址的完全限定域名
- String getCanonicalHostName()
- String getHostAddress()
- 返回IP地址字符串(以文本表现形式)
@Test
public void testSocket() throws IOException {
//构建Socket对象
Socket socket = new Socket("127.0.0.1",8088);
InetAddress add = socket.getLocalAddress();
System.out.println(add.getCanonicalHostName());
System.out.println(add.getHostAddress());
System.out.println(socket.getLocalPort());
}
获取远端地址的端口号
- 通过Socket获取远端地址以及端口号
- int getPost();
- 该方法用于获取远端使用的端口号
- InetAddress getIntAddress()
- 该方法用于获取套接字绑定的远端地址
聊天室示例
V1
-
问题java的Socket实现客户端和服务端之间的连接,并使得客户端向服务端发送一条数据
-
通信过程如下:
- 客户端
- 客户端发送:“你好,服务器”
- 服务器
- 接收客户端信息并显示
- 客户端
-
方案:
- 首先使用socket连接两个应用
- 使用相关API完成数据传递
-
步骤:
-
创建客户端类
-
创建Socket类的对象
-
创建客户端的工作方法start()
-
实现链接服务器并发送信息
-
首先Socket类的getOutputStream方法获取对应的Socket对象的网络字节输出流对象
-
然后,为了写数据(中文),构造缓冲字符输出流PrintWriter类对象,使用该对象的println方法向服务器发送数据
-
-
-
为客户端定义main方法
-
创建服务器端类
-
创建ServerSocket类对象
- 在server类中声明全局变量ServerSocket表示服务器端的ServerSocket对象,并在构造方法中实例化
-
创建服务器工作方法start(),用于读取客户端发来的信息
- 首先监听客户端的连接,得到Socket对象,并使用Socket类的getInputStream方法获取对应Socket对象的网络字节输出流对象
- 然后,为了读取数据,构造缓冲字符输入流BufferedReader类的对象,并调用该对象的readLine方法获取客户端发来的数据
PS: 这里需要进行异常处理,并在finally语句中,关闭Socket对象
-
创建main方法
-
测试
- 先启动服务器端,再启动客户端
-
package part1;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端应用程序
*/
public class ServerClient {
private ServerSocket serverSocket;
public ServerClient() {
try {
serverSocket = new ServerSocket(8088);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开启方法
*/
public void start(){
System.out.println("等待客户端连接");
//监听客户端连接并且得到客户端的socket对象
try {
Socket socket = serverSocket.accept();
System.out.println("客户端已连接");
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
System.out.println("客户端说:"+bufferedReader.readLine());
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(serverSocket != null){
serverSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ServerClient serverClient = new ServerClient();
serverClient.start();
}
}
package part1;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
/**
* 客户端应用程序
* 实现向服务端发送一条数据
*/
private Socket socket;
public Client(){
try {
this.socket = new Socket("localhost",8088);
} catch (IOException e) {
e.printStackTrace();
}
}
public Client(String ip, int port) {
}
/**
* 客户端的工作方法
*/
public void start(){
//获取网络字节输出流
try {
OutputStream outputStream = socket.getOutputStream();
//构建字符流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8");
PrintWriter printWriter = new PrintWriter(outputStreamWriter,true);
printWriter.println("你好,服务器");
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(socket != null){
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
V2
- 问题
- 改善聊天室V1,实现客户端重复发送数据到服务器端的功能,即用户可以在控制台不断输入内容并将内容逐一发送给服务器端,服务器端接收并显示
package part1;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端应用程序
*/
public class ServerClient {
private ServerSocket serverSocket;
public ServerClient() {
try {
serverSocket = new ServerSocket(8088);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开启方法
*/
public void start(){
System.out.println("等待客户端连接");
//监听客户端连接并且得到客户端的socket对象
try {
Socket socket = serverSocket.accept();
System.out.println("客户端已连接");
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
while (true){
String string = bufferedReader.readLine();
if(string.equals("exit")){
System.out.println("客户端已退出");
break;
}else {
System.out.println("客户端说:"+string);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(serverSocket != null){
serverSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ServerClient serverClient = new ServerClient();
serverClient.start();
}
}
package part1;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class Client {
/**
* 客户端应用程序
* 实现向服务端发送一条数据
*/
private Socket socket;
public Client(){
try {
this.socket = new Socket("localhost",8088);
} catch (IOException e) {
e.printStackTrace();
}
}
public Client(String ip, int port) {
}
/**
* 客户端的工作方法
*/
public void start(){
//获取网络字节输出流
try {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要发送的信息");
OutputStream outputStream = socket.getOutputStream();
//构建字符流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8");
PrintWriter printWriter = new PrintWriter(outputStreamWriter,true);
while (true){
String string = scanner.nextLine();
if (string.equals("")){
System.out.println("发送内容不能为空");
} else {
printWriter.println(string);
if(string.equals("exit")){
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(socket != null){
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
V3
- 问题
- 重构聊室案例,使用线程来实现一个服务器端可以同时接收多个客户端的信息(客户端是不用改变的)
package part1;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端应用程序
*/
public class ServerClient {
private ServerSocket serverSocket;
public ServerClient() {
try {
serverSocket = new ServerSocket(8088);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
*线程体,用于并发处理不同客户端的交互
*/
private class ClientHandler implements Runnable{
//线程用于处理客户端
private Socket socket;
//初始化socket
public ClientHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
InputStream in = null;
try{
System.out.println(Thread.currentThread().getName()+"客户端已连接");
in = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(in,"UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
while (true){
String string = bufferedReader.readLine();
if(string.equals("exit")){
System.out.println(Thread.currentThread().getName()+"客户端已退出");
break;
}else {
System.out.println(Thread.currentThread().getName()+"客户端说:"+string);
}
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 服务端开启方法
*/
public void start(){
try{
while (true){
Socket socket = serverSocket.accept();
ClientHandler clientHandler = new ClientHandler(socket);
new Thread(clientHandler).start();
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ServerClient serverClient = new ServerClient();
serverClient.start();
}
}
V4
-
问题
-
使得服务端可以将用户信息转发给所有客户端,并在每个客户端控制台上显示
-
首先在服务器端定义一个集合类型属性,用于存储所有客户端的输出流
-
然后在Server的内部类中的run方法中最开始处将客户端的输出流存入该集合.
-
之后,每当客户端发送信息就遍历集合,将信息写入集合中所有的输出流中(相当于将信息转发给所有的客户端)
-
-
-
步骤:
- 定义Server类
- 定义属性allout,该属性使用PrintWriter作为集合泛型,用于存储输出流.
- 定义Server的内部类ClientHandler
- 实现向集合中添加客户端的输出流
- 在内部类的run方法中,通过该线程处理的客户端的Socket,获取向该客户端发送信息输出流,并将该输出流包装为PrintWriter后存储结合allout中,当客户端断线,需要将输出流从集合allout中删除.
- 修改run方法,将客户端发送的信息转发给所有客户端
- 在获取到客户端发送过来的信息后,遍历集合allout,将获取到的信息写入该集合 的每一个输出流中,从而将该信息发送给所有客户端.
- 定义Server的start和mian方法
- start方法和main方法于之前V3相同
- 定义客户端线程要执行的任务
- 在Client类中定义内部类Handler,该内部类实现了Runnable并重写了run方法,此线程要执行的任务循环接收服务端的消息并打印在控制台
- 修改Client类的start方法,创建并启动线程
- 定义Server类
package part2;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class ServerClient {
//所有客户端的输出流
private List<PrintWriter> allout;
//服务端socket
private ServerSocket serverSocket;
/*
构造方法,用于初始化
*/
public ServerClient(){
try{
serverSocket = new ServerSocket(8088);
allout = new ArrayList<PrintWriter>();
} catch (IOException e) {
e.printStackTrace();
}
}
public class ClientHandlar implements Runnable{
//该线程用于处理的客户端的socket对象
private Socket socket;
public ClientHandlar(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
PrintWriter printWriter = null;
//创建向对应客户端发送信息的输出流对象
try {
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream,"UTF-8");
printWriter = new PrintWriter(outputStreamWriter,true);
//将该流存入共享集合
allout.add(printWriter);
//构建输入流获取客户端发送的信息
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
//通过输入流读取信息
String message = null;
while ((message = bufferedReader.readLine())!=null){
for (PrintWriter writer : allout) {
writer.println(message);
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
/*
当客户端断线,要将输出流从共享集合中删除
*/
}
}
}
/**
* 服务端开启方法
*/
public void start(){
try{
while (true){
Socket socket = serverSocket.accept();
ClientHandlar clientHandlar = new ClientHandlar(socket);
new Thread(clientHandlar).start();
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ServerClient serverClient = new ServerClient();
serverClient.start();
}
}
package part2;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Client {
/**
* 客户端应用程序
* 实现向服务端发送一条数据
*/
private Socket socket;
public Client(){
try {
this.socket = new Socket("localhost",8088);
} catch (IOException e) {
e.printStackTrace();
}
}
private class Handler implements Runnable{
@Override
public void run() {
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"UTF-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
//循环接收服务端信息并且打印
while (true){
System.out.println(bufferedReader.readLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 客户端的工作方法
*/
public void start(){
//获取网络字节输出流
try {
//将接收服务器端线程启动
Handler handler = new Handler();
Thread thread = new Thread(handler);
thread.setDaemon(true);
thread.start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要发送的信息");
OutputStream outputStream = socket.getOutputStream();
//构建字符流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8");
PrintWriter printWriter = new PrintWriter(outputStreamWriter,true);
while (true){
String string = scanner.nextLine();
if (string.equals("")){
System.out.println("发送内容不能为空");
} else {
printWriter.println(string);
if(string.equals("exit")){
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(socket != null){
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
V5
-
问题
- 客户端 的频繁连接与断开,会使得服务器端频繁创建和销毁线程
- 线程的过度切换也会为服务器带来崩溃的风险
- 多个线程会共享服务器端集合属性allout,这里还存在着多线程并发的安全问题
-
方案:
- 使用线程池技术来解决服务器端多线程的问题,并解决多线程并发安全问题
-
步骤
-
创建线程池属性,使用线程池创建线程
-
修改start方法,将原来创建并启动线程的代码替换为使用线程池管理的方式
-
解决allout并发安全问题
- 在Server中添加三个方法,用于操作属性allout,并使用同步锁,使得三个方法变为同步的
- 在程序中调用同步方法完成操作
package part3; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.*; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ServerClient { //所有客户端的输出流 private volatile HashMap<String,PrintWriter> allout; //服务端socket private ServerSocket serverSocket; private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); /* 构造方法,用于初始化 */ public ServerClient(){ try{ serverSocket = new ServerSocket(8088); allout = new HashMap<String,PrintWriter>(); } catch (IOException e) { e.printStackTrace(); } } public class ClientHandlar implements Runnable{ //该线程用于处理的客户端的socket对象 private Socket socket; public ClientHandlar(Socket socket) { this.socket = socket; } @Override public void run() { PrintWriter printWriter = null; //创建向对应客户端发送信息的输出流对象 try { OutputStream outputStream = socket.getOutputStream(); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream,"UTF-8"); printWriter = new PrintWriter(outputStreamWriter,true); //将该流存入共享集合 add(Thread.currentThread().getName(),printWriter); //构建输入流获取客户端发送的信息 InputStream inputStream = socket.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"UTF-8"); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); //通过输入流读取信息 String message = null; while ((message = bufferedReader.readLine())!=null){ String[] strings = message.split("@"); if(strings.length>1){ seedOnly(strings); }else { seed(strings); } } } catch (IOException e) { e.printStackTrace(); }finally { /* 当客户端断线,要将输出流从共享集合中删除 */ remove(); if (socket!=null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } } private void add(String string,PrintWriter printWriter){ reentrantReadWriteLock.writeLock().lock(); allout.put(string,printWriter); reentrantReadWriteLock.writeLock().unlock(); } private void remove(){ reentrantReadWriteLock.writeLock().lock(); allout.remove(Thread.currentThread().getName()); reentrantReadWriteLock.writeLock().unlock(); } private void seedOnly(String[] strings){ reentrantReadWriteLock.readLock().lock(); Set<Map.Entry<String,PrintWriter>> entrys = allout.entrySet(); for(Map.Entry<String,PrintWriter> entry : entrys){ if(entry.getKey().equals(strings[strings.length-1])){ entry.getValue().println(Thread.currentThread().getName()+"对你说:"+strings[0]); } } reentrantReadWriteLock.readLock().unlock(); } private void seed(String[] strings){ reentrantReadWriteLock.readLock().lock(); Set<Map.Entry<String,PrintWriter>> entrys = allout.entrySet(); for(Map.Entry<String,PrintWriter> entry : entrys){ entry.getValue().println(Thread.currentThread().getName()+":"+strings[0]); } reentrantReadWriteLock.readLock().unlock(); } /** * 服务端开启方法 */ public void start(){ ThreadPoolExecutor threadPoolExecutor = null; try{ while (true){ Socket socket = serverSocket.accept(); ClientHandlar clientHandlar = new ClientHandlar(socket); LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(5); threadPoolExecutor = new ThreadPoolExecutor( 2,10,60, TimeUnit.SECONDS,queue ); threadPoolExecutor.execute(clientHandlar); } }catch (IOException e){ e.printStackTrace(); }finally { if(serverSocket!=null){ try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } if(threadPoolExecutor!=null){ threadPoolExecutor.shutdown(); } } } public static void main(String[] args) { ServerClient serverClient = new ServerClient(); serverClient.start(); } }
-
线程池
-
线程池的概念
- 在实际项目中专门用于管理线程数量,生命周期,创建策略等一系列特性的角色
-
使用ExecutorService实现线程池
- 线程池有两个主要作用
- 控制线程数量
- 重用线程
- 线程池有两个主要作用
-
当一个程序中若创建大量线程,并在任务结束后销毁会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃,为此我们应使用线程池来解决这个问题
-
线程池的概念
- 首先创建一些线程,它们的集合称为线程池,当服务器收到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完之后不关闭该线程而是将线程还回到线程池
- 在线程池的编辑模式下,任务使提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程
- 一个线程同时只能执行一个任务,但同时可以向一个线程池提交多个任务
-
线程池有几种实现策略:
- Exectors.newCachedThreadPool()
- 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们
- Exectors.newFixedThreadPool(int Threads)
- 创建一个可重用固定线程集合的线程池,以共享的无界队列queue来运行这些线程.
- Exectors.newCachedThreadPool()
package ThreadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 测试线程池
*/
public class TestExecutorService {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
Handler handler = new Handler();
threadPool.execute(handler);
}
threadPool.shutdown();
}
}
class Handler implements Runnable{
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("执行当前任务的线程为"+name);
for (int i = 0; i < 10; i++) {
System.out.println(name+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+":任务完成");
}
}
Ps : 实际开发中并不建议使用这种便捷的方式使用线程池因为这种方式已经将线程池中各种参数固定了,而别人固定的参数往往并不是最适合我们业务,建议还是使用原生线程池对象ThreadPoolExecutor创建,根据具体业务需求,指定参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
corePoolSize : 线程池核心线程数量
maximumPoolSize : 线程池中中最大线程数量
keepAliveTime : 当活跃线程大于核心线程数时,空闲的多余线程最大存活时间
Timeunit unit : 存储时间的单位
workQueue : 存放任务的队列
handler : 超过线程范围和队列容器的任务的处理程序
线程池执行原理
- 提交一个任务到线程池,线程池流程如下:
- 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有信息线程没有创建)则创建一个新的工作线程来执行任务,如果核心线程都在执行任务,进入下个流程
- 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务储存在这个工作队列中,如果工作队列满了,则进入下个流程
- 判断线程处理的线程是否满了并且都处于工作状态,如果没有则创建一个新的工作线程来执行任务,如果满了则交给饱和策略处理这个任务
package ThreadPool;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExample implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(5);//FIFO 效率略高于ArrayBlockingQueue
//使用原生的方式创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5,10,60, TimeUnit.SECONDS,queue
);
for (int i = 0; i < 16; i++) {
threadPoolExecutor.execute(new Thread(new ThreadPoolExample(),"Thread"+i));
System.out.println("线程中活跃的线程数"+ threadPoolExecutor.getPoolSize());
if(queue.size()>0){
System.out.println("队列中阻塞的线程数" + queue.size());
}
}
threadPoolExecutor.shutdown();
}
}
线程池饱和策略
- 当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理,这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常
- JAVA中提供4种策略
- AbortPolicy: 直接抛出异常RejectedExecutionException
- 为线程池默认的阻塞策略,不执行此任务,而且会抛出一个运行时异常,该异常可以被捕获
- DisCardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
- DiscardPolicy : 不处理,不丢弃
- CallerRunsPolicy : 让调用execute方法的此线程command,会阻塞入口,这个调节机制,不抛弃任务也不抛出异常,而是将某些任务回到调用者本身,让当调用者所在线程去执行
- AbortPolicy: 直接抛出异常RejectedExecutionException
- 第五种策略
- 捕获RejectedExecutionException,然后自己处理该任务
单例模式
- 懒汉式
- 饿汉式
package model;
public class SingLton {
//饿汉式
private static SingLton instance = new SingLton();
public static SingLton getInstance(){
return instance;
}
// private static SingLton instance;
// //私有化构造方法
// private SingLton(){}
//
// //懒汉式
// public static SingLton getInstance(){
// //实例化instance
// if(instance==null){
// synchronized (SingLton.class){
// if(instance ==null){
// instance = new SingLton();
// }
// }
// }
// return instance;
// }
}
UDP
package UDP;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class MyUDPRecciver {
public static void main(String[] args) throws IOException {
//UDP使用DatagramSocket建立发送者和接收者的连接
DatagramSocket receiver = new DatagramSocket(8888);
System.out.println("接收者启动了");
//接收消息
//缓冲区
byte[] buffer = new byte[1024*1024];//一般设置为64kb以上
//buffer"接收数据所用的缓冲区
DatagramPacket pack = new DatagramPacket(buffer,buffer.length);
receiver.receive(pack);
System.out.println("收到消息了");
int len = pack.getLength();
String string = new String(buffer,0,len);
System.out.println(string);
}
}
package UDP;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
public class MyUDPSender {
public static void main(String[] args) throws Exception {
//数据包套接字
DatagramSocket socket = new DatagramSocket(9999);
//数据包
byte[] buffer = new byte[1024*1024];
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
//在包中设置数据
packet.setData("HelloWorld".getBytes());
//设置包接收地址
packet.setSocketAddress(new InetSocketAddress("localhost",8888));//绑定IP地址和端口
//255可以和任何ip地址匹配
// packet.setSocketAddress(new InetSocketAddress("192.168.124.255",8888));
//发送数据
socket.send(packet);
System.out.println("send over");
}
}
反射
- 通过java反射机制,可以在程序种访问已经转载到 JVM 种的Java对象的描述,实现访问,检测和修改Java对象本身信息的功能
- java反射功能强大,存在java.lang.reflect
- 所有类继承Object所以每个对象均有getClass()方法,该方法返回一个类型为Class的对象
Cell cell = new Cell();
Class test = cell.getClass();//test为一个Class类型的对象
-
利用Class类对象test,可以访问用来返回该对象的cell对象的描述信息
-
可以访问的信息如下
组成部分 访问方法 返回值 说明 包路径 getPackage() Package对象 获得该类的存放路径 类名称 getName() String对象 获得该类的名称 继承类 getSuperclass() 父类对象 获得该类继承的类 实现接口 getInterfaces() Class型数组 获得该类实现的所有接口 内部类 方法 getMethods() Method数组 获得所有权限为public的方法 getDeclaredMethods() Method数组 获得所有的方法,按声明顺序返回 构造方法 getConsructors Consructors数组 获得所有权限为public的构造方法 getDeclaredConsructors Consructors数组 获得所有的构造方法,按声明顺序返回 成员变量 getFileds() Filed数组 获得所有权限为public的成员变量 getDeclaredFileds() Filed数组 获得所有成员变量,按声明顺序返回 -
访问构造方法
- Constructor对象代表一个构造方法,利用Constructor对象可以操纵相应的构造函数
方法 | 说明 |
---|---|
isVarArgs | 查看该构造方法是否带有可变数量的参数,如果允许则返回true,否则返回false |
getParameterTypes() | 按照声明顺序以Class数组的形式获得该构造函数的各个参数的类型 |
getExceptionTypes() | 以Class数组的形式获得该构造函数可能抛出的异常类型 |
setAccessible(boolean flag) | 如果该构造函数方法权限为private,默认为不允许通过反射利用newinstance(Objec…initargs)方法创建对象,如果先执行该方法,并将入口参数设为true,则允许创建 |
newinstance(Objec…initargs) | 通过构造方法利用指定参数创建一个该类的对象,如果未设置参数则表示采用默认无参数的构造方法 |
String… strings写法
-
java语言对方法参数支持的一种写法,叫做可变长度参数列表
-
语法格式:
- 类型X… 参数名:表示此处接收参数为0到多个类型x的对象,或者是一个类型为X的数组
package TestReflect;
public class Test {
public Test() {
System.out.println("无参");
}
public Test(String... strings){
/**
*有这种类型的构造方法之后
* 1.允许写无参构造
* 2.不允许写Test(String[] strings)
* 3.可以按照这样调用
* Test test1 = new Test("abc","bcd");
* Test test2 = new Test(new String[]{"abc","bcd","dfg"});
* Test test = new Test();会优先调用无参,如果没有则调用本构造函数
*/
System.out.println("有参");
}
public static void main(String[] args) {
Test test = new Test();
Test test1 = new Test("abc","bcd");
Test test2 = new Test(new String[]{"abc","bcd","dfg"});
}
}
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
//在包中设置数据
packet.setData(“HelloWorld”.getBytes());
//设置包接收地址
packet.setSocketAddress(new InetSocketAddress(“localhost”,8888));//绑定IP地址和端口
//255可以和任何ip地址匹配
// packet.setSocketAddress(new InetSocketAddress(“192.168.124.255”,8888));
//发送数据
socket.send(packet);
System.out.println("send over");
}
}
# 反射
- 通过java反射机制,可以在程序种访问已经转载到 JVM 种的Java对象的描述,实现访问,检测和修改Java对象本身信息的功能
- java反射功能强大,存在java.lang.reflect
- 所有类继承Object所以每个对象均有getClass()方法,该方法返回一个类型为Class的对象
```java
Cell cell = new Cell();
Class test = cell.getClass();//test为一个Class类型的对象
-
利用Class类对象test,可以访问用来返回该对象的cell对象的描述信息
-
可以访问的信息如下
组成部分 访问方法 返回值 说明 包路径 getPackage() Package对象 获得该类的存放路径 类名称 getName() String对象 获得该类的名称 继承类 getSuperclass() 父类对象 获得该类继承的类 实现接口 getInterfaces() Class型数组 获得该类实现的所有接口 内部类 方法 getMethods() Method数组 获得所有权限为public的方法 getDeclaredMethods() Method数组 获得所有的方法,按声明顺序返回 构造方法 getConsructors Consructors数组 获得所有权限为public的构造方法 getDeclaredConsructors Consructors数组 获得所有的构造方法,按声明顺序返回 成员变量 getFileds() Filed数组 获得所有权限为public的成员变量 getDeclaredFileds() Filed数组 获得所有成员变量,按声明顺序返回 -
访问构造方法
- Constructor对象代表一个构造方法,利用Constructor对象可以操纵相应的构造函数
方法 | 说明 |
---|---|
isVarArgs | 查看该构造方法是否带有可变数量的参数,如果允许则返回true,否则返回false |
getParameterTypes() | 按照声明顺序以Class数组的形式获得该构造函数的各个参数的类型 |
getExceptionTypes() | 以Class数组的形式获得该构造函数可能抛出的异常类型 |
setAccessible(boolean flag) | 如果该构造函数方法权限为private,默认为不允许通过反射利用newinstance(Objec…initargs)方法创建对象,如果先执行该方法,并将入口参数设为true,则允许创建 |
newinstance(Objec…initargs) | 通过构造方法利用指定参数创建一个该类的对象,如果未设置参数则表示采用默认无参数的构造方法 |
String… strings写法
-
java语言对方法参数支持的一种写法,叫做可变长度参数列表
-
语法格式:
- 类型X… 参数名:表示此处接收参数为0到多个类型x的对象,或者是一个类型为X的数组
package TestReflect;
public class Test {
public Test() {
System.out.println("无参");
}
public Test(String... strings){
/**
*有这种类型的构造方法之后
* 1.允许写无参构造
* 2.不允许写Test(String[] strings)
* 3.可以按照这样调用
* Test test1 = new Test("abc","bcd");
* Test test2 = new Test(new String[]{"abc","bcd","dfg"});
* Test test = new Test();会优先调用无参,如果没有则调用本构造函数
*/
System.out.println("有参");
}
public static void main(String[] args) {
Test test = new Test();
Test test1 = new Test("abc","bcd");
Test test2 = new Test(new String[]{"abc","bcd","dfg"});
}
}