创建线程

有两种方法可以创建线程:

  • 需要从Java.lang.Thread类派生一个新的线程类,重载它的run()方法
  • 实现Runnable接口,重载Runnable接口中的run()方法

为什么要提供两种方法来创建线程呢?

在Java中,类仅支持单继承,也就是说,当定义一个新的类的时候,它只能扩展一个外部类.这样,如果创建自定义线程类的时候是通过扩展 Thread类的方法来实现的,那么这个自定义类就不能再去扩展其他的类,也就无法实现更加复杂的功能。因此,如果自定义类必须扩展其他的类,那么就可以使用实现Runnable接口的方法来定义该类为线程类,这样就可以避免Java单继承所带来的局限性。另外,使用实现Runnable接口的方式创建的线程可以处理同一资源,从而实现资源的共享。

Runnable这个接口只有一个方法:public void run();

通过Runnable接口创建线程

  • 建立Runnable对象
    • Runnable threaJob=new MyRunnable();
  • 建立Thread对象并赋值Runnable任务

    • Thread就像一个工人,需要一个任务才能进行工作,而这个任务就在Runnable()的run()方法中
    • Thread myThread new Thread(threadJob);
  • 启动Thread

    • 工人有了任务滞后,就可以让他去工作了
    • myThread.start();

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//每个任务都是Runable接口的一个实例,任务是可运行对象,线程是便于任务执行的对象。必须创建任务类,重写run方法定义任务
public class ThreadDemo1 implements Runnable {
private int countDown = 10;
@Override
//重写run方法,定义任务
public void run() {
while(countDown-- >0)
{
System.out.println("$" + Thread.currentThread().getName()
+ "(" + countDown + ")");//该方法若要访问当前线程,应使用Thread.currentThread()
}
}
//调用start方法会启动一个线程,导致任务中的run方法被调用,run方法执行完毕则线程终止

public static void main(String[] args) {
Runnable demo1 = new ThreadDemo1();

Thread thread1 = new Thread(demo1);
Thread thread2 = new Thread(demo1);
thread1.start();
thread2.start();

System.out.println("火箭发射倒计时:");


}

}

运行结果:

火箭发射倒计时:
$Thread-0(9)
$Thread-0(8)
$Thread-0(7)
$Thread-0(6)
$Thread-0(5)
$Thread-0(4)
$Thread-0(3)
$Thread-0(2)
$Thread-0(1)
$Thread-0(0)

如果同时运行两个任务对象,体会其不同的之处:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
Runnable demo1 = new ThreadDemo1();
Runnable demo2 = new ThreadDemo1();
Thread thread1 = new Thread(demo1);
Thread thread2 = new Thread(demo2);
thread1.start();
thread2.start();

System.out.println("火箭发射倒计时:");


}

运行结果:

火箭发射倒计时:
$Thread-0(9)
$Thread-0(8)
$Thread-0(7)
$Thread-0(6)
$Thread-1(9)
$Thread-0(5)
$Thread-1(8)
$Thread-0(4)
$Thread-1(7)
$Thread-0(3)
$Thread-1(6)
$Thread-1(5)
$Thread-0(2)
$Thread-1(4)
$Thread-1(3)
$Thread-1(2)
$Thread-1(1)
$Thread-1(0)
$Thread-0(1)
$Thread-0(0)

继承Thread类来创建线程

  • 首先创建一个任务类extends Thread类,因为Thread类实现了Runnable接口,所以自定义的任务类也实现了Runnable接口,重写run()方法,其中定义具体的任务代码或处理逻辑
  • 创建一个任务类对象,可以用Thread或者Runnable作为自定义的变量类型
  • 调用自定义对象的start()方法,启动一个线程

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//每个任务都是Runable接口的一个实例,任务是可运行对象,线程即可运行对象。必须创建任务类,重写run方法定义任务
public class ExtendFromThread extends Thread {
private int countDown = 10;
@Override
//重写run方法,定义任务
public void run() {
while(countDown-- >0)
{
System.out.println("$" + this.getName()
+ "(" + countDown + ")");
}
}
//调用start方法会启动一个线程,导致任务中的run方法被调用,run方法执行完毕则线程终止

public static void main(String[] args) {

ExtendFromThread thread1 = new ExtendFromThread();
ExtendFromThread thread2 = new ExtendFromThread();
thread1.start();
thread2.start();

System.out.println("火箭发射倒计时:");


}

}

运行结果:

