Java多线程编程---线程基础

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

线程与进程

进程

    要解释线程,就必须明白什么是进程,就好象要搞清中国历史,就必须要了解春秋战国。什么是进程呢?进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间),比如用户点击桌面的IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击桌面的IE浏览器,又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。目前操作系统都支持多进程。

    要点:用户每启动一个进程,操作系统就会为该进程分配一个独立的内存空间。

线程

    线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。

    1、线程是轻量级的进程

    2、线程没有独立的地址空间(内存空间)

    3、线程是由进程创建的(寄生在进程)

    4、一个进程可以拥有多个线程-->这就是我们常说的多线程编程

    5、线程有几种状态:a、新建状态(new)b、就绪状态(Runnable)c、运行状态(Running)d、阻塞状态(Blocked)e、死亡状态(Dead)

    目前绝大部分应用程序都会涉及到多并发的问题。只要应用程序涉及到并发,就离不开多线程编程。

进程与线程

    进程与线程的区别:

    1每个进程都有独立的代码和数据空间,进程间的切换会有较大的开销;

    2、线程可以看成是轻量级的进程,同一进程内的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器,线程切换的开销小;

    3、多进程:在操作系统中能同时运行多个任务(程序);

    4、多线程:在同一应用程序中有多个顺序流同时执行。

    进程和线程的出现,一句话概括:为了充分利用CPU资源。那么结合计算机我们再来理解一下,进程:我们计算机中运行的各个应用程序,线程:每个应用程序内部的子任务。

    换句话说,进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。

    但是要注意,一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的。进程是操作系统进行资源分配的基本单位(各个进程间互不干扰),而线程是操作系统进行调度的基本单位(线程间的互相切换)。

    进程并发:操作系统已经帮我们处理完成

    线程并发:Java采用的是单线程编程模型,即在我们自己的程序中如果没有主动创建线程的话,只会创建一个线程,通常称为主线程。但是要注意,虽然只有一个线程来执行任务,不代表JVM(在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程))中只有一个线程,JVM实例在创建的时候,同时会创建很多其他的线程(比如垃圾收集器线程)。

 

线程的状态转换

    线程的状态转换是线程控制的基础。线程状态总的可分为五大状态:分别是新建状态、就绪(可运行)状态、运行状态、阻塞(等待)状态、死亡状态。用一个图来描述如下:


    1、新建状态(new):线程对象已经创建,还没有在其上调用start()方法。

    2、就绪状态(Runnable):当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。

    3、运行状态(Running):线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

    4、等待/阻塞/睡眠状态(Blocked):这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。

    5、死亡态(Dead):当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

 

创建线程的基本方法

    在java中一个类要当作线程来使用有两种方法

    1、继承Thread类,并重写run函数

    2、实现Runnable接口,并重写run函数

    因为java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能,java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程。

1、通过继承Thread类来实现建立线程实例

/**
 * 
 * @Description: 创建线程的第一种方法,继承Thread类(Thread类本质也是实现了Runnable接口)
 *
 * @author: zxt
 *
 * @time: 2018年4月6日 下午12:17:48
 *
 */
public class TestThread extends Thread {

	public static void main(String[] args) {
		TestThread t1 = new TestThread("张三");
		TestThread t2 = new TestThread("李四");

		t1.start();
		t2.start();
	}

	public TestThread(String name) {
		super(name);
	}

	public void run() {
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(new Random().nextInt(10) * 100);
				
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

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

/**
 * 
 * @Description: 创建线程的第二种方法,实现Runnable接口,尽量使用这种方法,体现面向对象编程的特点
 *
 * @author: zxt
 *
 * @time: 2018年4月6日 下午12:21:47
 *
 */
public class TestRunnable {

	public static void main(String[] args) {
		DoSomething ds1 = new DoSomething("阿三");
		DoSomething ds2 = new DoSomething("李四");

		Thread t1 = new Thread(ds1);
		Thread t2 = new Thread(ds2);

		t1.start();
		t2.start();
	}
}

// 实现Runnable接口的类
class DoSomething implements Runnable {
	private String name;

	public DoSomething(String name) {
		this.name = name;
	}

	public void run() {
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(new Random().nextInt(10) * 100);
				
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(name + ": " + i);
		}
	}
}

3、多线程实例演示

/**
 * 
 * @Description: 多线程举例
 *
 * @author: zxt
 *
 * @time: 2018年4月6日 下午4:10:17
 *
 */
public class MutliRunnable {

