多线程(下)&IO加强

多线程(下)&IO加强

今日目标

  1. 理解多线程问题产生的原因
  2. 能够使用同步锁解决线程安全问题
  3. 能够使用propertis读取文件中的内容
  4. 了解打印流

1.多线程安全性问题(理解)

我们通过一个案例,演示线程的安全问题:

分析:

最近万达影城上映:《一出好戏》 , 我们现在模拟一下电影院卖票:

我就卖一个放映厅中100张票。

我们有多个售票窗口,同时对外出售这100张票。

我们可以用线程来模拟售票的窗口。每个窗口可以认为是一个线程。窗口售票的过程,就可以认为是线程的任务

步骤:

1)定义一个测试类SellTicektDemo ,并定义一个main函数;

2)定义一个线程类SellTicketTask 来实现Runnable接口;

3)在SellTicketTask 任务类中定义一个变量tickets来存储100张票;

4)在run函数中使用循环模拟一直卖票,使用判断结构根据变量票数tickets是否大于0来确定是否还有余票;

5)如果有余票,使用打印语句来模拟卖票,然后票数量变量tickets-1;

6)在main函数中创建任务类对象stt,同时并创建四个线程类对象来模拟四个窗口,最后启动线程;

package com.SiyualChen.day02;

/**
 * 1)定义一个测试类SellTicektDemo ,并定义一个main函数;
 *
 * 2)定义一个线程类SellTicketTask 来实现Runnable接口;
 *
 * 3)在SellTicketTask 任务类中定义一个变量tickets来存储100张票;
 *
 * 4)在run函数中使用循环模拟一直卖票,使用判断结构根据变量票数tickets是否大于0来确定是否还有余票;
 *
 * 5)如果有余票,使用打印语句来模拟卖票,然后票数量变量tickets-1;
 *
 * 6)在main函数中创建任务类对象stt,同时并创建四个线程类对象来模拟四个窗口,最后启动线程;
 */
public class SellTicketTask implements Runnable{

    private int tickets =100;
    @Override
    public void run() {
        while (true){
            if (tickets>0){
                System.out.println(Thread.currentThread().getName()+" " +"出票"+tickets);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tickets--;
            }
        }
    }
}
package com.SiyualChen.day02;

public class SellTicektDemo {
    /**
     * 在main函数中创建任务类对象stt,同时并创建四个线程类对象来模拟四个窗口,最后启动线程;
     * @param args
     */
    public static void main(String[] args) {
        SellTicketTask sellTicketTask = new SellTicketTask();
        new Thread(sellTicketTask,"窗口1").start();
        new Thread(sellTicketTask,"窗口2").start();
        new Thread(sellTicketTask,"窗口3").start();
        new Thread(sellTicketTask,"窗口4").start();



    }
}

运行结果:

窗口4 出票15
窗口3 出票14
窗口2 出票13
窗口1 出票14
窗口4 出票12
窗口2 出票10
窗口3 出票11
窗口1 出票9
窗口4 出票8
窗口3 出票6
窗口1 出票5
窗口2 出票7
窗口4 出票4
窗口3 出票3
窗口1 出票2			//重复
窗口2 出票2			//重复
窗口4 出票1

1.1 多线程安全问题分析

通过上述代码,我们发现输出的结果有如下问题:

上述代码出现的问题:重复票、跳票等问题。
原因分析:

​ A:多线程程序,如果是单线程就不会出现上述买票的错误信息;

​ B:多个线程操作共享资源,如果多线程情况下,每个线程操作自己的也不会出现上述问题;

​ C:操作资源的代码有多行,如果代码是一行或者很少的情况下,那么一行代码很快执行完毕,也不会出现上述情况;

​ D:CPU的随机切换。本质原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间进行切换导致的;

1.2多线程安全问题解决

线程安全性问题产生的原因? – 多个线程争抢同一个资源

解决方案:

​ A:无法改变,就是多线程程序。

​ B:无法改变,多个线程就是要操作同一资源。

​ C:无法改变,因为就是有多行代码

​ D:CPU的运行我们无法解决。针对CPU的切换,由操作系统去控制,而我们人为是无法干预。因此这个问题解决不了。

