您的位置:首页 > 编程语言 > Java开发

java多线程详细解析

2016-07-28 16:14 232 查看
世间万物都可以同时完成很多工作!这种思想在java中被称为并发,而将并发完成的每一件事情称为多线程。讲多线程之前我们先来补充一点概念性知识!

一、线程相关基础概念

二、java线程常见三种形式

三、线程调度

四、常用函数说明

五、Java提供的线程同步方式

六、线程常见异常

一、

进程:一个计算机程序的运行实例,包含了需要执行的指令;每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。

线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用

相同的地址空间,同时共享进进程所拥有的内存和其他资源,线程切换开销小。

  多进程是指操作系统能同时运行多个任务(程序)。

  多线程是指在同一程序中有多个顺序流在执行。

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。





Java线程具有五中基本状态

新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期

二、

JAVA多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。

1、继承Thread类实现多线程

继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。

<span style="font-size:14px;">class Thread1 extends Thread{
private String name;
public Thread1(String name) {
this.name=name;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
}
public class Main {

public static void main(String[] args) {
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();

}

}
/*输出:
A运行 : 0
B运行 : 0
A运行 : 1
A运行 : 2
A运行 : 3
A运行 : 4
B运行 : 1
B运行 : 2
B运行 : 3
B运行 : 4

再运行一下:

A运行 : 0
B运行 : 0
B运行 : 1
B运行 : 2
B运行 : 3
B运行 : 4
A运行 : 1
A运行 : 2
A运行 : 3
A运行 : 4
说明:
程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。

注意:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。*/</span>2.实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为
Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。

有代码有真相:
<span style="font-size:14px;">class MyRunnable implements Runnable {
private int i = 0;

@Override
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}

public class ThreadTest {

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
Thread thread2 = new Thread(myRunnable);
thread1.start(); // 调用start()方法使得线程进入就绪状态
thread2.start();
}
}
}
}

//Thread和Runnable之间到底有什么猫腻,接着看
public class ThreadTest {

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Runnable myRunnable = new MyRunnable();
Thread thread = new MyThread(myRunnable);
thread.start();
}
}
}
}

class MyRunnable implements Runnable {
private int i = 0;

@Override
public void run() {
System.out.println("in MyRunnable run");
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}

class MyThread extends Thread {

private int i = 0;

public MyThread(Runnable runnable){
super(runnable);
}

@Override
public void run() {
System.out.println("in MyThread run");
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}</span>同样的,与实现Runnable接口创建线程方式相似,不同的地方在于

 Thread thread = new MyThread(myRunnable);

那么这种方式可以顺利创建出一个新的线程么?答案是肯定的。至于此时的线程执行体到底是MyRunnable接口中的run()方法还是MyThread类中的run()方法呢?通过输出我们知道线程执行体是MyThread类中的run()方法。其实原因很简单,因为Thread类本身也是实现了Runnable接口,而run()方法最先是在Runnable接口中定义的方法。

 public interface Runnable {

    

    public abstract void run();

     

 }

我们看一下Thread类中对Runnable接口中run()方法的实现:

  @Override

    public void run() {

        if (target != null) {

            target.run();

        }

    }

也就是说,当执行到Thread类中的run()方法时,会首先判断target是否存在,存在则执行target中的run()方法,也就是实现了Runnable接口并重写了run()方法的类中的run()方法。但是上述给到的列子中,由于多态的存在,根本就没有执行到Thread类中的run()方法,而是直接先执行了运行时类型即MyThread类中的run()方法。

总结:

实现Runnable接口比继承Thread类所具有的优势:

1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免java中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

提醒一下大家:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。

3.使用Callable和Future接口创建线程。具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

实例解析

ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。想要详细了解Executor框架的可以访问http://www.javaeye.com/topic/366591 ,这里面对该框架做了很详细的解释。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。

可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下

<span style="font-size:14px;">import java.util.concurrent.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;

/**
* 有返回值的线程
*/
@SuppressWarnings("unchecked")
public class Test {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
System.out.println("----程序开始运行----");
Date date1 = new Date();

int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取Future对象
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 关闭线程池
pool.shutdown();

// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + f.get().toString());
}

Date date2 = new Date();
System.out.println("----程序结束运行----,程序运行时间【"
+ (date2.getTime() - date1.getTime()) + "毫秒】");
}
}

class MyCallable implements Callable<Object> {
private String taskNum;

MyCallable(String taskNum) {
this.taskNum = taskNum;
}

public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任务启动");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
}
}</span>

代码说明:

上述代码中Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。

public static ExecutorService newFixedThreadPool(int nThreads) 

创建固定数目线程的线程池。

public static ExecutorService newCachedThreadPool() 

创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一

个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

public static ExecutorService newSingleThreadExecutor() 

创建一个单线程化的Executor。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 

创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没

有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

三、

1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。

 

Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:

static int MAX_PRIORITY

          线程可以具有的最高优先级,取值为10。

static int MIN_PRIORITY

          线程可以具有的最低优先级,取值为1。

static int NORM_PRIORITY

          分配给线程的默认优先级,取值为5。

 

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

 

每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。

线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。

JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅

使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

 

2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。

当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

 

3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 

notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。

 

4、线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

 

5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态

,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

 

6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,

则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象

的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在

该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣

