美文网首页面试题
[Java]开发安卓,你得掌握的Java知识12

[Java]开发安卓,你得掌握的Java知识12

作者: 入梦瞌睡 | 来源:发表于2019-08-18 23:45 被阅读5次

    0.前言

    • 上一篇文章,我们讲解了ArrayList的相关用法
      看本文之前,推荐先去看一遍该文章

    • 今天我们主要讲解Set中的HashSet与HashMap

    • 若想要了解“类”等其他主要知识,可以去看前面的文章

    1.文章主要内容

    • 线程的基础概念

    • 多线程的使用方法

    • 多线程的同步

    • 线程安全

    2.基础知识讲解

    2.1线程与进程

    • 一个程序的运行就是一个进程
      比如,QQ运行了,QQ就是一个进程

    • 一个进程中有许多线程
      比如使用QQ的时候,接受信息,打开聊天框,下载文件等,都是不同的线程

    • 一个进程中必定会有一个主线程

    • 线程之间是共享(由进程申请的)内存资源的

    2.2线程的一些细节

    为什么要创建子线程:
    • 如果在主线程中存在有比较耗时的操作:
      下载视频 、上传文件等操作
      为了不阻塞主线程,需要将耗时的任务放在子线程中去处理
    • 一个线程有可能处于不同的状态:
    状态名字 具体描述
    NEW 新建状态,即线程刚被创建好
    RUNNABLE 就绪状态,即只要抢到时间片,就可以运行这个线f程
    BLOKCED 阻塞状态,即通过sleep()或wait()暂停线程
    WAITING 等待状态
    TIMED_WAITING 这个以后再说
    TERMINATED 终止状态
    线程的运行过程.jpg

    如何创建子线程

    方法1.继承Thread的类

    • 创建一个继承于Thread的类
    class TestThread extends Thread{
    
    }
    
    • 创建好后,要重写Thread中的run方法
    class TestThread extends Thread{
        @Override
        public void run() {
            //获得当前线程名字
            String name = Thread.currentThread().getName();
            
            //一个循环输出
            for (int i = 0; i < 10; i++) {
                System.out.println(name + " " + (i + 1));
                }
            }
    }
    
    • 在主函数中创建TestThread类的对象,并且调用start()方法
      (不是run(),原因是start()才是开启子线程的方法,但是开启线程后run()肯定会被调用)
    • 线程的run()方法其实运行在当前线程,而不会单独开启一个子线程
    public static void main(String[] args){
          Thread t = new TestThread();
            t.start();
    }
    

    输出结果为:
    Thread-0 1
    Thread-0 2
    Thread-0 3
    Thread-0 4
    Thread-0 5
    Thread-0 6
    Thread-0 7
    Thread-0 8
    Thread-0 9
    Thread-0 10

    • Thread-0就是这个线程的名字

    如何创建子线程

    方法2.实现Runnable接口

    思路:创建一个类实现Runnable接口,然后将这个类的对象作为参数放在Thread的构造函数中,调用Thread对象的构造方法

    • 先创建一个类,实现Runnable接口
    class PXDThread implements Runnable{
    
    }
    
    • 在这个类中实现run方法(与第一种方法一样)
    class PXDThread implements Runnable{
         @Override
         public void run() {
             for (int i = 0; i < 10; i++) {
             System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }
    
    • (1)在主函数中,先建立PXDThread的一个对象

    • (2)使用Thread来操作这个任务

    • (3)启动这个线程

    //1.创建一个任务:创建一个类实现Runnable接口
    PXDThread pt = new PXDThread();
    
    //2.使用Thread来操作这个任务
    Thread t = new Thread(pt);
    
    //3.启动这个线程
    //setName可以给线程起名字
    t.setName("子线程1");
    t.start();
    

    输出结果为:
    子线程1 0
    子线程1 1
    子线程1 2
    子线程1 3
    子线程1 4
    子线程1 5
    子线程1 6
    子线程1 7
    子线程1 8
    子线程1 9

    方法2.2

    • 如果这个方法只要执行一次,可以考虑用匿名类的方式
      而不用单独地额外创建一个类
    public static void main(String[] args){
           Thread t3 =new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println(Thread.currentThread().getName() + " "+i);
                    }
                }
            });
            t3.setName("子线程3");
            t3.start();
    }
    

    输出结果为:
    子线程3 0
    子线程3 1
    子线程3 2
    子线程3 3
    子线程3 4
    子线程3 5
    子线程3 6
    子线程3 7
    子线程3 8
    子线程3 9

    方法2.3匿名类的时候就使用start()

    • 如果这个方法只要执行一次,且想要更简便的表达式
      可以考虑在匿名类的后面加.start()
    public static void main(String[] args){
        new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println(Thread.currentThread().getName() + " "+i);
                    }
                }
            }).start();
    }
    

    Thread-0 1
    Thread-0 2
    Thread-0 3
    Thread-0 4
    Thread-0 5
    Thread-0 6
    Thread-0 7
    Thread-0 8
    Thread-0 9
    Thread-0 10
    注意:

    • 使用匿名类是没有办法为线程设置名字的

    方法2.4使用Lambda表达式

    • 如果这个方法只要执行一次,且想要更简便的表达式
      可以考虑使用Lambda表达式
    public static void main(String[] args){
           new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " "+i);
                }
            }).start();
    }
    
    • 优点:十分简洁
    • 缺点:对于初级开发人员而言过于复杂难懂


    2.5线程的同步

    • 试想一下,如果一个卖票网站还剩一张票,两个人同时买票,
      由于线程是同时执行的,那会不会发生一张票卖给两个人呢?
    • 会的话,应该怎么解决呢
    class Ticket implements Runnable{
        //一共一百张票
        public static int NUM = 100;
        public String name;
    
        public Ticket(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if (NUM > 0) {
                    //当多个线程操作的时候,可能线程1会先打印,然后时间片被抢去,线程2会打印然后减减,
                    //时间片还给线程1之后,NUM--,这就会导致少了一张票,而且两个线程还出现了重票
                    //多线程并没有办法知道什么时候被打断
                    System.out.println(name + "出票: " + (NUM + 1));
                    NUM--;
                }else{
                    break;
                }
    
            }
        }
    
    public static void main(String[] args) {
            Ticket ticket1 = new Ticket("重庆");
            Thread t1 = new Thread(ticket1);
            t1.start();
            Ticket ticket2 = new Ticket("上海");
    
            Thread t2 = new Thread(ticket2);
            t2.start();
        }
    

    输出结果为:
    重庆出票: 100
    上海出票: 100
    重庆出票: 99
    上海出票: 98
    重庆出票: 97
    上海出票: 96
    .......
    (为了观感这里就不全部列出结果,没必要)

    • 我们可以看到,两个线程同时运行的时候,很可能会卖出重复的票(如重庆和上海都卖出了编号为100的这张票),原因是线程的时间片的获得,与失去是随机的
    线程的同步

    就是为了解决多个线程同时运行的时候,某个线程不知道在何时会被抢走时间片无法继续执行,导致一些错误
    (比如卖重票,少卖了票等)

    保证线程同步的方法:

    方法1.synchornized

    方法1.1使用同步代码块
      static final Object obj = new Object();
     @Override
      public void run() {
            for (int i = 0; i < 100; i++) {
                //圆括号中放一个监听器/对象
                synchronized (obj){
                    if (NUM > 0) {
                        System.out.println(name + "出票: " + (NUM));
                        NUM--;
                }
          }
    }
    

    输出结果为:
    重庆出票: 100
    重庆出票: 99
    重庆出票: 98
    重庆出票: 97
    重庆出票: 96
    重庆出票: 95
    ......

    重庆出票: 9
    重庆出票: 8
    重庆出票: 7
    上海出票: 6
    上海出票: 5
    上海出票: 4
    上海出票: 3
    上海出票: 2
    上海出票: 1
    (为了观感不全部列出)
    加了锁之后,可以看到不会卖出重复票了

    注意:

    • 每一个对象都有一个自己的锁,因此 synchronized后面的括号可以放任意一个对象

    • 但是为了前后两个线程使用的是同一个锁,则
      定义的obj必须为static静态类型,以保证它先于类被创建,且所有对象用的是同一个obj变量

    方法1.2使用同步方法
    • 使用同步方法的实质就是在使用同步代码块,只是写法不同
    public void run() {
            test();
        }
    public synchronized void test(){
                for (int i = 0; i < 100; i++) {
                    if (NUM > 0) {
                        System.out.println(name + "出票: " + (NUM + 1));
                        NUM--;
                    } else {
                        break;
                    }
    
                }
            }
    

    这段代码相当于(当然这么写编译器过不了):

    synchronized (this) {
                test();
    }
    public void test(){
                for (int i = 0; i < 100; i++) {
                    if (NUM > 0) {
                        System.out.println(name + "出票: " + (NUM + 1));
                        NUM--;
                    } else {
                        break;
                    }
    
              }
     }
    
    • 这里这么写的话必须保证是同一个this,就是说得保证是同一个对象调用run()
      但是上述例子其实不能保证,所以这个例子是没有办法用这种写法的

    3.实例应用

    • 要求:
      (1)用代码模拟客户找房屋中介买房子,中介找到合适的房子后返回消息给客户
      (2)希望这件事情在一个子线程中实现
    • 代码思路:

    • (1)首先写一个Person类,一个Agent类,Agent类要继承Thread,这样才能在子线程中执行

    • (2)Person类的对象要有一个方法A,来调用Agent类的方法B

    • (3)Agent类的这个方法B完成寻找房屋的过程,然后返回消息给Person对象

    • (4)这个“返回消息”靠的是接口回调,即Agent类中定义一个接口,以及定义一个接口变量,Person类实现这个接口。

    • (5)方法A中要把this赋值给Agent类的接口变量,然后在方法B的最后,就可以靠接口变量来调用Person类中实现的接口的方法了
      ,以此来(返回消息)告诉Person找到房子了

    public class Agent extends Thread{
    
        AgentInterface target;
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
    
            System.out.println("开始找房");
            System.out.println("--------");
            System.out.println("--------");
            System.out.println("房子找到了,即将返回数据");
            target.callBack("西南大学");//回调
            super.run();
        }
    
        public interface AgentInterface{
            void callBack(String desc);
        }
    }
    
    • 继承Thread是因为“找房子的过程”要在子线程中实现
    • target为接口变量,有了它才能实现接口回调
    public class Person implements Agent.AgentInterface {
    
        public void needHouse(){
            Agent xw = new Agent();
            xw.target = this;
            xw.start();
        }
    
        //接口就是一种统一,让创建接口的类可以轻松地通过调用实现该接口的类的方法
        //来告诉实现接口的类,已经做完事情了
        //就好比中介会让客户都装一个微信,然后通过微信来告诉他们,这个微信,就是一个接口
        @Override
        public void callBack(String desc) {
            System.out.println("我是小王,接收到你的数据了:" + desc);
        }
    }
    
    • needHouse()方法是为了能够调用Agent类的run()方法

    • 如果Person类对象(设为person)想要在run()的最后能够告诉person能做完了的话,必须在Person中把this赋给Agent中的target
      ( xw.target = this;这句话是能够实现接口回调的关键)

    • 只有xw.target = this;,Agent中的run 方法中的
      target.callBack("西南大学");(接口回调)才能够成立

    • (接口回调说白了就是把消息返回给实现接口的类)

    public static void main(String[] args) {
            Person ls = new Person();
            ls.needHouse();
        }
    

    输出结果为:
    Thread-0
    开始找房



    房子找到了,即将返回数据
    我是小王,接收到你的数据了:西南大学

    • 其中,Thread-0就是该线程的名字,不是main就证明不是在主线程中执行的,而是单独开启了一个子线程

    4.总结

    (1)本文讲解了线程的概念、基本使用、线程安全等问题
    (2)线程是Java中十分重要的一个知识点,只要看一些例子,再结合现实中的实际情况,线程的相关内容其实挺好理解的,语法也不难,可能只是第一次见到时,会感觉有点绕,关键就是要在实际情况中多去使用即可。

    相关文章

      网友评论

        本文标题:[Java]开发安卓,你得掌握的Java知识12

        本文链接:https://www.haomeiwen.com/subject/dqlrsctx.html