Java 多线程

多线程的特性

什么是线程?

线程是操作系统能够进行运算调度的最小单位,它被包含于进程之中,是进程中实际运作单位。程序员可以通过线程进行多处理器编程,可以使用多线程对运算密集型任务提速,可以通过线程让一个进程同时执行多个任务。

线程和进程的区别?

线程是进程子集,一个进程中包含一个或多个线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而线程共享一片相同的内存空间。注意:这里线程共享内存指的是共享方法区和堆中的内存,每个线程都有自己独立的栈内存。


线程创建方式

在语法上创建线程有两种方式:继承java.lang.Tread类,实现java.lang.Runnable接口。通常我们都是用实现java.lang.Runnable接口来实现的,因为java是单继承多实现的语法结构。

继承java.lang.Tread创建线程实例:

package com;

public class Test {
	public static void main(String args[]) {
		
		ThreadDemo td1 = new ThreadDemo("Window 1");
		ThreadDemo td2 = new ThreadDemo("Window 2");
		
		td1.start();
		td2.start();
	}
}

class ThreadDemo extends Thread {
	public Long ticket = 100L;
	ThreadDemo(String name) {
		super(name);
	}
	public void run() {
		while (ticket > 0) {
			System.out.println(Thread.currentThread().getName() + ": " + ticket--);
		}
	}
}
该实例中的两个线程虽说运行的都是相同的代码,但彼此相互独立,并且有各自的资源,互不干扰。

实现java.lang.Runnable创建线程实例:

package com;

public class Test {
	public static void main(String args[]) {		
		ThreadDemo td = new ThreadDemo();
		Thread t1 = new Thread(td, "Window 1");
		Thread t2 = new Thread(td, "Window 2");
		Thread t3 = new Thread(td, "Window 3");		
		t1.start();
		t2.start();
		t3.start();		
	}
}

class ThreadDemo implements Runnable {
	private Long ticket = 100L;
	public void run() {
		while (ticket > 0) {
			System.out.println(Thread.currentThread().getName() + ": " + ticket--);
		}
	}
}
该实例程序在内存中仅创建一个资源,而三个线程都访问同一个资源,很好的实现了资源的共享,而且该类还能继承其它类,因此大多数情况都是通过implements Runnable来实现多线程。


线程安全问题

上面的implements Runnable方法确实实现了多线程和资源的共享,但如果运行了上面代码的人可能会发现打印出0,很明显代码的意思是打印到1就停止,如果看不到现象请在输出语句前加上Thread.sleep(10)。至于产生这种问题就不细说了,简单的称之为java线程安全问题,下面直接给出解决这类问题的两种方案。

synchronized方法:

package com;

public class Test {
	public static void main(String args[]) {
		ThreadDemo td = new ThreadDemo();
		Thread t1 = new Thread(td, "Window 1");
		Thread t2 = new Thread(td, "Window 2");
		Thread t3 = new Thread(td, "Window 3");	
		t1.start();
		t2.start();
		t3.start();	
	}
}

class ThreadDemo implements Runnable {
	private Long ticket = 100L;
	public synchronized void run() {
		while (ticket > 0) {
			try { Thread.sleep(10); } catch (InterruptedException e) {}
			System.out.println(Thread.currentThread().getName() + ": " + ticket--);
		}
	}
}
对run方法加上synchronized关键字后能成功的解决线程安全问题(打印值不再有小于1的),但是另一个问题来了,整个程序只有一个线程在打印,完全没有起到多线程的效果,相当于是单线程;而且加锁后每次都要判断锁标记影响性能。如果是这样的话那还搞这么麻烦干嘛,直接单线程不就玩了。(注意:这里是操作共享资源的代码正好都在这一个run方法里,所以synchronized run方法后就会只有一个线程在运行,造成单线程的效果,而不是只要在函数上加synchronized都会造成这种现象)

synchronized代码块:

package com;

public class Test {
	public static void main(String args[]) {
		ThreadDemo td = new ThreadDemo();
		Thread t1 = new Thread(td, "Window 1");
		Thread t2 = new Thread(td, "Window 2");
		Thread t3 = new Thread(td, "Window 3");	
		t1.start();
		t2.start();
		t3.start();	
	}
}

class ThreadDemo implements Runnable {
	private Long ticket = 100L;
	public void run() {
		while (ticket > 0) {
			try {Thread.sleep(10);} catch (InterruptedException e) {}
			synchronized(this) {
				if (ticket > 0) {
					System.out.println(Thread.currentThread().getName() + ": " + ticket--);
				}
			}
		}
	}
}
使用synchronized代码块就比较好的解决了上面的问题,既能实现线程安全,又能让多个线程并发执行。在该例中synchronized写到了while循环里面,再用if做判断;如果synchronized写到while外面则无法实现并发的效果,原因略。

