一、进程与线程

1、进程

进程是处于运行状态的程序:当一个程序进入内存运行时,就会变成一个进程。进程是系统分配资源和调度的基本单位。

2、线程

线程是进程的组成部分,也被称为轻量级进程。线程是进程的执行单元,一个进程可以创建多个线程。线程可以拥有自己的堆栈、程序计数器等,但不再拥有系统资源,它与(父)进程的其他线程共享进程的资源。

一个线程可以创建和撤销另一个线程,同一个进程中的多个线程可以并发执行,线程的调度和管理由进程完成。

3、多线程优势

  • 与进程相比,线程之间的隔离度小,可以很方便的共享内存、文件句柄和进程的状态等

  • 由于创建线程的代价比创建进程的代价小很多,因此使用多线程比多进程效率高

二、线程的创建

1、继承Thread类

  • 继承Thread类,重写run方法
public class MyThread extends Thread{

	@Override
	public void run() {
		//Do Something
		System.out.println(getName());
	}
}
  • 创建对象,调用该对象的start方法启动线程
new MyThread().start();
注意:使用此种方式创建线程,多条线程之间无法共享线程类的实例变量。

2、实现Runnable接口

  • 实现Runnable接口,重写run方法
public class MyThread implements Runnable{

	@Override
	public void run() {
		//Do Something
		System.out.println(Thread.currentThread().getName());
	}
}
  • 创建Runnable接口实现类的实例,并将此实例作为Thread的target来创建Thread对象,调用Thread对象的start方法启动线程
Runnable target = new MyThread();
new Thread(target).start();
new Thread(target, "another thread").start();
使用此种方式创建的多条线程可以共享线程类(target)的实例属性。
public class ThreadShare implements Runnable{

	private int count = 0;
	
	@Override
	public void run() {
		for(int i = 0; i < 5; i++){
			System.out.println(count++);
		}
	}
	
	public static void main(String[] args) {
		Runnable share = new ThreadShare();
		new Thread(share).start();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(share).start();
	}
}

运行上面的程序,可以看到依次输出了:0~9

三、线程生命周期

线程的生命周期可以大致分为五种状态:新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)、终止(Terminated)。

当使用new关键字创建一个线程后,该线程就处于新建状态。当调用了线程对象的start()方法后,该线程就处于就绪状态。如果就绪状态的线程获得了CPU,则该线程处于运行状态。

当线程发生下面的情况时,将会进入阻塞状态:

  • 调用sleep()方法主动放弃占用的处理器资源

  • 在线程中调用了一个阻塞式IO,在返回之前,该线程被阻塞

  • 尝试获取一个正被其他线程持有的同步监视器

  • 在等待某个通知(notify)

  • 使用suspend()方法将线程挂起

线程的阻塞状态被解除后会重新进入就绪状态等待再次被调度。当发生下面的情况时,线程的阻塞状态被解除,重新进入就绪状态:

  • 已经过了sleep()方法的时间

  • 调用的阻塞式IO已返回

  • 成功获取同步监视器

  • 其他线程发出了一个正在等待的通知

  • 处于挂起状态的线程被resume

当线程的run()方法执行完成、线程抛出一个未捕获的异常时或直接调用了线程的stop()方法(此方法容易导致死锁,不推荐使用)时,线程将处于终止(死亡)状态。可以使用isAlive()方法判断线程是否存活。

四、控制线程的执行

1、后台线程

如果一种线程是在后台运行的,并为其他线程提供服务,那么这种线程被称为后台线程或守护线程(Daemon Thread)。可以调用Thread对象的setDaemon(true)方法将指定的线程设置为后台线程。JVM的垃圾回收线程就是典型的后台线程。

后台线程的特征:如果所有的前台线程死亡,后台线程自动死亡。

public class DaemonThread implements Runnable{

	@Override
	public void run() {
		for(int i = 0; i < 100; i++){
			System.out.println(String.format("%s: %s", Thread.currentThread().getName(), i));
		}
	}
	
