第2章 Java并行程序基础(一)

2.1 有关线程你必须知道的事

  • 进程是系统进行资源分配和调度的基本单位,是程序的基本执行实体。

  • 线程就是轻量级进程,是程序执行的最小单位。

  • 线程的生命周期,如图2.3所示。

  • 线程的所有状态都在Thread中的State枚举中定义,如下所示:

public enum State {
    NEW,
    RUNABLE,
    BLOKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
  • NEW状态表示刚刚创建的线程,这种线程还没有开始执行。等到线程的start()方法调用时,才表示线程开始执行。当线程执行时,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了。如果线程在 执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁。WAITING和TIMED_WAINTING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAINTING会进入一个有时限的等待。那等待的线程究竟在等什么呢?一般来说,WAINTING的线程正是在等待一个特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。

2.2 初始线程:线程的基本操作

2.2.1 新建线程

  • 只要使用new关键字创建一个线程对象,并且将它start()起来即可。
Thread t1 = new Thread();
t1.start();
  • 线程Thread,有一个run()方法,start()方法就会新建一个线程并让这个线程执行run()方法。
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("Hello, I am t1");
    }
};
t1.start();
  • 上述代码使用匿名内部类,重载了run()方法。
  • 考虑Java是单继承的,也就是说继承本身也是一种很宝贵的资源,因此,我们也可以使用Runnable接口来实现同样的操作。Runable接口是一个单方法接口,它只有一个run()方法:
public interface Runnable {
    public abstract void run();
}
  • Thread类有一个非常重要的构造方法:
public Thread(Runnable target)
public class T1 implements Runnable {
    public static void main(String[] args) {
        Thread t1 = new Thread(new T1());
        t1.start();
    }
    
    @Override
    public void run() {
        System.out.println("Oh, I am Runnable");
    }
}

2.2.2 终止线程

  • Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。而这些锁恰恰是用来维持对象一致性的。如果此时,写线程写入数据正写到一半,并强行终止,那么对象就会 被写坏,同时,由于锁已经被释放,另外一个等待该锁的读线程就顺理成章的读到了这个不一致的对象,悲剧也就此发生。整个过程如图2.4所示。

  • 可以自行决定线程何时退出,如下所示:

public static class ChangeObjectThread extends Thread {
    volatile boolean stopme = false;
    
    public void stopMe() {
        stopme = true;
    }
    
    @Override
    public void run() {
        while (true) {
            if (stopme) {
                System.out.println("exit by stop me");
                break;
            }
            synchronized (u) {
                int v = (int) (System.currentTimeMillis() / 1000);
                u.setId(v);
                
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                u.setName(String.valueOf(v));
            }
            Thread.yield();
        }
    }
}

2.2.3 线程中断

  • 与线程中断有关的三个方法:
public void Thread.interrupt()  //中断线程,设置中断标志位
public boolean Thread.isInterrupted()  //判断是否被中断(通过检查中断标志位)
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态
  • 下面这段代码对t1线程进行了中断,那么中断后,t1会停止执行吗?
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            while (true) {
                Thread.yield();
            }
        }
    };
    t1.start();
    Thread.sleep(2000);
    t1.interrupt();
}
  • 在这里,虽然对t1进行了中断,但是在t1中并没有中断处理的逻辑,因此,即使t1线程被置上中断状态,但是这个中断不会发生任何作用。
  • 如果希望t1在中断后退出,就必须为它增加相应的中断处理代码:
Thread t1 = new Thread() {
    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Interruted");
                break;
            }
            Thread.yield();
        }
    }
}
  • 下面,先来了解一下Thread.sleep()函数,它的签名如下:
public static native void sleep(long millis) throws InterruptedException
  • Thread.sleep()方法会让当前线程休眠若干时间,它会抛出一个InterruptException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并且处理它,当线程在sleep()休眠时,如果被中断,这个异常就会产生。
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("Interrupted!");
                    break;
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    System.out.println("Interrupted When Sleep");
                    //设置中断状态
                    Thread.currentThread().interrupt();
                }
                Thread.yield();
            }
        }
    };
    t1.start();
    Thread.sleep(2000);
    t1.interrupt();
}
  • 执行Thread.sleep(),线程被中断,则程序会抛出异常,进入catch中,但没有立即退出线程。因为还要进行后续的处理,保证数据的一致性和完整性,因此,执行了Thread.interrupt()方法再次中断自己,置上中断标记位。只有这样做,在Thread.isInterrupted()检查中,才能发现当前线程已经被中断。
  • 注意:Thread.sleep()方法会清除中断标记,故再次设置中断标记位。