下面对这两种方法做个简单的总结:

synchronized方法是一种粗粒度的并发控制,某一时刻只能有一个线程执行该synchronized方法。
synchronized代码快则是一种细粒度的并发控制,只会将块中的代码同步,其它部分的代码则可以被其它线程访问。


线程间通信

Java线程间的通信是通过共享内存来实现的。这里暂时将Java运行时内存粗略分为“栈内存”、“堆内存”、“方法区”、“程序计数器”,其它的就不细说了。其中“栈内存”、“程序计数器”是线程私有的,共享的是“堆内存”、“方法区”,我们就是通过对这两块共享的内存来实现线程的通信。下面来看一个实例,其中就是通过共享storage这个堆内存中的变量来实现线程之间的通信:

import java.util.LinkedList;

public class TestThread {
	public static void main(String args[]) {
		Storage storage = new Storage();
		
		Produce p = new Produce(storage);
		Consume c = new Consume(storage);
		
		Thread p1 = new Thread(p, "生产者一:");
		Thread p2 = new Thread(p, "生产者二:");
		Thread p3 = new Thread(p, "生产者三:");
		Thread c1 = new Thread(c, "消费者一:");
		Thread c2 = new Thread(c, "消费者二:");
		Thread c3 = new Thread(c, "消费者三:");
		
		p1.start();
		p2.start();
		p3.start();
		c1.start();
		c2.start();
		c3.start();
	}
}

// 资源
class Storage {
	// 最大存储量
	private final static int MAX_SIZE = 10;
	// 存储的资源
	private LinkedList<Object> list = new LinkedList<Object>();
	// 生产资源
	public void produce() {
		// 同步代码块
		synchronized(list) {
			while (list.size() >= MAX_SIZE) {
				System.out.println(Thread.currentThread().getName() + "存储资源空间已满,不能再生产了!!!");
				try{list.wait();} catch(Exception e){};
			}
			list.add(new Object());
			list.notifyAll();
			System.out.println(Thread.currentThread().getName() + "已生产一个资源+++");
		}
	}
	// 消费资源
	public void consume() {
		// 同步代码块
		synchronized(list) {
			while (list.size() < 1) {
				System.out.println(Thread.currentThread().getName() + "存储资源空间为空,不能再消费了!!!");
				try{list.wait();} catch(Exception e){};
			}
			list.remove();
			list.notifyAll();
			System.out.println(Thread.currentThread().getName() + "已消费一个资源---");
		}
	}
}

class Produce implements Runnable {
	private Storage storage = null;
	Produce(Storage storage) {
		this.storage = storage;
	}
	public void run() {
		while (true) {
			storage.produce();
		}
	}
}

class Consume implements Runnable {
	private Storage storage = null;
	Consume(Storage storage) {
		this.storage = storage;
	}
	public void run() {
		while (true) {
			storage.consume();
		}
	}
}
运行实例发现几个线程都能交叉访问到storage,那么基本的线程通信就算是实现了,至于优化就留到Java1.5新特性中再做讨论。下面对程序中用到的wait()和notify()/notifyAll()做个总结。

wait():释放占有的对象锁,线程进入等待池,释放cpu资源,而其它线程可以抢占此锁运行程序。插说下与sleep()的差别,线程调用sleep()方法后,会休眠一段时间,休眠期间会暂时释放cpu资源,但并不释放锁对象。wait()和sleep()最大差别在于wait()会释放对象锁,而sleep()不会。

notify:该方法唤醒因为调用wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获得对象锁,notify()唤醒的是该锁等待队列中的第一个。调用notify()方法后当前线程并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完才会释放锁对象。Jvm会在等待的线程中调取一个线程去获取锁对象,继续执行代码。

notifyAll():唤醒所有应该锁而阻塞的线程。
这三个方法都是Object的方法,因为锁是Object类型,而这三种方法又都是操作锁对象的;也因为只有在synchronized方法或代码块中才会有锁资源,所以wait()/notify()/notifyAll()必须在synchronized中调用。


线程类其它方法

Thread类中的sleep()/yield()/join()方法:

sleep():使当前线程暂停执行一段时间,让出cpu资源,但不释放对象锁;

yield():暂停当前线程,让当前线程回到就绪状态,cpu重新到就绪队列中选取线程执行。注意:该线程被cpu重新选择的概率和其它线程相同,并无差别;对线程设置了setPriority()属性只是改变了cpu选择执行该线程的概率,而不是执行顺序。