势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

 注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。

四、

a、方法sleep()进入的阻塞状态,不会释放对象的锁(即大家一起睡,谁也别想执行代码),所以不要让sleep方法处在

synchronized方法或代码块中,否则造成其他等待获取锁的线程长时间处于等待。

b、方法join()则是主线程等待子线程完成,再往下执行。例如main方法新建两个线程A和B

在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,

但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,

这个时候就要用到join()方法了。

public static void main(String[] args) throws InterruptedException {    

Thread t1 = new Thread(new ThreadTesterA());    

Thread t2 = new Thread(new ThreadTesterB());    

t1.start();    

t1.join(); // 等t1执行完再往下执行  

t2.start();    

t2.join(); // 在虚拟机执行中,这句可能被忽略  

}  

c、方法interrupt(),向被调用的对象线程发起中断请求。如线程A通过调用线程B的d的interrupt方法来发出中断请求

,线程B来处理这个请求,当然也可以忽略,这不是必须的。Object类的wait()、Thread类的join()和sleep方法都会抛

出受检异常java.lang.InterruptedException,通过interrupt方法中断该线程会导致线程离开等待状态。对于wait()

调用来说,线程需要重新获取监视器对象上的锁之后才能抛出InterruptedException异常,并致以异常的处理逻辑。

可以通过Thread类的isInterrupted方法来判断是否有中断请求发生,通常可以利用这个方法来判断是否退出线程(类似

上面的volatitle修饰符的例子);Thread类还有个方法Interrupted(),该方法不但可以判断当前线程是否被中断,还会清楚线程内部的中断标记,如果返回true,即曾被请求中断,同时调用完后,清除中断标记。如果一个线程在某个对象的等待池,那么notify和interrupt 都可以使该线程从等待池中被移除。如果同时发生,那么看实际发生顺序。如果是notify先,那照常唤醒,没影响。如果是interrupt先,并且虚拟机选择让该线程中断,那么即使nofity,也会忽略该线程,而唤醒等待池中的另一个线程。

e、yield(),Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。

       yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,

使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让

步的线程还有可能被线程调度程序再次选中。(在实际开发中,测试阶段频繁调用yeid方法使线程切换更频繁,从而让一

些多线程相关的错误更容易暴露出来)。

结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态

,但有可能没有效果。

sleep()和yield()的区别

        sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被

执行。

        sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU  的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程

       另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield()  方法执行时,当前线程仍处在可运行状态

,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 

sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。 

五、

生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。如果用while 和 volatile也可以做,不

过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。

wait: 将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,并且释放B的

锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,否则直接抛出java.lang.IllegalMonitorStateException异常)。

notify:将该对象中等待池中的线程,随机选取一个放入对象的锁池,当当前线程结束后释放掉锁, 锁池中的线程即可

竞争对象的锁来获得执行机会。

notifyAll:将对象中等待池中的线程,全部放入锁池。

(notify锁唤醒的线程选择由虚拟机实现来决定,不能保证一个对象锁关联的等待集合中的线程按照所期望的顺序被唤醒

,很可能一个线程被唤醒之后,发现他所要求的条件并没有满足,而重新进入等待池。因为当等待池中包含多个线程时,

一般使用notifyAll方法,不过该方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待池,对性能有影响,

不过能保证程序的正确性)

工作流程:

a、Consumer线程A 来 看产品,发现产品为空,调用产品对象的wait(),线程A进入产品对象的等待池并释放产品的锁。

b、Producer线程B获得产品的锁,执行产品的notifyAll(),Consumer线程A从产品的等待池进入锁池,Producer线程B

      生产产品,然后退出释放锁。

c、Consumer线程A获得产品锁,进入执行,发现有产品,消费产品,然后退出。

例子:

public synchronized String pop(){  

  this.notifyAll();// 唤醒对象等待池中的所有线程,可能唤醒的就是 生产者(当生产者发现产品满,就会进入对象

的等待池,这里代码省略,基本略同)  

   while(index == -1){//如果发现没产品,就释放锁,进入对象等待池  

      this.wait();  

   }//当生产者生产完后,消费者从this.wait()方法再开始执行,第一次还会执行循环,万一产品还是为空,则再等待

,所以这里必须用while循环,不能用if  

   String good = buffer[index];  

   buffer[index] = null;  

   index--;  

   return good;// 消费完产品,退出。  

}  

注:wait()方法有超时和不超时之分,超时的在经过一段时间,线程还在对象的等待池中,那么线程也会推出等待状态。

wait和sleep区别

共同点: 

1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。 

2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException

   如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在

wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。 

   需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 

interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线

程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。 

不同点: 

1. Thread类的方法:sleep(),yield()等 

   Object的方法:wait()和notify()等 

2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。 

   sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 

3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 

4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

所以sleep()和wait()方法的最大区别是:

    sleep()睡眠时,保持对象锁,仍然占有该锁;

    而wait()睡眠时,释放对象锁。

         但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出 

              InterruptedException(但不建议使用该方法)。

六、异常请参见下面word文档!

更多内容:http://blog.csdn.net/escaflone/article/details/10418651
http://www.mamicode.com/info-detail-517008.html http://blog.csdn.net/aboy123/article/details/38307539
若有错漏,欢迎指正!!如转载请注明出处:
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: