一、并发与并行
1.并行通常指在同一时刻有多个代码在处理器上执行,这往往需要很多个处理器。
2.并发通常是指在单个处理器上,同一时刻只能执行一个代码,但在同一段时间内,多个代码交替执行。
在Java中各个代码段并发执行,即各个线程之间并发执行。
二、线程
1.说明
线程化是让多个活动共存于一个进程的方式。线程也称作轻量级进程,就像进程一样线程在程序中是独立、并发的执行的。每个线程都有自己的堆栈、程序计数器和局部变量。但进程和线程在相互之间的分隔程度上,线程要比进程小的多。各线程之间共享内存、文件句柄及每个线程所产生的状态。
一个进程中的多个线程看似同时执行,但相互之间并不同步。它们共享相同的内存地址空间,即可以访问相同的变量和对象,且从同一个堆中分配对象。
2.线程的优点
- 使UI响应更快
- 利用多处理器系统增加系统吞吐量和性能
- 简化建模
- 执行异步或后台处理
下面对这些优点进行简单的说明:
(1)使UI响应更快
通过对不同的事件分配与之相符合的线程,使之可以在处理其他事件时也可以处理UI事件,以防止界面停滞给用户带来的不舒适感。
(2)利用多处理器系统增加系统吞吐量和性能
将多个线程同时放在多个处理器上处理,增加系统的吞吐量和性能。
(3)简化建模
在某些情况下(当程序有多个独立的实体),使用线程可以使程序编写和维护起来更加方便。比如一个需要多个部分一起工作的程序,则就可以把每个部分都分配一个线程,在一段时间内共同工作。
(4)异步或后台处理
一个程序往往需要进行异步或者在运行时做一些后台处理,这时候就可以单独提出一个线程来完成这些工作,也不影响程序的正常运行。注:由于多个线程共享相同的变量,所以在编程时注意线程间的相互影响。
1 | // 一个线程执行,一个线程检测,当到达监测点则停止主线程 |
2.1 程序、进程、多任务与线程
- 1.程序(Program)
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态代码。 - 2.进程(Process)
进程是程序的一次执行过程,是系统运行程序的基本单位。
进程的特点:
(1)进程是动态的。
(2)进程是独立的。 3.多任务(Multi task)
多任务是指在一个系统中可以同时运行多个进程,即有多个独立运行的任务,每一个任务对应一个进程。每个进程都有一段专用的内存区域,即使是多次启动同一段程序产生不同的进程也是一样。所谓同时运行的进程,其实是指由操作系统将系统资源分配给各个进程,每个进程在CPU上交替运行。每个进程占有不同的内存空间,内存消耗很大,这使系统在不同的程序之间切换时开销很大,进程之间的通信速度很慢。4.线程(Thread)
(1)由于在进程之间协同工作进行信息交互会对系统产生很大的负担,为了减少不必要的系统负担,所以才产生了线程的概念。
(2)运行一个进程时,程序内部的代码是按照先后执行的,由于线程是比进程更小的单位,所以线程的存在使得进程间的代码块更加细分,同时可以相互协调工作,使得提高进程的运行效率。
(3)一个进程在其执行过程中可以产生多个线程,形成多条路径,但与进程相互独立不同的是,同类的多个线程共享一块内存空间和一组系统资源,所以系统在用线程工作时负担要比进程小的多。
2.2 线程的状态与生命周期
Java程序都有一个默认的主线程,对于应用程序来说就是主线程是main()方法执行的线程,对于小程序来说,其主线程进行浏览器的加载并执行Java小程序。实现多线程就是在主线程中创建新的线程。
一个线程的完整生命周期通常要经历五种状态:
(1)新建状态(newborn)
当一个Thread类或其子类的对象被声明并创建,但还未被执行的时候,就处于一种特殊的新建状态。此时,线程对象已经被分配了内存空间和其他资源,并已经被初始化,但没有被调度。
(2)就绪状态(runnable)
就绪状态即可运行状态,处于新建状态的线程被启动后,将进入线程队列排队等待CPU时间片,此时就是就绪状态。一旦轮到它来想用CPU资源时,就可以脱离创建它的主线程独立开始自己的生命周期。另外,原来处于阻塞状态的线程被解除阻塞后也将进去就绪状态。
(3)运行状态(running)
当就绪状态的线程被调度获得CPU资源是,便进入运行状态。线程在运行时都是从run()方法开始的。
一个线程将在以下情况下让出CPU资源:
- 线程运行完毕
- 有比当前线程优先级更高的线程处于就绪状态
- 线程主动休眠一段时间
- 线程在等待某一资源
(4)阻塞状态(blocked)
在某些特殊的情况下,线程让出CPU的控制权并暂时中止自己的执行,即为阻塞状态。
下面是使线程进入阻塞状态的方法:
- 调用sleep()或者yield()方法
- 为了等待一个条件变量,线程调用wait()方法
- 当前线程与另一个线程join()在一起
(5)消亡状态(dead)
处于消亡状态的线程不具有继续运行的能力。
导致线程消亡的原因有两个:
- 线程运行完毕
- 当进程因故停止运行,该进程中的所有线程将被强行终止。
线程处于消亡状态并没有线程对象的引用时,垃圾回收器会从内存中删除该线程对象。
2.3 线程的调度与优先级
1.调度
调度就是指在各个线程之间分配CPU资源。多个线程的并发执行实际上是通过一个调度来进行的。
线程的调度有两种模型:
- (1)分时模型:在分时模型中,CPU资源是按照时间片分配的,获得CPU资源的线程只能在指定时间片内执行,一旦时间片使用完毕,就必须把CPU让给另一个处于就绪状态的线程。
- (2)抢占模型:当前活动的线程一旦获得执行权,将一直执行下去,直到执行完毕或由于某种原因主动放弃执行权。比如一个优先级低的线程正在运行,但是当一个优先级高的线程在就绪队列,它就会优先执行,为了防止优先级低的线程无法获取CPU执行,优先级高的会时不时的进入睡眠状态让低优先级的执行,相互协调。
2.优先级
优先级决定了在多线程中线程的调用顺序,优先级高的线程可以在一段时间内获得比优先级低的线程更多的执行时间。
Java语言中线程的优先级从低到高分1~10级。Thread类有三个关于线程优先级的静态变量,MIN_PRIORITY表示最小优先级,通常是1。MAX_PRIORITY表示最高优先级,通常为10;NORM_PRIORITY表示普通优先级,默认值为5。 - 新建线程指定优先级的原则:
(1)新建线程将继承创建它的父线程的优先级,父线程是指执行创建新建线程对象语句所在的线程。
(2)一般情况下,主线程具有普通优先级。 - 线程优先级的改变
调用线程对象的setPriority()方法。三、Thread线程类与Runnable接口
Java语言实现多线程的方法有两种:(1)继承java.lang包中的Thread类。(2)用户在定义自己的类中实现Runnable接口。3.1 利用Thread类的子类来创建线程
继承Thread类是实现线程的一种方法,通过继承它,我们可以创建、执行、终止、或者查看一个线程的状态。
而要在一个Thread的子类里激活线程,需要两个条件:
(1)必须继承Thread类
(2)线程所执行的代码必须写在run()方法中
注意:线程执行时是从run()方法开始执行的,run()方法是起点。1
2
3
4
5
6
7
8
9class 类名 extends Thread{
类中成员变量;
类中成员方法;
修饰符 run(){ //覆盖父类Thread类里的run()方法
线程代码
}
}
说明:run()方法规定了线程要执行的任务,但一般不是直接调用run()方法,而是通过线程的start()方法。
- Thread类的构造方法及常用方法
- 举个例子:用Thread创建一个线程
1 | public class CreateNewThread { |
说明:从这个程序中可以看出,Thread1和Thread2都是随机执行的,谁先抢到CPU资源谁就先执行,由于设置了sleep()所以才会有1和2随机交替执行。如果没有sleep,其中任意1或2不论谁先抢到资源都会执行完自己的,然后才轮到后一个。
至于为什么main语句在start后面还先输出,这是由于main也是一个线程,它也是要和1和2抢资源的,谁抢到谁先执行,通常情况下会执行main是由于它不用经过线程激活的过程。
3.2 利用Runnable接口来创建线程
由于Java不支持多重继承,所以如果一个类已经有父类了,这时就无法再继承Thread类了,这种情况下就需要Runnable接口。这种创建线程的方法更加具有灵活性,也是最常用的。
Runnable接口定义在java.lang包中,其中只提供了一个抽象方法run()的声明。用户可以实现Runnable接口,定义自己的run()方法,然后将这个类所创建的对象将作为参数传给Thread类的构造方法,创建一个Thread类的对象,这个对象将使用用户在实现Runnable接口时实现的run方法作为其执行代码,还可以使用用户自己定义的类的对象中的数据作为其操作的数据。
注意:用Runnable接口实现并定义run()方法,由于Runnable接口并没有任何对线程的支持,所以还需要创建Thread类的实例。
Runnable好处:(1)间接地解决多重继承。(2)有利于多个线程处理统一资源。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37public class ThreadOfRunnable {
public static void main(String[] args) {
ThreadRunnable a=new ThreadRunnable("Thread1");
ThreadRunnable b=new ThreadRunnable("Thread2");
Thread t1=new Thread(a);//产生Thread类的对象
Thread t2=new Thread(b);
t1.start();
t2.start();
}
}
class ThreadRunnable implements Runnable{
private String who;
public ThreadRunnable(String who) {
this.who=who;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<5;i++) {
try {
Thread.sleep((int)(1000*Math.random()));//随机睡眠0~1秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(who+"在跑");
}
}
}
1 | public class ThreadOfRunnable { |
说明:由于在激活t1后,有t1.join()所以程序流会在执行完线程t1后再执行下面。下面依此类推。此时由于有join所以main就在最后输出了。
注:Thread类和Runnable接口各自的特点
- 直接继承Thread类:编写简单,可以直接操纵线程。缺点是继承Thread类就不能再继承其他类。
- Runnable接口:可以将Thread类与所需要处理的任务的类分开,形成清晰的模型,还可以从其他类继承,实现多重继承。
直接使用Thread类,在类中this即指当前线程,若使用Runnable接口,要在类中获得当前线程,必须使用Thread.currentThread()方法。3.3 线程间的数据共享
当多个线程的执行代码来自同一个类的run()方法时,则称它们共享相同的代码;当共享访问相同的对象时,则称它们共享相同的数据。 - 1.Thread类实现共享数据
1 | public class Sale { |
- 2.Runnable接口实现共享数据
1 | public class Sale { |
上面两个例子都是通过多个线程共享同一个对象的run方法实现的。
四、多线程的同步控制
一般独立的(异步的)线程不必考虑其他线程的状态或行为,即没有共享的数据。当有共享数据的时候,一个线程要操作共享数据的话,就需要使之成为一个“原子操作”,即没有完成相关操作之前,不允许其他线程打断。否则会破坏数据的完整性。
此时就应该考虑到多线程的同步控制,即在多个线程共享数据的同时,保证数据的完整性。即在同一时刻只有一个线程在执行操作。
在java中,为了保证线程互斥,使一个线程成为原子操作,用关键字synchronized来标识同步资源,这里的同步资源可以是一种类型的数据(对象),也可以是一个方法,还可以是一段代码。
使用格式:
- 1.同步语句
1 | Synchronized(对象) |
对象是多个线程共同操作的公共变量,即需要锁定的临界资源,它将被互斥使用。
- 2.同步方法
1 | public synchronized 返回类型 方法名() |
synchronized的功能是:首先判断对象或方法的互斥锁是否在,若在就获得互斥锁,然后就可以执行紧随其后的临界代码段或方法体,如果对象或方法的互斥锁不在(以被其他线程拿走),就进入等待状态,直到获得互斥锁。
注意:synchronized所限定的代码段执行完,就会自动释放互斥锁
- 列:从银行取款
1 | public class ThreadTestOfBank { |
take()同步方法,限制同一时刻只有一个线程调用。当一个线程释放了互斥锁,其他线程才可以调用该方法。防止资源的不完整性。即使有sleep也限制同一个线程调用。
注意,其中Thread.sleep()是睡眠任意一个调用他的线程。但这。里给不了其他线程,因为有synchronized。
五、线程间的通信
为了更有效地协调不同线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”来解决线程间的同步问题。
java.lang.Object类中用于线程通信的主要方法
注意:1.对于一个线程,若基于对象x调用wait()或notify()方法,该线程必须已经获得对象x的互斥锁。换句话说,wait()和notify()只能在同步代码块里调用。
2.wait()与sleep()的区别:wait()方法在放弃CPU资源的同时交出了资源的控制权,而sleep()方法则无法做到这一点。模拟一个售票系统
1 | public class TicketsSell { |