Fork me on GitHub

Java基础知识回顾之五 ----- 多线程

前言

上一篇文章中,回顾了Java的集合。而在本篇文章中主要介绍多线程的相关知识。主要介绍的知识点为线程的介绍、多线程的使用、以及在多线程中使用的一些方法。

线程和进程

线程

表示进程中负责程序执行的执行单元,依靠程序进行运行。线程是程序中的顺序控制流,只能使用分配给程序的资源和环境。

进程

表示资源的分配和调度的一个独立单元,通常表示为执行中的程序。一个进程至少包含一个线程。

进程和线程的区别

  1. 进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间;
  2. 进程是资源分配和拥有的单位,而同一个进程内的线程共享进程的资源;
  3. 线程是处理器调度的基本单位,但进程不是;

生命周期

线程和进程一样分为五个阶段:创建就绪运行阻塞终止

  • 新建状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start() 这个线程。
  • 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
  • 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  • 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
  • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
  • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
  • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

可以用下述图来进行理解线程的生命周期:
 

注:上述图来自http://www.runoob.com/wp-content/uploads/2014/01/java-thread.jpg。

在了解了线程和进程之后,我们再来简单的了解下单线程和多线程。
单线程
程序中只存在一个线程,实际上主方法就是一个主线程。

多线程
多线程是指在同一程序中有多个顺序流在执行。 简单的说就是在一个程序中有多个任务运行。

那么在什么情况下用多线程呢?

一般来说,程序中有两个以上的子系统需要并发执行的,这时候就需要利用多线程编程。通过对多线程的使用,可以编写出高效的程序。

那么是不是使用很多线程就能提高效率呢?

不一定的。因为程序中上下文的切换开销也很重要,如果创建了太多的线程,CPU
花费在上下文的切换的时间将多于执行程序的时间!这时是会降低程序执行效率的。

所以有效利用多线程的关键是理解程序是并发执行而不是串行执行的。

线程的创建

一般来说,我们在对线程进行创建的时候,一般是继承Thread 类或实现Runnable 接口。其实还有一种方式是实现 Callable接口,然后与Future 或线程池结合使用, 类似于Runnable接口,但是就功能上来说更为强大一些,也就是被执行之后,可以拿到返回值。

这里我们分别一个例子使用继承Thread 类、实现Runnable 接口和实现Callable接口与Future结合来进行创建线程。
代码示例:
注:线程启动的方法是start而不是run。因为使用start方法整个线程处于就绪状态,等待虚拟机来进行调度。而使用run,也就是当作了一个普通的方法进行启动,这样虚拟机不会进行线程调度,虚拟机会执行这个方法直到结束后自动退出。

代码示例:

public class Test {
	public static void main(String[] args) {
		ThreadTest threadTest=new ThreadTest();
		threadTest.start();

		RunalbeTest runalbeTest=new RunalbeTest();
		Thread thread=new Thread(runalbeTest);
		thread.start();
		
		CallableTest callableTest=new CallableTest();
		FutureTask<Integer> ft = new FutureTask<Integer>(callableTest);  
		Thread thread2=new Thread(ft);
		thread2.start();
		try {
			System.out.println("返回值:"+ft.get());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
	}
}

class ThreadTest extends Thread{
	 @Override
     public void run() {
        System.out.println("这是一个Thread的线程!");
    }
}

class RunalbeTest implements Runnable{
	 @Override
     public void run() {
        System.out.println("这是一个Runnable的线程!");
    }
}

class CallableTest implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		 System.out.println("这是一个Callable的线程!");  
		return 2;
	}
}

运行结果:

	这是一个Thread的线程!
	这是一个Runnable的线程!
	这是一个Callable的线程!
	返回值:2

通过上述示例代码中,我们发现使用继承 Thread 类的方式创建线程时,编写最为简单。而使用Runnable、Callable 接口的方式创建线程的时候,需要通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用start方法来运行线程代码。顺便说下,其实Thread类实际上也是实现了Runnable接口的一个类。

