JavaSE 进阶-javase进阶(二)https://developer.aliyun.com/article/1469543
HashMap原理简单理解
HashMap重要属性:
hashMap构造器:
put方法:
新增方法中的hashCode算法:
计算位置的方法和entry对象:
经典面试题:
装填因子,负载因子,加载因子 为什么是0.75
装填因子设置为1:空间利用率得到了很大的满足,但是很容易碰撞,产生链表,查询效率边低
装填因子:0.5:碰撞几率低,扩容,产生链表几率低,查询快
于是HashMap做了个折中
主数组的长度为什么是2^n
原因1:
数组长度影响位置,H&length-1等效,等效的前提就是length必须是2的整数倍,
原因2:
防止哈希冲突,位置冲突
验证整数倍:
验证非正数倍
非整数倍,位置一样,会产生链表,导致效率变低
Collections工具类
Collections不支持创建对象
package CollectionsPrc; import java.util.ArrayList; import java.util.Collections; public class test1 { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("aa"); list.add("bb"); list.add("cc"); Collections.addAll(list,"dd","ee","ff"); System.out.println(list); //必须在有需要的集合查找 //binarySearch Collections.sort(list); System.out.println(Collections.binarySearch(list, "cc")); // copy ArrayList<String> list2 = new ArrayList<>(); Collections.addAll(list2,"dd1","e1e","f1f"); Collections.copy(list,list2); System.out.println(list); // fill填充 Collections.fill(list2,"yyy"); System.out.println(list2); } }
io流
file类
文件,目录
文件:内存中存放的数据计算机关机后会消失,要长久的保存数据,就要使用硬盘,光盘等,为了便于数据的管理和检索,引入了文件的概念,可以有一篇文章,一段视频,一个可执行程序。
目录(文件夹):
成千上万的文件,如果不分类的话,会很难管理,于是我们用文件夹去分类,管理。
操作系统:
如果需要查看文件/目录星系,右键属性就好了,
现在我想用java来操纵文件/目录,我该如何?
java程序最典型的特点,面向对象,java程序最擅长的就是操纵对象,盘符上的 文件目录,将它们的各种信息封装,封装成一个对象,
java最擅长就是操作对象,这个对象属于---》file类
盘符上的文件---》封装为对象---?对象属于file类的对象--》有了这个对象,我们的程序就可以直接操作这个对象,通过获取文件的各种信息,还可以对文件进行操作,创建或者删除等等等.
对文件的操作:
public static void main(String[] args) throws IOException { File f = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\src\\test.txt"); File f1 = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\src\\test.txt");//跨平台建议使用这种 // 常用方法 System.out.println("文件是否可读 : "+f.canRead()); System.out.println("文件是否可写 : "+f.canWrite()); System.out.println("文件的名字 : "+f.getName()); System.out.println("文件的上级目录 :"+f.getParent()); System.out.println("是否是一个目录 :"+f.isDirectory()); System.out.println("是否是一个文件 :"+f.isFile()); System.out.println("是否隐藏 :"+f.isHidden()); System.out.println("文件大小 : "+f.length()); // if (f.exists()){ 如果有那就删除,没有就创建 // f.delete(); // }else { // f.createNewFile(); // } System.out.println(f==f1); System.out.println(f.equals(f1)); //和路径相关的 System.out.println("绝对路径 :"+f.getAbsolutePath()); System.out.println("相对路径 :"+f.getPath()); System.out.println("toString :"+f.toString()); File file = new File("demo.txt"); if (!file.exists()){ file.createNewFile();//创建到了项目根目录里 } // 绝对路径指一个精确,完成的路径 System.out.println("绝对路径 :"+file.getAbsolutePath()); // 相对路径有一个参会造物,相对这个·参照物的路径 // 相对这个位置,main方法的相对位置为项目根路径 System.out.println("相对路径 :"+file.getPath()); // toString的效果永远是相对路径 System.out.println("toString :"+file.toString()); }
对目录的操作
public static void main(String[] args) { File f = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\src"); File f1 = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\src");//跨平台建议使用这种 // 常用方法 System.out.println("文件是否可读 : "+f.canRead()); System.out.println("文件是否可写 : "+f.canWrite()); System.out.println("文件的名字 : "+f.getName()); System.out.println("文件的上级目录 :"+f.getParent()); System.out.println("是否是一个目录 :"+f.isDirectory()); System.out.println("是否是一个文件 :"+f.isFile()); System.out.println("是否隐藏 :"+f.isHidden()); System.out.println("文件大小 : "+f.length()); File f2 = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\src\\a\\b\\c"); // f2.mkdir();创建单层目录 // f2.mkdirs();创建多层目录 // f2.delete();删除,只删除一层,如果是删除目录的话只删除没有内容的,如果有内容就不会删除 String[] list = f.list();//遍历目录下的对应的名字 for (String s : list) { System.out.println(s); } System.out.println("============"); File[] files = f.listFiles(); for (File file : files) {//作用相对上面发那个发,多了file对象,作用更加广泛 System.out.println(file.getName()); } }
file:封装文件/目录各种信息,对文件和目录进行操作,但是我们不可以获取到文件和目录的内容,
于是乎!io!!!流!!!
字符流
IO流体系结构:
I/O:input/output的缩写,用于处理设备之间的数据的传输
形象理解:
案例:通过java程序完成文件的复制
功能分解:使用到第一个流 file reader
功能分解1:初体验io流的文件内容读取
public static void main(String[] args) throws IOException { // 文件--》程序 // 1.有一个文件---》创建file类对象 File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo.txt"); // 2.利用filereader这个流将流连接到源文件--》file流对象 FileReader fr = new FileReader(file); // 3.进行操作 --》读取操作 // 下面的代码我们验证了,如果到了文件的结尾,那么读取的内容为-1 /* int i = fr.read(); int i2 = fr.read(); int i3 = fr.read(); int i4 = fr.read(); int i5 = fr.read(); System.out.println(i); System.out.println(i2); System.out.println(i3); System.out.println(i4); System.out.println(i5); */ //方法一 // int i = fr.read(); // while (i!=-1){ // System.out.println(i); // i = fr.read(); // } // 方法二 int i =0; while ((i = fr.read())!=-1){ System.out.print((char) i); } // 4.关闭流--》关闭 // 流,数据库,网络资源,考jvm无法帮助我们关闭,必须程序元手动关闭 fr.close(); }
新需求,我每次读取5个,不够我在继续读,读到没有为止。
public static void main(String[] args) throws IOException { // 1.有一个文件---》创建file类对象 File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo.txt"); // 2.利用filereader这个流将流连接到源文件--》file流对象 FileReader fr = new FileReader(file); // 3.进行操作 --》读取操作 char [] ch = new char[5]; int len = fr.read(ch);//一次读取五个,返回值是数组中的有效长度 while (len!=-1){ /* 方法1 for (int i = 0; i < len; i++) { System.out.println(ch[i]); } System.out.println("!"); len = fr.read(ch);*/ String str = new String(ch,0,len); System.out.println(str); len = fr.read(ch); } // 4.关闭操作 fr.close(); }
利用缓冲数组,做一个操作
功能分解2:程序输出文件,filewriter
一个一个向外输出,
public static void main(String[] args) throws IOException { //目标文件 File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo01.txt"); // fw连接到文件 FileWriter fw = new FileWriter(file); //开始操作 String str = "你好,小朋友"; for (int i = 0; i < str.length(); i++) { fw.write(str.charAt(i)); } // 关闭流 fw.close(); }
发现:如果目标文件不存在,会自动创建文件写入,如果存在,
new FileWriter(file);,相当于对源文件覆盖,而并不是追加
new FileWriter(file,true);为true的时候才会有追加操作
new FileWriter(file,false);为false的时候会是覆盖操作,默认也是覆盖
批量,使用缓冲数组向外输出,
package TestIO.com.hyc.IO01; import java.io.File; import java.io.FileWriter; import java.io.IOException; public class Test03 { public static void main(String[] args) throws IOException { //目标文件 File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo01.txt"); // fw连接到文件 FileWriter fw = new FileWriter(file,true);//追加 //开始操作 String str = "老子开心"; char[] ch =str.toCharArray(); fw.write(ch); // 关闭流 fw.close(); } }
接下来是合并操作!
使用reader和writer完成文件复制
package TestIO.com.hyc.IO01; import java.io.*; public class Test04 { public static void main(String[] args) throws IOException { // 创建file类 // 一个复制本体,一个是接受复制 File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo01.txt"); File file2 = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo02.txt"); // 创建输出流 FileReader reader = new FileReader(file); // FileReader reader1 = new FileReader(file2); FileWriter writer = new FileWriter(file2,true);//用追加的形式不用覆盖的形式 //region 1.一个一个字符的形式 int n = reader.read(); while (n!=-1){ writer.write(n); n = reader.read(); } //endregion //region 2.缓冲数组的形式 char[] ch = new char[5]; int len = reader.read(ch); while (len!=-1){ writer.write(ch,0,len);//输出缓冲数组有效的长度 len = reader.read(ch); } //region 3.缓冲数组转成字符串的形式 char[] ch1 = new char[5]; int len1 = reader.read(ch1); while (len1!=-1){ String s =new String(ch1,0,len1); writer.write(s);//输出缓冲数组有效的长度 len1 = reader.read(ch1); System.out.println(len1); } //endregion //关闭流,后用先关 writer.close(); reader.close(); } }
注意:不要用字符流去操作文本格式以外的文件
字节流
文本文件建议使用字符流,
非文本文件,如图片,视频等建议使用字节流。
以读取图片为例
public static void main(String[] args) throws IOException { // 功能:利用字节流将文件内容读到程序中 File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\image-20210702102548410.png"); //创建一个字节流 FileInputStream inputStream = new FileInputStream(file); /* * 细节1:文件是utf-8储存的,所以英文字母底层占1字节,中文底层占3字节 * 细节2:如果是文本文件建议使用字符流,非文本文件使用字节流 * 细节3: * read()读取的一个细节返回是int类型,不是byte类型 * 这个方法底层做了处理,让返回的数据是正数 * 就是为例避免如果字节返回的是-1,那到底是读入字节还是最后一个字符? * */ // 操作 int read = inputStream.read(); int count = 0;//计数器,计算多少个字节 while (read!=-1){ count++; System.out.println(read); read = inputStream.read(); } System.out.println("count"+count); // 关闭 inputStream.close(); }
缓冲数组输入
public static void main(String[] args) throws IOException { File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\image-20210702102548410.png"); File file2 = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\hyc.png"); FileInputStream inputStream = new FileInputStream(file); FileOutputStream OutputStream = new FileOutputStream(file2); byte[] b = new byte[1024*6]; int len = inputStream.read(b); while (len!=-1){ OutputStream.write(b,0,len); len = inputStream.read(b); } inputStream.close(); OutputStream.close(); }
缓数组读取
public static void main(String[] args) throws IOException { File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\image-20210702102548410.png"); FileInputStream inputStream = new FileInputStream(file); byte[] b = new byte[1024*6]; int len = inputStream.read(b); while (len!=-1){ for (int i = 0; i < len; i++) { System.out.println(b[i]); } len = inputStream.read(b); } inputStream.close(); }
缓冲字节流
public static void main(String[] args) throws IOException { File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\image-20210702102548410.png"); File file2 = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\hyc1.png"); FileInputStream inputStream = new FileInputStream(file); FileOutputStream OutputStream = new FileOutputStream(file2); BufferedInputStream bis = new BufferedInputStream(inputStream); BufferedOutputStream bos = new BufferedOutputStream(OutputStream); byte[] b = new byte[1024*6]; int len = bis.read(b); while (len!=-1){ bos.write(b,0,len);//底层帮助我们刷新过缓冲区了 len = bis.read(b); } bis.close(); bos.close(); }
缓冲字符流
字符流特殊的功能,读一行
public static void main(String[] args) throws IOException { File file = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo02.txt"); File file2 = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo03.txt"); FileReader fl = new FileReader(file); FileWriter fw = new FileWriter(file2); BufferedReader bf = new BufferedReader(fl); BufferedWriter bw = new BufferedWriter(fw); char [] ch = new char[6]; /* int len = bf.read(ch); while(len!=-1){ bw.write(ch,0,len); len =bf.read(ch); }*/ String s = bf.readLine(); while (s!=null){ bw.write(s); bw.newLine(); s = bf.readLine(); } bw.close(); bf.close(); }
转换流
作用:
- 作用:将字节流和字符流进行转换。
- 属于字节流还是字符流?,一般通过后缀查看。
InputStreamReader---》字节输入流---》字符的输入流
OutputStreamWriter--》字符输出流---》字节的输出流
理解图:
功能:将输入的字节流转换为输入的字符流,然后完成对程序的查看
功能实战:用转换流实现文件复制
public static void main(String[] args) throws IOException { // 首先有一个源文件 File f = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo.txt"); // 有一个输出的目标文件 File f1 = new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo4.txt"); //输入方向 FileInputStream fis = new FileInputStream(f); InputStreamReader isr = new InputStreamReader(fis, "utf-8"); //输出方向 FileOutputStream fos = new FileOutputStream(f1); OutputStreamWriter osw = new OutputStreamWriter(fos,"utf-8"); // 开始操作文件 char[] ch = new char[10]; int len = isr.read(ch); while (len!=-1){ osw.write(ch,0,len); len = isr.read(ch); } osw.close(); isr.close(); }
对象流,序列化与反序列化
对象流:用于存数和读取基本类型数据或对象的处理流,他的强大之处及时可以把java的对象写入到文件中,也可以从文件中还原。
序列化与反序列化:
序列化:
把内存中的java对象转换成与平台无关的二进制数据,从而把这种二进制数据持久的保存在磁盘上,或者通过网络将这种二进制数据的传输到另一个网络节点
反序列化:当其他程序获取了这种二进制数据,可以把数据恢复成java对象,---》反序列化
对象流代码:
public static void main(String[] args) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo.txt"))); // 将内存中的字符串写入到文件 oos.writeObject("你好"); oos.close(); }
运行结果:
我们看不懂文件的内容,但是程序是可以看懂的
我们写一个程序来读取我们放到文件中的数据
public static void main(String[] args) throws IOException, ClassNotFoundException { // 将文件中保存的字符串,读入到内存 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\java工程师\\javaSE代码\\javaSE源码\\javaSE\\demo.txt"))); String s = (String) (ois.readObject()); System.out.println(s); ois.close(); }
运行结果
如果我们想要将自定义的类序列化,那么必须实现一个接口:Serializable
接口内部,什么都没有,这种接口叫标识接口,只有实现了这个接口的类才可以实现序列化,直接序列化会导致 程序异常
多线程
程序,进程,线程
程序:为了完成特定任务,用某种语言编写的一组指令的集合,他是静态的
进程:程序的一次执行过程,他是动态的,给正在运行的程序分配资源,进程的生命周期,:创建,存在,消亡的过程
线程:进程可以进一步细化为线程,是一个程序内部的执行途径,若一个进程同一时间并且执行多个线程,就是支持多线程
单核cpu:
多核cpu:
并行和并发:
并行:多个cpu同时执行多个任务
并发:一个cpu采用时间片切换的方式,“同时”执行多个任务,其实就是时间片很短,来回切换不同的任务,造成一个同时的假象
创建线程的多种方式
在学习,多线程前,我们的代码看起来是单线程,其实我们的代码是三个线程的
继承thread类
线程类----》线
继承thread类
线程类----》线程对象
子线程类:
public class TestThread extends Thread { // 一会线程对象就要开始抢夺资源了,但是不是随便写就可以的 // 线程的任务或者逻辑要卸载run方法中 @Override public void run() { for (int i = 0; i < 10;i++) { System.out.println(i); } } }
测试类:
package Thread.com.hyc.Thread; public class Test01 { public static void main(String[] args) { for (int i = 0; i < 10; i++) { System.out.println("主线程1 :"+i); } // 制造线程和主线程抢夺资源 TestThread tt = new TestThread(); // tt.run(); 想要执行run方法的任务--》run方法不能直接调用,直接调用会被当成普通方法 // 想要tt子线程有作用必须要开启线程 tt.start();//是thread类中的方法 for (int i = 0; i < 10; i++) { System.out.println("主线程 :"+i); } } }
执行原理图:
设置和读取线程名字的方法
- setname,getname方法来设置
- 线程类构造器。实际上是调用了父类的有参构造器
小案例:买火车票:
每一个窗口是一个对象
目前出现了重复的问题,我们用static修饰变量来解决,但是还是会有重复或者负数的出现
public class TestThread extends Thread{ // 每个窗口是i一个线程对象,每个窗口对象执行的方法放入run方法中 static int ticketNum = 10;//多个对象共享十张票 public TestThread(String name) { super(name); } @Override public void run() { for (int i = 1; i <=100; i++) { if (ticketNum>0){//只有邮票的时候才买 System.out.println(Thread.currentThread().getName()+" : "+"我买到了哈尔冰第"+ticketNum--+"票"); } } } }
用实现接口的方式创建线程
实现runnable接口
实现run方法,
package Thread.com.hyc.Thread.demo02; public class TestThread implements Runnable { @Override public void run() { for (int i = 1; i <=10; i++) { System.out.println(Thread.currentThread().getName()+" : "+i); } } }
简单输出:
package Thread.com.hyc.Thread.demo02; public class test { public static void main(String[] args) { TestThread tt = new TestThread(); Thread t = new Thread(tt); t.setName("线程10086"); t.start(); for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName()+" : "+i); } } }
实际开发中,继承thread类和实现接口,那个更适用于开发---------》方式2
- 方式1的话有java的单继承局限性,因为继承了Thread就没办法继承其他类1了
- 方式2的共享资源能力会强一些,不需要非得用static去修饰
继承thread和实现接口有练习吗
关系图:
实现callable接口
对比第一第二中方法创建线程的方式发现,无论第一钟继承Thread类还是第二种实现runnable接口都需要一个run方法,这个run方法有不足
- 没有返回值
- 不能抛出异常
基于上面两个不足,jdk1.5之后出现了第三种创建线程的方法。
实现callble接口的好处:有返回值,可以抛出异常
缺点:创建线程相对麻烦
代码示例
package Thread.com.hyc.Thread.demo04; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class Test01 implements Callable<Integer> { /* * 1.实现callable是个泛型接口 * 从call方法:又返回值可以抛出异常 * */ @Override public Integer call() throws Exception { return new Random().nextInt(10); } static class Test{ public static void main(String[] args) throws ExecutionException, InterruptedException { Test01 trn =new Test01(); FutureTask ft = new FutureTask(trn); Thread t = new Thread(ft); t.start(); // 获取线程得到的返回值 Object o = ft.get(); System.out.println(o); } } }
线程的生命周期:
线程声明周期:线程开始--》线程消亡
线程经历了什么
线程常见方法
- start():启动当前线程,表面上调用start方法,
- run():线程类继承thread类或者实现runnable接口,都要重新实现run,run方法写要实现的逻辑代码。
- currentThread类中一个静态方法:获取当前正在执行的线程
- setNmae:这只线程名字
- getName:读取线程名字
设置线程优先级
- 同优先级的线程,采用的侧落就是先到先服务,谁先就先服务谁
- 如果优先级别高,被cpu调度的概率更高
Thread线程的优先级:1-10,默认是5
测试代码示例:
package Thread.com.hyc.Thread.demo05; public class TestThread01 extends Thread { @Override public void run() { for (int i = 1; i <=10; i++) { System.out.println(Thread.currentThread().getName()+i); } } } class testThread02 extends Thread{ @Override public void run() { for (int i = 20; i <30; i++) { System.out.println(Thread.currentThread().getName()+i); } } } class Test{ public static void main(String[] args) { // 测试让两个子线程抢资源 TestThread01 t1 = new TestThread01(); testThread02 t2 = new testThread02(); t1.setPriority(10); t1.setName("hyci"); t1.start(); t2.setPriority(1); t2.setName("hycq"); t2.start(); } }
join方法
案例:白金卡,黄金卡,插队特权
当一个线程调用了join方法,这个线程就会先被执行,他执行完才会执行别的线程,
PS:必须先就绪,才可以执行join方法
代码示例:
package Thread.com.hyc.Thread.demo06; public class TestThread extends Thread { @Override public void run() { for (int i = 1; i <=10; i++) { System.out.println(this.getName()+i); } } public TestThread(String name) { super(name); } } class test { public static void main(String[] args) throws InterruptedException { for (int i = 1; i <=100; i++) { System.out.println(Thread.currentThread().getName()+i); if (i==6){ TestThread tt = new TestThread("child--Thread"); tt.start(); tt.join();//钻石vip无条件插队 } } } }
sleep
可以让当前线程按照设置时间进入阻塞状态,时间够了,回到就绪状态
public class Test01 { public static void main(String[] args) throws InterruptedException { Thread.sleep(4000); System.out.println("hhhhhhhhh"); } }
小案例,秒表:
class Test2{ public static void main(String[] args) { while (true){ Date d = new Date(); DateFormat df = new SimpleDateFormat("HH:mm:ss"); System.out.println(df.format(d)); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
伴随线程
SetDaemon,
我想将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不要执行了。
案例:皇上---》驾崩---》妃子陪葬
PS:先设置伴随,让后再启动
代码案例
package Thread.com.hyc.Thread.demo08; public class TestThread extends Thread { @Override public void run() { for (int i = 1; i <=1000; i++) { System.out.println("子线程---"+i); } } } class Test{ public static void main(String[] args) { TestThread tt = new TestThread(); tt.setDaemon(true); tt.start(); // 主线程中还要输出1-10的数字 for (int i = 1; i < 10; i++) { System.out.println("main"+i); } } }
同步方法:
用静态方法会出现下面的问题:
1.会出现,还没有--就被抢走资源的情况,导致连续有同一张票被抢
2.会出现负数票
上面的代码,出现了重复,或者是错票的问题,就是线程安全引发的问题,
原因:多个线程,争抢资源的过程中,导致共享的资源出现问题,一个线程没有执行完毕,另一个线程就来抢资源了
解决方法:同步代码块,在我的程序上加上锁
这也有几个操作,
方法1:runnable接口
使用锁来完成一个次只有一个线程可以执行锁中的代码
package Thread.com.hyc.Thread.demo09; public class BuyTicketThread implements Runnable { int ticketNum = 10; @Override public void run() { for (int i = 1; i <100; i++) { synchronized (this){//把具有安全隐患的代码锁住即可,如果锁多了就会效率降低 if (ticketNum>0){ System.out.println("我在"+Thread.currentThread().getName()+"窗口抢到了第"+ticketNum--+"张票"); } } } } }
Thread
如果我们直接和之前的接口一样在synchronized (this)
还是用this的话,还是有问题,我们要保证锁只有一把。
class BuyTicketThread1 extends Thread{ int ticketNum = 10; @Override public void run() { for (int i = 1; i <100; i++) { synchronized (this){//把具有安全隐患的代码锁住即可,如果锁多了就会效率降低 if (ticketNum>0){ System.out.println("我在"+Thread.currentThread().getName()+"窗口抢到了第"+ticketNum--+"张票"); } } } } }
如果我们创建了三个实例,那么this就不是同一把锁,所以我们不能用this,我this换成字符串abc,就解决了问题,为什么?因为常量池里就只有一个abc,所以大家用的都是这个,就不会出现不是同一把锁的问题,不过我们不会直接用abc,一般放线程类的字节码信息,
public void run() { for (int i = 1; i <100; i++) { synchronized (BuyTicketThread1.class){//把具有安全隐患的代码锁住即可,如果锁多了就会效率降低 } } }
锁中括号内放的一定是引用类型,不可以是基本类型
尽量不要用String和包装类来做同步监视器
如果有,建议使用final修饰同步监视器
小结:
- 第一线程来到同步代码块,发现是open状态,需要关闭,之后第一线程去执行其中代码
- 第一线程执行过程中,发生线程切换也无所谓,但是切换之后线程一样调用不了代码,应为被第一个线程锁住了,其他的线程都无法调用,等待到资源重新回到第一线程,继续执行
- PS:同步代码块中能发生CPU资源线程切换嘛,是可以的
- 当多个代码块使用同一个同步监视器,那么锁住一个代码块的同时,也会锁住其他同一同步监视器的代码块,其他的线程无法直接使用方法
- 当多个代码块使用同一个同步监视器,那么锁住一个代码块的同时,也会锁住其他同一同步监视器的代码块,但是没有锁住其他的代码块,线程还可以去访问其他同步监视器的代码块
方法2:同步方法
实现接口和继承Thread类的两种使用同步方法的形式
public class BuyTicketThread implements Runnable { int ticketNum = 10; @Override public void run() { for (int i = 1; i <100; i++) { buyticket();//锁住的是this } } public synchronized void buyticket(){ if (ticketNum>0){ System.out.println("我在"+Thread.currentThread().getName()+"窗口抢到了第"+ticketNum--+"张票"); } } } class BuyTicketThread1 extends Thread{ static int ticketNum = 10; @Override public void run() { for (int i = 1; i <100; i++) { buyticket();//锁住的是当前的字节码文件BuyTicketThread1.class } } public static synchronized void buyticket(){ if (ticketNum>0){ System.out.println("我在"+Thread.currentThread().getName()+"窗口抢到了第"+ticketNum--+"张票"); } } }
小结:
多线程在争抢资源的时候,就需要实现线程同步,需要加锁,锁必须是同步的,一般使用引用类型 数据
目的:解决线程安全问题
关于同步方法:
- 不要将run定义为同步方法
- 非静态同步方法的同步监视器的this
- 静态同步方法的同步监视器是类名。class的字节码信息对象
- 同步代码块效率高于同步方法
- 因为,方法是将线程挡在了方法的外部,而代码块是将线程挡在了代码的外部,方法的内部
- 同步方法的锁住的是this,一旦方法锁住了一个方法,就锁住了这个类里所有的同步方法,代码块只是锁住了同步监视器的代码块,而没有锁住其他监视器的代码块
lock锁
和传统的syn相比,lock提供了多种锁方案,jdk1.5后新增的线程同步方式,
syn是java中的关键字,这个关键自的识别是靠jvm来识别完成的,是虚拟机级别,但是lock是API级别,提供相应的接口和对应的实现类,这个方式更加的灵活,表现出的性能也比之前更好
示例代码
public class BuyTicketThread implements Runnable { int ticketNum = 10; // 拿来一把锁 Lock lock = new ReentrantLock(); @Override public void run() { for (int i = 1; i <100; i++) { lock.lock(); try { if (ticketNum>0){ System.out.println("我在"+Thread.currentThread().getName()+"窗口抢到了第"+ticketNum--+"张票"); } } catch (Exception e) { e.printStackTrace(); }finally { lock.unlock(); } } } public synchronized void buyticket(){ } }
lock和syn的区别:
- lock是显示锁(手动开启关闭,)syn是隐式的锁
- lock只有代码块锁,syn有代码块锁,方法锁
- 使用lock锁,jvm将花费较少的时间,来调度线程,性能更好,并且具有更好的扩展性,提供了更多的子类
优先使用顺序:
lock---》同步代码块 ---》同步方法
线程同步的有缺点
对比:
线程安全:效率低
线程不安全:效率高
形象举例:
线程安全 = 一个厕所,一个人一个人的上。效率低
线程不安全=一起上厕所,效率高
可能造成死锁:
不同线程分别占用了对方需要的同步资源,不放弃,都在等待对方放弃自己的资源,出现死锁,不会出现异常,不会出现提示,只有所有的线程都处于阻塞状态,无法继续
解决方法:少使用同步代码块嵌套
线程通信
应用场景:生产者,消费者
案例:仓库稚嫩而过放一件商品,生产者将生产出来的商品加入到仓库,消费者购买,
如果仓库没有产品,消费者等等待,生产者就绪
如果仓库中有放有产品,生产者等待,消费者就绪
1.商品类:品牌,名字
package Thread.com.hyc.Thread.demo10; public class Product { // 品牌 private String brand; // 名字 private String name; public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
2.线程1:生产者
package Thread.com.hyc.Thread.demo10; public class ProduerThread extends Thread{//生产者线程 //共享商品 private Product product; public ProduerThread( Product product){ this.product = product; } @Override public void run() { for (int i = 1; i <= 10; i++) { synchronized (product){ if (i%2==0){ // 生产巧克力 product.setBrand("费列罗"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } product.setName("巧克力"); }else { // 啤酒 product.setBrand("哈尔冰"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } product.setName("啤酒"); } System.out.println("生产者生产了"+product.getBrand()+"-----"+product.getName()); } } } }
3.线程2:消费者
package Thread.com.hyc.Thread.demo10; public class CustomerThread extends Thread { private Product product; @Override public void run() { for (int i = 1; i <=10 ; i++) { synchronized (product){ System.out.println("消费者消费了"+product.getBrand()+" : "+product.getName()); } } } public CustomerThread(Product product){ this.product = product; } }
功能分解1:问题:
1.生产者和消费者输出错乱
解决:提取公共的代码,为同步方法
public synchronized void setProduct(String brand,String name){ this.setBrand(brand); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.setName(name); System.out.println("生产者生产了"+this.getBrand()+"-----"+this.getName()); } public synchronized void getProduct(){ System.out.println("消费者消费了"+this.getBrand()+" : "+this.getName()); }
功能分解2,问题:
1没有交替输出
我们用一个变量来控制同信
package Thread.com.hyc.Thread.demo11; import javax.naming.Name; public class Product { // 品牌 private String brand; // 名字 private String name; private boolean flag =false;//默认没有商品,生产者先生产 public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public String getName() { return name; } public void setName(String name) { this.name = name; } public synchronized void setProduct(String brand,String name){ if (flag == true){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.setBrand(brand); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.setName(name); System.out.println("生产者生产了"+this.getBrand()+"-----"+this.getName()); flag = true; notify(); } public synchronized void getProduct(){ if (flag == false){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("消费者消费了"+this.getBrand()+" : "+this.getName()); flag = false; notify(); } }
我们用wait和notify两个方法,来完成交替,
必须放在同步代码块使用。
sleep和wait的区别:sleep进入阻塞状态,没有释放锁,wait进入阻塞状态但是同时释放了锁
概念图: