线程概述
- 线程和进程
操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。(一个程序运行后至少有一个进程,一个进程至少包含一个线程) - 多线程的优势
(相较于多进程的优势)
- 进程之间不能共享内存,而线程非常容易
- 创建进程要重新分配资源,但创建线程代价小得多,使用线程实现并发任务效率高。
- java内置多线程支持,方便调度。
线程的创建和启动
- 继承Thread类创建线程类
- 进行多线程编程时不要忘记了Java程序运行时默认的主线程,main()方法的方法体就是主线程线程的执行体
- 使用继承Thread类的方法来创建线程类时,多个线程间无法共享线程类的实例变量。(从下面代码可以看出线程1和线程2打印出的i变量是不连续的)
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 (int i = 0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
new FirstThread().start();
new FirstThread().start();
}
}
}
}
main 0
// main 1
// main 2
// main 3
// main 4
// main 5
// main 6
// Thread-1 0
// Thread-1 1
// Thread-0 0
// Thread-1 2
// Thread-1 3
// Thread-1 4
// Thread-1 5
// Thread-1 6
// main 7
// main 8
// Thread-1 7
// Thread-1 8
// Thread-1 9
// Thread-1 10
// Thread-1 11
// Thread-1 12
// Thread-1 13
// Thread-1 14
// Thread-0 1
// 等等等
- 实现Runnable接口创建线程类
从下面例子可以看出,采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。(i是连续打印的)
public class SecondThread implements Runnable {
private int i;
@Override
public void run() {
for (;i<20;i++){
// 此处与Thread方式不同
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public SecondThread() {
super();
}
public static void main(String[] args) {
for (int i = 0;i<20;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==5){
SecondThread st = new SecondThread();
new Thread(st,"新线程1").start();
new Thread(st,"新线程2").start();
}
}
}
// main 0
// main 1
// main 2
// main 3
// main 4
// main 5
// 新线程1 0
// 新线程1 1
// 新线程1 2
// 新线程1 3
// 新线程1 4
// 新线程1 5
// 新线程1 6
// 新线程1 7
// 新线程1 8
// 新线程1 9
// 新线程2 9
// 新线程1 10
// 新线程2 11
// 新线程1 12
// 新线程2 13
// 新线程1 14
// 新线程2 15
// 新线程1 16
// 新线程1 18
// 新线程1 19
// 新线程2 17
// main 6
// main 7
// main 8
// main 9
// main 10
// main 11
// main 12
// main 13
// main 14
// main 15
// main 16
// main 17
// main 18
// main 19
}
- 使用Callable和 FutureTask创建线程
public class ThirdThread {
public static void main(String[] args) {
ThirdThread rt = new ThirdThread();
FutureTask<Integer> task = new FutureTask<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();
}
}
}
- 创建线程的三种方式对比
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口(可以和实现Runnable接口算为一种方式,不同之处是Callable定义的方法中有返回值,允许抛异常)
- 实现Runnable、Callable接口的方式中,多个线程可以共享一个target对象所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。还有….综上,一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
线程的生命周期
线程的生命周期中要经过新建(New),就绪(Runnable),运行(Running),阻塞(Blocked)、死亡(Dead)5中状态
* 新建和就绪状态
使用new关键字创建一个线程后->新建状态
调用start()方法->就绪状态(start()方法只能是新建的进程调用)
* 运行和阻塞状态
运行状态:处于就绪状态的线程获得了CPU资源,则变为运行状态
阻塞状态:发生5种情况,线程变为阻塞状态,如果阻塞状态解除,线程要变为就绪状态。
* 线程死亡
线程死亡,3种情况:
1. run()方法或call()方法执行结束
2. 线程抛出一个未捕获的Exception或Error
3. 直接调用该线程的stop()方法来结束该线程–该方法容易导致死锁,不推荐
4. 可以使用该线程的isAlive()方法来判断线程的状态,如果返回false,则该线程处于新建或死亡状态
控制线程
- join线程
Thread提供了让一个线程等待另一个线程完成的方法-join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
public class JoinThread extends Thread{
public JoinThread(String name){
super(name);
}
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println(getName()+i);
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
if(i==1){
JoinThread jt = new JoinThread("被join线程");
jt.start();
jt.join();
}
System.out.println(Thread.currentThread().getName()+i);
}
// 输出如下
/*
main0
被join线程0
被join线程1
被join线程2
被join线程3
被join线程4
main1
main2
main3
main4
*/
}
}
- 后台线程
1.后台线程(Daemon Thread),又被称作“守护线程”,“精灵线程”。JVM的垃圾回收线程就是典型的后台进程。
- 后台线程有个特征:如果所有前台线程都死亡,后台线程会自动死亡
- 调用Thread对象的setDaemon(true)方法,可将线程设置为后台线程
- Thread类提供了isDaemon()方法,可判断是否是后台线程
- 前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程,主线程默认是前台线程
- 线程睡眠:sleep
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep方法来实现。
// 输出间隔1s
public class SleepTest extends Thread {
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<10;i++){
Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+i);
}
}
}
- 线程让步:yield
1.yield()方法与sleep()方法类似,也是Thread类提供的一个静态方法,但是它不是阻塞该线程,而是使该线程进入就绪状态,从而让线程调度器重新调度一下(只有优先级与当前线程小童或优先级比当前线程更高的线程才会获得执行的机会)
- 多CPU下yield()方法的功能有时候并不明显,可能看不到明显效果
- sleep()方法和yield()方法的区别如下:
sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield方法来控制并发线程的执行
public class YieldTest extends Thread {
public YieldTest(String name){
super(name);
}
@Override
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.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldTest yt2 = new YieldTest("低级");
// yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}
- 改变线程优先级
1.优先级高的线程通常有较多的执行机会,可使用Thread类的静态常亮设置,或setPriority()方法(1-10整数):Thread.MAX_PRIORITY(10),Thread.MIN_PRIORITY(1),Thread.NORM_PRIORITY(5)
- 尽量避免直接指定优先级,因为不同操作系统支持不同;推荐使用常量指定,这样可移植性较好
public class PriorityTest extends Thread {
public PriorityTest(String name){
super(name);
}
@Override
public void run() {
for(int i=0;i<50;i++){
System.out.println(getName()+",优先级是:"+getPriority()+",循环变量的值为:"+i);
}
}
public static void main(String[] args) {
// 改变主线程的优先级
Thread.currentThread().setPriority(6);
for(int i=0;i<30;i++){
if(i==10){
PriorityTest low = new PriorityTest("低级");
low.start();
System.out.println("创建之初的优先级"+low.getPriority());
low.setPriority(Thread.MIN_PRIORITY);
}
if(i==20){
PriorityTest high = new PriorityTest("高级");
high.start();
System.out.println("创建之初的优先级"+high.getPriority());
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}