美文网首页
android 多线程 — Thread

android 多线程 — Thread

作者: 前行的乌龟 | 来源:发表于2018-05-30 21:31 被阅读138次

java 的线程类型是 Thread ,所以多线程的学习也是从 Thread 开始的

继承 Thread 类


启动一个线程,执行我们定义的任务,最简单的方式就是继承 Thread 类了

// 创建 Thread 类
class MyThread extends Thread{
    private String name ;

    public MyThread(){
        name = "AA";
    }

    @Override
    public void run() {
        System.out.println(name);
    }
}

// 启动 Thread 线程对象
MyThread thread = new MyThread();
        thread.start();

注意线程对象的启动是通过 start() 执行的

实现 Runnable 接口


创建自己的线程类型,我们还可以把线程的核心代码写到 Runnable 接口里,然后通过参数传递给 Thread 对象

// Runnable 对象类型
class MyRunnable implements Runnable{
    public MyRunnable() {
    }

    @Override
    public void run() {
        System.out.println("子线程ID:"+Thread.currentThread().getId());
    }
}

// 把 Runnable 作为参数传递给 Thread 对象
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();

接口的方式更灵活,我们不用去专门写一个 Thread 类出来了,Runnable 直接一个匿名实现类多方便

Callable、Future、FutureTask


这3个类总体来是是处理返回数据的线程的,这3个都是一起用,不难

上面我们可以把一个 Runnable 类型的接口对象传进 Thread 线程对象里面去运行,不知道大家注意到没有,这个 Runnable 是没有返回值的,我们启动线程之后只能使用 handle 做跨线程通信

那么有没有其他的方式啊,有啊 Callable 接口,启动线程之后,线程获取到结果后,可以把这个结果返回给我们,这个结果有个统一的包装类型,就是 Future,然后 Future.get() 方法我们就能拿到这个结果了,但是呢有一点,这个 Future.get() 是需要等待其他线程运行知道结束繁返回数据,也就是说会阻塞我们的当前线程,不过 Callable、Future 都是配合 java 的Executor 线程池框架的,也不会想我们上面那样直接传给 Thread 使用。

不过完事没有绝对,FutureTask 就是能在 Thread 里面运行的 Callable、Future。Thread 构造方法里面只能接受 Runnable 类型的接口对象,FutureTask 内部间接的实现 Runnable 接口,所以能直接在 Thread 里使用

