一、认识线程
1、概念
什么是线程呢?
线程是进程划分成的更小的运行单位。就好比电脑QQ是一个进程,里面还有各种子模块,比如QQ空间,个性皮肤等子功能。
这里出现了另外一个名词进程。
进程是系统运行程序的基本单位。就好比是一个个应用程序QQ、微信等等。
看概念确实是一脸懵逼,举个例子就明白了。我们打开电脑的任务管理器,会发现上面就有进程,点击这个进程我们就能看到一个个应用程序,这就是进程。
那什么是线程呢?不知道我们注意到了没有,每一个进程最左边都有一个小箭头>,我们打开来看看:
这些小的模块就好比是一个个线程。有了这个印象我们重新来认识一下线程和进程就容易多了。
线程:
线程是一个比进程小的执行单位,也被称为轻量级进程。一个进程可以产生多个线程。多个线程共享同一块内存和系统资源,CPU在这多个线程间来回切换去执行。
进程:
进程是系统运行程序的基本单位,也就是一个程序。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
有了对线程的基本认识接下来我们就可以去理解一下,这一系列文章常用到的一些概念了。
2、并行与并发
其实他们俩区分很容易区分。
并发是多个任务交替使用CPU,同一时刻还是只有一个任务在跑,并行是多个任务同时跑。举个例子就明白了。
桌子上有三个馒头,每一时刻,小明只能咬一个馒头。
桌子上有三个馒头,每一时刻,小明、小红、小华三个人同时咬三个馒头。
明白了吧。
3、同步和异步
这两个名词我们在学习的时候经常会遇到,举个例子去理解,
对于同步:我们去餐厅吃饭,只有一个客户的时候商家比较容易处理,但是当有两个人三个人的时候,这时候就需要排队了,也就是拥塞了,这就是同步。换个例子来说,就是我们请求服务器的时候,必须要等到服务器的反馈我们才能够去做其他的事。
对于异步:就好比微信聊天,我们只管把信息发送给对方,不管对方有没有回复我们,我们都可以去做其他的事,也就是说执行完函数之后,不必等到反馈就可以去做其他的事。
4、死锁
和操作系统里面的死锁意思一样,也就是说多个人同时竞争一个资源,
例子一:好比多个男孩追求同一个女孩,这时候女孩就不知道该嫁给谁了。
例子二:我们去图书馆借书,发现这本书被借走了,我们只能等到那个人把书还到图书馆才可以看。
5、原子变量与原子操作
所谓原子操作,就是“不可中断的一个或一系列操作”。就好比你高考考试的时候,就算天塌下来也要把卷子做完。再急的事也不能抢夺他的优先权。
原子变量其实是一个抽象的意思,因为本质上并没有严格意义上的原子变量,但是在这里,我们可以这样理解,原子变量提供原子操作,就好比变量a,多个线程对其操作改变时,每一次只能有一个线程拿到他的执行权进行操作。
在这里我们基本上列举了一些基本的概念,但其实还有很多,我们在遇到的时候再去分析和理解会比较容易。我们说了这么久的线程,下面我们来认识一下java中的线程。
二、基础案例
我们以一个生活中的案例来解释说明,比如我们敲代码的同时还想听音乐。
java中创建一个线程有两种方式,继承Thread类和实现runnable接口。我们两种都实现一下。
1、继承Thread类
在这里我们定义两个线程
public class ThreadTest01 extends Thread{ @Override public void run() { for (int i=0;i<10;i++) System.out.println("听音乐"); } }
还有一个线程可以敲代码
public class ThreadTest02 extends Thread{ @Override public void run() { for (int i=0;i<10;i++) System.out.println("敲代码"); } }
最后我们就可以一边敲代码一边听音乐了
public class ThreadTest { public static void main(String[] args) { ThreadTest01 thread1=new ThreadTest01(); ThreadTest02 thread2=new ThreadTest02(); thread1.start(); thread2.start(); } }
我们运行一边会发现,敲代码和听音乐是交叉输出的。这就体现了多线程的含义,因为要是平时输出,肯定就是谁在前先输出谁,比如说先输出十个听音乐,在输出十个敲代码。不会交叉输出。
当然我们也可以使用一个线程类去演示,在这里,首先我们创建了两个类ThreadTest01和ThreadTest02,并且都继承了Thread,然后再测试类中,我们只需要调用相应的start方法即可。
使用一个线程类和使用多个线程类的区别你可以这样理解,一个是多个不同的线程分别完成自己的任务,一个是多个相同的线程共同完成一个任务。
2、实现Runnable接口
我们同样拿上面的例子进行说明
public class MyRunnable01 implements Runnable{ @Override public void run() { for (int i=0;i<10;i++) System.out.println("听音乐"); } }
然后还可以敲代码
public class MyRunnable02 implements Runnable{ @Override public void run() { for (int i=0;i<10;i++) System.out.println("敲代码"); } }
最后我们可以测试一下了
public class ThreadTest { public static void main(String[] args) { MyRunnable01 runnable01=new MyRunnable01(); MyRunnable02 runnable02=new MyRunnable02(); Thread thread1=new Thread(runnable01); Thread thread2=new Thread(runnable02); thread1.start(); thread2.start(); } }
每次运行的时候,在控制台你都会看到不一样的效果。不过多运行几次依然能够发现交叉运行的效果。
其实呢还有一种方式也可以实现线程,那就是实现Callable接口,不过很少用到,这种方式在以后的文章中再进行详细的介绍。毕竟这是第一篇文章。只是认识了解一下线程。
通过Runnable接口的方式,我们依然发现,创建了两个MyRunnable,然后直接赋给Thread即可,调用的时候同样是使用start方法来启动。这就是简单的使用一下线程。
不知道我们注意到没有,java其实为我们已经提供了Thread,我们可以直接进行实例化。有时候我们会经常使用匿名内部类的方式来创建一个线程,比如说下面这种
new Thread("th1") { @Override public void run() { System.out.println( "匿名内部类"); } }.start();
注意:上面的两种创建线程的方式中,明明都是重写的run方法,为什么要去调用start启动线程呢?而且这两种方式有什么区别呢?在这里先留一个悬念,下一篇文章将会介绍道。
三、分析多线程
1、使用线程有什么好处呢?
好处你已经能够看到了,就是我们可以同时做好几件事,在玩游戏的时候可以听歌,还可以看电影等等,多方便。
2、使用线程有什么坏处嘛?
坏处其实也很多,比如说对于单核 CPU,CPU 在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换,上下文切换是一个复杂的过程,比如要记录程序计数器、CPU寄存器的状态等信息,耗时又耗空间。
为了解决这个问题,才有了现在的多核CPU。我们经常会听到手机或者是电脑是八核的,就是减少上下文切换带来的时间空间损耗,提高程序运行的效率。
3、多线程带来一个问题
从上面的例子其实我们发现只是各干各的事,相互之间互不干扰。还有一种情况是一个资源被多个线程所用到了,这就带来了线程安全问题。我们使用例子来演示一下这个问题,
public class ThreadTest { private int value=0; public int getValue() { return value++; } public static void main(String[] args) throws InterruptedException { final ThreadTest test = new ThreadTest(); //我们的本意可能是th1执行后value变为1 new Thread("th1") { @Override public void run() { System.out.println( test.getValue()+" "+super.getName()); } }.start(); //然后th2执行后value变为2 new Thread("th2") { @Override public void run() { System.out.println(test.getValue()+" "+super.getName()); } }.start(); } }
在上面,我们直接创建了两个线程,在第一个线程执行完之后value,在第二个线程执行完之后value变为2。但是结果与我们想的往往不一样,我测试了N多次,结果总是0 th2和1 th1。或者是反过来,再或者是00、11等等。这就是线程不安全的例子。如何去确保线程安全呢?方式其实有很多种,我们一点一点深入之后再去解决。
下一篇文章,我们将对线程的生命周期,常用API、以及源码(构造函数等)进行一个讲解,正式开始我们的多线程之旅。感谢支持。