多线程

线程的创建和启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java使用线程执行体来代表这段程序流。

继承Thread类创建线程类

通过继承Thread来创建和启动多线程的步骤如下:

  • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就是线程需要完成的任务。因此run()方法称为线程执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的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
29
30
package demo;

public class FirstThread extends Thread{
private int i;
//重写run()方法,run()方法的方法体就是线程执行体
@Override
public void run() {
for(;i<100;i++) {
//当线程体继承法Thread时,直接使用this即可获取当前线程
//Thread对象的getName()返回当前线程的名字
//因此直接调用getName()方法返回当前线程名字
System.out.println(getName() + " " +i );
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(var i=0;i<100;i++) {
//调用Thread的currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName());
if(i == 20) {
//创建并启动第一个线程
new FirstThread().start();
//创建并启动第二个线程
new FirstThread().start();

}
}
}

}

注意:使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

实现Runnable接口创建线程类

实现Runnable接口来创建并启动多线程的步骤如下:

  • 定义Runnable接口的实现类,并重写该接口的run(),该run()方法的方法体同样是该线程的执行体。
  • 创建Runnable实现类的实例,并以此实例作为Threadtarget来创建Thread对象,该Thread对象才是真正的线程对象。代码如下:
1
2
3
4
//创建Runnable实现类的对象
var st=new SecondThread();
//以Runnable实现类的对象作为Thread的Target来创建Thread对象,即线程对象
new Thread(st);
  • 调用线程对象的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
29
package demo;

public class SecondThread implements Runnable {

private int i;
//run()方法同样是线程执行体
@Override
public void run() {
for(;i<100;i++) {
//当线程类实现Runnable接口时
//如果想要获取当前线程,只能使用Thread.currentThread()方法
System.out.println(Thread.currentThread().getName() + " "+i);
}

}

public static void main(String[] args) {
for(var i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName() + " " +i);
if( i==20 ) {
var st=new SecondThread();
//通过new Thread(target,name)方法来创建新线程
new Thread(st,"新线程1").start();
new Thread(st,"新线程2").start();
}
}
}

}

注意:使用实现Runnable接口的方法来创建线程类时,多个线程之间可以共享线程类的实例变量,这是因为程序所创建的Runnable实例只是Thread的target。

