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

前言

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

线程和进程

线程

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

进程

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

进程和线程的区别

  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。

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

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

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

通常来讲,程序中有两个以上的子系统须要并发执行的,这时候就须要利用多线程编程。经过对多线程的使用,能够编写出高效的程序。this

那么是否是使用不少线程就能提升效率呢?spa

不必定的。由于程序中上下文的切换开销也很重要,若是建立了太多的线程,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

相关文章
相关标签/搜索