多线程基础
基本概念
进程
所谓进程就是运行在操作系统的一个任务,进程是计算机任务调度的一个单位,操作系统在启动一个程序的时候,会为其创建一个进程,JVM就是一个进程。进程与进程之间是相互隔离的,每个进程都有独立的内存空间。
计算机实现并发的原理是:CPU分时间片,交替执行,宏观并行,微观串行。同理,在进程的基础上分出更小的任务调度单元就是线程,我们所谓的多线程就是一个进程并发多个线程。
线程
在上面我们提到,一个进程可以并发出多个线程,而线程就是最小的任务执行单元,具体来说,一个程序顺序执行的流程就是一个线程,我们常见的main就是一个线程(主线程)。
线程的组成
想要拥有一个线程,有这样的一些不可或缺的部分,主要有:CPU时间片,数据存储空间,代码。
CPU时间片都是有操作系统进行分配的,数据存储空间就是我们常说的堆空间和栈空间,在线程之间,堆空间是多线程共享的,栈空间是互相独立的,这样做的好处不仅在于方便,也减少了很多资源的浪费。代码就不做过多解释了,没有代码搞个毛的多线程。
小编是一个有着5年工作经验的java程序员,对于java,自己有做资料的整合,一个完整学习java的路线,学习资料和工具,相信这里有很多学习java的小伙伴,我创立了一个2000人学习扣群,479121291。每晚都有java的直播课程。无论是初级还是进阶的小伙伴小编我都欢迎!
线程的创建和启动
传统创建线程有两种方式
1.继承Thread类,覆盖run方法
2.实现Runnable接口,覆盖run方法
Runnable并不是线程对象,而是一个任务对象。那么Runnable和Thread有什么样的关系呢?
通过查阅API,我们发现创建一个线程除了使用Thread的无参构造方法以外有一个有参构造方法是这样 :Thread(Runnable target),通过这个方法会分配一个新的Thread 对象。
其中的参数是一个类型为Runnable的target属性。
Runnable接口最大的作用就是为非Thread子类的类提供了一种实现线程的方式,只需要实现Runnable接口就可以借助Thread创建一个线程;另一方面,如果只想重写run方法,不想得到其他的Thread的方法,实现Runnable是一个好的选择。
JDK1.5
线程池
ExecutorService(线程池 interface)
//通过工具类中的方法能够新建一个线程池,用ExecutorService接受
ExecutorService es = Executors.newFixedThreadPool(2);
Callable对象
类似于Runnable(描述任务的interface)。
//创建一个Callable的实现类
Callable<Integer> task1 = new Callable<Integer>(){
public Integer call() throws Exception{
int result = 0;
for(int i=2;i<=100;i+=2){
result += i;
Thread.sleep;
}
return result;
}
}
//用Future对象接收fask1的返回值 将任务提交给线程池
Future<Integer> f = es.submit(task1);
//通过get方法获取Future中的值 在这个时候主线程主动的调取get 如果分支线程还没有结束,主线程会在这里阻塞
int result = f1.get();
//关闭线程池
es.shutdown();
从以上这段代码我们可以看到很多不一样的地方,首先在Callable对象中是可以抛出异常的,其次有返回值,在这个基础上也就引出了一个新的问题,如果接收该线程的对象?JDK1.5中也给出了解决的方法是Future对象.
启动线程
在这里我们需要明白,上面两种方式并不会让我们得到真正的线程,只是得到了线程对象,只有启动线程,才算得到了真正的线程。
通过执行start()方法能够启动一个线程,但是启动线程并不是立即执行,成功启动的线程会处于就绪状态,什么时候执行需要等到拿到时间片之后。
线程的分类
用户线程和守护(Daemon)线程。
守护线程:守护线程会一直运行,直到其他非守护线程都结束的时候,才会结束。有一个典型的守护线程就是:垃圾回收线程,和虚拟机共存亡,直到虚拟机中没有任何线程的时候虚拟机关闭的时候才会终止,简单说就是虚拟机在,它就在,虚拟机亡便亡。
线程的状态
Java
上面我们提到过,一个线程在启动之后不会立马执行,而是处于就绪状态(Ready),就绪状态就是线程的状态的一种,处于这种状态的线程意味着一切准备就绪, 需要等待系统分配到时间片。为什么没有立马运行呢,因为同一时间只有一个线程能够拿到时间片运行,新线程启动的时候让它启动的线程(主线程)正在运行,只有等主线程结束,它才有机会拿到时间片运行。
线程的状态:初始状态(New),就绪状态(Ready),运行状态(Running)(特别说明:在语法的定义中,就绪状态和运行状态是一个状态Runable),等待状态(Waitering),终止状态(Terminated)
RUNNABLE),等待状态(Waitering),终止状态(Terminated)
初始状态(New)
线程对象被创建出来,便是初始状态,这时候线程对象只是一个普通的对象,并不是一个线程。
Runable
就绪状态(Ready):执行start方法之后,进入就绪状态,等待被分配到时间片。
运行状态(Running):拿到CPU的线程开始执行。处于运行时间的线程并不是永久的持有CPU直到运行结束,很可能没有执行完毕时间片到期,就被收回CPU的使用权了,之后将会处于等待状态。
等待状态(Waiting)
等待状态分为有限期等待和无限期等待,所谓有限期等待是线程使用sleep方法主动进入休眠,有一定的时间限制,时间到期就重新进入就绪状态,再次等待被CPU选中。
而无限期等待就有些不同了,无限期并不是指永远的等待下去,而是指没有时间限制,可能等待一秒也可能很多秒。至于进入等待的原因也不尽相同,可能是因为CPU时间片到期,也可能是因为一个比较耗时的操作(数据库),或者主动的调用join方法。
wait和sleep的区别
waitsleep
wait()方法是Object类里的方法sleep()是Thread类的static(静态)的方法
wait()睡眠时,释放对象锁sleep()睡眠时,保持对象锁,仍然占有该锁
常用于线程间通信常用于暂停执行
wait和notify/notifyAll是成对出现的, 必须在synchronize块中被调用
阻塞状态(Blocked)
在我看来,阻塞状态实际上是一种比较特殊的等待状态,处于其他等待状态的线程是在等着别的线程执行结束,等着拿CPU的使用权;而处于阻塞状态的线程等待的不仅仅是CPU的使用权,主要是锁标记,没有拿到锁标记,即便是CPU有空也没有办法执行。(关于锁见下节:线程同步)
等待和阻塞的区别
等待阻塞
已经拿到锁对象,或者说不存在拿不到执行不了的情况等待拿到锁对象
等待被唤醒等待拿到锁对象
终止线程(Terminated)
已经终止的线程会处于该种状态。
总结
总体上来说,作为一个线程挺倒霉的,首先,不会知道自己什么时候被选中;其次在执行过程中随时可能被打断让出CPU,最后碰到数据库等耗时的操作也要让出CPU去等待,并且就算数据准备好了, 仍然需要等着被挑选。