javase 多线程

2018-11-02

线程基础

进程

进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

线程

  • 线程是进程中的一个执行单元(执行路径),负责当前进程中程序的执行,一个进程中至少有一个线程
  • 单线程程序:即若有多个任务只能依次执行,当上一个任务执行结束后,下一个任务开始执行
  • 多线程程序:一个进程可以让多个线程同时执行。一个核心的CPU在多个线程之间进行着切换动作,由于切换时间很短(毫秒甚至是纳秒级别),导致我们感觉不出来

线程的运行模式

  • 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间
  • 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度

java中的线程

主线程

JVM启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。

多线程内存图解

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

多线程内存图解

Thread类

构造方法

  • Thread(),分配新的Thread对象
  • Thread(String name),分配名为nameThread对象
  • Thread(Runnable target),使用Runnable的实现类对象创建Thread对象

常用方法

  • void run(),线程实际要执行的任务
  • void start(),启动线程
  • void setName(String name),设定线程名字
  • String getName(),获得线程的名字
  • static Thread currentThread(),获得当前运行的的线程的引用
  • static void sleep(long ms),让当前线程休眠指定毫秒数

创建线程

可以通过继承Thread类或实现Runnable接口创建运行新线程。

继承Thread类

  • 定义一个类继承Thread
  • 重写run方法
  • 创建子类对象,就是创建线程对象
  • 调用start方法,开启线程并让线程执行,同时还会告诉JVM去调用run方法

实现Runnable接口

  • 定义类实现Runnable接口
  • 覆盖接口中的run方法
  • 创建Thread类的对象
  • Runnable接口的子类对象作为参数传递给Thread类的构造函数
  • 调用Thread类的start方法开启线程

相对创建Thread的继承类,避免了单继承的限制,可以多继承。

实现接口方式的好处

  • 实现Runnable接口避免了单继承的局限性,所以较为常用。
  • 降低了耦合性。更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务
    • 继承Thread类,线程对象和线程任务耦合在一起;一旦创建Thread类的子类对象,既是线程对象,又有线程任务。
    • 实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

匿名内部类实现多线程程序

实现接口Runnable的匿名内部类

new Thread(new Runnable() {
  public void run() {
    for(int i = 0; i < 50; i++) {
      System.out.println("inner " + i);
    }
  }
}).start();

继承Thread的匿名内部类

new Thread() {
  public void run() {
    for(int i = 0; i < 50; i++) {
      System.out.println("inner: " + i);
    }
  }
}.start();

线程的状态图

线程状态图

线程池

线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

  • 在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大;除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。
  • 线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

创建线程池对象

线程池创建工厂类Executors,使其它创建线程池对象:

  • public static ExecutorService newFixedThreadPool(int nThreads),返回线程池对象(ExecutorService实现类对象)

使用线程池对象

  • Future<?> submit(Runnable task),传入Runnable实现类对象,并执行
  • <T> Future<T> submit(Callable<T> task),传入Callable实现类对象,并执行

传入Runnable接口任务

步骤

  • 创建线程池对象
  • 创建Runnable接口子类对象
  • 提交Runnable接口子类对象
  • 关闭线程池

代码示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolUsingRunnable {
	public static void main(String[] args) {
		ExecutorService service = Executors.newFixedThreadPool(2);
		
		Task task = new Task();
		service.submit(task);
		service.submit(task);
		service.submit(task);
		
    // 手动销毁线程池
		// service.shutdown();
	}
}

class Task implements Runnable{
	@Override
	public void run() {
		System.out.println("Running task in Thread " + Thread.currentThread().getName());
	}
}

/*==============
运行结果:
Running task in Thread pool-1-thread-2
Running task in Thread pool-1-thread-1
Running task in Thread pool-1-thread-2
===============*/

传入Callable接口任务

Callable可以抛异常,也可以有返回值。

步骤

  • 创建线程池对象
  • 创建Callable接口子类对象
  • 提交Callable接口子类对象
  • 关闭线程池

代码示例

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ThreadPoolUsingCallable {
	public static void main(String[] args) throws InterruptedException, ExecutionException{
		ExecutorService service = Executors.newFixedThreadPool(5);
		Future<Integer> result;
		
		result = service.submit(new ComputeTask(1,1));
		System.out.println(result.get());
		result = service.submit(new ComputeTask(2,3));
		System.out.println(result.get());
		result = service.submit(new ComputeTask(3,5));
		System.out.println(result.get());
		
		service.shutdown();
	}
}

class ComputeTask implements Callable<Integer>{
	private int x;
	private int y;	
	public ComputeTask() {}
	public ComputeTask(int x, int y) {
		this.x = x;
		this.y = y;
	}
	public int getSum() {
		return x + y;
	}
	public Integer call() throws Exception {
		return getSum();
	}
}

/*
 * running result:
 * 2
 * 5
 * 8
 */

线程安全

如果一个程序中有多个线程,这些线程可能共同操作某些数据,每个线程每次在操作数据时可能需要一系列的步骤,如果这些步骤由于多线程被分割成几次来执行,可能导致逻辑错误、数据被破坏。

解决方案:synchronized代码块、synchronized方法、Lock接口。

使用synchronized

使用同步的方式可以解决以上的问题。

同步代码块

synchronized (/*锁对象*/) {
	// 可能会产生线程安全问题的代码
}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

同步方法

public synchronized void method(){
   	// 可能会产生线程安全问题的代码
}
  • 同步的非静态方法中实际的锁对象是本类对象this
  • 同步的静态方法中实际的锁是本类.class

使用Lock接口

Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能;可以获得比synchronized广泛的锁定操作。

  • void lock(),上锁
  • void unlock(),解锁

使用示例:

 Lock l = ...;
 l.lock();
 try {
   // access the resource protected by this lock
 } finally {
   l.unlock();
 }

死锁

当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况需要避免掉。

线程1:A锁 > B锁

线程2:B锁 > A锁

// 若线程1拿到了A锁,同时线程2拿到了B锁,那么就出现了死锁

等待唤醒机制

  • void wait(): 等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
  • void notify(): 唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
  • void notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

这上面的方法都在继承自Object,因为同步锁可以是任意对象;上面所说的线程池,实际是指在该对象监视器(object's monitor)上等待的那些线程

(本文完)

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。