零基础学习java------day18------properties集合,多线程(线程和进程,多线程的实现,线程中的方法,线程的声明周期,线程安全问题,wait/notify.notifyAll,死锁,线程池),
1.Properties集合
1.1 概述:
Properties类表示了一个持久的属性集。Properties可保存在流中或从流中加载。属性列表中每个键及其对应值都是一个字符串
一个属性列表可包含另一个属性列表作为它的“默认值”;如果未能在原有的属性列表中搜索到属性值,则搜索第二个属性列表
因为Properties继承于Hashtable,所以可以对Properties对象应用put和putAll方法。但不建议使用这两个方法,因为它们允许调用者插入其键或值不是String的项。相反,应该使用setProperty方法。如果在“不安全”的Properites对象(即包含非String的值或键)上调用store或save方法,则该调用将失败。类似的,如果在“不安全”的Properites对象上调用propertyNames或list方法,则该调用失败
1.2 Properites类的特点
(1)该集合不能写泛型
(2)可以持久化的属性集,键值可以存储到集合中,也可以存储到硬盘、U盘等
(3)可以和IO流有关的技术结合使用
1.3 Properites类中的一些方法
练习(来源:https://blog.csdn.net/gafeng123456/article/details/50846620)
1. 我有一个文本文件(user.txt),我知道数据是键值对形式的,但是不知道内容是什么。 请写一个程序判断是否有“lisi”这样的键存在,如果有就改变其实为”100” 分析: A:把文件中的数据加载到集合中 B:遍历集合,获取得到每一个键 C:判断键是否有为"小明"的,如果有就修改其值为"30岁的女人" D:把集合中的数据重新存储到文件中
Reader是抽象类,所以想获得reader需要使用其子类创建
public class PropertiesExer1 { public static void main(String[] args) { //把文件中的数据加载到集合中去 Properties pro = new Properties(); try ( FileReader fr = new FileReader("e:/b.txt"); FileWriter fw = new FileWriter("e:/b.txt"); ){ pro.load(fr); fr.close(); // 遍历集合,获取得到每一个键 Set<String> set = pro.stringPropertyNames(); for (String str : set) { if("赵雷".equals(str)) { pro.setProperty(str, "30岁的女人"); break; } } // 将集合中的数据重新存储到文件中 pro.store(fw,null); fw.close(); } catch (Exception e) { e.printStackTrace(); } } }
2. 我有一个猜数字小游戏的程序,请写一个程序实现在测试类中只能用5次,超过5次提示:游戏试玩已结束,请付费。
public class PropertiesTest2 { public static void main(String[] args) throws IOException { // 读取某个地方的数据,如果次数不大于5,可以继续玩。否则就提示"游戏试玩已结束,请付费。" // 创建一个文件 // File file = new File("count.txt"); // if (!file.exists()) { // file.createNewFile(); // } // 把数据加载到集合中 Properties prop = new Properties(); Reader r = new FileReader("count.txt"); prop.load(r); r.close(); // 我自己的程序,我当然知道里面的键是谁 String value = prop.getProperty("count"); int number = Integer.parseInt(value); if (number > 5) { System.out.println("游戏试玩已结束,请付费。"); System.exit(0); } else { number++; prop.setProperty("count", String.valueOf(number)); Writer w = new FileWriter("count.txt"); prop.store(w, null); w.close(); GuessNumber.start(); } } }
1.4 Properties配置文件的读取
目的:便于维护(如有很多类,每个类中都需要时间格式,若一个类一个类的去定义会很麻烦,若直接从Properties中的配置文件中读取就很方便,要改时间格式也只是改下配置文件中的,类只要从中读取相应的格式就行)
步骤:
(1)创建Properties对象
(2)加载配置文件
(3)使用对象获取配置文件中的信息
新建配置文件:在src右键--->new---->file---->config.properties
public class PropertiesDemo { public static void main(String[] args) { try { //1 创建一个Properties对象 Properties pro = new Properties(); //2 使用p对象,加载配置文件, 下面可可以创建一个InputStream对象传进去,但是由于配置文件的路径不好确定,所以用下面的方式比较好 pro.load(PropertiesDemo.class.getClassLoader().getResourceAsStream("config.properties")); //3 使用p对象获取配置文件中的内容 String value = pro.getProperty("date.format"); System.out.println(new SimpleDateFormat(value).format(new Date())); //获取所有的key Set<String> set = pro.stringPropertyNames(); for (String key : set) { System.out.println(key+","+pro.getProperty(key)); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
2. 多线程
2.1 内容结构
2.2 线程和进程
进程:
正在运行的程序,是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源
线程:
线程是进程的一个实体(执行单元或执行路径),是cpu调度和分派的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中不可少的资源(如程序计数器,一组寄存器和栈)但是它可与同属于一个进程的其他线程共享进程所拥有的的全部资源。
进程至少有一个线程,如果进程中有许多个线程----->多线程的程序
并行:某一时间点,有多个程序同时执行,多核cpu
并发:某一个时间段,有多个程序同时执行,并不是真正意义上的同时执行------>多线程
并发真的是同时执行吗?
不是,而是时间间隔很短,造成了同时执行的错觉
多线程的程序有什么优点?
提高了用户的体验,提高了程序的运行效率?----->提高了cpu的使用率
前面的代码都是单线程的程序,都是main方法中依次执行
主线程:用于执行main方法的线程
java程序运行原理:
java命令会启动java虚拟机,JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类的main方法,所以main方法运行在主线程中
2.3 实现多线程的第一种方式
1. 继承Thread:
(1). 继承Thread
(2). 重写run方法(线程要做的事,定义在该方法中)
(3).创建子类的对象
(4).使用该对象调用start方法
Thread.currentThread:获取当前的进程
getName:获取线程的名字
用来执行main方法的线程叫做主线程,主线程的名字是main
public class ThreadDemo { public static void main(String[] args) { Demo d1 = new Demo("牛郎");// 这里的对象就是线程对象 Demo d2 = new Demo("织女"); d1.start(); d2.start(); } } class Demo extends Thread{ String name; public Demo(String name) { this.name = name; } public void run() { for(int i=0;i<40;i++) { System.out.println(name+"-------"+i+Thread.currentThread().getName()); } } }
得到的打印结果是牛郎和织女错乱出现
2. 线程名字的改写
(1)setName() :改写线程的名字,如上面需要改变使用d1对象开启的线程的名字,则用d1.setName("牛郎线程"),若要改写主线程的名字,则,代码如下:
Thread.currentThread().setName("我是主线程");
(2)通过创建带有参数的构造方法实现线程名字的改写
public class ThreadDemo { public static void main(String[] args) { Demo d1 = new Demo("牛郎","我是牛郎线程"); Demo d2 = new Demo("织女","我是织女线程"); d1.start(); d2.start(); } } class Demo extends Thread{ String name; public Demo(String name,String threadName) { //定义带有参数的构造方法 super(threadName); this.name = name; } public void run() { for(int i=0;i<40;i++) { System.out.println(name+"-------"+i+Thread.currentThread().getName()); } } }
3. start方法和run方法的区别
run(): 只是普通的方法调用,没有开启新的线程
start(): 开启了一个新的线程,调用run方法
run方法和main方法:
主线程的任务定义在main方法中
自定义的线程任务定义在run方法中
2.4. 多线程中常用的方法
1. 线程阻塞:
public static void steep(long millis) 静态方法,进入阻塞状态,时间结束后进入可执行状态
public class ThreadDemo1 { public static void main(String[] args) { Demo1 d1 = new Demo1(); Demo1 d2 = new Demo1(); d1.start(); d2.start(); try { Demo1.sleep(3000);// 不是让d1线程睡了3秒,而是让主线程睡得,因为它定义在主线程中 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("over"); } } class Demo1 extends Thread{ public void run() { for(int i=0;i<30;i++) { System.out.println(i+"----"+Thread.currentThread().getName()); } } }
public final void join() 成员方法,哪个线程调用它,哪个线程就先执行,执行完毕后,在执行该语句所在的线程,其只能控制两个线程
public class ThreadDemo2 { public static void main(String[] args) { Sum s = new Sum(); s.start(); try { //被哪个对象调用,就让哪个线程先执行,执行完毕后,再执行他所在的线程 s.join();//不加这句让此线程先执行的话,在sum嘉禾还没做完,主线程就先打印了这个Sum值了 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Sum.sum); } } class Sum extends Thread{ static int sum = 0; public void run() { for(int i=0;i<10000;i++) { sum += i; } } }
2. 所在的线程:
public static void yield(),让步,让其他线程先执行,不一定生效,因为是线程的执行时由CPU决定的,但其也有一定的影响
3. 中断线程
public final void stop() 停止一个线程,已过时,不推荐使用
public void interrupt 打断线程的阻塞状态,进入可执行状态,会抛出异常。
public class InterruptDemo1 { public static void main(String[] args) { Demo3 d1 = new Demo3(Thread.currentThread()); d1.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Over"); } } class Demo3 extends Thread{ Thread th; //通过构造方法,获取主线程 public Demo3(Thread th) { this.th = th; } public void run() { try { Thread.sleep(40);//确保主线程能够进入阻塞状态,再去打断 } catch (InterruptedException e) { e.printStackTrace(); } th.interrupt(); } }
运行结果:
2.5 实现多线程的第二种方法
实现多线程方式二:
1. 实现Runnable接口
2. 重写run方法
3. 创建Runnable接口的子类对象
4. 创建Thread类的对象,把第三步的对象传到构造方法中
5. 使用Thread类的对象,调用start方法
既然有了第一种方式,为什么还要有第二种?
解决单继承的局限性
好处:
解决单继承的局限性
符合面向对象的思想:把线程任务和线程对象分离开了(实现Runnable接口的类的对象本身并不是线程对象,只是作为一个创建线程对象的目标对象使用)
public class ThreadDemo3 { public static void main(String[] args) { Demo4 d = new Demo4(); Thread th1 = new Thread(d); Thread th2 = new Thread(d); th1.start(); th2.start(); } } //实现Runnable接口 class Demo4 implements Runnable{ @Override public void run() { for(int i=0;i<=20;i++) { System.out.println(Thread.currentThread().getName()+"----"+i); } } }
线程周期图
2.6 线程安全问题
2.6.1 产生原因:
假设有三个窗口售卖100张火车票,在卖票的过程中,出现了多个窗口卖同一张票,或者票数为0及负数的情况,这就是传说中的线程安全问题。
public class SellTicketsDemo { public static void main(String[] args) { Tickets t = new Tickets(); Thread th1 = new Thread(t); Thread th2 = new Thread(t); Thread th3 = new Thread(t); th1.start(); th2.start(); th3.start(); } } class Tickets implements Runnable{ static int tickets = 100; @Override public void run() { while(true) { if(tickets>0) { try { Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets--+"张票"); } else { break; } } } }
运行结果如下:
由结果可知,当只剩最后一张票时,三个线程都对这一张票进行了售卖。原因:假设刚好剩一张票可售卖,Thread-1先通过循环判断条件,得知有票可卖,这时,cpu中途去执行Thread-2,恰好也是执行完判断条件,cpu去执行Thread-3,这时,Thread-3卖出去这剩下的一张票,票数变为0,cpu执行完Thread-3,又转而去执行前面的Thread-1和Thread-2,从而得到正在卖第0张和第1张票的结果
线程安全问题产生的条件是什么?
1. 具备多线程的环境
2. 多个线程操作共享数据
3. 操作(修改)共享数据的代码有多条(若是访问就不会出现线程安全问题)
怎样解决这个线程安全问题呢?
加锁,让某一时刻只能有一个线程进行操作
2.6.2 解决方案
1. 同步代码块
synchronized(锁对象){ //锁对象可以是任意对象,要求多个线程使用的是同一个对象 容易产生线程安全问题的代码 }
// 判断对象是不是使用同一个对象的方法是:看该对象所在的类new了几次,若只有一次则使用的是一个对象
解决上述线程安全代码如下(此处只写了Tickets类,其他代码同上):
class Tickets implements Runnable{ static int tickets = 100; Object o = new Object(); //创建一个锁对象 @Override public void run() { while(true) { synchronized(o) { // 看该对象所在的类被new了几次 if(tickets>0) { try { Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets--+"张票"); } else { break; } } } } }
运行结果如下图:
以下代码为不是一个对象的情况(改用继承的形式创建多线程),发现还是存在多个窗口售卖同一张车票的情况
public class SellTicketsDemo1 { public static void main(String[] args) { Tickets1 th1 = new Tickets1(); Tickets1 th2 = new Tickets1(); Tickets1 th3 = new Tickets1(); th1.start(); th2.start(); th3.start(); } } class Tickets1 extends Thread{ static int tickets = 100; Object o = new Object(); @Override public void run() { while(true) { synchronized(o) { // if(tickets>0) { try { Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets--+"张票"); } else { break; } } } } }
这种情况下该怎么解决呢?
第一种方法:通过构造方法传入一个对象,传给三个线程都是同一个对象,具体如下:
public class SellTicketsDemo1 { public static void main(String[] args) { // 创建一个Object对象,作为构造方法的参数,传入各线程对象 Object o = new Object(); Tickets1 th1 = new Tickets1(o); Tickets1 th2 = new Tickets1(o); Tickets1 th3 = new Tickets1(o); th1.start(); th2.start(); th3.start(); } } class Tickets1 extends Thread{ static int tickets = 100; Object o; // 创建带参数的构造方法 public Tickets1(Object o) { this.o = o; } @Override public void run() { while(true) { synchronized(o) { // if(tickets>0) { try { Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets--+"张票"); } else { break; } } } } }
第二种方法:使用大class对象(大class对象是唯一的)
synchronized(Tickets1.class) { }
第三种方法:使用静态对象
static Object b = new Object(); ...... synchronized(b) { }
第四种方法:使用字符串常量,常量为于常量池,是同一个对象
synchronized("aaa") {}
2.同步方法
把synchronized放到方法的修饰符上,锁得是整个方法
成员方法:默认的锁对象是this
静态方法:默认的锁对象是 类名.class
public class SellTicketsDemo2 { public static void main(String[] args) { // 创建一个Tickets2对象,作为创建线程对象的参数 Tickets2 t = new Tickets2(); Thread th1 = new Thread(t); Thread th2 = new Thread(t); Thread th3 = new Thread(t); th1.start(); th2.start(); th3.start(); } } class Tickets2 implements Runnable{ static int tickets = 100; @Override // 使用同步方法,其上锁的是整个方法 public synchronized void run() { //同步方法,synchronized置于修饰符的位置 while(true) { if(tickets>0) { try { Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets--+"张票"); } else { break; } } } }
对于上面的售卖票的例子,用此方法相当于使用单线程(运行结果是一个线程作用的结果,因为当一个线程(称作Thread1)进入run方法时,由于上了锁,其他线程会等Thread1执行完毕才会执行run方法,但Thread1执行完毕的条件是tickets<=o,此时其他线程执行run方法票数已经为0了,就会执行break。
补充:
前面的单例模式中的饿汉模式便可以使用同步方法来上锁。
前面学的StringBuild也是线程不安全的,也可以使用同步方法的形式来上锁(可以直接看StringBuild的源码,其方法定义的时候就加了synchronized)
3.Lock锁的使用
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5后提供了一个新的所对象LOCK
void lock()
void unlock()
public class SellTicketsDemo3 { public static void main(String[] args) { // 创建一个Tickets2对象,作为创建线程对象的参数 Tickets3 t = new Tickets3(); Thread th1 = new Thread(t); Thread th2 = new Thread(t); Thread th3 = new Thread(t); th1.start(); th2.start(); th3.start(); } } class Tickets3 implements Runnable{ static int tickets = 100; Lock lock = new ReentrantLock(); @Override public void run() { while(true) { lock.lock(); // 上锁 try { if(tickets>0) { try { Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在卖第"+tickets--+"张票"); } else { break; } }finally { lock.unlock(); //关锁,此处一定要用finally,不然卖最后一张票时,执行不到unlock } } } }
注意:使用锁的时候要注意,最好把线程安全问题的代码用try......finally包起来,把unlock的代码放到finally中,以保证他一定能够执行,否则程序会一直启动无法结束(锁无法关闭,程序会一直处于等待锁关闭的状态)
4. wait,notify/notifyAll方法的使用
wait ,notify以及notifyAll是Object类中的方法,所以任何一个类中都会有这三个方法,此外这三个方法必须在同步方法或者同步代码块中使用
必须使用相同的锁对象在同步代码块中调用notify/notifyAll进行唤醒(即解铃还须系铃人,哪个锁对象调用wait,就由哪个锁对象解开)
wait:让线程进入阻塞状态,进入等待状态的线程会释放锁对象(也就是允许其他线程进入同步代码快),直到使用相同的锁对象调用notify/notifyAll方法才会结束阻塞状态
notify:唤醒,会从等待池中唤醒一个处于等待状态的线程,使其进入可执行状态
notifyAll:唤醒去全部,会将等待池中所有处于等待状态的线程唤醒,使其进入可执行状态
notify或notifyAll以后,被唤醒的线程并不是马上执行,需要等到notify,notifyAll所在的代码块执行完毕后才会执行,因为只有同步代码块执行完毕后,才会释放锁对象,其他线程才可以进来。
wait方法会释放锁对象,也就是一个线程使用wait进入等待状态后,允许其他线程进入同步代码块
sleep方法不会释放锁对象,到时间自己醒
wait, notify,notifyAll三个方法为什么位于Object类中?
因为三个方法必须使用锁对象调用,锁对象可以是任意对象,所以在Object类中
无唤醒代码
public class WaitNotifyDemo { public static void main(String[] args) { Object o = new Object(); Demo5 d = new Demo5(o); d.start(); } } class Demo5 extends Thread{ Object o; public Demo5(Object o) { this.o = o; } public void run() { synchronized(o) { System.out.println("wait....start...."); try { o.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("wait...end..."); } } }
运行结果:如下图
此外,由上图红点可知,该程序还一直处于运行状态,需要"人"去唤醒这个wait状态
public class WaitNotifyDemo { public static void main(String[] args) { Object o = new Object(); Demo5 d = new Demo5(o); Demo6 d1 = new Demo6(o); d.start(); d1.start(); } } class Demo5 extends Thread{ ////创建带参构造方法,用于传对象,达到锁对象相同的目的 Object o; public Demo5(Object o) { this.o = o; } public void run() { synchronized(o) { System.out.println("wait....start...."); try { o.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("wait...end..."); } } } // 用来唤醒线程的wait状态,必须使用同一个对象唤醒 class Demo6 extends Thread{ //创建带参构造方法,用于传对象,达到锁对象相同的目的 Object o; public Demo6(Object o) { this.o = o; } public void run() { synchronized(o) { System.out.println("notify....start...."); o.notify(); System.out.println("notify....end...."); } } }
运行结果如下图
可见,线程的wait状态,被唤醒。
notifyAll唤醒所有线程
public class WaitNotifyAllDemo { public static void main(String[] args) { Object o = new Object(); WaitDemo wd = new WaitDemo(o); WaitDemo wd1 = new WaitDemo(o); WaitDemo wd2 = new WaitDemo(o); WaitDemo wd3 = new WaitDemo(o); WaitDemo1 d1 = new WaitDemo1(o); wd.start(); wd1.start(); wd2.start(); wd3.start(); //确保以上线程都进入了阻塞状态,再开启唤醒线程 try { Thread.sleep(30); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } d1.start(); } } // 使线程进入阻塞状态 class WaitDemo extends Thread{ Object o; public WaitDemo(Object o) { this.o = o; } public void run() { synchronized(o) { System.out.println(Thread.currentThread().getName()+"wait....start...."); try { o.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"wait...end..."); } } } // 利用notifyAll唤醒所有进入阻塞状态的线程 class WaitDemo1 extends Thread{ Object o; public WaitDemo1(Object o) { this.o = o; } public void run() { synchronized(o) { System.out.println("notifyAll....start...."); o.notifyAll(); System.out.println("notifyAll....end...."); } } }
运行结果:
2.7 死锁
同步的弊端:效率低,如果出现了同步嵌套,就容易产生死锁问题
概述:
是指两个或两个以上的线程在执行的过程中,因争夺资源而产生的一种互相等待的现象
弊端:
效率低
如果出现了同步嵌套,就容易产生死锁问题
案例:
public class DiedLock { public static void main(String[] args) { new MyThread(true).start(); new MyThread(false).start(); } } class Lock { public static Object lockA = new Object(); public static Object lockB = new Object(); } class MyThread extends Thread { private boolean flag; public MyThread(boolean flag) { this.flag = flag; } @Override public void run() { while (true) { if (flag) { synchronized (Lock.lockA) { System.out.println("lockA"); synchronized (Lock.lockB) { System.out.println("lockB"); } } } else { synchronized (Lock.lockB) { System.out.println("lockB"); ynchronized (Lock.lockA) { System.out.println("lockA"); } } } } } }
2.8 线程池
为什么要是用线程池?
1. 减少了创建和和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
2.可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
创建线程的方法:
第一种:
public static ExecutorService newCachedThreadPool() //创建一个具有缓存功能的线程池,其能活跃60秒,如果池中没有可用的线程,会一直创建新的线程
public class ThreadPoolDemo { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool();//获取线程池 // 使用线程池执行任务 pool.execute(new Runnable() { @Override public void run() { // TODO Auto-generated method stub System.out.println("生活是苦难的"); } }); } }
第二种:
public static ExecutorService newFixedThreadPool(int nThreads) //创建一个可重用的,具有固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(10); pool.execute(new Runnable() { @Override public void run() { // TODO Auto-generated method stub } });
第三种:
public static ExecutorService newSingleThreadExecutor() //创建一个只有单线程的线程池,相当于上个方法中的参数为1