一、什么是多线程
要理解什么是多线程,先理清楚什么是进程,什么是线程。
先看看百度百科上如何解释进程的概念:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
那么线程就可以被理解成进程中可独立运行的子任务。
既然是在一个进程内独立运行的子任务,那么单进程意思就是当前进程只能同时允许一个进程在运行,而多进程可以允许多个进程间来回切换,进而更快的完成更多的任务。
举个例子:
有一把锤子,两家人共用。小明家需要用这把锤子打造一个柜子(需要一天时间),小新家只需要用这个锤子在墙上钉一个钉子(最多五分钟)。既然是共用的锤子,而两家人都需要使用,在单线程的环境下,如果想小明家先拿到锤子,那么就需要独占一天的时间,小新家虽然只需要使用五分钟,但是却要等一天时间小明家才会把锤子空出来。而在多线程的环境下,虽然小明家先拿到锤子,但是也不是一天都在使用,中间也需要吃饭,也需要等待其他材料到齐,总有锤子空闲的时期,这个时候可以先让小新家拿去使用五分钟,然后使用完归还小明家继续使用。
在多进程的环境下,CPU 完全可以在两个任务间来回切换,使耗时短的任务不致于等待耗时长的任务完成才能得到执行,系统的运行效率将大大的得到提升。
但是需要注意的是,凡事都有一个度,虽然多线程间切换任务可以加快多个任务执行的效率,但是同时,在切换任务的时候,也是有一定的开销的,频繁的切换任务可能切换任务消耗的时间会更多。
二、使用多线程
在 Java 的 JDK 中,已经存在来对多线程技术的支持,如果想使用多线程,有两种方式:
继承 Thread 类。
实现 Runnable 接口。
可以看到 Thread 是一个 class,而 Runnable 是一个 Interface 。由于 Java 的单继承限制,如果有更灵活的继承要求,可以考虑实现 Runnable 。但是不管是实用那种方式,都需要一个 Thread 对象来承载。
这里为来偷懒,直接用一个 Android 项目来讲解来。可以看到,实用起来非常的简单,只需要实现它们的 run() 方法,然后在需要的时候,调用 Thread.start() 方法即可。
1、start()和run()有什么不同
那么问题来来,既然我们重写的是 run() 方法,但是为什么最终调用的却是 start() 方法?
Thread 这个类中的 start() 方法就是通知「线程调度器」此线程已经准备就绪,随时等待创建好线程,并且调用 run() 方法执行。这样的启动线程,由「线程调度器」来调用 run() 方法,具有异步执行的效果。而如果我们直接调用 run() 方法的话,就只是和调用一个普通方法一样,是在当前线程的同步执行。
2、线程的执行是无序的
虽然我们启动线程去执行的代码,可以有先后,但是实际上,线程的执行是无序的。也就是说,线程被执行的顺序是随机的,看谁运气好。
上面例子中,分别在各个阶段输出来对应的 Log,可以看到,其实它们执行的顺序是无序的。
三、线程安全
已经讲解完线程的基本使用,如果多条线程之间,没有任何共享的实例对象被改变,那么到这里就算是完结了。
但是实际情况来说,并不是这样的。实际上,我们经常会使用多线程的技术操作同一个实例变量。在多个线程之间,操作同一个实例变量,就变成了多线程的技术难点,如何保持多个线程能准确并且安全的操作到同一个实例变量,这就是线程安全。
拿上面的锤子的例子来说,如果锤子是由物业来保管的,这个时候在物业的管理系统上显示锤子是有一个的,这个小明家和小新家都来借锤子。两个不同的物业管理员在同一个系统中查到还有一个锤子,就同时做了一个出库的动作。这个时候就看物业系统是如何设计的了。
使用计数自减的方式,那么就会在锤子的库存上出现 -1 的计数。
使用直接改变库存的方式,那么虽然两个物业管理员都会讲库存从 1 改为 0,这个时候库存数据是对的。
虽然这两种方式,看着第二种在账面上库存数据是正确的,而第一种出现了负数的情况。但是实际上当两个物业管理员真的去库房取锤子的时候,一定有一个是取不到的,就看这个时候谁运气好了。这就是一个典型的「非线程安全问题」。
这个时候就需要考虑线程安全的问题,在同时操作同一个实例对象的时候,需要保证数据一致性。
简单的保证线程安全,最常用的方式,就是使用 synchronized 关键字。synchronized 可以被加在任意的对象或者方法上,表示对这个方法或者对象加锁,而加锁的这段代码被称为「互斥区」。当一个线程想要执行 synchronized 中的代码的时候,会首先尝试拿这把锁,如果能够拿到的话,就可以继续执行 synchronized 内的代码。如果拿不到的话,就会不断尝试去拿这把锁,直到能拿到并且完成执行为止。当获取到锁并且执行完 synchronized 中的代码之后,就会放弃锁的持有,以便于其他线程能得到锁。
下面举个例子来说明使用方法。
new 五个线程来操作同一个计数,然后对齐进行自减,并输出 Log ,先看看没有加 synchronized 的情况。
可以看到,线程指定的顺序是无章的,输出的 count 的值也是无序的。
如果想让他们有序的输出可以使用 synchronized 来标记 run() 方法。
这个时候,虽然线程的执行顺序依然是无需的,但是却可以保证对数据的处理是有序进行的。
四、Thread的一些其他方法
Thread 中还提供了一些其他的API,可以供我们使用,这里简单的介绍一下。
1、currentThread()
currentThread() 方法啊可以获取到当前代码正运行的线程的信息。
从签名可以看到,它是一个静态的方法,可以在任何地方调用它。
2、isAlive()
isAlive() 方法用于判断当前指定线程数否处于活动状态。活动状态并不仅仅指的是在运行的状态,只要是已经准备运行并且没有结束运行,使用 isAlive() 都会返回 true。
也就是说,在调用了 Thread.start() 之后,一直到 Thread.run() 执行完成之前,isAlive()都是 true。
3、sleep()
sleep() 方法和名称一样,可以使当前进程休眠指定的毫秒数。
它也是一个 static 的方法,可以直接调用,用于休眠当前运行的线程。注意 sleep() 方法可能会抛出一个 InterruptedException 的异常,需要我们对其 catch 住。
4、getId()
虽然在 new Thread() 的时候,可以通过构造方法对 Thread 指定一个名称,但是名称是可以重复的,如果要唯一确定一个 Thread 的 ID,可以使用 getId() 来获取一个线程的唯一标志。
五、结语
今天简单介绍了如何正确的开启一个线程和包装开启的线程是安全的,之后再讲如何优雅的停止一个线程。
【本文为51CTO专栏作者“张旸”的原创稿件,转载请通过微信公众号联系作者获取授权】