前言
最开始学习java时,头都大了,也没学好多线程,甚至都不明白啥是多线程...慢慢的不断学习,发现多线程其实并没有我们想象中的困难。
进程(Processes)与线程(Threads)
在操作系统里面,存在着非常多的进程与线程。在每个单核处理器中,某个时刻仅有一个线程在执行。但是为什么我们在平时使用中,却感觉是有多个线程在运行呢?因为处理器使用了时间分片技术。也就是将处理器的执行时间分割成很多片段,然后将每个时间片段分配给不一样的线程,由于处理器的运行速度非常快,所以就给我们造成一种假象,处理器在同时执行多个线程。随着科技的发展,现在出现了多核处理器电脑,并且每个核心可超线程执行任务,由于每个核都是独立的,大大提高了电脑的并发执行能力。
进程(Processes)
进程通常拥有自己的执行环境,特别的,还拥有私有的内存空间。在某种程度上看,进程可以看做是一个程序或者是应用,比如我们使用win7/win8的任务管理器,可查看到系统的进程列表。为了让多个进程之间能够正常通讯,现代的操作系统一般使用Inter Process Communication(IPC)技术,也称之为进程间通讯。通畅使用管道(Pipes)、Socket等。在java里面,一般一个jvm代表了一个进程。
线程(Threads)
线程有时候也叫做轻量级的进程。在一个进程中,包含了一个或者多个线程,这些线程共享进程的执行环境,资源,内存。所以创建线程以及销毁线程的代价却比创建进程小的多。
创建线程
方式1:实现Runnable接口
public class HelloRunnable implements Runnable { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new Thread(new HelloRunnable())).start(); } }
方式二:继承Thread类
public class HelloThread extends Thread { public void run() { System.out.println("Hello from a thread!"); } public static void main(String args[]) { (new HelloThread()).start(); } }
注意点:启动线程使用的是start方法,不是调用run()方法。
在这两种方式中,我们应该使用哪种呢?
推荐方式一,因为这样子代码看上去更加简洁,并且灵活性更高。比如一个类本身已经继承了别的Thread,那么就无法实现多次继承Thread。
暂停线程的执行(Pausing Execution)
为什么需要暂停线程的执行呢?大家可以回想下,在使用ServerSocket时,有个accept方法,这个方法是阻塞的,也就是会在等待客户端的socket连接。所以线程的暂停有着非常多的用处,比如等待别的任务执行,等待IO完成,等待网络等。那么如何停止一个线程呢?直接使用Thread.sleep(int time)
public class SleepMessages { public static void main(String args[]) throws InterruptedException { String importantInfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" }; for (int i = 0; i < importantInfo.length; i++) { //Pause for 4 seconds Thread.sleep(4000); //Print a message System.out.println(importantInfo[i]); } } }
注意点,Thread.sleep()方法会抛出一个InterruptedException,这个异常的抛出,是由于中断了这个线程,那么这个线程就不能继续存活下去了。
线程的中断
在一个线程中,可以执行各种操作,假设某个操作非常耗时,或者进入了长时间的睡眠,这时候想要中断这个线程,那么可以使用Thread.interrupted()来中断线程。被中断的线程会直接被杀死。
for (int i = 0; i < inputs.length; i++) { heavyCrunch(inputs[i]); if (Thread.interrupted()) { throw new InterruptedException(); } }
线程的Join
在多个线程中,如果一个线程调用了join(),那么主线程就会被停止,直到调用了join()的那个线程执行完毕。
public class ThreadDemo{ public static void main(String[] args){ Thread mainThread = Thread.currentThread(); System.out.println(mainThread.getName() + " start"); Thread joinThread = new Thread(new Runnable(){ @Override public void run(){ System.out.println("I am joinThread"); try{ Thread.sleep(5000); }catch(InterruptedException e){ e.printStackTrace(); } } }); joinThread.start(); try{ joinThread.join(); }catch(InterruptedException e){ e.printStackTrace(); } //this partition code has to wait the joinThread to complete System.out.println("end"); } }
在这个例子中,最后主线程部分的System.out.println("end");部分的代码,需要一直等待joinThread的执行完成,这里为了简单,只有一句输出,其实可以为任何的代码。
下面这个简单的例子,有两个线程,一个是MessageLoop线程,不断的输出数据,另外一个是Main Thread。Main Thread由jvm直接创建,所以每个程序至少有一个线程,那就是主线程。MessageLoop t启动之后,就不断的输出数据,然后主线程就不断的询问该线程t是否存活,如果存活就让t暂停1秒钟,这里使用了t.join(1000),然后判断程序的运行时间是否已经超过了patience标量,如果是,那就调用t.interrupt()方法来中断线程,最后主线程输出了Finally,表示结束。
public class SimpleThreads { // Display a message, preceded by // the name of the current thread static void threadMessage(String message) { String threadName = Thread.currentThread().getName(); System.out.format("%s: %s%n", threadName, message); } private static class MessageLoop implements Runnable { public void run() { String importantInfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" }; try { for (int i = 0; i < importantInfo.length; i++) { // Pause for 4 seconds Thread.sleep(4000); // Print a message threadMessage(importantInfo[i]); } } catch (InterruptedException e) { threadMessage("I wasn't done!"); } } } public static void main(String args[]) throws InterruptedException { // Delay, in milliseconds before // we interrupt MessageLoop // thread (default one hour). long patience = 1000 * 60 * 60; // If command line argument // present, gives patience // in seconds. if (args.length > 0) { try { patience = Long.parseLong(args[0]) * 1000; } catch (NumberFormatException e) { System.err.println("Argument must be an integer."); System.exit(1); } } threadMessage("Starting MessageLoop thread"); long startTime = System.currentTimeMillis(); Thread t = new Thread(new MessageLoop()); t.start(); threadMessage("Waiting for MessageLoop thread to finish"); // loop until MessageLoop // thread exits while (t.isAlive()) { threadMessage("Still waiting..."); // Wait maximum of 1 second // for MessageLoop thread // to finish. t.join(1000); if (((System.currentTimeMillis() - startTime) > patience) && t.isAlive()) { threadMessage("Tired of waiting!"); t.interrupt(); // Shouldn't be long now // -- wait indefinitely t.join(); } } threadMessage("Finally!"); } }
线程的同步
在线程中,由于资源、运行环境、内存等共享,所以会带来许多的问题,比如著名的死锁。下面的例子是一个简单的计数器
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }
如果这个类的对象只是被一个线程访问,那么每次increment()、decrement(),c就会+1或者-1。但是如果这个对象同时被多个线程持有,并且并发的访问,那么程序的预期,将和我们所想象的不一致。上面的程序,使用的是c++,c--语法,但是并不代表c++,c--的执行流程只有一个指令。实际上,c++分为下面三个步骤。
1、获得当前c的值
2、当前的值+1
3、将第2步的值保存起来
下面我们假设Thread A,B同时访问increment(),那么很可能发生如下的过程:
1、Thread A获得c的值,c=0
2、Thread B获得c的值,c=0,
3、Thread A将值+1,c=1
4、Thread B将值-1,c=-1
5、Thread A将值保存起来,c=1
6、Thread B将值保存起来,c=-1
假设c执行之前为0,那么之后的结果会是多少呢?没错,是-1。这和我们预期的结果0并不一致。
那么是什么造成了这个问题呢?大家可以参考下其他文档,总体来说就是,多个线程对同一个变量的修改,对其他线程来说,可能是不可见的。比如Thread A对c的修改,Thread B是不可见的。
同步方法(Synchronized Methods)
在java中,提供了对同步方法的原语支持,关键字就是synchronized。如下例子:
public class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } }
在方法中加入了synchronized,那么系统就会保证如下的两个特性:
1、当一个线程访问一个synchronized方法时,其他方法只能等待。比如在上面例子中,Thread A假设在执行increment(),那么Thread B就必须等待Thread A的执行完毕。
2、一个线程执行完毕之后的结果,对于其他线程来说,结果是可见的。比如Thread A执行increment()之后,c的值为1,那么这时候Thread B获得的值也就是1。
内部锁(Intrinsic Locks)
锁,就是用来保护某一段代码只能被一个Thread访问。其他程序如果想要访问这段代码,就需要获得锁。使用synchronized methods,虚拟机会为整个方法都加入锁。在方法中加入synchronized,虽然实现了线程之间的同步,但是却降低了多线程的并发能力,如果一个方法中,仅仅只有一行代码需要同步,那么使用同步方法将会锁住整个方法,导致其他线程无法访问。所以在java中,支持同步代码块。语法也很简单,如下所示:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
原子访问(Atomic Access)
在编程中,一个原子的动作,是不可分割的。类似于数据库中的原子操作,所有操作要么执行成功,要么执行失败,不存在半执行。在java中,大部分的原始数据类型的操作都是原子的,或者是加入了volatile关键字的域。一般而言,使用原子技术会让代码更加难以控制,理解起来也更加困难,所以应该避免大范围的使用。
死锁(Deadlock)
死锁就是两个线程互相等待对方的完成。下面的例子就说明了,两个friend都在等待对方bowBack(),所以程序会一直无限期的进入等待。
public class Deadlock { static class Friend { private final String name; public Friend(String name) { this.name = name; } public String getName() { return this.name; } public synchronized void bow(Friend bower) { System.out.format("%s: %s" + " has bowed to me!%n", this.name, bower.getName()); bower.bowBack(this); } public synchronized void bowBack(Friend bower) { System.out.format("%s: %s" + " has bowed back to me!%n", this.name, bower.getName()); } } public static void main(String[] args) { final Friend alphonse = new Friend("Alphonse"); final Friend gaston = new Friend("Gaston"); new Thread(new Runnable() { public void run() { alphonse.bow(gaston); } }).start(); new Thread(new Runnable() { public void run() { gaston.bow(alphonse); } }).start(); } }
下面我们通过代码来演示一个生产者-消费者关系。这里一共有三个线程,一个是生产者,另外一个是消费者,另外一个是主线程。生产者负责生产,然后每次生产完之后,都需要notify消费者来取数据,消费者负责消费数据,每次消费完之后,都notify生产者开始生产数据。需要注意的一点是,生产者和消费者都是访问同一个对象,在例子中就是
Drop对象。 //可以假设为生产线 public class Drop { // Message sent from producer // to consumer. private String message; // True if consumer should wait // for producer to send message, // false if producer should wait for // consumer to retrieve message. private boolean empty = true; public synchronized String take() { // Wait until message is // available. while (empty) { try { wait(); } catch (InterruptedException e) {} } // Toggle status. empty = true; // Notify producer that // status has changed. notifyAll(); return message; } public synchronized void put(String message) { // Wait until message has // been retrieved. while (!empty) { try { wait(); } catch (InterruptedException e) {} } // Toggle status. empty = false; // Store message. this.message = message; // Notify consumer that status // has changed. notifyAll(); } } //生产者 import java.util.Random; public class Producer implements Runnable { private Drop drop; public Producer(Drop drop) { this.drop = drop; } public void run() { String importantInfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" }; Random random = new Random(); for (int i = 0; i < importantInfo.length; i++) { drop.put(importantInfo[i]); try { Thread.sleep(random.nextInt(5000)); } catch (InterruptedException e) {} } drop.put("DONE"); } } //消费者 import java.util.Random; public class Consumer implements Runnable { private Drop drop; public Consumer(Drop drop) { this.drop = drop; } public void run() { Random random = new Random(); for (String message = drop.take(); ! message.equals("DONE"); message = drop.take()) { System.out.format("MESSAGE RECEIVED: %s%n", message); try { Thread.sleep(random.nextInt(5000)); } catch (InterruptedException e) {} } } } //主线程 public class ProducerConsumerExample { public static void main(String[] args) { Drop drop = new Drop(); (new Thread(new Producer(drop))).start(); (new Thread(new Consumer(drop))).start(); } }
不可变对象
有时候,为了防止一些多线程问题的出现,可以使用不可变对象来代替可变对象。下面我们来看一个例子,一个颜色类。
public class SynchronizedRGB { // Values must be between 0 and 255. private int red; private int green; private int blue; private String name; private void check(int red, int green, int blue) { if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) { throw new IllegalArgumentException(); } } public SynchronizedRGB(int red, int green, int blue, String name) { check(red, green, blue); this.red = red; this.green = green; this.blue = blue; this.name = name; } public void set(int red, int green, int blue, String name) { check(red, green, blue); synchronized (this) { this.red = red; this.green = green; this.blue = blue; this.name = name; } } public synchronized int getRGB() { return ((red << 16) | (green << 8) | blue); } public synchronized String getName() { return name; } public synchronized void invert() { red = 255 - red; green = 255 - green; blue = 255 - blue; name = "Inverse of " + name; } }
这个类的getRGB()、getName()、invert()都是同步方法,但是还是可能会出现一些问题。比如下面的代码
SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black"); ... int myColorInt = color.getRGB(); //Statement 1 String myColorName = color.getName(); //Statement 2
在Stament1之后,如果一个线程在调用了getRGB()之后,另外一个方法调用set()方法,那么getName()和getRGB()得到的就不是同一个RGB颜色值了。那么我们如何防止这种情况的发生呢?没错,使用不可变对象的技术,去掉set()方法。
final public class ImmutableRGB { // Values must be between 0 and 255. final private int red; final private int green; final private int blue; final private String name; private void check(int red, int green, int blue) { if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) { throw new IllegalArgumentException(); } } public ImmutableRGB(int red, int green, int blue, String name) { check(red, green, blue); this.red = red; this.green = green; this.blue = blue; this.name = name; } public int getRGB() { return ((red << 16) | (green << 8) | blue); } public String getName() { return name; } public ImmutableRGB invert() { return new ImmutableRGB(255 - red, 255 - green, 255 - blue, "Inverse of " + name); } }
不可变对象的基本准则:
1、不提供setter方法修改fields
2、将所有fields变成private final
3、不允许子类override方法,可以使用final关键字修饰方法,或者使用private修饰
4、如果一个对象的field是一个引用对象,这个引用对象是可变的,那么就不允许修改该field,可以使用final修饰,并且不要将这个引用共享给其他对象,可以使用copy技术,将该引用对象的值赋值一份,再丢给其他对象。
使用java.util.concurrecy.*包来解决并发问题
在这个包中,有个很重要的抽象类,那就是Lock。这个方法的语义更加清晰,需要注意的一点就是,每次使用lock()之后,都需要在finally中,unlock()锁。下面使用Lock来改写前面死锁的例子:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.Random; public class Safelock { static class Friend { private final String name; private final Lock lock = new ReentrantLock(); public Friend(String name) { this.name = name; } public String getName() { return this.name; } public boolean impendingBow(Friend bower) { Boolean myLock = false; Boolean yourLock = false; try { myLock = lock.tryLock(); yourLock = bower.lock.tryLock(); } finally { if (! (myLock && yourLock)) { if (myLock) { lock.unlock(); } if (yourLock) { bower.lock.unlock(); } } } return myLock && yourLock; } public void bow(Friend bower) { if (impendingBow(bower)) { try { System.out.format("%s: %s has" + " bowed to me!%n", this.name, bower.getName()); bower.bowBack(this); } finally { lock.unlock(); bower.lock.unlock(); } } else { System.out.format("%s: %s started" + " to bow to me, but saw that" + " I was already bowing to" + " him.%n", this.name, bower.getName()); } } public void bowBack(Friend bower) { System.out.format("%s: %s has" + " bowed back to me!%n", this.name, bower.getName()); } } static class BowLoop implements Runnable { private Friend bower; private Friend bowee; public BowLoop(Friend bower, Friend bowee) { this.bower = bower; this.bowee = bowee; } public void run() { Random random = new Random(); for (;;) { try { Thread.sleep(random.nextInt(10)); } catch (InterruptedException e) {} bowee.bow(bower); } } } public static void main(String[] args) { final Friend alphonse = new Friend("Alphonse"); final Friend gaston = new Friend("Gaston"); new Thread(new BowLoop(alphonse, gaston)).start(); new Thread(new BowLoop(gaston, alphonse)).start(); } }
在java.util.concurrent.*中,还提供了线程执行器,这个接口就是Executor。在之前的代码中,我们使用(new Thread(r)).start()来启动线程,有了执行器之后,可以使用e.execute(r)来启动一个线程。在Executor中,有两个比较重要的子类,ExecutorService和ScheduledExecutorService,具体的使用方式也比较简单,大家可以直接查看java doc便可。
线程池(Thread Pools)
如果你熟悉jdbc的操作,那么肯定知道数据库连接池。有关于池的概念,都很类似,就是把一些需要的对象,事先准备好,然后将这些对象放入内存中,以后每次需要的时候,就直接从内存中取出来,这样子可以大大节省new一个对象的开销。在java中,提供了几个不一样的线程池供我们选择。
1、java.util.concurrent.Executors.newCachedThreadPool()提供一个缓存池
2、java.util.concurrent.Executors.newSingleThreadExecutor()提供一个单线程池
3、java.util.concurrent.Executors.newFixedThreadPool()提供一个固定大小的线程池
支持多线程的容器(Concurrent Collections)
在java.util.Collection中,提供了很多容器类,但是这些类大部分都不是线程安全的,所以如果程序需要使用线程安全的容器类,那么可以使用java.util.concurrent包中的容器类,这些类最大的区别就是线程安全的,所以使用上对我们来说没什么新的难点,直接查看文档api便可。
原子变量(Atomic Variables)
在之前的例子中,说到了原子访问,比如下面的计数器:
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }
一种方式可以让程序变得是线程安全的,一个是使用线程方法,如下代码:
class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } }
但是这种方式的效率不高,可以使用java.util.concurrent.atomic的原子类,如下代码:
import java.util.concurrent.atomic.AtomicInteger; class AtomicCounter { private AtomicInteger c = new AtomicInteger(0); public void increment() { c.incrementAndGet(); } public void decrement() { c.decrementAndGet(); } public int value() { return c.get(); } }
总结:多线程的技术,一直是我们既向往又害怕的部分。向往是因为多线程的威力,害怕的是多线程的复杂性。在学习初,什么进程、线程、同步、原子、死锁等各种专业词汇搞得我们信心全无。只有不断的学习,总结,其实我们也可以掌握多线程。最后感谢大家的查询...继续苦逼写代码去了。