	public static void main(String[] args) {
		
		Pig pig = new Pig(10);
		Bird bird = new Bird(10);
		
		Thread t1 = new Thread(pig);
		Thread t2 = new Thread(bird);
		
		t1.start();
		t2.start();
	}
}

class animal {
	int age = 0;
	int times = 0;

	public animal(int age) {
		this.age = age;
	}
}

// 打印
class Pig extends animal implements Runnable {

	public Pig(int age) {
		super(age);
	}

	public void run() {
		while (true) {
			try {
				Thread.sleep(1000);
				
			} catch (Exception ex) {
				ex.printStackTrace();
			}

			times++;
			System.out.println("我是一个线程,在输出第" + times + "个 Hello World!");
			if (times == age) {
				break;
			}
		}
	}
}

// 计算
class Bird extends animal implements Runnable {
	int res = 0;

	public Bird(int age) {
		super(age);
	}

	public void run() {
		while (true) {
			try {
				Thread.sleep(1000);
				
			} catch (Exception ex) {
				ex.printStackTrace();
			}

			res += (++times);
			System.out.println("当前结果是:" + res);
			if (times == age) {
				System.out.println("最后结果是:" + res);
				break;
			}
		}
	}
}

    不管是通过继承Thread,还是通过实现Runnable接口创建线程,它们的一个对象只能启动(即:start())一次。否则就会有异常抛出。

继承Thread与实现Runnable的区别

    从java的设计来看,通过继承Thread类或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口,如果一定要说它们有什么区别,总结几点

    1、尽可能使用实现Runnable接口的方式来创建线程

    2、在使用Thread的时候只需要new一个实例出来,调用start()方法即可以启动一个线程,如:

    Thread test = new Thread();

    test.start();

    3、在使用Runnable的时候需要先new一个实现Runnable接口的实例,之后用Thread调用,如:

    Test implements Runnable {}

    Test t = new Test();

    Thread test = new Thread(t);

    tset.start();

    注意:必须调用start()方法才会创建新的线程,如果是调用run()方法,则只是在主线程里面进行方法调用,而没有创建新的线程。

用实现Runnable接口的特点

    1、用实现Runnable接口的方法创建线程对象可以避免java单继承机制带来的局限;

    2、用实现Runnable接口的方法,可以实现多个线程共享同一段代码(数据);因此建议大家如果你的程序有同步逻辑需求,则使用Runnable的方法来创建线程。

 

常用的线程相关函数

    1、Thread.sleep(longmillis),一定是当前线程调用此方法,当前线程进入阻塞,但不释放对象锁,millis后线程自动苏醒进入可运行状态。作用:给其它线程执行机会的最佳方式。

    2、Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。

    3、t.join()/t.join(longmillis),当前线程里调用其它线程1的join方法,当前线程阻塞,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。例如在main()函数中:

    ThreadJoinTest t1 = newThreadJoinTest("小明");

    ThreadJoinTest t2 = newThreadJoinTest("小东");

    t1.start();

    t1.join();

    t2.start();

    其中ThreadJoinTest 继承了Thread类,程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕。所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会。

    4、obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(longtimeout)timeout时间到自动唤醒。

    obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

 

一些常见问题

    1、线程的名字,一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是main,非主线程的名字不确定。

    2、线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。

    3、获取当前线程的对象的方法是:Thread.currentThread()。

    4、当线程目标run()方法结束时该线程完成。

    5、一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。

关于线程调度

    1、对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。Java线程的调度是JVM的一部分,在一个CPU的机器上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。    

    2、尽管通常采用队列形式,但这是没有保障的。队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列运行的事实。

    3、尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。

猜你喜欢

转载自blog.csdn.net/zengxiantao1994/article/details/79839221