在编程的时候,经常会遇到一个问题,我该什么时候用多线程?为什么用多线程?
在此讲讲自己的理解,同时也参考了网上诸多资料。。。
一、概念
为了直观对比单线程与多线程,举一个简单例子:
假设我要做3件事:烧开水,举杠铃100下,洗衣服
烧开水这件事,我要做的有:准备烧开水(1分钟)、等开水烧开(8分钟)、关掉烧水机(1分钟)
举杠铃这件事,我要做的有:举杠铃100个(10分钟)
洗衣服这件事,我要做的有:准备洗衣服(1分钟)、等待衣服洗好(5分钟)、关掉洗衣机(1分钟)
单线程情况下,我是执行完一件事情再继续另外一件,
那么我需要的时间为:1+8+1+10+1+5+1=27分钟
如果用多线程的做法,在最理想的情况下:
线程1 准备烧开水1 sleep 1 sleep 5 sleep 1 sleep 2 关开水 1分钟 exit
线程2 sleep 1 sleep 1 举杠铃50 5分钟 sleep 1 举杠铃20 2分钟 sleep1 举杠铃30下 3分钟
线程3 sleep 1 准备洗衣服1 分钟 sleep 5 关洗衣机1分钟 exit
最后用了14分钟。但这是最理想的情况下,实际上运行的时候,线程不一定会这样切换。但是对比于单线程,相对更好的利用了cpu资源。
然后再讲讲并发并行的概念,套用Erlang 之父 Joe Armstrong 的一幅讲解图
并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机。
并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。其中两种并发关系分别是同步和互斥
并行:在单处理器中多道程序设计系统中,进程被交替执行,表现出一种并发的外部特种;在多处理器系统中,进程不仅可以交替执行,而且可以重叠执行。在多处理器上的程序才可实现并行处理。从而可知,并行是针对多处理器而言的。并行是同时发生的多个并发事件,具有并发的含义,但并发不一定并行,也亦是说并发事件之间不一定要同一时刻发生。
以上概念,并发对多线程更重要,多线程是程序设计的逻辑层概念,它是进程中并发运行的一段代码。多线程可以实现线程间的切换执行。
换句话解释,比如现在有进程A运行需要10s,结束后运行进程B需要10s。在实现多线程机制以后,进程A细化成10个线程,每个线程只需1s,然后多线程会在线程直接切换执行,当然进程B也是同理。所以多线程是同步并发的。
这里说一下同步的意思:进程之间的关系不是相互排斥临界资源的关系,而是相互依赖的关系。进一步的说明:就是前一个进程的输出作为后一个进程的输入,当第一个进程没有输出时第二个进程必须等待。具有同步关系的一组并发进程相互发送的信息称为消息或事件。
对于上面的多线程同步并发,以代码形式展示
class ThreadTest extends Thread{
private String name;
public ThreadTest(String name) {
this.name=name;
}
public void run() {
for (int i = 0; i <5; i++) {
System.out.println(name+"运行了"+i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
ThreadTest thread1=new ThreadTest("小明");
ThreadTest thread2=new ThreadTest("老师");
thread1.start();
thread2.start();
}
}
sleep方法目的是不让当前线程独霸该进程所获取的cpu资源。
输出:
小明运行了0
老师运行了0
小明运行了1
小明运行了2
小明运行了3
小明运行了4
老师运行了1
老师运行了2
老师运行了3
老师运行了4
再运行一次:
小明运行了0
小明运行了1
小明运行了2
小明运行了3
小明运行了4
老师运行了0
老师运行了1
老师运行了2
老师运行了3
老师运行了4
从结果中很直观的可以看出,多线程的切换是随机的。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
至于多线程的优点,自己感悟并不深,各位还是参考网上的论点,上面多核cpu的算是一点,可以更加充分发挥多核cpu的优势,提高cpu利用率等等;还有可以防止阻塞。至于多线程切换线程的调度策略。。太深奥,以后再看看
二、生命周期
简单讨论一下线程的生命周期
引用网上一张经典图示
java线程主要有五种基本状态
新建状态(new):当用new操作符创建一个新的线程对象时,该线程进入新建状态。处于此状态的线程只是一个空的线程对象,系统不为它分配资源
就绪状态(Runnable):当调用线程对象的start方法,线程即进入就绪状态,该状态的线程位于可运行线程池中,变得可运行,等待CPU调度执行。但并不是说调用了start方法以后,线程就会立即执行。
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行的线程执行wait()方法,导致该线程阻塞,该线程不可继续执行,并且该对象上的锁被释放。(想再次执行,等待wait时间到,或者notify()、notifyAll()唤醒线程,如上图所示)
2.同步阻塞:线程在获取synchronized同步锁,若该同步锁被其它线程所占用,它会进入同步阻塞状态
3.其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
三、java多线程的创建和启动
基本线程类
Thread类、Runnable接口
继承Thread类写的多线程代码,在上面已经提及,所以略过不提
实现Runnable接口
class RunnableTest implements Runnable{
@Override
public void run() {
System.out.println("子线程名字:"+Thread.currentThread().getName()+"\n子线程ID:"+Thread.currentThread().getId());
}
}
public class Test{
public static void main(String[] args) {
RunnableTest test=new RunnableTest();
Thread thread=new Thread(test);
thread.start();
}
}
RunnableTest类通过实现Runnable接口,并重写该接口的run()方法,然后我们创建了一个Thread对象,交给他们去执行,Thread构造对象的方法中有Thread(Runnable target),这样我们可以把RunnableTest对象引入,然后再通过Thread对象启动start()方法。
做个测试,如果RunnableTest不调用Thread类的方法,直接用自己重写后的run()方法看看能不能创建线程
public static void main(String[] args) {
RunnableTest test=new RunnableTest();
RunnableTest test1=new RunnableTest();
test.run();
test1.run();
System.out.println("===========");
new Thread(test).start();
new Thread(test1).start();
}
结果是:
子线程名字:main
子线程ID:1
子线程名字:main
子线程ID:1
===========
子线程名字:Thread-0
子线程ID:8
子线程名字:Thread-1
子线程ID:9
很明显,两个RunnableTest的对象,即便调用了run()方法也没有创建线程(之所以有线程,是因为main方法也是一个线程,所以线程名字为main)
后面两个就很明显创建了线程了
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
Thread与Runnable的优劣:
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
参考链接:
https://zhidao.baidu.com/question/395363469.html
http://blog.csdn.net/cqkxboy168/article/details/9026205/-----并发 并行 同步 异步 多线程的区别
https://www.cnblogs.com/lwbqqyumidi/p/3804883.html