要解决安全问题:
​ 可以人为的控制CPU在执行某个线程操作共享数据的时候,不让其他线程进入到操作共享数据的代码中去,这样就可以保证安全。
​ 上述的这个解决方案:称为线程的同步。

要想保证线程的安全:需要在操作共享数据的地方,加上线程的同步。

​ 加同步格式:

synchronized( 需要一个任意的对象(锁) ){

​			代码块中放操作共享数据的代码。

}

​ 上述的格式称为多线程中的同步代码块。

​ 同步代码块上的锁,可以是随便任意的一个对象。

同步代码块

1)加同步格式:

synchronized( 需要一个任意的对象(锁))

    {

​         代码块中放操作共享数据的代码。

​    }

说明:

1)上述的格式称为多线程中的同步代码块。

2)同步代码块上的锁,可以是随便任意的一个对象。但是必须是唯一的。

​ 问题1:对象是什么?

​ 不确定,所以随便给一个。

​ 问题2:哪些代码需要被同步?

​ 操作共享数据的代码。

2)代码

上述代码为了避免多线程的安全问题,我们需要把上述卖票的代码加上同步代码块,这样就可以解决多线程的安全问题。
不多说直接上代码:

package com.SiyualChen.day02.Demo02;

public class SellTicketTask  implements Runnable{
    private int  tickets=100;
    //定义一个对象充当同步代码块上的锁
    private Object obj = new Object();



    @Override
    public void run() {
        while (true){
            //为了解决多线程的安全问题,给操作的共享资源代码加同步
            synchronized (obj) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + " " + "出票" + tickets);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    tickets--;
                }
            }
        }

    }
}
package com.SiyualChen.day02.Demo02;



public class SellTicketDemo {
    public static void main(String[] args) {
        SellTicketTask sellTicketTask = new SellTicketTask();
        new Thread(sellTicketTask, "窗口1").start();
        new Thread(sellTicketTask, "窗口2").start();
        new Thread(sellTicketTask, "窗口3").start();
        new Thread(sellTicketTask, "窗口4").start();
    }
}

运行结果:

窗口4 出票53
窗口4 出票52
窗口4 出票51
窗口4 出票50
窗口4 出票49
窗口1 出票48
窗口1 出票47
窗口1 出票46
窗口1 出票45
窗口1 出票44
窗口1 出票43
窗口4 出票42
窗口4 出票41
窗口4 出票40
窗口3 出票39
窗口3 出票38
窗口3 出票37
窗口3 出票36
窗口3 出票35
窗口2 出票34
窗口2 出票33
窗口2 出票32
窗口2 出票31
窗口2 出票30
窗口2 出票29
同步方法
修饰符 synchronized 返回值类型 方法名(参数列表){

​	需要同步的代码!!

}

1)演示同步方法

代码和上述代码几乎差不多,只是将同步代码块处调用方法method,然后将同步代码块变为同步方法。

我这里有个需求:
/**
* 请按要求编写多线程应用程序,模拟多个人通过一个山洞:
* 1.这个山洞每次只能通过一个人,每个人通过山洞的时间为5秒;
* 2.随机生成10个人,同时准备过此山洞,并且定义一个变量用于记录通过隧道的人数。
* 显示每次通过山洞人的姓名,和通过顺序;
*/
老样子直接上代码:

package com.SiyualChen.day02.Demo03;

public class Tunnel implements Runnable {
    private int count =0;

    @Override
    public void run() {
        cross();
    }
    /**
     * 请按要求编写多线程应用程序,模拟多个人通过一个山洞:
     * 		1.这个山洞每次只能通过一个人,每个人通过山洞的时间为5秒;
     * 2.随机生成10个人,同时准备过此山洞,并且定义一个变量用于记录通过隧道的人数。
     * 显示每次通过山洞人的姓名,和通过顺序;
     */
    public synchronized void cross(){
        count++;
        System.out.println(Thread.currentThread().getName()+ " " +"第" + count+"过山洞的" );

    }

}
package com.SiyualChen.day02.Demo03;

public class TunnelDemo {
    public static void main(String[] args) {
        Tunnel tunnel = new Tunnel();
        for (int i = 1; i <=10; i++) {
            new Thread(tunnel,"老"+i).start();
        }


    }
}

运行结果:

