1. 进程和线程简介
1.1 进程
- 从狭义方面讲,进程是正在运行的程序的实例;从广义方面讲,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,是基本的分配单元。
- 进程使程序(任务)的执行过程,即动态性,它持有资源(共享内存,共享文件)和线程,即载体
1.2 线程
- 线程,有时被称为轻量级进程,是程序执行流的最小单元,一个进程可以拥有多个线程。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
- 多个线程之间需要相互通信才能工作,即线程的交互(互斥和同步)。
2. Java中的线程
2.1 Java中创建新线程的方法
Java对线程的支持,主要使体现在一个类Thread和一个接口Runnable,他们均继承自java.lang,他们有一个共同的方法:run()。
创建新执行线程有两种方法:
一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法,接下来可以分配并启动该子类的实例,如创建一个机器人行走的线程:
class WalkThread extends Thread { long speed; PrimeThread(long speed) { this.speed = speed; } public void run() { // The robot walk . . . } }
然后下列代码会启动一个线程:
WalkThread w = new WalkThread(20); w.start();
- 可以在run方法中使用Tread.sleep(long millis[,long naros])使线程休眠
- 可以通过线程的setName(String name)方法给线程实例设置名称
- 在线程中可以通过getName()方法获取它的名字
另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动:
class WalkRun implements Runnable{ long speed; WalkRun(long speed){ this.speed = speed; } public void run(){ //The robot walk . . . } }
然后,创建并启动一个线程:
WalkRun w = new WalkRun(20); new Thread(w).start();
- Runnable中没有getName()方法,可以通过Thread.currentThread().getName()得到当前进程的名称
- 可以通过new Thread(Runnable target,String name)设置线程名称
备注:计算机处理器的每个核在同一时间只能运行一个线程,一个线程休眠之后,另一个线程才会运行。
2.2 Java中线程常用方法介绍
代码示例
sleep方法可以使其所在线程休眠参数时间,该线程将停在使用sleep方法的代码位置,等待一定时间后继续执行后续代码
//线程休眠1000毫秒 Thread.sleep(1000);
join方法使调用它的线程执行,而其它进程等待该线程执行完毕后再执行或者最长等待参数时间后继续执行:
//其它进程等待RobotWalk执行完毕 RobotWalk.join(); //其他进程最长等待1000毫秒后,继续执行 RobotWalk.join(1000);
注意sleep和join方法都会抛出异常
3. Java中线程的正确停止
- 不要使用stop方法,它是java1.0提供的一个停止线程的一个方法,但是它有很多缺点,会使线程戛然而止,我们甚至无法知道线程完成了哪些工作,有哪些还没做,也完全没有机会做一些清理的工作。
应使用退出标志:
public RobotWalk extends Thread{ //退出标志 volatile boolean keepRunning = true; public void run(){ while(keepRunning){ //执行代码 …… …… } } }
在其它程序中,将keepRunning设置为false,当执行完while循环中的代码后,将跳出循环,结束线程中代码的执行。
volatile关键字:保证了线程可以正确的读取其它线程写入的值,即可以使其它线程来修改volatile所修饰的变量的值。- 不要使用interrupt方法:
当线程由于调用了某些方法(比如sleep方法)而进入了一种阻塞状态之时,此时如果该线程再被调用了interrupt方法,它会产生两个结果:
- 中断状态被清除,isInterrupted方法就不能返回一个是否被中断的正确状态
- sleep等阻塞状态的方法会抛出InterruptedException异常
4 线程的交互
4.1 争用条件Race Condition
当多个线程同时共享访问同一个数据(内存区域)时,每个线程都尝试操作该数据,从而导致数据被破坏(corrupted),这种现象称为争用条件
4.2 互斥与同步
为了解决争用条件的问题,引入了互斥和同步的概念。
- 临界资源:对于某些资源来说,其在同一时间只能被一个线程所占用。这些一次只能被一个线程所占用的资源就是临界资源,如CPU,打印机等都是临界资源。
- 互斥:多个并发执行的线程之间由于竞争临界资源会存在相互制约关系,当某个线程占用临界资源时,其它线程只有等待该线程释放资源后才可以占用资源运行。
- 同步:进程之间的直接制约关系,是为完成某种任务而建立的两个或多个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系,进程间的直接制约关系来源于他们之间的合作。
对于如下程序,定义一个n维数组,使n个线程并发执行从某个元素中取一部分值给另一个元素,每次交换完成数组总和应该不变。
/**
* 给定一个数组的两个索引,将一个数组值的一部分给另一个数组
* 最后数组元素总和应该不变
*
*/
public class ArraySum {
private double[] myArray;
/**
* 构造方法
*/
public ArraySum(int n,double initial){
this.myArray = new double[n];
for(int i = 0 ; i < n ; ++ i){
myArray[i] = initial;
}
}
/**
* 传递数值
*/
public void transfer( int from , int to , double amount ){
if(myArray[from] < amount){
return;
}else{
myArray[from] -= amount;
myArray[to] += amount;
System.out.printf("从第%d个元素转移%10.2f 给第%d个元素,元素总和为:%10.2f%n", from , amount , to,getSum());
}
}
public int getLength(){
return myArray.length;
}
private double getSum() {
double sum = 0;
for(int i = 0 ; i < getLength() ; ++ i ){
sum += myArray[i];
}
return sum;
}
}
/**
*创建线程,对数组进行操作
*/
public class TransferTaskSystem implements Runnable {
private ArraySum arraySum;
/**
* 创建的数组长度
*/
private final static int LENGTH = 100;
/**
* 每个数组元素初值
*/
private final static double INITIAL = 1000;
/**
* 转移数字值的元素索引,第i个线程对应fromArray = i
*/
private int fromArray;
public TransferTaskSystem(ArraySum as , int from) {
this.arraySum = as;
this.fromArray = from;
}
@Override
public void run() {
while(true){
int from = (int) (Math.random() * LENGTH);
int to = (int) (Math.random() * LENGTH);
double amount = Math.random() * INITIAL;
arraySum.transfer(from, to, amount);
try {
Thread.sleep((long) (10*Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ArraySum arraySum = new ArraySum(LENGTH , INITIAL);
for(int i = 0; i < LENGTH;++ i){
TransferTaskSystem system = new TransferTaskSystem(arraySum,i);
Thread t =new Thread(system);
t.start();
}
}
}
然而,运行结果:
显然,出现了争用条件!
需要给进程加同步锁:
对ArraySum类进行修改,添加同步锁对象:
private Object lockObj = new Object();
对transfer方法进行修改:
public void transfer(int from, int to, double amount) { synchronized (lockObj) { //while循环保证条件不满足时任务都会被条件阻挡 //而不继续竞争CUP资源 while(myArray[from] < amount){ try { //使线程处于等待状态 lockObj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } myArray[from] -= amount; myArray[to] += amount; System.out.printf("从第%d个元素转移%10.2f 给第%d个元素,元素总和为:%10.2f%n", from, amount, to, getSum()); //唤醒所有在lockObj对象上等待的线程 lockObj.notifyAll();S } }
互斥:
java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持:当任务要执行被synchronized关键字保护的代码片段时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。synchronized块必须给定一个在其上进行同步的对象:- 最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this),在这种方式中,如果获得了synchronized块上的锁,那么该对象其他的synchronized方法和临界区就不能被调用了
- 也可以在另一个对象上同步(如上代码),但是这样,必须确保所有相关的任务都是在同一个对象上同步的。
同步:
当使用线程来同时运行多个任务时,可以通过使用锁(互斥)来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。但是还需要使得任务彼此之间可以协作,以使得多个任务可以一起解决某个问题,使得彼此之间协调,某些任务必须在其它任务结束后执行。可以通过Object的方法wait()和notity()来安全实现。- wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力如上,第i个线程对应的数组的第i个资源不满足 myArray[from] >= amount 条件,此时调用wait方法,该线程将释放锁,只有等待其他线程某一时刻将它的数送给该线程的元素,满足上述条件,才可以使该线程重新有获得锁的能力
- 而使用notify()和notifyAll()可以使线程从wait()中恢复执行。