多线程基础(一)

一、线程与进程的概述

  1. 什么是进程?

进程就是应用程序在内存中分配的空间,也可理解为一个正在执行中的程序。每一个进程执行都有一个执行顺序,该顺序就是一个执行路径或者叫一个控制单元。

  1. 什么是线程?

线程就是进程中负责程序执行的执行单元,也可理解为进程中的一个独立的控制单元。线程在控制着进程的执行。

二、 多线程

1. 什么是多线程?

一个进程中至少有一个线程在负责该进程的运行。如果一个进程中出现了多个线程,就称该程序为多线程程序。所谓的多线程是指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,它们可以并发执行,多线程程序的执行过程如下图所示。
多线程基础(一)
图中所示的多条线程,看似是同时执行的,其实不然,它们和进程一样,也是由CPU轮流执行的,只不过CPU运行速度很快,故而给人同时执行的感觉。

2. 多线程技术解决的问题
多线程这门技术的出现解决了多部分代码同时执行的需求,这样做的好处就是可以提高用户的体验效果。 这里有一个疑问,多线程真的能提高效率吗?显然不是,反倒容易死机,但可合理的使用CPU资源。

3. JVM中的多线程——主线程和垃圾回收线程
多线程的运行是根据CPU的切换完成的,怎么切换CPU说了算,所以多线程运行有一个随机性(CPU的快速切换造成的)。在这一小节中,我以Java程序运行的原理来简单讲解一下JVM中的多线程。
java命令会启动java虚拟机,即启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的main方法,所以main方法运行在主线程中,在此之前的所有程序都是单线程的。现在思考这样一个问题——JVM虚拟机的启动是单线程的还是多线程的?答案是JVM启动时启动了多条线程,至少有两个线程我们是可以分析的出来:

执行main函数的线程,该线程的任务代码都定义在main函数中;
负责垃圾回收的线程,System类的gc()方法告诉垃圾回收器调用对象的finalize方法,但不一定立即执行。
下面我通过一个简单的案例来演示JVM中的多线程。例如,有如下实验代码:

package com.mfns.thread;

public class Thread1 {
	public static void main(String[] args) {
		new Demo();
		new Demo();
		new Demo();
		new Demo();
		new Demo();
		System.gc();
		System.out.println("hello world");
	}
	
}

class Demo{
	//定义垃圾回收方法
	public void finalize() {
		System.out.println("thread1 ok");
	}
}

运行Thread1 类中的main,可能在屏幕上打印(截图如下):
多线程基础(一)
通过实验会发现每次的结果不一定相同,那是因为随机性造成的。而且每一个线程都有自己的代码内容,这个称之为线程的任务,之所以创建一个线程就是为了去运行指定的任务代码,而线程的任务都封装在特定的区域中,比如:

主线程运行的任务都定义在main方法中;
垃圾回收线程在收垃圾时都会运行finalize方法。

4. 多线程的实现方案一
如何在自定义的代码中,自定义一个线程呢?也即如何建立一个执行路径呢?通过对API的查找,Java已经提供了对线程这类事物的描述,即Thread类,该类的描述中创建线程有两种方式,下面我就来讲解其第一种方式,即继承Thread类,重写run()方法。

继承Thread类;
1、重写Thread类中的run()方法。目的:将自定义的代码存储在run()方法,让线程运行;
创建子类对象也即创建线程对象;
调用线程的start()方法。该方法有2个作用:启动线程和调用run()方法。

package com.mfns.thread;

class TestThread2 extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("demo run ++ " + i);
		}
	}
}

public class Thread2 {
	public static void main(String[] args) {
		TestThread2 testThread2 = new TestThread2();
		testThread2.start();	//开启线程,并执行run()方法
		//testThread2.run();	//开启线程,但并没有运行线程,run()方法仍然运行在主线程上,相当于调用对象的方法
		
		for (int i = 0; i < 20; i++) {
			System.out.println("hello world ++ " + i);
		}
	}
}

运行以上程序,可发现运行结果每一次都有不同,这是因为多个线程都在获取CPU的执行权,CPU执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。CPU在做着快速的切换,以达到看上去是同时运行的效果,我们可以形象地把多线程的运行形容为互相抢夺CPU的执行权,这就是多线程的一个特点:随机性。谁抢到谁执行,至于执行多长,CPU说了算。

  • 为什么要覆盖run()方法呢?

Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run()方法,也就是说Thread类中的run()方法用于存储线程要运行的代码。

  • 调用start()方法和调用run()方法的区别?

调用start方法会开启线程,让开启的线程去执行run方法中的线程任务;而直接调用run方法,线程并未开启,去执行run方法的只有主线程。

  • Thread类的基本获取和设置方法

方法摘要 方法描述
static Thread currentThread() 获取当前线程对象
public final String getName() 获取线程的名称
public final void setName(String name) 设置线程的名称
Thread(String name) 通过构造方法给线程起名字
例,创建两个线程,和主线程交替执行。

package com.mfns.thread;

/**
 * 创建多个线程
 * @author mfns
 *
 */
public class Thread3 {
	public static void main(String[] args) {
		Demo1 d1 = new Demo1("张三");
		Demo1 d2 = new Demo1("李四");
		d1.setName("zhangsan");//设置线程名,不设置时默认为 Thread-编号
		d2.setName("lisi");
		d1.start();
		d2.start();
		
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName() + "-----" + i);
		}
	}
}

class Demo1 extends Thread {
	private String name;
	Demo1 (String name){
		this.name = name;
	}
	@Override
	public void run() {
		for (int i = 0; i < 40; i++) {
			System.out.println(Thread.currentThread().getName()+ "---"+  name + i);
		}
	}
}

通过上例,可发现原来线程都有自己默认的名称:Thread-编号,该编号从0开始。

多线程的运行状态
多线程基础(一)
多线程的运行状态用图来表示即为:
多线程基础(一)
5.多线程的实现方案二
创建线程的第二种方式:实现Runnable接口。

定义类实现Runnable接口;
1、覆盖Runnable接口中的run()方法。目的:将线程要运行的代码存放在该run()方法中;
2、通过Thread类建立线程对象;将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数呢?因为自定义的run()方法所属的对象是Runnable接口的子类对象,所以要让线程去运行指定对象的run()方法,就必须明确该run()方法所属的对象;
调用Thread类的start()方法开启线程并调用Runnable接口子类的run()方法。
例,简单的卖票程序,多个窗口同时卖票。

package com.mfns.thread;

public class Thread4 {
	public static void main(String[] args) {
		//只创建了一个线程任务对象
		CreateThread createThread = new CreateThread();
		//开启多个线程执行线程任务
		Thread t1 = new Thread(createThread);
		Thread t2 = new Thread(createThread);
		Thread t3 = new Thread(createThread);
		Thread t4 = new Thread(createThread);
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

class CreateThread implements Runnable{
	/**
	 * 模拟卖票,共十张
	 */
	private int tickets = 10;
	
	public void run() {
		while(true) {
			if (tickets>0) {
				System.out.println(Thread.currentThread().getName() + "-- "+  tickets--);
			}
		}
	}
	
}

问题:
上述代码运行后,可能会发生安全问题,不同的线程同时拿到一样的ticket,如下图所示,线程安全问题后面再说明。
多线程基础(一)

  • 实现Runnable接口的好处
    避免了继承Thread类的单继承的局限性;
    Runnable接口的出现更符合面向对象,将线程任务单独进行了对象的封装;
    Runnable接口的出现降低了线程任务和线程对象的耦合性。

PS:Thread类也是实现Runnable接口,只是加了许多自己的属性和方法。