多线程的特性
什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含于进程之中,是进程中实际运作单位。程序员可以通过线程进行多处理器编程,可以使用多线程对运算密集型任务提速,可以通过线程让一个进程同时执行多个任务。
线程和进程的区别?
线程是进程子集,一个进程中包含一个或多个线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而线程共享一片相同的内存空间。注意:这里线程共享内存指的是共享方法区和堆中的内存,每个线程都有自己独立的栈内存。
线程创建方式
在语法上创建线程有两种方式:继承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多线程有了一个笼统的介绍,具体的功能、用法、相关功能点,限于篇幅就到其它的文章中在谈。