一、并发与多线程
1.1 什么是进程、线程?线程和进程的区别?
进程
当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序。
Java VM 启动的时候会有一个进程java.exe.
- 该进程中至少一个线程负责java程序的执行。 而且这个线程运行的代码存在于main方法中。该线程称之为主线程。该线程称之为主线程。
- 其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
进程的三个特征:
独立性
独立存在的实体,每个进程都有自己独立私有的一块内存空间。动态性
程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。并发性
多个进程可在单处理器上并发执行。
并发性和并行性
- 并发
是指在同一时间点只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。 - 并行
指在同一时间点,有多条指令在多个处理器上同时执行。
线程
- 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
- 线程也被称作轻量级进程。线程在进程中是独立,并发的执行流。
线程和进程的区别
线程是进程的组成部分,一个进程可以有很多线程,每条线程并行执行不同的任务。
不同的进程使用不同的内存空间,而线程与父进程的其他线程共享父进程的所拥有的全部资源。
别把内存空间和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。线程拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源。线程的调度和管理由进程本身负责完成。操作系统对进程进行调度,管理和资源分配。
1.2多线程的优点
Java 是最先支持多线程的开发的语言之一,Java 从一开始就支持了多线程能力
1.2.1 资源利用率更好
想象一下,一个应用程序需要从本地文件系统中读取和处理文件的情景。比方说,从磁盘读取一个文件需要 5 秒,处理一个文件需要 2 秒。处理两个文件则需要:
5秒读取文件A
2秒处理文件A
5秒读取文件B
2秒处理文件B
---------------------
总共需要14秒
从磁盘中读取文件的时候,大部分的 CPU 时间用于等待磁盘去读取数据。在这段时间里,CPU 非常的空闲。它可以做一些别的事情。通过改变操作的顺序,就能够更好的使用 CPU 资源。看下面的顺序:
5秒读取文件A
5秒读取文件B + 2秒处理文件A
2秒处理文件B
---------------------
总共需要12秒
CPU 等待第一个文件被读取完。然后开始读取第二个文件。当第二文件在被读取的时候,CPU 会去处理第一个文件。记住,在等待磁盘读取文件的时候,CPU大 部分时间是空闲的。
总的说来,CPU 能够在等待 IO 的时候做一些其他的事情。这个不一定就是磁盘 IO。它也可以是网络的 IO,或者用户输入。通常情况下,网络和磁盘的 IO 比 CPU 和内存的 IO 慢的多。
1.2.2 程序设计更简单
在单线程应用程序中,如果你想编写程序手动处理上面所提到的读取和处理的顺序,你必须记录每个文件读取和处理的状态。相反,你可以启动两个线程,每个线程处理一个文件的读取和操作。线程会在等待磁盘读取文件的过程中被阻塞。在等待的时候,其他的线程能够使用 CPU 去处理已经读取完的文件。其结果就是,磁盘总是在繁忙地读取不同的文件到内存中。这会带来磁盘和 CPU 利用率的提升。而且每个线程只需要记录一个文件,因此这种方式也很容易编程实现。
1.2.3 程序响应更快
将一个单线程应用程序变成多线程应用程序的另一个常见的目的是实现一个响应更快的应用程序。设想一个服务器应用,它在某一个端口监听进来的请求。当一个请求到来时,它去处理这个请求,然后再返回去监听。
服务器的流程如下所述:
while(server is active){
listen for request
process request
}
如果一个请求需要占用大量的时间来处理,在这段时间内新的客户端就无法发送请求给服务端。只有服务器在监听的时候,请求才能被接收。另一种设计是,监听线程把请求传递给工作者线程(worker thread),然后立刻返回去监听。而工作者线程则能够处理这个请求并发送一个回复给客户端。这种设计如下所述:
while(server is active){
listen for request
hand request to worker thread
}
这种方式,服务端线程迅速地返回去监听。因此,更多的客户端能够发送请求给服务端。这个服务也变得响应更快。
桌面应用也是同样如此。如果你点击一个按钮开始运行一个耗时的任务,这个线程既要执行任务又要更新窗口和按钮,那么在任务执行的过程中,这个应用程序看起来好像没有反应一样。相反,任务可以传递给工作者线程(word thread)。当工作者线程在繁忙地处理任务的时候,窗口线程可以自由地响应其他用户的请求。当工作者线程完成任务的时候,它发送信号给窗口线程。窗口线程便可以更新应用程序窗口,并显示任务的结果。对用户而言,这种具有工作者线程设计的程序显得响应速度更快。
1.3 多线程的代价
从一个单线程的应用到一个多线程的应用并不仅仅带来好处,它也会有一些代价。不要仅仅为了使用多线程而使用多线程。而应该明确在使用多线程时能多来的好处比所付出的代价大的时候,才使用多线程。如果存在疑问,应该尝试测量一下应用程序的性能和响应能力,而不只是猜测。
1.3.1 设计更复杂
虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往非常复杂。不正确的线程同步产生的错误非常难以被发现,并且重现以修复。
1.3.2 上下文切换的开销
当 CPU 从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU 会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。
上下文切换并不廉价。如果没有必要,应该减少上下文切换的发生。
你可以通过维基百科阅读更多的关于上下文切换相关的内容:
http://en.wikipedia.org/wiki/Context_switch
1.3.3 增加资源消耗
线程在运行的时候需要从计算机里面得到一些资源。除了CPU,线程还需要一些内存来维持它本地的堆栈。它也需要占用操作系统中一些资源来管理线程。我们可以尝试编写一个程序,让它创建 100 个线程,这些线程什么事情都不做,只是在等待,然后看看这个程序在运行的时候占用了多少内存。
二 、多线程的实现方法
Java 中实现多线程有两种方法:继承 Thread 类、实现 Runnable 接口,在程序开发中只要是多线程,肯定永远以实现 Runnable 接口为主,因为实现 Runnable 接口相比继承 Thread 类有如下优势:
- 可以避免由于 Java 的单继承特性而带来的局限;
- 增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;
- 适合多个相同程序代码的线程区处理同一资源的情况。
下面以典型的买票程序(基本都是以这个为例子)为例,来说明二者的区别。
2.1 首先通过继承 Thread 类实现
- 代码如下:
class MyThread extends Thread {
private int ticket = 5;
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":ticket = " + ticket--);
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
new MyThread().start();
}
}
- 运行结果:
Thread-0:ticket = 5
Thread-0:ticket = 4
Thread-0:ticket = 3
Thread-0:ticket = 2
Thread-0:ticket = 1
Thread-1:ticket = 5
Thread-1:ticket = 4
Thread-1:ticket = 3
Thread-1:ticket = 2
Thread-1:ticket = 1
Thread-2:ticket = 5
Thread-2:ticket = 4
Thread-2:ticket = 3
Thread-2:ticket = 2
Thread-2:ticket = 1
从结果中可以看出,每个线程单独卖了 5 张票,即独立地完成了买票的任务,但实际应用中,比如火车站售票,需要多个线程去共同完成任务,在本例中,即多个线程共同买 5 张票。
2.2 下面是通过实现 Runnable 接口实现的多线程程序
- 代码如下:
class MyThread implements Runnable {
public int ticket = 5;
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":ticket = " + ticket--);
}
}
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
MyThread my = new MyThread();
new Thread(my).start();
new Thread(my).start();
new Thread(my).start();
}
}
- 运行结果
Thread-0:ticket = 5
Thread-0:ticket = 4
Thread-0:ticket = 3
Thread-1:ticket = 2
Thread-1:ticket = 1
从结果中可以看出,三个线程一共卖了 5 张票,即它们共同完成了买票的任务,实现了资源的共享。
thread 类中的start() 和 run() 方法有什么区别
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。
当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。 需要特别注意的是:不能对同一线程对象两次调用start()方法。
2.3 线程的生命周期
Java线程五种状态:
新建状态(New)
当线程对象创建后,即进入了新建状态。仅仅由java虚拟机分配内存,并初始化。如:Thread t = new MyThread();就绪状态(Runnable)
当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,java虚拟机创建方法调用栈和程序计数器,只是说明此线程已经做好了准备,随时等待CPU调度执行,此线程并 没有执行。运行状态(Running)
当CPU开始调度处于就绪状态的线程时,执行run()方法,此时线程才得以真正执行,即进入到运行状态。注:绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;阻塞状态(Blocked)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:等待阻塞
运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态,JVM会把该线程放入等待池中;同步阻塞
线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;其他阻塞
通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。死亡状态(Dead)
线程run()方法执行完了或者因异常退出了run()方法,该线程结束生命周期。 当主线程结束时,其他线程不受任何影响。
2.4 java控制线程方法
2.4.1 join线程
join方法用线程对象调用,如果在一个线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后再执行。
2.4.2 守护线程(Daemon Thread)
Java中有两类线程
User Thread(用户线程)、Daemon Thread(守护线程)
用户线程即运行在前台的线程,而守护线程是运行在后台的线程。
守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护这,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,VM就不会退出。 守护线程的特征:如果所有前台线程都死亡,后台线程会自动死亡。 守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。 虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。另外有几点需要注意:
- 1、setDaemon(true)必须在调用线程的start()方法之前设置,否则会跑出IllegalThreadStateException异常。
- 2、在守护线程中产生的新线程也是守护线程。
- 3、 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。
2.4.3 线程让步(yield )
- yield可以直接用Thread类调用,可以让当前正在执行的线程暂停,不会阻塞该线程,只是将该线程转入就绪状态。
- yield让出CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。