在 Java 中,创建线程去执行子任务一般有四种方式:
1.继承Thread类,并重写run方法
2.创建一个类去实现Runnable接口,然后将这个类以参数的形式传递给Thread类。
3.实现Callable接口
4.使用线程池的方法创建线程
这四种方法或多或少都要和Thread打交道,所以先研究好Thread类是进行java并发编程的第一步,我们对前两种方式进行简单分析,后两者在使用线程池再介绍。
第一种:继承Thread类,并重写run方法
public class ThreadTest extends Thread{
private int num;
public ThreadTest(int num) {
this.num = num;
}
@Override
public void run() {
while((num--)>0){
System.out.println(num);
}
}
}
class Test1{
public static void main(String[] args) {
final int NUM=10;
for (int i = 0; i < 2; i++) {
ThreadTest t = new ThreadTest(NUM); //新建一个线程
t.start(); //启动线程
}
}
}
通过继承Thread类创建的线程有一个缺点,就是该类不能再继承其他类了,另外,每个线程都是独立的,所以上面的例子中,两个线程都会输出10次。
第二种:创建一个类去实现Runnable接口,然后将这个类以参数的形式传递给Thread类。
public class RunnableTest implements Runnable {
private int num=10;
public RunnableTest(int num) {
this.num = num;
}
@Override
public void run() {
while((num--)>0){
System.out.println(num);
}
}
}
class Test{
public static void main(String[] args) {
RunnableTest task = new RunnableTest(10);
for (int i = 0; i < 2; i++) {
Thread t = new Thread(task);
t.start(); //启动一个线程
}
}
}
通过实现Runnable接口,这时可以成这个类为一个任务类,将这个类以参数的形式传递给Thread,这时两个线程只会输出10次,我们可以把Thread看作线程,实现Runnable接口的类看作任务,也就是两个线程执行的是同一个任务。
接下来分析一下,Thread类的常见方法。
1.run方法
我们先来看一下run的源码
//private Runnable target;
public void run() {
if (target != null) { //target为Thread类的成员变量,可以通过构造方法的方式传入target
target.run();
}
}
其中target为Runnable类型的变量,如果从构造方法中传入Runnable的实现类,target就不为空,调用target的run方法,如果继承了Thread类,就重写了run方法,这是就调用了子类中的run方法了 。
所以,run()方法是不需要用户来调用的。当通过start()方法启动一个线程之后,一旦线程获得了CPU执行时间,便进入run()方法体去执行具体的任务。 一般来说,有两种方式可以达到重写run()方法的效果:
直接重写:直接继承Thread类并重写run()方法;
间接重写:通过Thread构造函数传入Runnable对象 (注意,实际上重写的是 Runnable对象 的run() 方法)。
2.start方法
start() 用来启动一个线程,当调用该方法后,相应线程就会进入就绪状态,该线程中的run()方法会在某个时机被调用。
3)sleep 方法
调用sleep方法相当于让线程进入阻塞状态。该方法有如下两条特征: 如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出; sleep方法不会释放该线程所拥有的资源(例如:锁),也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
4)join 方法
先来看一下join的源码
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) { //millis为0时表示无限等待,只要你还活着,我就等你到天荒地老
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
当前线程调用其他线程的join方法,会阻塞当前线程,直到其他线程执行完毕,才会进入就绪状态。
join方法是被Synchronized关键字所修饰,访问时,需要获得其他线程对象的锁,如果有两个线程同时调用另外一个线程的join方法,会有一个线程成功得到锁,而另外一个则必须等待,进入阻塞状态,而在得到锁之后,才会执行join方法。
join()方法是通过wait()方法 (Object 提供的方法) 实现的。当 millis == 0 时,会进入 while(isAlive()) 循环,并且只要子线程是活的, 宿主线程就不停的等待。 join方法同样会会让线程交出CPU执行权限; join方法同样会让线程释放对一个对象持有的锁;
5)yield 方法
调用 yield()方法会让当前线程交出CPU资源,让CPU去执行其他的线程。但是,yield()不能控制具体的交出CPU的时间。需要注意的是,yield()方法只能让 拥有相同优先级的线程 有获取 CPU 执行时间的机会;
调用yield()方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新得到 CPU 的执行;
它同样不会释放锁。