2.2.4 等待(wait)和通知(notify)

  • 任何对象都可以调用这两个方法。
public final void wait() throws InterruptedException
public final native void notify()
  • 当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。一直等到其他线程调用了该对象的notify()方法为止。

  • 图2.5展示了两者的工作过程。

  • 强调一点,Object.wait()方法必须包含在对应的synchronized语句中,无论是wait()或者notify都需要首先获得目标对象的一个监视器。如图2.6所示,显示了wait()和notify()的工作流程细节。

  • 为了方便大家理解,这里给出了一个简单地使用wait()和notify()的案例:

public class SimpleWN {
    final static Object object = new Object();
    public static class T1 extends Thread {
        public void run() {
            synchronized (object) {
                System.out.println(System.currentTimeMillis() + ":T1 start!");
                try {
                    System.out.println(System.currentTimeMillis() + ":T1 wait for object");
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static class T2 extends Thread {
        synchronized (object) {
            System.out.println(System.currentTimeMillis() + ":T2 start! notify one thread");
            object.notify();
            System.out.println(System.currentTimeMillis() +":T2 end!");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                
            }
        }
    }
    
    public static void main(String[] args) {
        Thread t1 = new T1();
        Thread t2 = new T2();
        t1.start();
        t2.start();
    }
}
  • 上述代码中,开启了两个线程T1和T2。T1执行了object.wait()方法。在T1中执行wait()方法前,T1先申请object的对象锁。因此,在执行object.wait()时,它是持有object的锁的。wait()方法执行后,T1会进行等待,并释放object的锁。T2在执行notify()之前也会获得object的对象锁。这里为了让实验效果明显,特意安排在notify()通知后,让T2休眠2秒钟,这样做可以更明显地说明,T1得到notify()通知后,还是会先尝试重新得到object的对象锁。
T1 start!
T1 wait for object
T2 start! notify one thread
T2 end!
T1 end!
  • 注意:Object.wait()和Thread.sleep()方法都可以让线程等待若干时间。除了wait()可以被唤醒外,另外一个主要的区别是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。

2.2.5 挂起(suspend)和继续执行(resume)线程

  • 这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()操作后,才能继续指定。

  • 不推荐使用suspend()去挂起线程的原因,是因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它暂用的锁时,都会被牵连,导致无法正常运行(如图2.7所示)。直到对应的线程上进行了resume()操作,被挂起的线程才能继续,从而其他所有阻塞在相 关锁上的线程也可以继续执行。但是,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看,居然还是Runnable。

  • 为了方便大家理解suspend()的问题,这里准备一个简单的程序。演示了这种情况:

public class BadSuspend() {
    public static Object u = new Object();
    static ChangeOjectThread t1 = new ChangeObjectThread("t1");
    static ChangeOjectThread t2 = new ChangeObjectThread("t2");
    
    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super.setName(name);
        }
        @Override
        public void run() {
            synchronized (u) {
                System.out.println("in " + getName);
                Thread.currentThread().suspend();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
        t2.resume();
        t1.join();
        t2.join();
    }
}
 in t1
 in t2

2.2.6 等待线程结束(join)和谦让(yield)

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
  • 第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。第二个方法给出了最大等待时间,如果超出给定时间目标线程还在执行,当前线程也会因为“等不及了”,而继续往下执行。
public class JoinMain {
    public volatile static int i = 0;
    public static class AddThread extends Thread {
        @Override
        public void run() {
            for (i = 0; i < 10000000; i++);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AddThread at = new AddThread();
        at.start();
        at.join();
        System.out.println(i);
    }
}
  • 主函数中,如果不使用join()等待AddThread,那么得到的i很可能是0或者一个非常小的数字。因为AddThread还没开始执行,i的值就已经被输出了。但在使用join()方法后,表示主线程愿意等待AddThread执行完毕,故在join()返回时,AddThread已经执行完毕,故i总是10000000。
  • join()的本质是让调用线程wait()在当前线程对象实例上。
while (isAlive()) {
    wait(0);
}
  • 可以看到,它让调用线程在当前线程对象上进行等待。
  • 另外一个有趣的方法,是Thread.yield(),它的定义如下:
public static native void yield();
  • 这是一个静态方法,一旦执行,它会使当前线程让出CPU。
  • 如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield()。
posted @ 2018-01-19 16:37  _sanjun  阅读(176)  评论(0编辑  收藏  举报