一.线程概述
1.进程与线程
1.并发性和并行性
并行指在同一时刻,有多条指令在多个处理器上同时执行;
并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
2.多线程的优势
多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段,进程的公有数据等,利用这些共享的数据很容易实现线程间的通信。
总结,使用多线程编程具有如下优点:
1> 进程之间不能共享内存,但线程之间共享内存非常容易;
2> 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
3> Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化Java的多线程编程。
二.线程的创建和启动
1.继承Thread类创建线程类
1> 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务,因此把run方法称为线程执行体;
2> 创建Thread子类的实例,即创建了线程对象;
3> 调用线程对象的start方法来启动该线程。
// 通过继承Thread类来创建线程类
public class FirstThread extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 当线程类继承Thread类时,直接使用this即可获取当前线程
// Thread对象的getName()返回当前该线程的名字
// 因此可以直接调用getName()方法返回当前线程的名
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
// 调用Thread的currentThread方法获取当前线程
//currentThread是Thread类的静态方法,返回当前正在执行的线程对象
//getName是Thread类的实例方法,该方法返回调用该方法的线程名字
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
// 创建、并启动第一条线程
new FirstThread().start();
// 创建、并启动第二条线程
new FirstThread().start();
}
}
}
}
tips:
程序可以通过setName(String name)方法为线程设置名字,也可以通过getName方法返回指定线程的名字,在默认情况下,主线程的名字为main,用户启动的多个线程的名字依次为Thread-0…
使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实列变量的。
2.实现Runnable接口创建线程类
1>定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体;
2>创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3> 调用线程对象的start方法来启动该线程。
// 通过实现Runnable接口来创建线程类
public class SecondThread implements Runnable
{
private int i ;
// run方法同样是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 当线程类实现Runnable接口时,
// 如果想获取当前线程,只能用Thread.currentThread()方法。
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();
}
}
}
}
3.使用Callable和Future创建线程
Callable接口提供了一个call方法可以作为线程执行体,但call()方法比run()方法功能更强大。
call()方法可以有返回值。
call()方法可以声明抛出异常。
3.1 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例。
3.2 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3.3 使用FutureTask对象作为Thread对象的target创建并启动新线程。
3.4 调用FutureTask对象的get方法来获取子线程执行结束后的返回值。
import java.util.concurrent.*;
public class ThirdThread
{
public static void main(String[] args)
{
// 创建Callable对象
ThirdThread rt = new ThirdThread();
// 先使用Lambda表达式创建Callable<Integer>对象
// 使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)() -> {
int i = 0;
for ( ; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
}
// call()方法可以有返回值
return i;
});
for (int 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 ex)
{
ex.printStackTrace();
}
}
}
上面中使用Lambda表达式直接创建了Callable对象,这样就无须先创建Callable实现类,再创建Callable对象了。
三.线程的生命周期
在线程的生命周期中,需要经过新建(New), 就绪(Runnable), 运行(Running), 阻塞(Blocked),死亡(Dead) 5种状态。
1.新建和就绪状态
当使用new关键字创建一个线程之后,该线程就处于新建状态。线程对象调用start方法后,线程就处于就绪状态。切换到运行时是由底层平台控制,具有一定的随机性。
如果直接调用线程对象的run方法,系统把线程对象当成一个普通对象,而run方法也是一个普通方法,而不是线程执行体。
public class InvokeRun extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 直接调用run方法时,Thread的this.getName返回的是该对象名字,
// 而不是当前线程的名字。
// 使用Thread.currentThread().getName()总是获取当前线程名字
System.out.println(Thread.currentThread().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)
{
// 直接调用线程对象的run方法,
// 系统会把线程对象当成普通对象,run方法当成普通方法,
// 所以下面两行代码并不会启动两条线程,而是依次执行两个run方法
new InvokeRun().run();
new InvokeRun().run();
}
}
}
}
2.运行和阻塞状态
当发生如下情况时,线程会进入阻塞状态:
2.1 线程调用sleep方法主动放弃所占用的处理器资源;
2.2 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;
2.3 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;
2.4 线程等待某个通知notify;
2.5 程序调用了线程的suspend方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。
3.线程死亡
线程处于死亡方式:
3.1 run()或call()方法执行完成,线程正常结束;
3.2 线程抛出一个未捕获的Exception或Error;
3.3 直接调用该线程的stop方法来结束该线程,容易导致死锁,慎用;
为了测试某个线程是否已经死亡,可以调用线程对象的isAlive方法,当线程处于就绪,运行,阻塞三种状态时,该方法将返回true;当线程处于新建,死亡两种状态时,该方法将返回false.
public class StartDead extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
// 创建线程对象
StartDead sd = new StartDead();
for (int i = 0; i < 300; i++)
{
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
// 启动线程
sd.start();
// 判断启动后线程的isAlive()值,输出true
System.out.println(sd.isAlive());
}
// 只有当线程处于新建、死亡两种状态时isAlive()方法返回false。
// 当i > 20,则该线程肯定已经启动过了,如果sd.isAlive()为假时,
// 那只能是死亡状态了。
if (i > 20 && !sd.isAlive())
{
// 试图再次启动该线程,引发IllegalThreadState Exception异常
sd.start();
}
}
}
}
不要对处于死亡状态的线程调用start方法,程序只能对新建状态的线程调用start方法,对新建状态的线程两次调用start方法也是错误的,这会引发IllegalThreadState Exception异常。
四.控制线程
1 join线程
Thread提供了让一个线程等待另一个线程完成的方法----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 Exception
{
// 启动子线程
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的线程 执行完成主线程才能继续执行。
join方法有三种重载的形式:
join():等待被join的线程执行完成;
join(long millis):等待被join的线程的时间最长为millis毫秒
join(long millis, int nanos):等待被join的线程的时间最长为millis毫秒加nanos毫微秒。
2.后台线程
Thread对象的setDaemon(true)方法可将指定线程设置成后台线程,当所以的前台线程死亡时,后台线程随之死亡,当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就退出了的。
public class DaemonThread extends Thread
{
// 定义后台线程的线程执行体与普通线程没有任何区别
public void run()
{
for (int i = 0; i < 1000 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
DaemonThread t = new DaemonThread();
// 将此线程设置成后台线程
t.setDaemon(true);
// 启动后台线程
t.start();
for (int i = 0 ; i < 10 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
// -----程序执行到此处,前台线程(main线程)结束------
// 后台线程也应该随之结束
}
}
3.线程睡眠:sleep
//延时毫秒级别
Thread.sleep(1000);
4 线程让步:yield
是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,不会阻塞该线程,只是将该线程转入就绪状态。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);
// 当i等于20时,使用yield方法让当前线程让步
if (i == 20)
{
Thread.yield();
}
}
}
public static void main(String[] args)throws Exception
{
// 启动两条并发线程
YieldTest yt1 = new YieldTest("高级");
// 将ty1线程设置成最高优先级
yt1.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldTest yt2 = new YieldTest("低级");
// 将yt2线程设置成最低优先级
yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}
如果设置成优先级相同的情况下,线程调用yield之后将执行机会让给优先级相同的其他线程
如果在多cpu并行的环境下,yield方法可能不是明显。
5.改变线程优先级
Thread类提供了setPriority(int newPriority),getPriority()方法来设置和返回指定线程的优先级,其中setPriority方法的参数可以是一个整数,范围是1-10之间,
MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY: 5
Thread.currentThread().setPriority(6);