	public static void main(String[] args) {
		Thread daemonThread = new Thread(new DaemonThread());
		daemonThread.setDaemon(true);
		daemonThread.start();
		for(int i = 0; i < 5; i++){
			System.out.println(String.format("%s: %s", Thread.currentThread().getName(), i));
		}
	}
}
main线程默认是前台线程;前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。如果要将某个线程设置为后台线程,需要在该线程启动前设置,即setDaemon(true)start()方法前调用。

2、线程优先级

每个线程都具有优先级,默认的优先级与创建它的父线程的优先级相同。main线程的优先级默认为普通优先级,因此由main线程创建的子线程也具有普通优先级。

可以使用setPriority(int newPriority)方法设置线程的优先线,使用getPriority()方法获取线程的优先级。线程的优先级是一个整数,范围在1~10之间,Thread类也提供了三个表示优先级的常量:

public final static int MIN_PRIORITY = 1;
//默认优先线
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
public static void main(String[] args) {
	Thread thread = Thread.currentThread();
	System.out.println(thread.getPriority());//5
	thread.setPriority(7);
	System.out.println(thread.getPriority());//7
}
由于不同的操作系统线程的优先线并不相同,并不能很好的和Java的10个优先级对应,因此建议使用Java提供的优先级常量来设置线程优先级,使程序有更好的可移植性。

3、线程睡眠(sleep)

可以使用sleep()方法将正在执行的线程暂停一段时间,并进入阻塞状态;此线程在sleep时间内不会获得执行机会,即使没有其他可运行的线程。

public class SleepThread extends Thread{

	@Override
	public void run() {
		System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
	}
	
	public static void main(String[] args) {
		new SleepThread().start();
	}
}

程序输出:

19:27:19
19:27:21

4、线程加入(join)

可以使用join()方法让一个线程等待另一个线程完成。当某个线程调用其他线程的join()方法时,调用此方法的线程将被阻塞,直到使用join()方法加入的线程执行完成。

public class JoinThread extends Thread{

	@Override
	public void run() {
		for(int i = 1; i < 6; i++){
			System.out.println(String.format("%s: %s", getName(), i));
		}
	}
	
