前言
从今天开始,我将写下一系列关于多线程的文章,包括多线程基础、线程间通信、阻塞队列、线程池。今天写第一章《多线程基础》,如果你对线程还不是很了解,读完本篇文章你将会对线程有一个初步的认识,如果你对线程很熟悉了也希望你能够仔细的看一遍,回顾一下基础知识没啥坏处的吧。另外,内容中部分实例借鉴于毕向东老师的javaSE教程,在此呢也给大家安利一波,如果java基础不太扎实的同学可以去听一遍这套课程,讲的特别好,至今我看了四遍有余,每一遍都有不同的收获。
1 概述
1.1 进程
了解线程之前我们先来了解一下进程这个概念。
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
以上是进程官方的叙述,其实进程很好理解,比如我们windows下打开任务管理器,里面就有启动的各种进程,像里面的QQ、360之类的都是一个进程,它们运行后都会在内存中开辟一块空间,其实说的通俗一点进程就是:正在进行的程序。
1.2 多线程
说完进程我们来说一下线程
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
以上是线程的官方解释,下面我用通俗一点的语言给大家解释一遍,进程负责开辟一块内存空间,而具体的内容也就是我们写的代码是由线程去执行的,线程是CPU执行的基本单位,所以一个进程中应该至少有一个线程,而一个进程可以有多个线程存在。
1.3 多线程的好处与弊端
好处:首先跟大家举一个例子,比如你们手中的智能手机,如果只有一个线程的话,那么你聊天的时候不能听歌,听歌的时候不能浏览网页,更苦逼的是如果你在下载东西而且网速还很慢,那这段时间手机就成板砖了,只能看着等下载结束,这种体验是非常差的。多线程就解决了这个问题,将多个任务放到多个线程中去执行,而CPU会随机的切换到线程中去执行任务,这个切换的频率是非常快的,这样就会达到一个同时运行的效果。
弊端:多线程技术看起来很完美,那是不是以后每个任务都可以用多线程去执行?答案是否定的,因为线程过多的话CPU切换的频率也会增加,大大的降低了CPU执行任务的效率,所以一般我们都只在耗时的任务中开启一个线程去执行,比如网络请求、本地文件读写之类的。
1.4 JVM中的多线程
在Java语言当中也是支持多线程的
public class Demo {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.print("hello world");
}
}
上面这个hello world是在哪个线程中执行的呢?答案是主线程,主线程是JVM启动的时候开启的一条线程,往往也是执行我们开发者代码的一个入口。JVM启动后会开启多个线程,除了主线程外还有垃圾回收线程等等。
2 多线程的创建
2.1 通过Thread创建
通过继承Thread重写其run()进行创建
class MyThread extends Thread{
public void run(){
}
}
而我们的任务中就可以放在这个run()中进行执行。开启一个线程也非常简单,调用其start()方法即可。
new MyThread().start();
2.2 通过Runnable创建
通过实现Runnable接口进行创建
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
也很简单,实现Runnable接口重写其run()方法,创建一个实体传入Thread(),然后调用start()开启。
通过Runnable创建的好处:
public static void main(String[] args) {
// TODO Auto-generated method stub
ThreadDemo demo = new ThreadDemo();
new Thread(demo).start();
new Thread(demo).start();
}
static class ThreadDemo implements Runnable{
public void run() {
for(int i=0;i<100;i++){
//Thread.currentThread().getName()为当前线程名称
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
该段代码的执行结果:
Thread-1---64
Thread-0---65
Thread-1---66
Thread-0---67
Thread-1---68
Thread-0---69
可以看出两个线程共同把任务执行完毕,所以在需要多个线程执行同一任务时可以使用Runnable创建线程。我们开发的过程中也是应该首选Runnable方式创建线程。
2.3 通过Callbale创建
当我们要获取线程的执行结果时,一般的方法是用接口的回调,代码如下。
new Thread(new Runnable() {
@Override
public void run() {
int sum = 0;
for(int i =0;i<100;i++){
sum += i;
}
if(callback!=null){
callback.call(sum);
}
}
}).start();
这种方式获取执行结果有几个缺点
- 需要写一个回调接口
- 任务执行的过程中不能取消
- 想要重复的获取执行结果需要再次执行任务
说完了传统线程的创建方式我们就来引入Callbale的概念
Callable:它是一个接口,内部由一个call()方法,相当于Runnabl的run(),耗时任务写在call()方法内部即可。
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Future : Future也是一个接口,内部有cancel()、get(),用来取消任务和获取执行结果。
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;
}
FutureTask:它内部实现了Runnable和Future接口,也就是说它可以当做一个线程任务,并且能够对执行的任务操作。FutureTask内部原理我会在以后的文章中进行详细叙述,本章只介绍FutureTask的使用。使用步骤如下:
private void callable(){
final FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
Thread.sleep(2000);
for(int i =0;i<100;i++){
sum += i;
}
return sum;
}
});
new Thread(task).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.i("zs","sum="+task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}).start();
}
重写FutureTask中的call()方法,将耗时任务写在call()方法内,把FutureTask对象传入到Thread中并开启,可以通过调用FutureTask的cancel()方法和get()方法进行取消和获取执行结果操作。需要注意的是get()方法为阻塞式的,如果获取不到结果就会一直等待,所以说我们不要在主线程中进行调用。
3 线程的状态
在描述状态之前我要先讲一下几个方法和概念:
sleep(long time):通过调用该方法可以使当前线程处于休眠状态,睡眠时间为time。
wait():通过调用该方法,可以将当前线程处于等待状态,属于Object的方法。
notify:可以将一个线程唤醒,属于Object的方法。
CPU执行资格: 正在排队等待CPU执行。
CPU执行权:正在被CPU执行。
说完了这几个方法我将结合一张图为大家描述线程的状态(纯手工,略微粗糙)。
- 线程创建后通过start()进行开启
- run()方法结束后线程会消亡
- sleep(int time)可以让一个线程释放执行资格并释放执行权,处于冻结 状态,知道经过time时间后会被唤醒,被唤醒后可能立即运行也可能处于 临时阻塞状态。
- wait()可以让一个线程释放执行资格并释放执行权,处于冻结状态,知道被notify()后才会被唤醒,被唤醒后可能立即运行也可能处于临时阻塞状态。
4 线程同步概念
在讲同步之前我先给大家讲述一个案例:
class Demo02 implements Runnable{
int num= 10;
@Override
public void run() {
while (true){
if(num>0) {
System.out.println(Thread.currentThread().getName() + " " + --num);
}
}
}
}
...
//创建Demo02 实例
Demo02 demo02 = new Demo02();
//开启两个线程执行demo02中的任务
new Thread(demo02).start();
new Thread(demo02).start();
执行结果为:
Thread-7 9
Thread-7 8
Thread-7 7
Thread-7 6
Thread-7 5
Thread-7 4
Thread-8 3
Thread-7 2
Thread-7 1
Thread-8 0
我们看没什么问题的,两个线程共同把任务执行完毕。我在循环中加一个sleep再来看看结果
static class Demo02 implements Runnable{
int num= 10;
@Override
public void run() {
while (true){
if(num>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-- " + --num);
}
}
}
}
运行结果
Thread-0-- 9
Thread-1-- 8
Thread-1-- 7
Thread-0-- 6
Thread-1-- 5
Thread-0-- 4
Thread-1-- 3
Thread-0-- 2
Thread-1-- 1
Thread-0-- 0
Thread-1-- -1
我们可以看到出现了-1,哎,怎么会出现-1呢?当时我刚学的时候也是一脸懵逼,其实理由很简单的,我来跟大家一步一步分析。
当num=1时cpu切换到了Thread-0,Thread-0进行判断,num>0,但就在此时,Thread-0还未对num进行输出,cpu不执行Thread-0了,转而去执行Thread-1。Thread-1得到执行权后判断num>0,并输出num=0,然后cpu又切换到了Thread-0,因为现在num=0,所以输出num=-1。那既然出现了安全问题,有没有办法解决呢?当然是有的啦,方法总比困哪多的吗(゜-゜)つ。
为了解决上述的安全问题,我们就引入一个概念,"同步",什么是同步呢?我们先来看一下同步官方的解释:
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。
什么意思呢?我来用白话跟大家叙述一遍:当一个线程执行一个同步的任务时,在该任务执行完毕之前,其他线程是不得进入该任务的(好像描述的还是很官方(゜-゜)つ,哈哈,皮一波),我就结合上述例子来讲吧,如果例子中的任务是同步任务,当Thread-0执行完毕同步任务前其他线程是不准进来执行的,也就是说如果将判断和打印几句代码加上同步,当Thread-0在判断语句内的时候Thread-1是进不来的。所以加个同步就可以完美的解决例子中的安全问题。
java中的同步:在java中可以通过关键字synchronized对任务进行同步,具体的书写方式:
class Demo02 implements Runnable{
int count = 10;
@Override
public void run() {
while (true){
synchronized(this) {
if(count>0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-- " + --count);
}
}
}
}
}
把需要同步执行的任务用synchronized括起来,括号内参数为锁的意思,可传入任意对象,一般来说传入this即可,静态方法中传入类名.Class。好了,我们再来看一下运行结果。
Thread-0-- 9
Thread-0-- 8
Thread-0-- 7
Thread-0-- 6
Thread-0-- 5
Thread-0-- 4
Thread-0-- 3
Thread-0-- 2
Thread-0-- 1
Thread-0-- 0
加了同步代码块完美运行。这样就引出另外一个疑问,那是不是以后每个线程任务都可以加上同步代码块呢?答案是否定的,因为加上同步锁之后每次执行任务的时候都会判断一次锁,这样是很影响效率的。
拓展
单例设计模式我相信大家应该都很熟悉,那单例模式是否存在线程安全问题呢?我们先来看一下单例模式常规的写法:
public class Single {
private static Single instance = null;
private Single(){}
public static Single getInstance() {
if (instance == null){
instance = new Single();
}
return instance;
}
}
细心地同学可能已经发现,这种写法是存在线程安全问题的,假如Thread-0判断了instance,此时为空,所以Thread-0就进入了判断语句,就在此时Thread-1也调用了该方法,判断instance也为空,最后会创建出两个instance对象,这就违背了单例设计模式的概念,所以单例模式正确的写法应该是:
public class Single {
private static Single instance = null;
private Single(){}
public static Single getInstance() {
//多加一个判断是为了避免每次调用都判断锁
if (instance == null){
synchronized (Single.class) {
if(instance==null) {
instance = new Single();
}
}
}
return instance;
}
}
好了,本章关于同步的叙述到此为止,但Java中关于同步的内容远不止这些,在后面写线程间通信的时候我再为大家详细叙述。
5 总结
这篇文章讲述了线程的概念、线程的创建方式以及同步。为了解决多个任务同时进行和充分的利用CPU所以就有了线程这一概念。线程的创建方式有三种,这三种创建方式各有特点可以结合实际场景进行选择使用。多线程中可能会产生安全问题,可以结合同步去解决。好了,本篇文章对线程基础的描述到此为止,但并不代表线程就这点东西,如果详细的去写10篇文章都写不完,在这里只是起到一个抛砖引玉的作用。下一篇文章《多线程系列(二)线程间通信》。