但是在这里,我推荐大家创建单线程的时候使用继承 Thread 类方式创建,多线线程的时候使用Runnable、Callable 接口的方式来创建创建线程。
至于为什么呢?在下面中的描述已给出理由。

  • 继承 Thread 类创建的线程,可以直接使用Thread类中的方法,比如休眠直接就可以使用sleep方法,而不必在前面加个Thread;获取当前线程Id,只需调用getId就行,而不必使用Thread.currentThread().getId() 这么一长串的代码。但是使用Thread 类创建的线程,也有其局限性。比如资源不能共享,无法放入线程池中等等。
  • 使用Runnable、Callable 接口的方式创建的线程,可以实现资源共享,增强代码的复用性,并且可以避免单继承的局限性,可以和线程池完美结合。但是也有不好的,就是写起来不太方便,使用其中的方法不够简介。

总的来说就是,单线程建议用继承 Thread 类创建,多线程建议- 使用Runnable、Callable 接口的方式创建。

线程的一些常用方法

yield

使用yield方法表示暂停当前正在执行的线程对象,并执行其他线程。

代码示例:

public class YieldTest {
	public static void main(String[] args) {
		Test1 t1 = new Test1("张三");
		Test1 t2 = new Test1("李四");
		new Thread(t1).start();
		new Thread(t2).start();
	}
}

class Test1 implements Runnable {
	private String name;
	public Test1(String name) {
		this.name=name;
	}
	@Override
	public void run() {
        System.out.println(this.name + " 线程运行开始!");  
		for (int i = 1; i <= 5; i++) {
            System.out.println(""+this.name + "-----" + i);  
			// 当为3的时候,让出资源
			if (i == 3) {
				Thread.yield();
			}
		}
        System.out.println(this.name + " 线程运行结束!");  
	}
}

执行结果一:

	张三 线程运行开始!
	张三-----1
	张三-----2
	张三-----3
	李四 线程运行开始!
	李四-----1
	李四-----2
	李四-----3
	张三-----4
	张三-----5
	张三 线程运行结束!
	李四-----4
	李四-----5
	李四 线程运行结束!

执行结果二:

张三 线程运行开始!
李四 线程运行开始!
李四-----1
李四-----2
李四-----3
张三-----1
张三-----2
张三-----3
李四-----4
李四-----5
李四 线程运行结束!
张三-----4
张三-----5
张三 线程运行结束!

上述中的例子我们可以看到,启动两个线程之后,哪个线程先执行到3,就会让出资源,让另一个线程执行。
在这里顺便说下,yieldsleep的区别。

  • yield: yield只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
  • sleep:sleep使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;

join

使用join方法指等待某个线程终止。也就是说当子线程调用了join方法之后,后面的代码只有等待该线程执行完毕之后才会执行。

如果不好理解,这里依旧使用一段代码来进行说明。
这里我们创建两个线程,并使用main方法执行。顺便提一下,其实main方法也是个线程。如果直接执行的话,可能main方法执行完毕了,子线程还没执行完毕,这里我们就让子线程使用join方法使main方法最后执行。

代码示例:

public class JoinTest {
	public static void main(String[] args) {
		 System.out.println(Thread.currentThread().getName()+ "主线程开始运行!");  
		 Test2 t1=new Test2("A");  
		 Test2 t2=new Test2("B");  
		 t1.start();  
	     t2.start();  
	      try {  
	    	  t1.join();  
	        } catch (InterruptedException e) {  
	            e.printStackTrace();  
	        }  
	        try {  
	        	t2.join();  
	        } catch (InterruptedException e) {  
	            e.printStackTrace();  
	        }    
	     System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");  
	}

}

class Test2 extends Thread{  
    public Test2(String name) {  
        super(name);  
    }  
    public void run() {  
         System.out.println(this.getName() + " 线程运行开始!");  
       for (int i = 0; i < 5; i++) {  
           System.out.println("子线程"+this.getName() + "运行 : " + i);  
           try {  
               sleep(new Random().nextInt(10));  
           } catch (InterruptedException e) {  
               e.printStackTrace();  
           }  
       }  
       System.out.println(this.getName() + " 线程运行结束!");  
   }
}

执行结果:

	main主线程开始运行!
	B 线程运行开始!
	子线程B运行 : 0
	A 线程运行开始!
	子线程A运行 : 0
	子线程A运行 : 1
	子线程B运行 : 1
	子线程B运行 : 2
	子线程B运行 : 3
	子线程B运行 : 4
	B 线程运行结束!
	子线程A运行 : 2
	子线程A运行 : 3
	子线程A运行 : 4
	A 线程运行结束!
	main主线程运行结束!

上述示例中的结果显然符合我们的预期。

priority