	public static void main(String[] args) {
		for(int i = 1; i < 6; i++){
			if(i == 3){
				JoinThread thread = new JoinThread();
				thread.start();
				try {
					thread.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(String.format("%s: %s", Thread.currentThread().getName(), i));
		}
	}
}

程序输出:

main: 1
main: 2
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-0: 4
Thread-0: 5
main: 3
main: 4
main: 5

5、线程让步(yield)

此方法与sleep()方法类似也可以使当前线程暂停执行,不同的是调用此方法不会阻塞当前线程,只是将该线程变为就绪状态,使调度器重新调度。实际上,某个线程使用yield()方法暂停后,只有优先级大于等于当前线程的就绪状态的线程才会获得执行机会。

public static native void yield();
sleep()方法比yield()方法有更好的可移植性,通常不建议使用此方法控制并发线程的执行。

五、线程同步

1、线程安全问题

public class ATM {

	private Map<String, Integer> account = new HashMap<>();
	
	public void init(){
		account.put("BankCard-1", 1000);
	}

	public void drawMoney(String id, int money){
		int balance = account.get(id);
		String name = Thread.currentThread().getName();
		if(balance >= money){
			account.put(id, balance -= money);
			System.out.println(String.format("%s从账号%s中取出%s¥!", name, id, money));
		}else{
			System.out.println(String.format("%s想取钱:%s¥,余额(%s)不足!", name, money, balance));
		}
	}
	
	public int getBalance(String id){
		return account.get(id);
	}
}
public class DrawMoneyThread implements Runnable{

	private ATM atm;
	private String id;
	private int money;
	
	public DrawMoneyThread(ATM atm, String id, int money) {
		this.atm = atm;
		this.id = id;
		this.money = money;
	}
	
	@Override
	public void run() {
		atm.drawMoney(id, money);
	}
}
public static void main(String[] args){
	ATM atm = new ATM();
	atm.init();
	
	String id = "BankCard-1";
	DrawMoneyThread target = new DrawMoneyThread(atm, id, 600);
	new Thread(target, "路人甲").start();
	new Thread(target, "路人乙").start();
}

多运行几次可能会看到下面的结果:

路人乙从账号BankCard-1中取出600¥!
路人甲从账号BankCard-1中取出600¥!

上面的例子中,甲和乙同时取钱,ATM中共1000,但他们却分别取出了600,出现了线程安全问题。

2、同步代码块

为了解决线程同步问题,Java引入了同步监视器。使用同步监视器的通用方法就是同步代码块:

synchronized (obj) {
	//同步代码块	
}

它表示开始执行同步代码块中的代码时,必须先获得对同步监视器(obj)的锁定。任何时该只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行完成后会释放对该同步监视器的锁定。

一般使用可能被并发访问的共享资源做同步监视器。对上面的程序使用同步监视器如下:

public void drawMoney(String id, int money){
	synchronized (this) {
		int balance = account.get(id);
		String name = Thread.currentThread().getName();
		if(balance >= money){
			account.put(id, balance -= money);
			System.out.println(String.format("%s从账号%s中取出%s¥!", name, id, money));
		}else{
			System.out.println(String.format("%s想取钱:%s¥,余额(%s)不足!", name, money, balance));
		}
	}
}

3、同步方法

对于多线程的安全问题,Java还提供了同步方法:使用synchronized关键字修饰的方法被称为同步方法;同步方法无须显式指定同步监视器,同步方法的同步监视器是this;进入同步方法前同样需要先获得对同步监视器的锁定。修改上面程序中的drawMoney()方法为同步方法:

public synchronized void drawMoney(String id, int money){
	......
}

4、释放对同步监视器的锁定

线程在以下几种情况下会释放对同步监视器的锁定:

  • 同步代码块或同步方法执行结束时

  • 同步代码块或同步方法中出现了未处理的异常(Error或Exception)时

  • 执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法

在以下几种情况下不会释放对同步监视器的锁定:

  • 执行同步代码块或同步方法时,程序调用了sleep()yield()方法

  • 执行同步代码块或同步方法时,其他线程调用了该线程的suspend()方法

5、同步锁

Java还提供了一种线程同步机制:通过显式的定义同步锁(Lock)对象来实现同步。

Lock提供了比同步方法和同步代码块更广泛的锁定操作,它是控制多线程访问共享资源的工具。通常情况下,每次只能有一个线程对Lock对象加锁,线程访问共享资源前应该先获得Lock对象。

使用Lock对象可以显式的加锁和释放锁,对上面的例子使用Lock对象来同步:

//定义锁对象
private final ReentrantLock lock = new ReentrantLock();

public void drawMoney(String id, int money){
	//加锁
	lock.lock();
	try{
		int balance = account.get(id);
		String name = Thread.currentThread().getName();
		if(balance >= money){
			account.put(id, balance -= money);
			System.out.println(String.format("%s从账号%s中取出%s¥!", name, id, money));
		}else{
			System.out.println(String.format("%s想取钱:%s¥,余额(%s)不足!", name, money, balance));
		}
	}finally{
		//释放锁
		lock.unlock();
	}
}

6、死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁。

public class DeadLock implements Runnable{

	private Object obj = new Object();
	private Object another = new Object();
	
	@Override
	public void run() {
		String name = Thread.currentThread().getName();
		if("A".equals(name)){
			synchronized (another) {
				System.out.println(String.format("%s已获得对 another 对象的锁定!", name));
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(String.format("%s等待获取对 obj 对象的锁定!", name));
				synchronized (obj) {
					System.out.println(String.format("%s成功锁定 another 和 obj 对象!", name));
				}
			}
		}else{
			synchronized (obj) {
				System.out.println(String.format("%s已获得对 obj 对象的锁定!", name));
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(String.format("%s等待获取对 another 对象的锁定!", name));
				synchronized (another) {
					System.out.println(String.format("%s成功锁定 obj 和 another 对象!", name));
				}
			}
		}
	}
	
	public static void main(String[] args) {
		Runnable target = new DeadLock();
		new Thread(target, "A").start();
		new Thread(target, "B").start();
	}
}

程序输出:

A已获得对 another 对象的锁定!
B已获得对 obj 对象的锁定!
B等待获取对 another 对象的锁定!
A等待获取对 obj 对象的锁定!
参考资料: