多线程
进程
进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。进程存在的标志是进程控制块,由操作系统创建和管理。
线程
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
进程与线程
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。
因而近年来推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标。
线程与进程的区别可以归纳为以下几点:
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
(上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。)
4)在多线程OS中,进程不是一个可执行的实体。
线程的状态
新建态,就绪态,运行态,阻塞态,消亡态
创建线程的方式
这里列出的方式可能不够全面,具体可以参考其他资料,但是就创建线程来讲个人认为本质上差不多,实现线程进行无返回值的任务处理必须实现Runnable接口(在run方法里实现任务),若要进行有返回值的认为则需要实现Callable接口(在call方法里实现任务)
1)实现Runnable接口,重写run方法,在run方法里描述所需执行的任务,然后在main方法里使用”对象名.run()”启动线程,事实上这里使用的线程为分配给main方法的那个线程,也就是说实际上依旧只有一个线程
public class Task implements Runnable{
@Override
public void run(){
for(int i = 0; i <= 10; i++){
System.out.println(i);
}
}
}
public class TestThread {
public static void main(String[] args){
Task task = new Task();
task.run();
}
}
2)实现Runnable接口,重写run方法,在run方法里描述所需执行的任务,然后将该类的对象作为新建Thread类时的参数,通过”线程对象名.start()”启动线程,这里和上面不一样的地方在于新建了另一个线程,此时有两个线程,在执行Task的run方法时原来的main方法并发执行
public class Task implements Runnable{
@Override
public void run(){
for(int i = 0; i <= 10; i++){
System.out.println(i);
}
}
}
public class TestThread {
public static void main(String[] args){
Thread taskThread = new Thread(new Task());
taskThread.start();
}
}
3)直接继承Thread类
public class Task extends Thread{
@Override
public void run(){
for(int i = 0; i <= 10; i++){
System.out.println(i);
}
}
public static void main(String[] args){
Task task = new Task();
task.start();
}
}
4)实现Runnable接口后使用线程池
public class Task implements Runnable{
@Override
public void run(){
for(int i = 0; i <= 10; i++){
System.out.println(i);
}
}
}
public class TestThread {
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<1;i++){
exec.execute(new Task());
}
}
}
5)实现Callable接口
public class Task implements Callable<Object>{
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 0; i <= 10; i++){
sum +=i;
System.out.println(i);
}
return sum;
}
}
public class TestThread {
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<1;i++){
Future<Object> future = exec.submit(new Task());
try {
System.out.println("返回值为:"+future.get().toString());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
多线程常用方法
将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器,如上面创建线程的第二种方式
常用方法
1)start方法
使用该方法启动线程并自动执行run方法
2)run方法
放置需要执行的任务代码
3)sleep方法
使线程休眠一定时间,让出cpu给其他线程(可能是低优先级也可能是高优先级)运行的机会,防止低优先级的线程得不到运行机会。需要注意的是使用sleep方法时该线程不释放对象锁。也就是说如果存在synchronized同步块,其他线程不能访问共享数据,因为该块被调用了sleep方法的线程占用。
public class Task implements Runnable{
private int priority;
public Task(int priority){
this.priority = priority;
}
@Override
public void run(){
Thread.currentThread().setPriority(priority);
for(int i = 0; i <= 10; i++){
System.out.println(i);
}
}
}
public class TestThread {
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=1;i<3;i++){
Task task = new Task(i);
exec.execute(task);
}
}
}
上面的输出为
0
1
2
3
4
5
0
1
2
3
4
5
6
7
8
9
10
6
7
8
9
10
由于后一个线程优先级高,所以它抢占cpu后会一直执行到任务结束才让出cpu
若加入sleep方法调用
public class Task implements Runnable{
private int priority;
public Task(int priority){
this.priority = priority;
}
@Override
public void run(){
Thread.currentThread().setPriority(priority);
for(int i = 0; i <= 10; i++){
System.out.println(i);
if(i%2 == 0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
则此时结果为
0
0
1
2
1
2
3
4
3
4
5
5
6
6
7
8
7
8
9
10
9
10
4)yield方法
暗示可以给其他同优先级线程让出cpu,不具有强制性,只是一种建议,具体让不让出cpu不确定,不会释放锁
public class Task implements Runnable{
private int priority;
public Task(int priority){
this.priority = priority;
}
@Override
public void run(){
Thread.currentThread().setPriority(priority);
for(int i = 0; i <= 10; i++){
System.out.println(i);
if(i%2 == 0){
Thread.yield();
}
}
}
}
5)join方法
使用t.join()加入一个线程t,使用了该语句的线程将被挂起直到线程t生命结束才恢复,不过可以为join方法设置一个超时参数,若超时则join方法总能返回
public class Task extends Thread{
private String name;
private int type;
private Task joinTask = null;
public Task(int type,String name){
this.type = type;
this.name = name;
start();
}
public Task(int type,String name,Task task){
this.type = type;
this.name = name;
joinTask = task;
start();
}
@Override
public void run() {
try {
if (type == 1) {
joinTask.join();
}
for (int i = 0; i <= 10; i++) {
System.out.println(name + " " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
System.out.println(name + "被中断");
return;
}
}
}
public class TestThread {
public static void main(String[] args){
Task task1 = new Task(0,"线程1");
Task task2 = new Task(1,"线程2",task1);
}
}
输出为
线程1 0
线程1 1
线程1 2
线程1 3
线程1 4
线程1 5
线程1 6
线程1 7
线程1 8
线程1 9
线程1 10
线程2 0
线程2 1
线程2 2
线程2 3
线程2 4
线程2 5
线程2 6
线程2 7
线程2 8
线程2 9
线程2 10
6)interrupt方法
public class TestThread {
public static void main(String[] args){
Task task1 = new Task(0,"线程1");
Task task2 = new Task(1,"线程2",task1);
task1.interrupt();
}
}
输出为
线程1 0
线程1被中断
线程2 0
线程2 1
线程2 2
线程2 3
线程2 4
线程2 5
线程2 6
线程2 7
线程2 8
线程2 9
线程2 10
7)wait方法和notify方法
wait方法:在其他线程调用对象的notify方法或者notifyAll方法前,或者超过指定的时间量前,导致当前线程等待。线程调用wait方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify或者notifyAll方法),这样它才能重新获得锁的拥有权和恢复执行。
notify方法:唤醒一个等待当前对象的锁的线程。notify方法应该是被拥有对象的锁的线程所调用。被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁。
notifyAll方法:唤醒所有等待锁的线程。
上面的三个方法要求在调用时线程已经获得了对象的锁,因此对这两个方法的调用需要放在synchronized方法或synchronized块中。
8)后台线程
调用setDaemon方法(参数为true时)将调用该方法的线程设置为后台线程,必须在线程启动前调用才有效。通过调用isDaemon方法判断是否为后台线程。后台线程创建的任何线程会自动被设置为后台线程,当所有非后台线程结束时,所有后台线程会被杀死同时程序终止。
9)设置优先级
线程优先级范围为1~10,调用setPriority(int priorityLevel)方法设置线程优先级,调用getPriority()方法查看优先级
同步与互斥
同步
线程间的一种通信机制,当一个线程完成对临界区的操作后通知其他线程它已经做完任务准备释放锁标志,实现同步的方法是上面说过的wait()、notify()、notifyAll() 3个方法
互斥
多个线程在竞争共享资源时,可能会引起冲突,比如当一个线程正在读取共享资源的数据时,另一个线程正在往该资源写数据,此时可能发生问题。为了防止该冲突发生,需要引入互斥机制,即使得线程不能同时访问共享资源。说白了,就是使线程排队等待获取共享资源的使用权。使用互斥的时刻是访问可变的共享资源时,或者说是临界区。实现互斥的方法是使用synchronized关键字,可以通过获取对象的锁实现对方法或代码块以及静态方法所在类的所有对象的互斥访问。
锁(也称为监视器)
为了实现互斥访问,需要引入锁的概念。以排队上卫生间为例子,为了实现安全的上卫生间,进入时必须要把该区域锁住才行,而出去时则需要解开锁以便后面的对象能使用该区域。而这个方式便实现了对某个区域的互斥访问。
所有对象均有单一的锁,这也意味着当该对象的synchronized方法被某个任务调用时,该对象上的其他synchronized方法将只能在前一个方法调用完毕并释放锁后才能被其他任务调用,同时其他试图调用该对象的synchronized方法的任务都将被阻塞直至该对象的锁被释放。对某个特定对象来说,其内的所有synchronized方法共用同一把锁。需要注意的是同一个任务在获取锁后可以多次获得该对象的锁,对象被加锁的次数递增,而每当该任务离开一个synchronized方法时对象被加锁计数递减,当计数为0时表示该任务释放了该对象的锁,此时其他任务可以调用该对象的synchronized方法。
原子性与原子操作
原子性指操作不可分,一步到位。
原子操作指不可中断的一个或一系列操作
相关测试如下
public class TestObject {
private int sum = 0;
public void changeSum(String name){
System.out.println(name+": "+sum);
sum++;
System.out.println(name+": "+sum);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum--;
System.out.println(name+": "+sum);
}
public void printSum(String name){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+": result = "+sum);
}
}
public class Task extends Thread{
private String name;
private TestObject testObject;
public Task(String name,TestObject testObject){
this.name = name;
this.testObject = testObject;
start();
}
@Override
public void run() {
testObject.changeSum(name);
testObject.printSum(name);
}
}
public class TestThread {
public static void main(String[] args){
TestObject testObject = new TestObject();
Task task1 = new Task("线程1",testObject);
Task task2 = new Task("线程2",testObject);
}
}
其中容易看出i++不是原子操作的一条输出为
线程1: 0
线程2: 0
线程2: 2
线程1: 1
线程1: 1
线程2: 1
线程1: result = 1
线程2: result = 1
事实上,我们期待的输出为
线程1: 0
线程1: 1
线程1: 0
线程1: result = 0
线程2: 0
线程2: 1
线程2: 0
线程2: result = 0
结果都应该为0,而出现上面的不正常的输出的一部分原因是两个线程同时访问了同一个临界区而且在执行该区域的非原子操作时线程睡眠,另一个线程在前一个线程未执行完sum++的基础上继续执行导致结果出错。为防止出现像这样多个线程同时访问临界区的情况,我们改下代码,实现互斥访问。
public class TestObject {
private int sum = 0;
public synchronized void changeSum(String name){
System.out.println(name+": "+sum);
sum++;
System.out.println(name+": "+sum);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum--;
System.out.println(name+": "+sum);
}
public void printSum(String name){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+": result = "+sum);
}
}
此时输出如下
线程1: 0
线程1: 1
线程1: 0
线程2: 0
线程2: 1
线程2: 0
线程1: result = 0
线程2: result = 0
这里为changeSum方法加上synchronized关键字实现了该对象的加锁和释放锁操作,从前面的sleep介绍中我们知道调用sleep方法并不会释放锁,所以当线程1获取锁后直到其运行完changeSum方法才释放锁,这时线程2才能进入changeSum方法。接下来试下wait方法和notify方法。
public class TestObject {
private int sum = 0;
public synchronized void changeSum(String name){
if (name.equals("线程1")) {
try {
this.wait();//使线程1阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name + ": " + sum);
sum++;
System.out.println(name + ": " + sum);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum--;
System.out.println(name+": "+sum);
this.notify();//唤醒被阻塞的线程1
}
public void printSum(String name){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+": result = "+sum);
}
}
此时输出为
线程2: 0
线程2: 1
线程2: 0
线程1: 0
线程1: 1
线程1: 0
线程2: result = 0
线程1: result = 0
Lock对象
实现显式的互斥机制,Lock对象必须被显式的创建、锁定以及释放。
public class TestObject {
private int sum = 0;
private Lock lock = new ReentrantLock();
public void changeSum(String name){
lock.lock();//锁定
System.out.println(name + ": " + sum);
sum++;
System.out.println(name + ": " + sum);
try {
Thread.yield();
sum--;
System.out.println(name+": "+sum);
}finally{
lock.unlock();//释放锁
}
}
public void printSum(String name){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+": result = "+sum);
}
}
死锁
两个线程相互等待被对方锁定的资源导致双方一直处于等待状态,无法继续往下运行
了解有限,不足之处还望指出,关于多线程的更深入的理解以后有机会再补充。