多线程的神秘面纱...还在恐惧吗???

前言

最开始学习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();
    }
}




总结:多线程的技术,一直是我们既向往又害怕的部分。向往是因为多线程的威力,害怕的是多线程的复杂性。在学习初,什么进程、线程、同步、原子、死锁等各种专业词汇搞得我们信心全无。只有不断的学习,总结,其实我们也可以掌握多线程。最后感谢大家的查询...继续苦逼写代码去了。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。