java线程学习总结(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ITdevil/article/details/78724148

(声明:并非原创,只是一个简单总结)

一、线程和进程的概念:

           进程:进程是处于运行过程中的程序,并且具有一定的对功能,是系统进行资源分配和调度的一个独立单位。

           线程:线程是进程的组成部分,一个进程至少拥有一个线程;线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它和父进程的其线程共享该进程的全部资源。(较为官方的定义)

以下解释来源于知乎网友回答,觉得非常好,在此引用:

1、单进程单线程:一个人在一个桌子上吃菜。

2、单进程多线程:多个人在同一个桌子上吃菜。

3、多进程单线程:多个人每个人在自己的桌子上吃菜。

           a.多线程的问题就是多个人同时吃一道菜的时候发生争抢,例如两个人同时夹一道菜,一个人刚深处筷子,结果伸到的时候菜已经被夹走了。。此时就必须等一个人夹一口之后,再夹菜,也就是说资源共享就会引发冲突争抢。

          b. 对于windows用户来说,【开桌子】的开销很大,因此windows鼓励大家在一个桌子上吃菜。因此windows多线程学习的重点是面对资源争抢与同步方面的问题。

          c.对于linux系统来说,【开桌子】的开销很小,因此linux鼓励大家尽量每个人都开自己的桌子吃菜。这带来的新的问题是:坐在两张不同的桌子上说话不方便。因此,linux下学习的重点是“进程”间的通讯方法。(这里需要强调的是,linux下并没有真正意义上的线程)

二、线程创建的三种方式以及对比(疯狂java讲义,P710)

          1.继承自Thread类创建线程类

              (1).定义thread类的子类,并重写该run方法,该run方法的执行体就代表了线程要完成的任务。因此run方法成为执行体。

              (2).创建了Thread子类实例,即创建了线程对象。

              (3).调用线程对象的start()方法来启动该线程。

public class ThreadTest1 extends Thread{
	private int i;
	
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(this.getName()+"----"+i);
		}
	}
	
	public static void main(String[] args){
		for (int i = 0; i < 100; i++) {
			//main()执行,会创建主线程,主线程时候main()确定的
			System.out.println(Thread.currentThread().getName()+"===="+i);//获取主线程的名字
			if(i==20){
				new ThreadTest1().start();//启动第一个线程
				new ThreadTest1().start();//启动第二个线程
			}
		}
	}
}
运行结果(部分):

main====20
main====21
main====22
Thread-0----0
Thread-1----0
main====23
Thread-1----1
Thread-0----1
Thread-0----2
         分析:主线程中存在判断,当i==20的时候,就启动第一个子线程Thread-0,但是为什么会在i==22时,才启动子线程呢?

这就要考虑到线程线程的生命周期了,线程有5种状态:新建、就绪、运行、消亡、阻塞。new ThreadTest1(),线程就被创建出来了,当调用start()方法时,线程就会处于就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是执行了t.start()此线程就会立即执行。(一步步深入,一口吃不成胖子)

        为什么Thread-0和Thread-1两个线程输出的i不连续?使用继承自Thread的方式来创建线程实例,多个线程之间是无法共享实例变量的。

        2.实现Runnable接口来创建线程类

           (1).定义Runnable接口的实现类,并重写该接口的run方法,该run()方法同样式该线程的执行体。

           (2).创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。(感觉不好理解,结合代码就so easy了)

public class ThreadTest2 implements Runnable{
	
	private int i;
	