使用setPriority表示设置线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式

  • static int MAX_PRIORITY 线程可以具有的最高优先级,取值为10。
  • static int MIN_PRIORITY 线程可以具有的最低优先级,取值为1。
  • static int NORM_PRIORITY 分配给线程的默认优先级,取值为5。

但是设置优先级并不能保证线程一定先执行。我们可以通过一下代码来验证。

代码示例:

public class PriorityTest {
  public static void main(String[] args) {
		Test3 t1 = new Test3("张三");
		Test3 t2 = new Test3("李四");
		t1.setPriority(Thread.MIN_PRIORITY);
		t2.setPriority(Thread.MAX_PRIORITY);
		t1.start();
		t2.start();
	}
}

class Test3 extends Thread {
	public Test3(String name) {
		super(name);
	}
	@Override
	public void run() {
        System.out.println(this.getName() + " 线程运行开始!");  
		for (int i = 1; i <= 5; i++) {
            System.out.println("子线程"+this.getName() + "运行 : " + i); 
            try {  
                sleep(new Random().nextInt(10));  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } 
		}
        System.out.println(this.getName() + " 线程运行结束!");  
	}
}

执行结果一:

李四 线程运行开始!
子线程李四运行 : 1
张三 线程运行开始!
子线程张三运行 : 1
子线程张三运行 : 2
子线程李四运行 : 2
子线程李四运行 : 3
子线程李四运行 : 4
子线程张三运行 : 3
子线程李四运行 : 5
李四 线程运行结束!
子线程张三运行 : 4
子线程张三运行 : 5
张三 线程运行结束!

执行结果二:

张三 线程运行开始!
子线程张三运行 : 1
李四 线程运行开始!
子线程李四运行 : 1
子线程张三运行 : 2
子线程张三运行 : 3
子线程李四运行 : 2
子线程张三运行 : 4
子线程李四运行 : 3
子线程张三运行 : 5
子线程李四运行 : 4
张三 线程运行结束!
子线程李四运行 : 5
李四 线程运行结束!

执行结果三:

李四 线程运行开始!
子线程李四运行 : 1
张三 线程运行开始!
子线程张三运行 : 1
子线程李四运行 : 2
子线程李四运行 : 3
子线程李四运行 : 4
子线程张三运行 : 2
子线程张三运行 : 3
子线程张三运行 : 4
子线程李四运行 : 5
子线程张三运行 : 5
李四 线程运行结束!
张三 线程运行结束!

线程中一些常用的方法

线程中还有许多方法,但是这里并不会全部细说。只简单的列举了几个方法使用。更多的方法使用可以查看相关的API文档。这里我也顺便总结了一些关于这些方法的描述。

  1. sleep:在指定的毫秒数内让当前正在执行的线程休眠(暂停执行);不会释放对象锁。
  2. join:指等待t线程终止。
  3. yield:暂停当前正在执行的线程对象,并执行其他线程。
  4. setPriority:设置一个线程的优先级。
  5. interrupt:一个线程是否为守护线程。
  6. wait:强迫一个线程等待。它是Object的方法,也常常和sleep作为比较。需要注意的是wait会释放对象锁,让其它的线程可以访问;使用wait必须要进行异常捕获,并且要对当前所调用,即必须采用synchronized中的对象。
  7. isAlive: 判断一个线程是否存活。
  8. activeCount: 程序中活跃的线程数。
  9. enumerate: 枚举程序中的线程。
  10. currentThread: 得到当前线程。
  11. setDaemon: 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)。
  12. setName: 为线程设置一个名称。
  13. notify(): 通知一个线程继续运行。它也是Object的一个方法,经常和wait方法一起使用。

结语

其实这篇文章很久之前都已经打好草稿了,但是由于各种原因,只到今天才写完。虽然也只是简单的介绍了一下多线程的相关知识,也只能算个入门级的教程吧。不过写完之后,感觉自己又重新复习了一遍多线程,对多线程的理解又加深了一些。
话已尽此,不在多说。
原创不易,如果感觉不错,希望给个推荐!您的支持是我写作的最大动力!

参考:https://blog.csdn.net/evankaka/article/details/44153709#t1

版权声明:
作者:虚无境
博客园出处:http://www.cnblogs.com/xuwujing
CSDN出处:http://blog.csdn.net/qazwsxpcm    
个人博客出处:http://www.panchengming.com

posted @ 2018-05-28 23:17  虚无境  阅读(2177)  评论(6编辑  收藏  举报