Java基础(二十六) 多线程

 

一、多线程概述

一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。能满足程序员编写高效率的程序来达到充分利用 CPU 的。

 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)

  线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

二、线程生命周期

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

三、线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

四、创建一个线程

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程。

1、通过实现 Runnable 接口来创建线程

为了实现 Runnable,一个类只需要执行一个方法调用 run():

class RunnableDemo implements Runnable{
	private String name;
 
	public RunnableDemo(String name) {
		this.name=name;
	}
 
	@Override
	public void run() {
		 for (int i = 0; i < 5; i++) {
	        System.out.println(name + "运行  :  " + i);
	      try {
	        	Thread.sleep((int) Math.random() * 10);
	      } catch (InterruptedException e) {
	               e.printStackTrace();
	          }
	       }		
	}
	
}
public class Main {
 
	public static void main(String[] args) {
		new Thread(new RunnableDemo("A")).start();
		new Thread(new RunnableDemo("B")).start();
	}
 
}

配合匿名方法实现多线程

 new Thread(new Runnable() {
                @Override
                 public void run() {
                     //execute
                 }
             }).start();

2、通过继承Thread来创建线程

继承 Thread 类,然后创建一个该类的实例。继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。

该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

class ThreadDemo extends Thread{
	private String name;
    public ThreadDemo(String name) {
       this.name=name;
    }
	public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "运行  :  " + i);
            try {
                sleep((int) Math.random() * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
       
	}
}
public class Main {
 
	public static void main(String[] args) {
		Thread1 mTh1=new ThreadDemo("A");
		Thread1 mTh2=new ThreadDemo("B");
		mTh1.start();
		mTh2.start();
 
	}
 
}

3、Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去处理同一个资源
  2. 可以避免java中的单继承的限制
  3. 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  4. 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

提醒一下大家:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM执行在就是在操作系统中启动了一个进程。
 

五、线程的挂起和恢复

线程的挂起操作实质上就是使线程进入“非可执行”状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。在线程挂起后,可以通过重新唤醒线程来使之恢复运行

    run() 和start() 是大家都很熟悉的两个方法。把希望并行处理的代码都放在run() 中;stat() 用于自动调用run(),这是JAVA的内在机制规定的。当一个线程进入“非可执行”状态,必然存在某种原因使其不能继续运行,这些原因可能是如下几种情况:
     A,通过调用sleep()方法使线程进入休眠状态,线程在指定时间内不会运行。
     B,通过调用join()方法使线程挂起,如果某个线程在另一个线程t上调用t.join(),这个线程将被挂起,直到线程t执行完毕为止。
     C,通过调用wait()方法使线程挂起,直到线程得到了notify()和notifyAll()消息,线程才会进入“可执行”状态。

1、sleep

sleep(long millis)是一个使线程暂时停止一段执行时间的方法,该时间由给定的毫秒数决定。

class ThreadA extends Thread
{
	public void run(){
		System.out.println("ThreadA is running");
	}
}
 
public class TestNew {
	public static void main(String[] args)throws InterruptedException {
		// TODO Auto-generated method stub
		Thread ta = new ThreadA();
		ta.start();
		ta.sleep(5000);//效果同Thread.sleep(5000);
		System.out.println("TestNew is running");
	}
}

  执行结果是:先ThreadA is running,5秒后,TestNew is running;这个语句是ta线程睡眠了5秒,为什么执行结果是main这个主线程也睡眠了5秒?两者是两个独立的线程才对?原因是:在哪个线程里声明sleep,哪个线程睡眠,所以是主线程睡眠了5000毫秒=5秒。(主线程:JVM调用程序main()所产生的线程)。

2、join

join()方法:能够使当前执行的线程停下来等待,直至join()方法所调用的那个线程结束,再恢复执行。例如如果有一个线程A正在运行,用户希望插入一个线程B,并且要求线程B执行完毕,然后再继续线程A,此时可以使用join()方法来完成这个需求。

public class TestNew extends Thread
{
	public void run(){
		for(int i = 0;i < 5;i ++){
            System.out.println(i);
		}
	}
 
