线程
前言
本文介绍了Java 中你所不知的线程创建的第三种方法。
线程存在的目的是位了在计算机中,在单位时间内(相对于人眼能够识别的速度)完成对个任务。在java中是为了在编写程序是能够并发(同时执行多个程序,或者一个程序的多个任务;在操作系统中使用时间片来完成)运行多个任务。
注意并发的同时是指,同一时间段而非同一时刻。
举例:如果是一个cpu的计算机,将cpu的时间划分成时间片分配给任务。当分配给任务的时间消耗完毕之后,当前任务推出cpu,cpu供其他任务执行,将cpu让给获得时间片的任务。
正文
java.lang.Thread
A thread is a thread of execution in a program一个Thread对象表示一个线程。
在cpu运行时,一个java程序可包含多个线程对象,可以并发执行每一个线程。
注:线程与进程
1.进程:
在操作系统中,运行的每一个程序就是一个进程。进程之间的切换效率低。
:线程:
在每一个程序中有多个任务,而且每一个任务可以同时运行,那么每一个任务称为线程。线程间的切换较快。
创建线程
方式一:继承Thread类
步骤:
1.子类继承Thread
2.重写run方法
将线程的功能书写在run方法内部
3.创建一个线程对象,并且调用start()方法启动线程。
1)start方法是启动程序,run方法是方法调用。
2)main方法本身是一个线程。
3)线程一定要在死循环前面启动,否则线程无法被执行。
4)线程的执行带有随机性。本质上线程的执行由操作系统决定,跟程序员无关。也就是线程执行的主动权在操作系统。
5)代码示例线程的创建:
方式二:实现Runnable接口
常使用匿名内部类的方式创建线程。
步骤:
1.实现Runnable接口
2.实现Run方法
3.创建线程
1)创建线程资源,产生实现Runnable接口的类的对象
2)将产生的线程资源对象作为Thread类的构造器的参数,来产生线程对象。
4)代码示例线程的创建:
方式三:利用java.util.concurrent.Callable 和 java.util.concurrent.Future
Callable 和 Future接口
Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。
Callable和Runnable有几点不同:
(1)Callable规定的方法是call(),而Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3)call()方法可抛出异常,而run()方法是不能抛出异常的。
(4)运行Callable任务可拿到一个Future对象,
Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。
1.FutureTask的类结构
Class FutureTask<V>
java.lang.Object
java.util.concurrent.FutureTask<V>
Type Parameters:
V - The result type returned by this FutureTask‘s get method
All Implemented Interfaces:
Runnable, Future<V>, RunnableFuture<V>
1)FutureTask的示例:
2.案例1:利用java.util.concurrent.Callable 和 java.util.concurrent.Future创建线程
结果:Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
子线程的返回值: 10
案例2:Java多线程之Callable接口的实现:
结果:
task1: flag = 0
looping.
looping.
looping.
task2 cancel: true
Interrupted
java.util.concurrent.ExecutionException: java.lang.Exception: Bad flag value!
小结
三种创建多线程的区别(三种其实可以归类为两类:继承Thread 和 实现接口):
采用实现接口创建多线程:
优势:
1.线程类只是实现了接口,这样还可以继承其他类,和实现其他接口。
2.在这种方式,多线程可以共享目标对象;它可以解决多对一的关系。
劣势:
编程稍微复杂一点,如果需要访问当前线程,必须使用Thread.currentThread()
采用继承Thread类创建多线程:
优势:
编程简单,要获取当前线程,无需使用Thread.currentThread()。
当启动多线程时,优先考虑使用继承Thread。继承Thread类的方法可以优先解决一对一的关系。
劣势:
不能再继承其他的类了。
线程的生命周期
线程状态转换图:
1、新建状态
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常。
2、就绪状态
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。
提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠(暂停当前运行中的线程,使之进入阻塞状态。经过指定的时间后自动醒来并且转入就绪状态。)一伙儿,转去执行子线程。
3、运行状态
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
当发生如下情况是,线程会从运行状态变为阻塞状态:
①、线程调用sleep方法主动放弃所占用的系统资源
②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
④、线程在等待某个通知(notify)
⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
4、阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行:
5、死亡状态
当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
线程状态的控制
重点关注start()、interrupt()、join()、sleep()、yield()等直接控制方法,和setDaemon()、setPriority()等间接控制方法。
线程的优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~10之间,也可以使用Thread类提供的三个静态常量:
MAX_PRIORITY =10
MIN_PRIORITY =1
NORM_PRIORITY =5
注:
线程的优先级只在jvm有效,线程的根本执行是在操作系统。所以线程设置的优先级别,不能决定线程是否根据优先级执行。
线程合并
当一个线程的运行需要另一个线程的结果时,考虑使用线程的合并(串联)
Thread类提供了join方法来完成这个功能:
void join()
Waits for this thread to die. 调用该方法的线程优先被执行。
void join(long millis)
Waits at most millis milliseconds for this thread to die.
void join(long millis, int nanos)
Waits at most millis milliseconds plus nanos nanoseconds for this thread to die.
线程让步
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。
1.apI
static void yield()
A hint to the scheduler that the current thread is willing to yield its current use of a processor.让运行中的线程主动放弃当前获得的cpu处理机会,但不使该线程进入阻塞状态,而是转入就绪状态。
注:
关于sleep()方法和yield()方的区别如下:
①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
互斥锁
同步该方法,这个方法会锁定当前对象属性。只允许当前线程去操作锁定的属性,方法执行完了,解除锁定;第二个线程继续操作,首先锁定方法然后再执行方法。方法执行完了,解除锁定。
1.目的:
保证当前任务只有一个线程操作。解决临界资源数据不一致的问题。
2.特性:
互斥锁存在于每个对象中,通过利用互斥锁(特殊的标记)实现某个时间上只能有一个线程对这个对象进行操作。
1)对静态方法加同步锁,锁住的是当前类的Class对象。
2(对非静态方法加同步锁,锁住的是当前类的对象。
注:
1)临界资源
多个线程操作同一,资源,这一资源称为临界资源。
2)互斥锁与synchronized关键字联动
1))通过synchronized关键字间接地使用互斥锁。
2))使用synchronized关键字修饰的方法,表示执行该方法时,当前对象只能被一个线程使用,这个线程使用完毕后该对象才能被其他线程使用。
3))synchronized关键字
1)))synchronized修饰代码段
语法:
synchronized(Object obj){
//代码段
}
注:
obj就是对象锁。obj可以是其他的内存对象,也可以使用当前对象this.对象锁就是临界资源。
2)))synchronized修饰方法
synchronized修饰方法的作用范围比修饰代码段的范围大。
小结
1)使用synchronized会降低程序的并发性。只有真正需要的时候才使用。
使用synchronized时候优先考虑修饰代码段。
2)使用synchronized的时候,对象锁会锁住当前被操作的对象,线程去争夺对象锁。线程获得对象锁之后,才能进入被锁住的代码块进行操作。否则线程只能进入等待状态。
获得对象锁的线程执行完任务之后,必须将锁还回,;否则(比如线程被意外终止,stop),就是死锁。
所以在线程进入阻塞状态时,应尽量释放该线程锁住的资源,为其他线程提供运行的机会;这样来避免死锁的发生。
补充:
1.public final void wait()
throws InterruptedExceptionCauses the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. In other words, this method behaves exactly as if it simply performs the call wait(0).,它是Object的API,当一个线程获得了锁定资源,当要进入阻塞状态时使用wait()方法来释放对象锁,并使自己进入阻塞池。等待notify或者notifyAll来唤醒线程进入对象池中等待获取对象锁。
注意:
在使用wait方法时,确保调用wait方法的对象是与synchronized 的代码段的对象锁的对象一致。
2.public final void notify()
Wakes up a single thread that is waiting on this object‘s monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object‘s monitor by calling one of the wait methods.唤醒一个线程从阻塞池到对象池去等待获得互斥锁。
把以调用该notify的对象为互斥锁的线程随机的一个送入对象池。
3.public final void notifyAll()
Wakes up all threads that are waiting on this object‘s monitor.把所有以调用notifyAll方法的当前对象为互斥锁的线程放入对象池中去竞争对象锁。
守护线程
public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。 Marks this thread as either a daemon thread or a user thread.默认为false。
终止线程
执行完run方法,这个线程就停止。
1.终止的方式:
1)stop方法
该方法终止线程并不释放对象锁,容易造成死锁。
2)采用标记的方式
1))提供boolean类型属性
2))提供getter和setter方法
3))在循环体中使用标识属性作为条件
4))在创建对象资源时设置标识属性为true。
3)采用守护线程的方式;
总结