来看个小例子,先启动一个线程,睡眠2秒,之后返回结果,我们在 UI 线程里等待结果(此时会阻塞 UI 线程) ,收到结果后再启动一个线程,2个线程都打印下时间,看看能不能符合我们的预期

        // 带返回结果的线程任务,String 泛型是我们预订的返回数据类型
        Callable<String> callable1 = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Log.d("AA", Thread.currentThread().getName() + "启动,time:" + System.currentTimeMillis());
                Thread.sleep(2000);
                return "AA";
            }
        };
        
        // 普通线程任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Log.d("AA", Thread.currentThread().getName() + "启动,time:" + System.currentTimeMillis());
            }
        };

        // 用 Callable 对象构建一个 FutureTask  线程返回结果
        FutureTask<String> futureTask = new FutureTask<String>(callable1);

        // 创建出2个线程
        Thread thread1 = new Thread(futureTask);
        Thread thread2 = new Thread(runnable);

        // 先启动会返回结果的线程
        thread1.start();
        try {
            // 这里阻塞 UI 线程,等待线程1 的返回数据,再去启动线程2
            futureTask.get();
            thread2.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
结果

从时间看对的上,线程2在线程12000毫秒后执行的

Runnable 和 Callable 的区别在接口声明上就能看的很明显了

public interface Runnable {  
    public abstract void run();  
}  
public interface Callable<V> {   
      V   call()   throws Exception;   
}

我们再看看 Future 的这个接口

public interface Future<V> {  
    boolean cancel(boolean mayInterruptIfRunning);  
    boolean isCancelled();  
    boolean isDone();  
    V get() throws InterruptedException, ExecutionException;  
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;  
}
  • get()
    获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。

  • get(Long timeout , TimeUnit unit)
    获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将返回null。

  • isDone()
    如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。

  • isCanceller()
    如果任务完成前被取消,则返回true。

  • cancel(boolean mayInterruptRunning)
    如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。

FutureT 执行示意图

好了 Callable、Future、FutureTask 就先说到这里,下面这个例子是 Callable、Future 在线程池中执行的例子,这个才是最常用的需求

        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "AA";
            }
        };
        
        // new 一个线程池对象
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        // 把任务添加到线程池中去执行,获取到一个 Future 任务结果操作对象
        Future<String> future = executorService.submit(callable);

        try {
            future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        Log.d("AA", Thread.currentThread().getName() + "获取到异步线程结果,time:" + System.currentTimeMillis());

Thread 线程的状态


线程作为一个复杂的系统,因各种情况的不同会有好几种状态,对这几种线程状态我们必须做到熟知才行

线程状态:

  • 创建(new)状态
    我们刚刚 创建 new 一个 Thread 对象时

  • 就绪(runnable)状态:
    Thread 对象所需要的都准备完毕,此时可以调用 start() 方法了,剩下的就是等待CPU进行调度

  • 运行(running)状态:
    执行 run() 方法让线程跑起来

  • 阻塞(blocked)状态
    暂时停止执行, 可能将资源交给其它线程使用

  • 终止(dead)状态:
    线程销毁

线程运行描述:

  • 通常当我们 new 一个线程对象来执行任务时,线程对象不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。

  • 当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。

  • 线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。

  • 当由于突然中断或者任务执行完毕,线程就会被消亡。

线程状态运行图

blocked、waiting、time waiting 都可以叫阻塞,详细区别下是有必要的,涉及的方法不同

Thread 类方法解析


上面我们说了如何创建一个 Thread 线程对象并执行,获取到结果。其实 Thread 自身还有一些很重要的方法需要记住。

我简单点直接放张图


Thread 方法列表
  • currentThread()
    返回所在线程对象,这是个静态方法
System.out.println(Thread.currentThread().getName());
  • sleep()
    让当先线程睡眠指定的毫秒时间,sleep 方法会阻塞当前线程,但是不会释放当前线程所持有的对象锁
// 当前线程睡眠1秒
Thread.currentThread().sleep(1000);
  • yield()
    让当前线程交出本次 CPU 时间,并释放所持有的对象锁,竞争下次 CPU 时间。需要注意的是 yield 只是释放本次的 CPU 执行机会,下次该竞争还竞争。调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。还有这也是一个静态方法
Thread.yield();
  • start()
    线程启动方法,当调用start方法后,系统才会开启一个新的线程并分配需要的资源

  • run()方法
    run方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

  • getId()
    获取当前线程唯一标识

Thread t= Thread.currentThread();
System.out.println(t.getName()+" "+t.getId());
  • isAlive()
    判断当前线程是否处于活动状态,不是 running 状态都不叫活跃状态
        Thread t1 = new Thread();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.d("AA", Thread.currentThread().getName() + "isAlive:" + t1.isAlive());
Snip20180530_2.png

可以看到就绪状态都不行,都不算是活跃状态,其他的可想而知

  • join()
    让当前线程进入阻塞,等待被 join 方法标记的线程执行完毕再执行。感觉上是不是有点像上面那个 callable 和 Future ,相当于给被 join 标记的线程添加一个返回值,我们等待这个返回值获取到数据再继续我们当前的线程。
        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                try {
                    Thread.sleep(2000);
                    Log.d("AA", Thread.currentThread().getName() + "时间,time:" + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        try {
            Log.d("AA", Thread.currentThread().getName() + "时间,time:" + System.currentTimeMillis());
            t2.start();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.d("AA", Thread.currentThread().getName() + "时间,time:" + System.currentTimeMillis());
Snip20180530_3.png

可以看到 UI 线程被成功的阻塞了,和我们的预期一样,和 callable 和 Future 的效果是一样的

  • getName和setName
    用来得到或者设置线程名称。

  • getPriority和setPriority
    用来获取和设置线程优先级。

  • setDaemon和isDaemon
    用来设置线程是否成为守护线程和判断线程是否是守护线程。

守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

找到张图表示线程各方法的效果:


9110701-a08a6158edf64957.jpeg

停止线程


线程停止是个重要的点,有3中写法:

  • Thread.stop()
    这个方法已经被废弃不用了,
  • interrup()
    interrup 有个问题是不一定能终止线程,这点我不是很了解,建议大家再去找找资料
  • 加标记位
    大家知道,线程执行方法完成了,那么自然就会终止线程了,所以我们在 线程的 run 方法里都是加一个无限虚循环的 while(true),我们只要把这个 true 用标志位来代替,想终止的时候把这个标记改成 false 自然就能安全的退出线程了。

线程的优先级


在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw new IllegalArgumentException()。
JDK中使用3个常量来预置定义优先级的值,代码如下:

  • public final static int MIN_PRIORITY = 1;
  • public final static int NORM_PRIORITY = 5;
  • public final static int MAX_PRIORITY = 10;
Thread thread = new Thread();
t1.setPriority( Thread.NORM_PRIORITY );

线程优先级特性:

  • 继承性
    比如A线程启动B线程,则B线程的优先级与A是一样的。
  • 规则性
    高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
  • 随机性
    优先级较高的线程不一定每一次都先执行完。

守护线程


在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:

thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)

在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)

不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。

最后

我这里写的也不是很全,有没看懂的建议大家再去找资料,这里主要是给我自己看,按照的我的记忆顺序写的, 以后忘了再看方便的多。

有不全的大家见谅,有错误请下面留言我更正

参考资料:

相关文章

网友评论

      本文标题:android 多线程 — Thread

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