	@Override
	public void run() {
		for (; i < 100; i++) {
			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){
				ThreadTest2 target=new ThreadTest2();
				new Thread(target).start();//这里:将ThreadTest2的对象作为,Thread类的target
				new Thread(target).start();//即new Thread(target)
			}
		}
	}
}
结果:
Thread-0----0
Thread-1----1
main==24
Thread-1----3
Thread-0----2
Thread-1----4
main==25
Thread-1----6
Thread-0----5
           分析:结果有两个点很重要,第一点:i 输出变成连续的了;第二点:i 输出虽然连续,但是顺序是乱的,为什么?

           首先由这么个规定:采用Runnable接口的方式创建的多个线程可以共享线程类的实例属性。这是因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享同一个线程(实际是线程的target类)的实例属性。

          为什么i的输出顺序是乱的,线程获取资源的方式:抢占式策略。当Thread-0本该输出Thread-0---2时,恰巧被Thread-1抢到了cpu,所以Thread-1---3先打印输出。

          3.使用Callable和Future创建线程(这种创建线程的方式经常被忽略,我也搞不懂。。。)

           前面两种方式已经很全面,很实用了,但是为什么还要有第三种方式呢?书中这样说到:通过实现Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程的执行体。那么是否可以直接把任意方法包装成线程的执行呢?Java目前不行!但是java的模仿者C#可以。受此启发,从java5开始,java提供了Callable接口,该接口怎么看都像是Runnable接口的增强版,Callable接口提供了一个call()方法可以作为线程的执行体,但call()方法比run()方法更强大。call()方法可以有返回值,可以声明抛出异常。
          因为Callable接口是java5新增的接口,而且不是Runnable的子接口,所以不能作为Thread的target,为此Java5提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口---可以作为Thread类的target.

          创建并启动有返回值的线程步骤如下:

          (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程的执行体,且该方法有返回值。

          (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

          (3)使用FutureTask对象作为Thread对象的target创建并启动线程。

          (4)调用FutureTask对象的get()方法来获得子线程结束后的返回值。

public class ThreadTest3 implements Callable<Integer>{
	
	//该方法作为线程的执行体
	@Override
	public Integer call() throws Exception {
		int i=0;
		for (; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"---"+i);
		}
		return i;
	}
	
	public static void main(String[] args) {
		//创建Callable对象
		ThreadTest3 rt = new ThreadTest3();
		//使用FutureTask来包装Callable对象
		FutureTask<Integer> task = new FutureTask<>(rt);
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"=="+i);
			if(i==20){
				new Thread(task).start();
			}
		}
		try{
			//获取线程的返回值
			System.out.println("子线程的返回值"+task.get());
		}catch(Exception e){
			e.printStackTrace();
		}
		
	}	
}

           4. 创建线程三种方式的对比:

            采用实现Runnable、Callable接口的方式创见多线程时,优势是:

            线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

            在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

           劣势是:

           编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。


           使用继承Thread类的方式创建多线程时,优势是

           编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

           劣势是

           线程类已经继承了Thread类,所以不能再继承其他父类。



三、线程的生命周期

         线程在被创建以后,在它的整个生命周期中,存在着以下五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Boocked)和死亡(Dead)。 

        1.新建和就绪状态

            (1)当程序使用New关键字创建了一个线程之后,该线程就处于新建状态,此时的线程对象没有任何的动态特征。

            (2)当线程对象调用了strat()方法之后,该线程处于就绪状态,处于就绪状态的线程并没有真正的运行,至于什么时候开始运行,取决于JVM里线程调度器的调度。                 (3)注意事项:启动线程是调用线程的start()方法,而不是run()方法,run()方法是线程的执行体---------线程要做的事情。如果我们调用了run()方法,会出现什么结果呢?那么就是线程对象被当作了普通对象,而线程的执行体run()方法,也被当成了普通的方法。代码如下:

public class ThreadRunTest implements Runnable{
	
	private int i=0;
	
	@Override
	public void run() {
		for (; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"---"+i);
		}
	}
	
	public static void main(String[] args) {
		ThreadRunTest target = new ThreadRunTest();
			
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"==="+i);
			if(i==20){
				Thread thread = new Thread(target);
				new Thread(target).run();    //调用的不是start()方法了
				new Thread(target).run();
			}
		}
	}
}

            打印结果:

