线程的创建和启动
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; @Override public void run() { for(;i<100;i++) { System.out.println(getName() + " " +i ); } } public static void main(String[] args) { for(var i=0;i<100;i++) { System.out.println(Thread.currentThread().getName()); if(i == 20) { new FirstThread().start(); new FirstThread().start(); } } }
}
|
注意:使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。
实现Runnable接口创建线程类
实现Runnable接口来创建并启动多线程的步骤如下:
- 定义
Runnable接口的实现类,并重写该接口的run(),该run()方法的方法体同样是该线程的执行体。
- 创建
Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。代码如下:
1 2 3 4
| var st=new SecondThread();
new Thread(st);
|
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; @Override public void run() { for(;i<100;i++) { 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(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()接口–可以作为Thread的target。
- 在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) { var st=new ThirdThread(); FutureTask<Integer> task=new FutureTask<Integer>((Callable<Integer>)() -> { var i=0; for(;i<100;i++) { System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i); } return i; }) ; for(var i=0;i<100;i++) { System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i); if(i==20) { 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; @Override public void run() { for(;i<100;i++) { System.out.println(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) { 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); } @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(); 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); } }
}
|
线程睡眠: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;
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()) ); 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;
public class PriorityTest extends Thread { 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); } } }
}
|

线程同步
线程安全问题
关于线程安全问题,有一个经典的问题—-银行取钱问题。银行取钱的基本流程可以分为如下几个步骤:
- 用户输入账号、密码,系统判断用户的账号、密码是否正确。
- 用户输入取款金额 。
- 系统判断账户余额是否大于取款金额 。
- 如果是,则取款成功,如果不是,则取款失败。
- 下面定义两个线程 模拟两个人使用同一个账号并发取钱的问题。