老1 第1过山洞的
老10 第2过山洞的
老9 第3过山洞的
老8 第4过山洞的
老7 第5过山洞的
老6 第6过山洞的
老5 第7过山洞的
老4 第8过山洞的
老3 第9过山洞的
老2 第10过山洞的

Process finished with exit code 0

以上被synchronized关键字修饰的方法称为同步方法。

注意:同步方法上的锁:

​ 同步方法的锁,不需要我们指定。证明,同步方法有自己默认的锁。

​ 这个锁肯定是一个对象。而这个对象是同步方法能拿到的。上述method方法是非静态方法,而任何非静态方法,都有一个隐含的对象,就是:this。

​ 所以,我们推测:同步方法的锁就是this。

注意:

​ A:如果一个方法内部,所有代码都需要被同步,那么就用同步方法;

​ B:非静态同步方法的锁是this;

静态同步方法

问题:非静态同步方法有隐式变量this作为锁,那么静态方法中没有this,那么静态同步方法中的锁又是什么呢?

静态同步方法上的锁

​ 静态同步方法,没有this,所以不能用this来做锁。

​ 静态方法,类加载的时候,就已经被加载了。这个时候,锁已经确定了。

​ 静态同步方法的锁,肯定也是一个对象,而且这个对象应该是在类加载时就已经存在。

​ 静态同步方法的锁是:当前类的字节码文件对象(Class对象)。

其实可以这么理解:什么是字节码文件对象呢?其实就是class文件,类名.class。比如这里,就是 SellTicketTask.class

​ 获取Class对象的方式:类名.class;

说明:

类名.class整体表示一个对象,class是作为类名的一个静态属性存在。这个class属性是所有的类型都具备的,API不会去专门描述他,他本身就在所有的类型中存在。

总结:

同步代码块:锁是任意对象,但是必须唯一;

非静态同步方法:锁是this;

静态同步方法:锁是当前类的字节码文件对象;类名.class

2.线程的生命周期

新建状态:当我们使用new去创建线程对象的时候。

就绪状态:当我们调用start方法后,线程就有了获得CUP执行权的资格

运行状态:当线程获得CPU的使用权后就进入了运行状态。

注意:就绪状态与运行状态是可以相互转换的,当线程获得CPU使用权就进入运行状态

失去CPU使用权后重新回到就绪状态,等待CPU切到当前线程。

阻塞状态:在运行时期的线程调用了sleep方法或者在等待同步锁的时候就进入了阻塞状态。

当获取到同步锁,或者sleep时间到了的时候则又进入了就绪状态。

死亡状态:当run方法执行完毕或者发生了异常后,线程进入死亡状态。

线程 – 并发执行

​ 线程不安全 – 执行效率会高!!

​ 线程是不是越多越好? 线程越多 被CPU切到几率越低

产生一些安全性问题 – 多个线程 争抢同一个资源

解决方案 – 加锁 – 效率会变低

3.IO加强

3.1Properties

能够做到 读取文件中的内容

关于Properties,是一个比较特殊的集合,集合的特有方法只能存字符串类型,可以通过IO流读取文件中特定格式的文本内容.

3.1.1Map 接口方法和特有方法演

当前类Properties实现了Map这个接口

既然间接 (通过父类Hashtable)实现了Map这个接口,说明当前Properties必然实现了Map中的方法

所以Properties必然拥有Map中的一些功能!~~

那么我们就一起来测试一下Properties中关于Map的一些方法

package com.SiyualChen.day02.Demo04;

import java.util.Map;
import java.util.Properties;
import java.util.Set;

public class PropertiesDemo {
    public static void main(String[] args) {
        //创建Properties对象
        Properties  properties = new Properties();
        //调用put方法,为Properties添加内容
        properties.put("001","Chen");
        properties.put("002","Si");
        properties.put("003","Yuan");
        //调用get方法,通过key取出对应的值
        System.out.println(properties.get("001"));
        System.out.println(properties.get("002"));
        System.out.println(properties.get("003"));
        //遍历方法1  keySet
        Set<Object> keys = properties.keySet();
        for (Object key: keys) {
            System.out.println(key+" "+properties.get(key));
        }
        System.out.println("-----美丽的分界线-------");
        //遍历方法2    entrySet
        Set<Map.Entry<Object, Object>> entries = properties.entrySet();
        for (Map.Entry<Object, Object> entry:entries) {
            System.out.println(entry);
        }

    }

}