main===17
main===18
main===19
main===20  //这里只是开始执行普通对象的普通方法
main---0
main---1
main---2
main---3
main---4
..........
main---96
main---97
main---98
main---99   //普通对象的普通方法即将执行完毕
main===21
main===22
main===23
main===24
main===25

              结果分析:从结果可以清晰的看出,整个程序只有一个主线程main,没有子线程。线程对象是普通对象,run()方法是普通方法,run()方法一经调用立即执行,在执行完毕之前不会并发执行其它线程。补充一点:正常线程对象调用run()方法之后,若已处于运行状态,则不能再调用start()方法了,否则会引发 IllegalThreadStateException异常。

           2.运行和阻塞状态

              (1)如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

              (2)当一个线程运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是为了给其它线程获得执行的机会。这时被中断的线程就处于阻塞态

              (3)当发生如下情况时,线程会进入阻塞状态。

                     》线程调用sleep方法主动放弃所占用的处理器资源。

                     》线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。

                     》线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有。(后面再写吧。。)

                     》线程等待某个通知(notify)

                     》程序调用了线程的suspend()方法将该线程挂起。但这个方法容易死锁,所以应该尽量避免使用该方法。  

               (4)当发生如下情况时,线程会解除上面的阻塞,重新进入运行状态:

                     》调用sleep()方法的线程超过了指定时间。

                     》线程调用阻塞式IO方法已经返回。

                     》线程成功获得了试图取得的同步监视器。

                     》线程正在等待某个通知时,其他线程正好发出一个通知。

                     》处于挂起的状态的线程被调用了resume()回复方法。  

             3.线程死亡:

             线程会以如下三种方式结束:

                     》run()或call()方法执行完成,线程正常结束。

                     》线程抛出一个未捕获的异常或ERROR。

                     》直接调用该线程的stop方法来结束该线程-----该方法容易导致死锁。   

四、线程的生命周期之状态转换

               

          1.初始状态(新建)------>可运行状态(就绪)
          2.运行状态---------->终止状态(死亡)

          3.运行状态---------->可运行状态(就绪)

                yield()方法:让当前正在执行的线程暂停,但它不会阻塞该线程,只是让线程转为就绪状态。系统调度器完全可以再重新对该就绪态的线程进行调度,但是要考虑到其它处于就绪状态线程的优先级,优先级高的线程,才会获得被执行的机会,从而进入运行状态。

          4.运行状态---------->阻塞状态
               (1)join()方法:目的是让一个线程等待join进来的线程,只有当join进来的线程完全执行完毕,等待的线程才可以继续执行。下面代码:
public class JoinThread extends Thread{
	//提供一个有参数构造器,用于设置该线程的名字
	public JoinThread(String name){
		super(name);
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(getName()+"---"+i);
		}
	}
	
	public static void main(String[] args) throws Exception {
		
		for (int i = 0; i < 100; i++) {
			if(i==20){
				JoinThread jt = new JoinThread("join进来的线程");
				jt.start();
				jt.join();
			}
			System.out.println(Thread.currentThread().getName()+"==="+i);
		}
	}
}

main===16
main===17
main===18
main===19
join进来的线程---0
join进来的线程---1
。。。。。。。
。。。。。。。
join进来的线程---97
join进来的线程---98
join进来的线程---99
main===20
main===21
main===22
main===23
             (2)sleep()方法:让当前正在执行的线程暂停,并进入阻塞状态。其重载方法static void sleep(long millis)代表睡眠时间。
public class SleepThread extends Thread{
	public static void main(String[] args) throws Exception {
		for (int i = 0; i < 10; i++) {
			System.out.println("打印中"+i);
			if(i==5){
				sleep(3000);
			}
		}
	}
}
              当i==5时,打印输出会停顿3秒,然后继续打印。
            (3)IO阻塞:简单的例子,使用Scanner对象,调用nextInt()方法等待用户输入数字,只有用户输入数字完成,程序才会继续往下走。
 

猜你喜欢

转载自blog.csdn.net/ITdevil/article/details/78724148