	public static void main(String[] args)throws InterruptedException {
		System.out.println("主线程开始运行");
		TestNew ta = new TestNew();
		ta.start();
		ta.join();
		System.out.println("主线程运行结束");
	}
}
 

执行结果: 主线程开始运行 0 1  2  3  4  主线程运行结束;主线程一定会等子线程都结束了才结束。

如果不使用join方法的执行结果:主线程开始运行 主线程运行结束 0 1 2 3 4;发现主线程比子线程早结束。

3、wait与notify

wait()方法同样可以使线程进行挂起操作,调用了wait()方法的线程进入了“非可执行”状态,wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常(也就是必须写在synchronized(obj) {...} 代码段内)

thread.wait(1000);//给定线程挂起时间,基本上与sleep()方法用法相同
//或者
thread.wait();
thread.notify();//让wait()方法无限等下去,直到线程接收到notify()或notifyAll()消息为止

具体使用方法参考文章

4、yield

 yield()方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态(让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会

public class YieldTest extends Thread {
    public YieldTest(String name) {
        super(name);
    }

    @Override public void run() {
        for (int i = 1; i <= 50; i++) {
            System.out.println("" + this.getName() + "-----" + i);
            // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
            if (i == 30) {
                this.yield();
            }
        }
    }

    public static void main(String[] args) {
        YieldTest yt1 = new YieldTest("张三");
        YieldTest yt2 = new YieldTest("李四");
        yt1.start();
        yt2.start();
    }
}

运行结果:

第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。

Java Thread.yield()例子

第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。

Java Thread.yield()例子

  1. 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。
  2. 用了yield方法后,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
  3. 通过yield方法来实现两个线程的交替执行。不过请注意:这种交替并不一定能得到保证。
  4. yield()只是使当前线程重新回到可执行状态,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行,所以yield()方法只能使同优先级的线程有执行的机会

5、interrupt

interrupt()方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。

具体使用参考文章

六、线程同步(synchronized)

详细见Java基础(二十六) 线程同步中synchronized用法

七、数据传递

在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。

1、通过构造方法传递数据 

在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。下面的代码演示了如何通过构造方法来传递数据: 

public class MyThread1 extends Thread 
{ 
    private String name; 
    public MyThread1(String name) 
    { 
        this.name = name; 
    } 

    public void run() 
    { 
        System.out.println("hello " + name); 
    } 

    public static void main(String[] args) 
    { 
        Thread thread = new MyThread1("world"); 
        thread.start(); 
    } 
} 

由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情况,就得通过类方法或类变量来传递数据。

2、通过变量和方法传递数据 

向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量: 

public class MyThread2 implements Runnable 
{ 
    private String name; 

    public void setName(String name) 
    { 
        this.name = name; 
    } 

    public void run() 
    { 
        System.out.println("hello " + name); 
    } 

    public static void main(String[] args) 
    { 
        MyThread2 myThread = new MyThread2(); 
        myThread.setName("world"); 
        Thread thread = new Thread(myThread); 
        thread.start(); 
    } 
} 

3、通过回调函数传递数据 

上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的。 

 class Data 
{ 
    public int value = 0; 
} 
class Work 
{ 
    public void process(Data data, Integer... numbers) 
    { 
        for (int n : numbers) 
        { 
            data.value += n; 
        } 
    } 
} 

public class MyThread3 extends Thread 
{ 
    private Work work; 
    public MyThread3(Work work) 
    { 
        this.work = work; 
    } 

    public void run() 
    { 
        java.util.Random random = new java.util.Random(); 
        Data data = new Data(); 
        int n1 = random.nextInt(1000); 
        int n2 = random.nextInt(2000); 
        int n3 = random.nextInt(3000); 
        work.process(data, n1, n2, n3); // 使用回调函数 
        System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"  + String.valueOf(n3) + "=" + data.value); 
 } 
    public static void main(String[] args) 
    { 
        Thread thread = new MyThread3(new Work()); 
        thread.start(); 
    } 
} 

参考原文:

JAVA线程之三:线程的挂起和恢复

Java多线程学习


 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值