join():阻塞所在的线程(也就是调用子线程的那个主线程),只有等待子线程结束了才能执行。


Java1.5新特性

在1.5之前是通过synchronized关键字来实现同步访问,从Java 5之后,在java.util.concurrent.locks包下提供了另一种方式来实现同步访问。

先用lock机制来实现上面synchronized的线程通信功能,下面是实例代码:

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestThread {
	public static void main(String args[]) {
		Storage storage = new Storage();
		
		Produce p = new Produce(storage);
		Consume c = new Consume(storage);
		
		Thread p1 = new Thread(p, "生产者一:");
		Thread p2 = new Thread(p, "生产者二:");
		Thread p3 = new Thread(p, "生产者三:");
		Thread c1 = new Thread(c, "消费者一:");
		Thread c2 = new Thread(c, "消费者二:");
		Thread c3 = new Thread(c, "消费者三:");
		
		p1.start();
		p2.start();
		p3.start();
		c1.start();
		c2.start();
		c3.start();
	}
}

// 资源
class Storage {
	// 最大存储量
	private final static int MAX_SIZE = 10;
	// 存储的资源
	private LinkedList<Object> list = new LinkedList<Object>();
	// 锁对象
	private final Lock lock = new ReentrantLock();
	// 用来监控资源是否存满的Condition实例
	Condition full = lock.newCondition();
	// 用来监控资源是否为空的Condition实例
	Condition empty = lock.newCondition();
	
	// 生产资源
	public void produce() {
		lock.lock();
		try {
			while(list.size() >= MAX_SIZE) {
				System.out.println(Thread.currentThread().getName() + "存储资源空间已满,不能再生产了!!!");
				full.await();
			}
			list.add(new Object());
			System.out.println(Thread.currentThread().getName() + "已生产一个资源+++");
			empty.signal();
		} catch (InterruptedException  e) {
			e.printStackTrace();
		}
		finally {
			lock.unlock();
		}
	}
	// 消费资源
	public void consume() {
		lock.lock();
		try {
			while(list.size() < 1) {
				System.out.println(Thread.currentThread().getName() + "存储资源空间为空,不能再消费了!!!");
				empty.await();
			}
			list.remove();
			System.out.println(Thread.currentThread().getName() + "已消费一个资源---");
			full.signal();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		finally {
			lock.unlock();
		}
	}
}

class Produce implements Runnable {
	private Storage storage = null;
	Produce(Storage storage) {
		this.storage = storage;
	}
	public void run() {
		for (int i=0; i<100; i++) {
			storage.produce();
		}
	}
}

class Consume implements Runnable {
	private Storage storage = null;
	Consume(Storage storage) {
		this.storage = storage;
	}
	public void run() {
		for (int i=0; i<100; i++) {
			storage.consume();
		}
	}
}

现在把用lock()方式和synchronized()实现方法做个简单的对比:

lock.lock()----lock.unlock()中间的代码就相当于synchronized(){}大括号中的同步代码,表示中间的代码是被加锁了,执行中间代码需要获得锁资源。

lock.newCondition()创建的full和empty对象,类似于synchronized中的object锁,不同的是lock中可以有多个Condition对象锁;condition对象锁是通过await()和signal()来实现挂起和唤醒的,功能同object锁中的wait()和notify();这样在唤醒的时候就能唤醒对应的线程队列,这样避免了唤醒的线程因为不满足执行条件又被阻塞了浪费资源。

通过运行我们发现两个程序都能很好的实现线程的通信问题,但为什么Java在有synchronized关键字能解决线程同步问题,还新增了java.util.concurrent.locks包来提供解决线程同步的新方法呢?

lock的锁是通过代码实现的,而synchronized是作为Java关键字在JVM层面上实现的;在一个lock锁上能绑定多个Condition对象,能够直接指定阻塞的线程和唤醒的线程队列,提高了程序的灵活性和性能,但它需要自己获取锁资源和释放锁资源,finally一定不可少;synchronized在锁定的时候如果发生了异常,JVM会自动将锁释放掉,不会因为异常没有释放锁资源而造成死锁。在资源竞争不是很激烈的情况下,使用synchronized是很合适,既简单易用、代码可读性强,编译程序通常会尽可能对synchronized优化;ReentrantLock在资源竞争不是很激烈的情况下,性能比synchronized略低,当同步资源竞争很激烈的时候synchronized性能能下降几十倍,而ReentrantLock还能维持常态。

写到这里算是对Java多线程有了一个笼统的介绍,具体的功能、用法、相关功能点,限于篇幅就到其它的文章中在谈。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值