火箭发射倒计时:
$Thread-0(9)
$Thread-0(8)
$Thread-0(7)
$Thread-0(6)
$Thread-0(5)
$Thread-0(4)
$Thread-0(3)
$Thread-0(2)
$Thread-0(1)
$Thread-0(0)
$Thread-1(9)
$Thread-1(8)
$Thread-1(7)
$Thread-1(6)
$Thread-1(5)
$Thread-1(4)
$Thread-1(3)
$Thread-1(2)
$Thread-1(1)
$Thread-1(0)

两种创建方法的比较

  • 使用实现Runnable接口方式创建线程可以共享同一个目标对象,实现了多个相同线程处理同一份资源,即一个线程对资源的操作会影响到另一个进程的操作
  • 而继承Thread创建线程的方式,new出了两个任务类对象,有各自的成员变量,相互之间不干扰。

各自的优缺点

  • 采用继承Thread类方式:
    • 优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程
    • 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类
  • 采用实现Runnable接口方式:
    • 优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想
    • 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法

本文以上主要内容转载于:java多线程总结一:线程的两种创建方式及比较

线程睡眠

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class MyRunnable implements Runnable {
public void run(){
go();
}

public void go(){

//线程睡眠代码块
try{
Thread.sleep(2000);
}catch(InterruptedException ex){
ex.printStackTrace();
}//代码块结束

doMore();
}

public void doMore(){
System.out.println("top 0' the stack");
}
}

class ThreadTestDrive{
public static void main(String[] args){
Runnable threadJob=new MyRunnable();
Thread mythread=new Thread(threadJob);

mythread.start();

System.out.println("back in main");
}
}

线程同步机制

为什么要线程同步?我们现在知道,可以实现多个线程共享同一个资源(变量或者对象)的情况,如果这些线程都对需要对资源进行读或者写操作,由于线程之间的调度等其它问题,会导致资源的状态出现紊乱,导致程序异常。举个例子:如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?由于是同时发生的两件事,很难说清楚。因此多线程同步就是要解决这个问题。

Java的同步机制是通过对象锁来实现的:

一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 Java里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池 等待队列中)。 取到锁后,他就开始执行同步代码(被synchronized修饰的代码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中 等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadTest2 extends Thread {     
private int threadNo; private String lock;
public ThreadTest2(int threadNo, String lock) {
this.threadNo = threadNo;
this.lock = lock; }
public static void main(String[] args) throws Exception {
String lock = new String("lock");
for (int i = 1; i < 10; i++) {
new ThreadTest2(i, lock).start();
Thread.sleep(1);
}
}
public void run() {
synchronized (lock) { //原子单位
for (int i = 1; i < 10000; i++) {
System.out.println("No." + threadNo + ":" + i);
}
}
}
}

该程序通过在main方法启动10个线程之前,创建了一个String类型的对象。并通过ThreadTest2的构造函数,将这个对象赋值 给每一个ThreadTest2线程对象中的私有变量lock。根据Java方法的传值特点,我们知道,这些线程的lock变量实际上指向的是堆内存中的 同一个区域,即存放main函数中的lock变量的区域
run方法加一个synchronized块来实现。这个同步块的对象锁,就是 main方法中创建的那个String对象。换句话说,他们指向的是同一个String类型的对象,对象锁是共享且唯一的!

于是,我们看到了预期的效果:10个线程不再是争先恐后的报数了,而是一个接一个的报数。

同步的实现方式可分为:

  • 同步方法
  • 同步代码块
  • 使用特殊域变量(volatile)实现线程同步
  • 使用重入锁实现线程同步
  • 详细介绍链接

多线程的使用场景

  1. 为了不阻塞主线程,启动其他线程来做耗时的事情。比如app开发中耗时的操作都不在UI主线程中做。
  2. 实现响应更快的应用程序, 即主线程专门监听用户请求,子线程用来处理用户请求。以获得大的吞吐量。
    感觉这种情况下,多线程的效率未必高。 这种情况下的多线程是为了不必等待, 可以并行处理多条数据。比如JavaWeb的就是主线程专门监听用户的HTTP请求,然后启动子线程去处理用户的HTTP请求。
  3. 某种优先级虽然很低的服务,但是却要不定时去做。比如Jvm的垃圾回收。
  4. 某种任务,虽然耗时,但是不耗CPU的操作时,开启多个线程,效率会有显著提高。
    比如读取文件,然后处理。 磁盘IO是个很耗费时间,但是不耗CPU计算的工作。 所以可以一个线程读取数据,一个线程处理数据。肯定比一个线程读取数据,然后处理效率高。 因为两个线程的时候充分利用了CPU等待磁盘IO的空闲时间。曾几何时想过使用多线程读取磁盘数据, 但是读取磁盘数据的性能瓶颈是IO,而不是CPU。 使用多线程的目的是为了不让CPU闲下来,明显不适合用于读取磁盘数据。