使用Callable和Funture创建线程

  • 前面已经指出,通过实现Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程执行体,那么是否可以直接把任意方法都包装成线程执行体呢?Java目前不行!但是Java模仿者C#可以(C#可以把任意方法包装成线程执行体,包括有返回值的方法)。
  • 也许受此启发,从Java5开始,Java提供Callable接口,该方法怎么看都像是Runnable接口的加强版,Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大。
  • call()方法可以有返回值。
  • call()可以声明抛出异常。
  • Java5提供了Fucture接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTaskTask实现类,该实现类实现Functure接口,并实现Runnable()接口–可以作为Threadtarget
  • 在Fucture接口中提供了如下几个公共方法来控制它关联的Callable任务。
    • boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务。
    • V get():返回Callable任务里call()方法的返回值。调用该方法将导致线程阻塞,必须等到子线程结束后才会得到返回值。
    • V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定的时间Callable任务依然没有返回值,将会抛出TimeoutException异常。
    • boolean isCancelled():如果此任务在正常完成之前取消,则返回 true 。
    • boolean isDone():如果Callable任务已完成,则返回true
  • 创建并启动有返回值的线程如下:
    • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程的执行体,且该call()方法有返回值,再创建Callable实现类的实例。从Java8开始,可以使用Lambda表达式创建Callable对象
    • 使用FutureTask类来包装Callable对象,该FutureTask对象包装了该Callable对象的call()方法的返回值。
    • 使用FutureTask对象作为Thread的target来创建并启动多线程
    • 调用FutureTask对象的get()方法来获取子线程执行结束后的返回值。
    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
    33
    34
    35
    36
    37
    package demo;

    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;

    public class ThirdThread {
    public static void main(String[] args) {
    //创建Callable对象
    var st=new ThirdThread();
    //先使用Lambda表达式创建Callable<Integer>对象
    FutureTask<Integer> task=new FutureTask<Integer>((Callable<Integer>)() -> {
    //var只能用于带有构造器的局部变量
    //除了局部变量,for循环是唯一可以使用 var的地方
    var i=0;
    for(;i<100;i++) {
    System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i);

    }
    //call()方法可以有返回值
    return i;
    }) ;
    for(var i=0;i<100;i++) {
    System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i);
    if(i==20) {
    //实质还是以Callable对象来创建并启动线程的
    new Thread(task,"有返回值的线程").start();
    }

    }
    try {
    //获取子线程的返回值
    System.out.println("子线程的返回值:" + task.get());
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

注意:Callable接口有泛型限制,Callable接口的泛型形参类型于call()方法的返回值类型相同,而且Callable()接口是一个函数式接口,因此可以使用Lambda表达式创建Callable对象。

创建线程的三种方式的对比

采用实现Runnable、Callable接口的方式创建多线程的优缺点

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

鉴于上面的分析,因此一般推荐使用实现Runnable接口或Callable接口的方式来创建多线程。

线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(new)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5中状态。尤其是当线程启动以后,它不可能一直“霸占”者CPU独自运行,所以CPU需要在多个线程之间切换,于是线程状态也会多次在运行、就绪之间切换。

新建和就绪状态

  • 当线程使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其它的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序此时也不会执行线程的线程执行体。
  • 当线程对象调用了start()方法之后,该线程就处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始执行,只是表示该线程可以执行了。至于改写该线程开始何时,取决于JVM里线程调度器调度。
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
package demo;

public class InvokeRun extends Thread{
private int i;
//重写run()方法,run()方法的方法体就是线程的执行体
@Override
public void run() {
for(;i<100;i++) {
//当线程体继承法Thread时,直接使用this即可获取当前线程
//Thread对象的getName()返回当前线程的名字
//因此直接调用getName()方法返回当前线程名字
System.out.println(getName() + " " +i );
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for(var i=0;i<100;i++) {
//调用Thread的currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName()+ " "+i);
if(i == 20) {
//直接调用线程对象的run()方法
new FirstThread().run();
new FirstThread().run();

}
}
}
}

运行和阻塞状态

  • 如果处于就绪状态的线程获得cpu,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个cpu,那个在任何时刻只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行执行。当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的线程。
  • 抢占式的调用策略:被动式释放资源。
  • 协作式的调度策略:主动式释放资源。
  • 当发生如下情况时,线程将会进行阻塞状态。
    • 线程调用sleep()方法主动放弃所占用的处理器资源。
    • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
    • 线程试图获得一个同步监听器,但该同步监听器正被其它线程所持有。
    • 线程在等待某个通知(notify)
    • 线程调用了线程的suspend()方法将该线程挂起。但是这个方法容易导致死锁,所以应该尽量避免使用该方法。
  • 针对上面几种情况,当发生如下特定情况时可以接触上面的阻塞,让该线程重新进入就绪状态。
    • 调用了sleep()方法的线程经过了指定时间。
    • 线程调用的阻塞式IO方法已返回。
    • 线程成功获得了试图获取的同步监听器。
    • 线程正在等待某个通知时,其它的线程发出了一个通知。
    • 处于挂起状态的线程被调用了resume()恢复方法。
  • yield()方法可以让线程从运行状态进入就绪状态。

死亡状态

  • 线程会以如下三种方式结束,结束后就处于死亡状态。
    • run()或call()方法执行完成,线程正常结束
    • 线程抛出一个未捕获的Exception或Error
    • 直接调用该线程的stop()方法来结束该线程–该方法容易导致死锁,通常不推荐使用。
  • isAlive():用于测试某个线程是否已经死亡,当线程处于就绪、运行、阻塞三种状态时,该方法返回true,当线程处于新建、死亡两种状态时,该方法返回false。

注意:不要对处于死亡状态的线程调用start()方法,程序只能对新建状态的线程调用start(),对象新建状态的线程两次调用start()方法也是错误的,这都会引发IllegalThreadStateException异常。

控制线程

join()线程

  • Thread提供了让一个线程等待另一个完成的方法–>join(),当某个程序执行流中调用其它线程的join(),调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
  • join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程,当所有的小问题处理完成后,再调用主线程来进一步操作。
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package demo;

public class JoinThread extends Thread{

public JoinThread(String name) {
super(name);
}
//重写run()方法,定义线程执行体
@Override
public void run() {
for(var i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) throws InterruptedException {
//启动子线程
new JoinThread("新线程").start();
for(var i=0;i<100;i++) {
if(i==20) {
var jt=new JoinThread("被Join的线程");
jt.start();
//main线程调用了jt线程的join(),main()线程必须等待jt执行完成才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

}

```
>* join()方法有如下三种重载形式:
>>* join():等待被join()的线程执行完成。
>>* join(long millis):等待被join的线程的时间最长为mills毫秒。如果在millis毫秒内被join的线程还没执行完成,则不再等待。
>>* join(long millis,int nanos):等待被join的线程的时长最长为millis毫秒加nanos毫微秒。
### 后台线程
>* 后台线程是在后台线程运行的,它的任务是为其它线程提供服务,又称为“守护线程”或“精灵线程”。
>* 后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
>* 调用Thread对象的setDaemon(true)可以将指定的线程设置为后台线程,且该方法的调用必须在线程启动之前,否则会抛出IllegalThreadStateException。
>* Thread类还提供了一个isDaemon(),判断线程是否为后台线程。
```java
package demo;

public class DaemonThread extends Thread{

//定义后台线程的线程执行体与普通线程没有任何区别
@Override
public void run() {
for(var i=0;i<1000;i++) {
System.out.println(getName()+ " "+ i);
}
}
public static void main(String[] args) {
var t=new DaemonThread();
//将此线程设置成后台线程
t.setDaemon(true);
//启动后台线程
t.start();
for(var i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName() + " "+i);
}
// ----- 线程执行到此处,前台线程(main线程)结束 -----
// 后台线程也应该执行结束
}

}

线程睡眠:sleep

  • 如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的静态方法sleep()方法来实现。sleep()方法有两种重载方式;
    • static void sleep(int millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到计时器和线程调度其的精度与准确度的影响。
    • static void sleep(int millis,int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入阻塞状态,该方法受到计时器和线程调度其的精度与准确度的影响。
  • 当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行机会,即使系统中没有其它可以执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停线程的执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package demo;

import java.text.SimpleDateFormat;
import java.util.Date;

/***
* sleep():用来使一个正在执行的线程暂停一段时间,并进入阻塞状态
* @author 朱楚利
*
*/
public class SleepTest {
public static void main(String[] args) throws InterruptedException {
for(var i=0;i<60;i++) {
System.out.println("当前时间:"+new SimpleDateFormat("HH:mm:ss").format(new Date()) );
//调用sleep()方法让当前线程暂停1s
Thread.sleep(1000);
}
}
}
  • 关于sleep()方法和yield()方法的区别
    • sleep()方法暂停当当前线程后,会给其它线程执行机会,不会理会其它线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程获得执行的机会。
    • sleep()方法会将线程进入阻塞状态,直到经过阻塞时间才会进入就绪状态;而yield()不会将线程进入阻塞状态,它只会强制当前线程进入就绪状态。因此完全有可能某个线程被yield()方法暂停之后,立即再次获得处理器资源被执行。
    • sleep()方法声明抛出了InterruptedException,所以调用sleep()方法时要么捕获该异常,要么显示声明出该异常;而yield()方法则没有声明抛出任何异常。
    • sleep()方法比yield()方法有更好的移植性,通常不建议使用yield()方法来控制并发线程的执行。

改变线程的优先级

  • 每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
  • 每个线程默认的优先级都与创建它的父线程的优先级相同,在默认的情况下,main线程具有普通的优先级,由main线程创建的子线程也有普通普通优先级。
  • Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围是1-10之间,也可以使用Thread类的如下三个静态常量。
    • MAX_PRIORITY:其值是10。
    • MIN_PRIORITY:其值是1。
    • NORM_PRIORITY:其值是5。
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
33
34
35
36
37
38
39
40
package demo;

/***
* 线程的优先级
* @author 朱楚利
*
*/
public class PriorityTest extends Thread {
//定义一个有参数的构造器,用于创建线程时指定name
public PriorityTest(String name) {
super(name);
}
@Override
public void run() {
for(var i=0;i<50;i++) {
System.out.println(getName() + ",其优先级是:"+getPriority()+",循环变量的值:"+i);
}
}
public static void main(String[] args) {
//改变主线程的优先级
Thread.currentThread().setPriority(6);
for(var i=0;i<30;i++) {
if(i==10) {
var low=new PriorityTest("低级");
low.start();
System.out.println("创建之初的优先级:"+low.getPriority());
//设置线程的优先级为最低级
low.setPriority(Thread.MIN_PRIORITY);
}
if(i==20) {
var high=new PriorityTest("高级");
high.start();
System.out.println("创建之初线程的优先级:"+high.getPriority());
//设置线程的优先级为最高级
high.setPriority(Thread.MAX_PRIORITY);
}
}
}

}
  • 运行结果为

avavtor.png

线程同步

线程安全问题

关于线程安全问题,有一个经典的问题—-银行取钱问题。银行取钱的基本流程可以分为如下几个步骤:

  • 用户输入账号、密码,系统判断用户的账号、密码是否正确。
  • 用户输入取款金额 。
  • 系统判断账户余额是否大于取款金额 。
  • 如果是,则取款成功,如果不是,则取款失败。
  • 下面定义两个线程 模拟两个人使用同一个账号并发取钱的问题。
1
2