运行结果:

Chen
Si
Yuan
001 Chen
002 Si
003 Yuan
-----美丽的分界线-------
001=Chen
002=Si
003=Yuan

Process finished with exit code 0

在Properties中也存在一些方法,可以存储以及获取当前Properties中的内容

可以看到,特有的方法只能操作String

获取当前Properties中所有的key

使用Properties特有的方法存储内容以及遍历Properties

package com.SiyualChen.day02.Demo04;

import java.util.Properties;
import java.util.Set;

public class PropertiesDemo01 {
    public static void main(String[] args) {
        Properties properties = new Properties();
        //使用特有方法存放字符串类型的key与value
        properties.setProperty("001","Chen");
        properties.setProperty("002","Si");
        properties.setProperty("003","Yuan");
        //遍历Properties集合 -- 拿到所有的key
        Set<String> keys = properties.stringPropertyNames();
        for (String key:keys) {
            //获取value
            String value = properties.getProperty(key);
            System.out.println(key+" "+value);
        }


    }
}

运行结果:

001 Chen
002 Si
003 Yuan

Process finished with exit code 0

Properties关于IO流的一些功能

Properties可以通过IO流将文件中的文本内容加载到当前的集合中来,但是只能加载固定格式的文本内容

那么,那是什么格式呢?

key=value的格式

创建一个文件 names.text

001=Chen
002=Si
003=Yuan

Properties使用IO流读取name.text文件中的内容

package com.SiyualChen.day02.Demo04;

import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
import java.util.Set;

public class PropertiesDemo03 {
    public static void main(String[] args) {
        //创建Properties对象
        Properties properties = new Properties();
        //调用load方法,将文件中的内容加载进来
        try {
            properties.load(new FileReader("name.txt"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        //遍历集合
        Set<String> keys = properties.stringPropertyNames();
        for (String key : keys) {
            System.out.println(key +" "+properties.getProperty(key));
        }
    }
}

运行结果:

001 Chen
002 Si
003 Yuan

Process finished with exit code 0

3.2打印流

3.2.1PrintStream

PrintStream中的方法

两个方法

print(任意类型)

println(任意类型)

两个方法的区别println有换行功能 print没有换行功能

package com.SiyualChen.day02.Demo04;

import java.io.FileNotFoundException;
import java.io.PrintStream;

public class Demo01 {
    public static void main(String[] args) {
        // printStream 不关闭, 不刷新, 数据也可以写入到文件中 (自动刷新)
        try {
            PrintStream ps = new PrintStream("1.txt");
            ps.println("不关闭, 不刷新, 数据也可以写入到文件中 (自动刷新)");
            ps.println("不关闭, 不刷新, 数据也可以写入到文件中 (自动刷新)");
            ps.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }


    }
}

4.可变参数

JDK5.0之后出现的技术,可以简化我们方法大量传参的操作

… 用在参数上,成为可变参数。

格式 方法名(参数类型… 参数名)

如果还存在其他参数

例如 方法名(参数类型 参数名,参数类型… 参数名)

可变参数必须要放在参数列表最后,否则编译失败。

参数列表… 底层其实就是数组 可以直接当作数组使用

格式

//可以将当前参数看作为一个数组
修饰符 返回值类型 方法名(参数类型... 参数名){
	方法体
}
package com.SiyualChen.day02.Demo04;

public class Demo03 {
    public static void main(String[] args) {
        //参数类型... 相当于一个数组
        //参数我们可以给 0 - n个  将我们传递过去的参数  封装到了数组中
        int sum1 = getSum();
        int sum2 = getSum(10);
        int sum3 = getSum(10, 20);
        System.out.println(sum1);
        System.out.println(sum2);
        System.out.println(sum3);

        //可变参数  底层是一个数组
        int[] arr = {10,20,30,40};
        int sum4 = getSum(arr);
        System.out.println(sum4);
    }
    //如果 可变参数与其他参数配合使用
    //可变参数  必须放在参数列表的最后面!!!!
    public static int getSum(int... a){
        int num =0;
        for (int i = 0; i < a.length; i++) {
            int i1 =a[i];
            num+=i1;
        }
        return num;
    }
}

运行结果:

0
10
30
100

Process finished with exit code 0
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值