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

0.前言

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

  • 今天我们主要讲解线程

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

  • 由于最终目的是要开发安卓app,
    因此这里使用的IDE是AS(Android Studio)
    (不会使用的可以参考下面这篇文章中的例子)
    《[Java]开发安卓,你得掌握的Java知识2》

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——线程)