- 基本概念
- 多线程的优缺点
- 线程的创建与启动
- 1.继承Thread类创建线程类
- 2、实现Runnable接口创建线程类
- 3、通过Callable和Future创建线程
- 创建线程的三种方式的对比
- 线程的生命周期
- 控制线程
- join线程
- 后台进程
- 线程睡眠:sleep
- 线程让步:yield
- 改变线程优先级
- 线程同步
- 同步代码块
- 同步方法
- 今日推歌
基本概念
单线程的程序只有一个顺序执行流,多线程的程序可以包含多个顺序执行流且互不干扰。几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。一个程序运行后至少有一个进程,一个进程可以包含多个线程,但至少要包含一个线程。
对于一个CPU而言,它在某个时间点只能执行一个程序,也就是说,只能运行一个进程,CPU不断地在这些进程之间轮换执行。那为什么用户感觉不到任何中断现象呢?这是因为CPU的执行速度相对人的感觉来说实在是太快了(如果启动的程序足够多,用户依然可以感觉到程序的运行速度下降),所以虽然CPU在多个进程之间轮换执行,但用户感觉到好像有多个进程在同时执行。
多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread) 也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个主线程,但也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程,每个线程也是互相独立的。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。 线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方便;但必须更加小心,因为需要确保线程不会妨碍同一进程里的其他线程。
线程可以完成一定的任务,可以与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。一个线程可以创建和撤销另一个线程, 同一个进程中的多个线程之间可以并发执行。
从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序中可以有多个执行部分同时执行,但操作系统无须将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配。线程的调度和管理由进程本身负责完成。
多线程的优缺点
多线程的主要优点包括:
- 多线程技术使程序的响应速度更快 ,因为用户界面可以在进行其它工作的同时一直处于活动状态;
- 占用大量处理时间的任务使用多线程可以提高CPU利用率,即占用大量处理时间的任务可以定期将处理器时间让给其它任务;
- 多线程可以分别设置优先级以优化性能。
以下是最适合采用多线程处理:
- 耗时或大量占用处理器的任务阻塞用户界面操作;、
- 各个任务必须等待外部资源 (如远程文件或 Internet连接)。
多线程的主要缺点包括:
- 等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源 ,如打印机等。
- 对线程进行管理要求额外的 CPU开销,线程的使用会给系统带来上下文切换的额外负担。
- 线程的死锁。即对共享资源加锁实现同步的过程中可能会死锁。
- 对公有变量的同时读或写,可能对造成脏读等;
线程的创建与启动 1.继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
//继承Thread类
public class FirstThread extends Thread{
private int i;
//重写run方法,run方法的方法体就是线程执行体
public void run(){
for( ; i<100 ; i++){
//当线程类继承Thread类时,直接使用this即可获取当前进程
//Thread对象的getName()方法返回当前线程的名字,可直接调用
System.out.println(getName() + " " +i);
}
}
public static void main(String[] args){
for (int i = 0 ; i<100 ; i++){
//调用Thread的currentThread()方法获取当前进程
System.out.println(Thread.currentThread().getName() + " " + i);
if (i==20){
//创建并启动第一个线程
new FirstThread().start();
//创建并启动第二个线程
new FirstThread().start();
}
}
}
}
上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。GetName()方法返回调用该方法的线程的名字。
注意上面的程序只是显式地创建并启动了了2个线程,但实际有三个线程,还有一个主线程,主线程的执行体不是由run()方法确定的,而是由main()方法确定的,main()方法的方法体代表主程序的线程执行体。大家可以自行运行上面 的代码看一下。可以看出,Thread-0和Thread-1两个线程输出的i变量不连续,注 意 i 变量是FirstThread 的实例变量,而不是局部变量,但因为程序每次创建线程对象时都需要创建FirstThread对象,所以Thread-0 和Thread-1不能共享该实例变量。
程序可以通过setName(String name)方法为线程设置名字,也可以通过getName(方法返回指定线程的名字。在默认情况下,主线程的名字为main, 用户启动的多个线程的名字依次为Thread-0 , Thread-1 , Thread-2 , Thread-n等。
2、实现Runnable接口创建线程类使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。
-
定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
-
创建 Runnable实现类的实例,并以此实例作为Thread的target来创Thread对象,该Thread对象才是真正的线程对象。
//创建Runnable实现类对象 SecondThread st = new SecondThread(); //以Runnable实现类的对象作为Thread的target来创建Thread对象 new Thread(st);
也可以在创建Thread对象时为该Thread对象指定一个名字
//创建Thread对象时为该对象指定target和新线程的名字 new Thread(st,"新线程1")
-
调用线程对象的start()方法来启动该线程。
示例代码为:
//实现runnable接口
public class SecondThread implements Runnable{
private int i;
//run()方法同样是线程执行体
@Override//提示你接下来的这个方法需要重写,要是不重写的话会出错。
public void run() {
for( ; i<100 ; i++){
//当线程实现Runnable接口时
//如果想要获取当前线程时,只能用Thread.currevtThread()方法
System.out.println(Thread.currentThread().getName() + " " +i);
}
}
public static void main(String[] args){
for (int i = 0 ; i<100 ; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
SecondThread st = new SecondThread();
//new Thread(target,name)方法创建新线程
new Thread(st,"新线程1").start();
new Thread(st,"新线程2").start();
}
}
}
}
线程的执行流程很简单,当执行代码start()时,就会执行对象中重写的void run();方法,该方法执行完成后,线程就消亡了。
对比发现,通过继承Thread类来获得当前线程对象比较简单,直接使用this 就可以了;但通过实现Runnable 接口来获得当前线程对象,则必须使用Thread.currentThread()方法。
运行上面的代码会发现很奇妙的事情,两个子线程的 i 变量是连续的,也就是这种方式创建的多个线程可以共享线程类的实例变量。这是因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例变量。
3、通过Callable和Future创建线程创建并启动有返回值的线程的步骤:
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值,创建Callable实现类的实例,从Java8开始可以直接使用Lambda表达式创建Callable对象。
public interface Callable
{
V call() throws Exception;
}
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class CallableThreadTest implements Callable{ public static void main(String[] args) { CallableThreadTest ctt = new CallableThreadTest(); FutureTask ft = new FutureTask<>(ctt); for(int i = 0;i < 100;i++) { System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i); if(i==20) { new Thread(ft,"有返回值的线程").start(); } } try { System.out.println("子线程的返回值:"+ft.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } @Override public Integer call() throws Exception { int i = 0; for(;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } return i; } }
线程创建和匿名内部类
创建线程的三种方式的对比1、采用实现Runnable、Callable接口的方式创建多线程时,
-
优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
-
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2、使用继承Thread类的方式创建多线程时,
-
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
-
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
3、Runnable和Callable的区别
- Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
- call方法可以抛出异常,run方法不可以。
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
线程的生命周期
经过五种状态:
新建(New),就绪(Runnable),运行(Running),阻塞(Blocked),死亡(Dead)
-
用new关键字新建:新建状态(分配内存)
-
线程对象调用start()方法后:就绪状态(创建方法调用栈以及程序计数器)记住永远不要调用run()方法!!!
如果希望调用子线程的start()方法后子线程立即开始执行,程序可以使用Thread.sleep(1)来让当前运行的线程(主线程)睡眠1毫秒,1毫秒就够了,因为在这1毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。
-
若处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,就会进入运行状态,当一个线程开始运行后,它不可能一直处于运行状态 (除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。
-
当发生如下情况时,线程将会进入阻塞状态。
- 线程调用sleep()方法主动放弃所占用的处理器资源。
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
- 线程在等待某个通知(notify)。
- 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。
-
被阻塞的线程会在何时的时候重新进入就绪状态,但是不能直接进入运行状态。
针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态。
- 调用sleep()方法的线程经过了指定时间。
- 线程调用的阻塞式IO方法已经返回。
- 线程成功地获得了试图取得的同步监视器。
- 线程正在等待某个通知时,其他线程发出了一个通知。
- 处于挂起状态的线程被调用了resume()恢复方法。
-
就绪与运行之间的转换是由系统线程调度决定的,当就绪态的线程获得处理器资源时候就进入运行态,当处于运行态的线程失去处理器资源时候,线程会进入就绪态。
例外:调用yield()方法可以让运行态的线程转入就绪态。
-
线程会以如下三种方式结束,结束后就处于死亡状态。
- **run()或call()**方法执行完成,线程正常结束。
- 线程抛出一个未捕获的 Exception或 Error。
- 直接调用该线程的**stop()**方法来结束该线程-----该方法容易导致死锁,通常不推荐使用。
一旦子线程启用起来,就拥有了和主线程相同的地位,主线程结束其他线程不会受影响。
为了测试某个线程是否已经死亡,可以调用线程对象的**isAlive()**方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回true;当线程处于新建、死亡两种状态时,该方法将返回false。
不要对处于死亡状态的线程调用start()方法,程序只能对新建状态的线程调用start()方法,对新建状态的线程两次调用start()方法也是错误的。这都会引发IllegalThreadStateException异常。
控制线程 join线程
join线程可以让一个线程等待另一个线程执行完毕以后再执行。当在某程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()房啊加入的线程执行完毕为止。
join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。
public class JoinThread extends Thread{
//提供一个有参数的构造器,用于设置该线程的名字
public JoinThread(String name){
super(name);
}
//重写run()方法,定义线程执行体
public void run(){
for (int i=0 ; i<100; i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws InterruptedException {
//启动子线程
new JoinThread("新线程").start();
for (int i=0; i<100; i++){
if (i==20){
JoinThread jt = new JoinThread("被Join的线程");
jt.start();
//main线程调用了jt线程的join()方法,main线程必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
//部分结果如下:
被Join的线程 94
被Join的线程 95
被Join的线程 96
被Join的线程 97
被Join的线程 98
被Join的线程 99
main 20
main 21
main 22
main 23
main 24
//一开始,主线程开始时就启动了名为“新线程”的子线程,二者并发执行;当i=20时,启动了名为“被Join的线程”的线程,此时2个子线程并发执行,而主线程会一直处于阻塞状态,知道“被Join的线程”的线程执行完成。
join方法有三个重载方法:
- join(): 等待被join的线程执行完成
- join(long millis): 等待被join的线程最长时间为millis毫秒,若在millis毫秒内被join的线程还没执行完,就不再等待
- join(long millis,int nanos): 等待被join的线程最长时间为millis毫秒+nanos毫微秒(这个方法很少使用)
我们尝试写一个代码,T1、T2、T3三个线程,保证T2在T1执行完后执行,T3在T2执行完后执行,T1代表听歌,T2代表吃饭,T3代表学习。
public class demo1 {
public static void main(String[] args){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<4;i++){
System.out.println("听歌"+" "+i);
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<4;i++){
System.out.println("吃饭"+" "+i);
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i=0;i<4;i++){
System.out.println("学习"+" "+i);
}
}
});
thread.start();
thread1.start();
thread2.start();
}
}
//结果
听歌 0
听歌 1
听歌 2
听歌 3
吃饭 0
吃饭 1
吃饭 2
吃饭 3
学习 0
学习 1
学习 2
学习 3
后台进程
java中有一种线程只在后台运行,为其他线程提供服务,这种线程就是后台线程(Daemon Thread)。也叫守护线程,精灵线程。JVM的垃圾回收线程就是典型的后台线程。
后台线程的特点就是,当所有的前台线程进入死亡状态以后,后台线程也会随之死亡,因为他就是为其他线程服务的,其他线程都没了,他也就没有存在的意义了。
调用Thread对象的**setDaemon(true)**方法可将指定线程设置成后台线程。
举个栗子吧!有家西餐厅在餐厅有客人时候一直会有专人拉小提琴,当最后一位客人离开时,琴声也随之停止(意思清楚即可):
public class TestDaemon {
public static void main(String[] args){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("拉小提琴中"+" "+i);
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i=10; i>=0;i--){
System.out.println("还有"+i+"位客人");
if (i==0){
System.out.println("客人已经全部离开");
}
}
}
});
thread.setDaemon(true);
thread.start();
thread1.start();
}
}
//结果 可以看出客人走完后拉小提琴的线程也会进入死亡状态
拉小提琴中 0
拉小提琴中 1
还有10位客人
还有9位客人
还有8位客人
还有7位客人
还有6位客人
还有5位客人
还有4位客人
还有3位客人
还有2位客人
还有1位客人
还有0位客人
客人已经全部离开
拉小提琴中 2
拉小提琴中 3
拉小提琴中 4
拉小提琴中 5
拉小提琴中 6
拉小提琴中 7
拉小提琴中 8
拉小提琴中 9
拉小提琴中 10
拉小提琴中 11
Process finished with exit code 0
前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说,setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException 异常。
线程睡眠:sleep线程对象的sleep()方法,一般用来暂停线程,使线程从运行状态转到阻塞状态,直至sleep()时间结束,再重新回到就绪状态。
public class SleepTest {
public static void main(String[] args) throws InterruptedException {
for (int i=0; i<10;i++){
System.out.println("当前时间:"+ new Date());
//调用sleep()方法让当前线程暂停1s
Thread.sleep(1000);
}
}
}
//结果
//每隔一秒输出一条字符串
当前时间:Sat Oct 09 22:58:59 CST 2021
当前时间:Sat Oct 09 22:59:00 CST 2021
当前时间:Sat Oct 09 22:59:01 CST 2021
当前时间:Sat Oct 09 22:59:02 CST 2021
当前时间:Sat Oct 09 22:59:03 CST 2021
当前时间:Sat Oct 09 22:59:04 CST 2021
当前时间:Sat Oct 09 22:59:05 CST 2021
当前时间:Sat Oct 09 22:59:06 CST 2021
当前时间:Sat Oct 09 22:59:07 CST 2021
当前时间:Sat Oct 09 22:59:08 CST 2021
和join方法类似,sleep()方法也有两个重载方法,用来指定线程休眠的时间:
- sleep(long milis):
- sleep(long milis,int nanos):
yield()方法和sleep()方法不同,sleep()会让线程回到阻塞状态,而yield()方法会让线程回到就绪状态,直接等到cpu重新分配资源,但只有优先级和该线程相等或大于该线程的其他线程才有机会被执行。使用sleep()方法需要捕获异常,yield()不需要;sleep()方法比yield()方法有更好的移植性,不建议使用yield();
yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。
public class YieldTest extends Thread{
public YieldTest(String name){
super(name);
}
//定义run()方法作为线程执行体
public void run(){
for (int i =0; i<50;i++){
System.out.println(getName()+" "+i);
if (i==20){
Thread.yield();
}
}
}
public static void main(String[] args){
//启动两个并发线程
YieldTest yt1 = new YieldTest("高级");
//将yt1设置为最高优先级
yt1.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldTest yt2 = new YieldTest("低级");
yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}
将设置优先级的那两行代码注释会得到不一样的结果,可以自己运行着看一下。
改变线程优先级每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。
Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中 setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以使用Thread类的如下三个静态常量。
- MAX_PRIORITY:其值是10。
- MIN_PRIORITY:其值是1。
- NORM_PRIORITY:其值是5。
线程的优先级
线程同步
当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。
同步代码块为了上述问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:
synchronized (同步监视器){
同步代码块;
}
上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
虽然Java程序允许使用任何对象作为同步监视器,但想一下同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
同步方法与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征。
- 该类的对象可以被多个线程安全地访问。
- 每个线程调用该对象的任意方法之后都将得到正确结果。
- 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
—《时光背面的我》
遇见是场意外
心却在风里摇摆
你的笑被夏季晕开
镜子前的彩排
我反复练习告白
常常在我梦里徘徊
玫瑰的名字若为爱
为何凋零成花海



