多线程

作者: SingleDiego | 来源:发表于2018-03-29 15:34 被阅读22次

    创建线程

    Java 中“一切皆对象”,线程也被封装成一个对象。我们可以通过继承 Thread 类来创建线程。

    线程类中的的 run() 方法包含了该线程应该执行的指令。我们在衍生类中覆盖该方法,以便向线程说明要做的任务:

    class NewThread extends Thread {
    
        private static int threadID = 0; // shared by all
    
        // 构造器
        public NewThread() {
            super("ID:" + (++threadID));
        }
    
        // 将对象转为字符串
        // 当我们打印该对象时,Java 将自动调用该方法
        public String toString() {
            return super.getName();
        }
    
        // 该线程进行什么操作
        public void run() {
            System.out.println(this);
        }
    }
    
    
    public class Test
    {
        public static void main(String[] args)
        {
            NewThread thread1 = new NewThread();
            NewThread thread2 = new NewThread();
            thread1.start(); // start thread1
            thread2.start(); // start thread2
        }
    }
    

    输出:

    ID:1
    ID:2
    

    ++ 是 Java 中的累加运算符,即让变量加 1。这里 ++ 出现在 threadID 之前,说明先将 threadID 加 1,再对周边的表达式求值。

    toStringObject 根类的方法,我们通过覆盖该方法,来将对象转换成字符串。当我们打印该对象时,Java 将自动调用该方法。(类似于 Python 的 __str__ 方法)。

    Thread 基类的构建方法(super())可以接收一个字符串作为参数。该字符串是该线程的名字,并使用 getName() 返回。

    定义类之后,我们在 main() 方法中创建线程对象。每个线程对象为一个线程。创建线程对象后,线程还没有开始执行。

    我们调用线程对象的 start() 方法来启动线程。start() 方法可以在构造方法中调用。这样,我们一旦使用 new 创建线程对象,就立即执行。

    Thread 类还提供了下面常用方法:

    • join(Thread tr) 等待线程 tr 完成

    • setDaemon() 设置当前线程为后台daemon (进程结束不受daemon线程的影响)

    Thread类官方文档: http://docs.oracle.com/javase/6/docs/api/java/lang/Thread.html




    Runnable

    实现多线程的另一个方式是实施 Runnable 接口,并提供 run() 方法。实施接口的好处是容易实现多重继承(multiple inheritance)。然而,由于内部类语法,继承 ··Thread·· 创建线程可以实现类似的功能。我们在下面给出一个简单的例子:

    class NewThread implements Runnable {
        public String toString() {
            return Thread.currentThread().getName();
        }
    
        public void run() {
            System.out.println(this);
        }
    }
    
    
    public class Test {
        public static void main(String[] args) {
            Thread thread1 = new Thread(new NewThread(), "first");
            Thread thread2 = new Thread(new NewThread(), "second");
            thread1.start(); // start thread1
            thread2.start(); // start thread2
        }
    }
    

    输出:

    first
    second
    




    synchronized(同步)

    多任务编程的难点在于多任务共享资源。对于同一个进程空间中的多个线程来说,它们都共享堆中的对象。某个线程对对象的操作,将影响到其它的线程。

    在多线程编程中,要尽力避免竞争条件(racing condition),即运行结果依赖于不同线程执行的先后。线程是并发执行的,无法确定线程的先后,所以我们的程序中不应该出现竞争条件。

    然而,当多任务共享资源时,就很容易造成竞争条件。我们需要将共享资源,并造成竞争条件的多个线程线性化执行,即同一时间只允许一个线程执行。

    下面是一个售票程序。3个售票亭(Booth)共同售卖 100 张票(Reservoir)。

    每个售票亭要先判断是否有余票,然后再卖出一张票。如果只剩下一张票,在一个售票亭的判断和售出两个动作之间,另一个售票亭卖出该票,那么第一个售票亭(由于已经执行过判断)依然会齿形卖出,造成票的超卖。

    为了解决该问题,判断和售出两个动作之间不能有“空隙”。也就是说,在一个线程完成了这两个动作之后,才能有另一个线程执行。

    在 Java 中,我们将共享的资源置于一个对象中,比如下面 Reservoir 对象。它包含了总共的票数;将可能造成竞争条件的,针对共享资源的操作,放在 synchronized 方法中,比如下面的 sellTicket()

    synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用 synchronized 方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

    使用 synchronized 修饰符。我们就能排除了竞争条件的可能。

    class Reservoir {
        private int total; // 票数总量
    
        // 构造器
        public Reservoir(int t) 
        {
            this.total = t;
        }
    
        // 使用 synchronized(同步) 方法
        // 该方法用于保证线程安全
        public synchronized boolean sellTicket() 
        {
            if(this.total > 0) {
                this.total = this.total - 1;
                return true; // 售出一张票
            }
            else {
                return false; // 余票不足
            }
        }
    }
    
    // 继承 Thread 类,用于多线程
    class Booth extends Thread {
        private static int threadID = 0; // 表示线程ID的类属性
    
        private Reservoir release;      // sell this reservoir 
        private int count = 0;          // owned by this thread object
    
        // 构造器
        public Booth(Reservoir r) {
            super("ID:" + (++threadID));
            this.release = r;          // all threads share the same reservoir
            this.start();
        }
    
        public String toString() {
            return super.getName();
        }
        
        public void run() {
            while(true) {
                // 如果符合售票条件
                if(this.release.sellTicket()) {
                    this.count = this.count + 1;
                    System.out.println(this.getName() + ": sell 1");
                    try {
                        sleep((int) Math.random()*100);   // 休眠(随机数)
                    }
                    catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                else {
                    break;
                }
            }
            System.out.println(this.getName() + " I sold:" + count);
        }
    }
    
    
    public class Test {
        public static void main(String[] args) {
            Reservoir r = new Reservoir(10);
            Booth b1 = new Booth(r);
            Booth b2 = new Booth(r);
            Booth b3 = new Booth(r);
        }
    }
    

    Java 的每个对象都自动包含有一个用于支持同步的计数器,记录 synchronized 方法的调用次数。执行synchronized 方法时线程获得该计数器,计数器加 1。

    synchronized 方法调用结束并退出时,计数器减 1。其他线程如果也调用了同一对象的 synchronized 方法,必须等待该计数器变为 0,才能锁定该计数器,开始执行。




    关键代码

    上面,我们利用 synchronized 修饰符同步了整个方法。我们可以同步部分代码,而不是整个方法。这样的代码被称为 关键代码(critical section)。我们使用下面的语法:

    synchronized (syncObj) {
    
      ...;
    
    }
    

    花括号中包含的是想要同步的代码,syncObj 是任意对象。我们将使用 syncObj 对象中的计数器,来同步花括号中的代码。

    相关文章

      网友评论

        本文标题:多线程

        本文链接:https://www.haomeiwen.com/subject/hwtycftx.html