多线程基础

  1. 云栖社区>
  2. 博客>
  3. 正文

多线程基础

~信~仰~ 2019-08-21 13:14:08 浏览232
展开阅读全文

需要了解的概念

并发和并行

并发侧重于任务的交替执行,同一时间只能执行一个任务;而并行是任务的同时执行,统一时间可以有多个任务被执行。

单核CPU与多核CPU下任务表现分别为并发与并行。

临界区

临界区用于表示一种公共资源或是共享数据,可以被多个线程使用,但是同一时间内,只能有一个线程在使用它。一旦临界区资源被占用,其他线程要想使用这个资源,则必须等待。

死锁、饥饿和活锁

死锁

指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

饥饿

饥饿是指线程因为种种原因无法获取所需要的资源,导致一直无法执行。比如它的线程优先级可能太低,而高优先级的线程不断抢占他需要的资源。

活锁

当其他线程要使用临界资源时,如果线程主动放弃资源供其他线程使用,而其它线程也主动放弃来使其他线程使用。这样你让我,我让你,最后无论哪个线程都无法使用资源。

线程的状态

Java 线程有6种状态,其定义在Thread.State中:

public class Thread implements Runnable {
    /**
     * A thread can be in only one state at a given point in time.
     * These states are virtual machine states which do not reflect any operating system thread states.
     */
    public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or
         * reenter a synchronized block/method after calling {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }
}

NEW:初始状态,线程被构建,但是还没有调用start方法;

RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一称为RUNNABLED;

BLOCKED:阻塞状态,例如进入同步锁中;

WAITING:等待状态,例如调用Object.waitThread.join(with no timeout)、LockSupport.park等;

TIME_WAITING:超时等待状态,例如调用Thread.sleep以及带超时间的Object.waitThread.join等,超时以后自动返回;

TERMINATED:终止状态,表示当前线程执行完毕。

示意图如下:

image

线程的基本使用

java中多线程使用方式主要为继承Thread类、实现Runnable接口、实现Callable接口。如果使用线程池,可以使用ExecutorService等。

线程的新建

继承Thread

示例:

class ThreadDemo extends Thread {
    
    public void run() {
        System.out.println("run");
    }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();
    }
}

Thread类本质上是实现了Runnable接口的一个实例。启动线程的唯一方法就是通过Thread类的 start()实例方法:

public class Thread implements Runnable {
    
    ...
    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     */
    public synchronized void start() {
        ...
        try {
            start0();
            started = true;
        } finally {...}
    }
}

start()中最终调用了start0(),而start0()是一个native方法,它会启动一个新线程,并在该线程中执行run方法。

start()执行涉及两个线程:当前线程(threadDemo在该线程中调用自己的start())和新的线程(jvm会在该线程中调用run方法,并使其在在该线程中执行)。

值得注意的是,threadDemo直接调用run()不会开启新的线程,因为没有最终调用start0(),此时仅是在当前线程中普通的方法调用;threadDemo直接调用start()则会开启新线程,并让jvm调用run方法在开启的线程中执行。

另外,多次启动线程是不合法的,尤其是线程执行完成后不能重新启动。

实现Runnable接口

如果自己的类已经继承了其它类,无法直接继承Thread,此时可以实现Runnable接口:

class ThreadDemo implements Runnable {

    public void run() {
        System.out.println("run");
    }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();
    }
}

实现方式虽然变了,但是最终还是要调用start()。

实现Callable接口

当需要新线程提供返回值给主线程时,例如主线程需要依赖该返回值进行后续的处理,此时可以使用Callable方式:

class ThreadDemo implements Callable {

    public String call() {
        return "run";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(new ThreadDemo());
        new Thread(futureTask).start();
        String result = futureTask.get();
        System.out.println(result);
    }
}

Callable和Runnable

Callable方式属于Executor框架的功能类,相对于runable体系:

1. callable在任务结束时可以提供返回值,runnable无法提供;

2. run()不能抛出任何检查型异常,但是,非检查型异常会导致线程终止;

3. 运行callable可以拿到future,future能监视目标线程调用call方法的情况,当调用future的get方法以获取结果时,当前线程就会被阻塞,直到call方法结束返回结果。

对于第二条:

检查型异常(Checked Exception):指编译器要检查这类异常,这类异常的发生通常是难以避免的,编译器强制让开发者去解决掉这类异常(通过throws或try-catch),所以称为检查型异常。如果不处理这类异常则不会通过编译。

例如FileNotFoundException,编译器认为文件找不到是不可避免的,如果不处理这些异常,程序将来肯定会出错,此时编译器会提示捕获并处理这种可能发生的异常,不处理就不能通过编译。

非检查型异常(Unchecked Exception):指编译器不会检查这类异常,编译器认为此类异常不是必须处理的,因此即使不处理这类异常,编译器也不会给出错误提示。

例如数组越界、空指针异常等。

run()不能抛出检查型异常,因为接口定义中就没有throws异常:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

并且throws是向上级调用者抛出异常,主线程调用start()后,是由jvm去调用run()方法的,因此run()的最终调用者是jvm。

但是线程依然有可能抛出unchecked exception,此类异常抛出时会导致线程终止,但是对于主线程和其他线程则完全感知不到该异常的抛出(当然也无法法catch到该异常),且该异常的抛出对主线程和其他线程完全没有影响。

因此,对于Runnable体系的线程,我们不能捕获在线程中出现的异常,因此无论是checked exception还是unchecked exception,run方法内进行try-catch并处理掉,即:

class ThreadDemo0 implements Runnable {

    public void run() throws Exception { // throws Exception会导致编译不通过
        System.out.println(1 / 0);
    }

    public static void main(String[] args) {
        try {
            ThreadDemo0 threadDemo0 = new ThreadDemo0();
            new Thread(threadDemo0).start();
        } catch (Exception e) {
            e.printStackTrace(); // 无法catch到除0异常
        }
    }
}

java5之后,我们可以通过Executor来解决run()抛出的unchecked exception问题。Thread.UncaughtExceptionHandler是java5的新接口,它允许在每一个Thread对象上添加一个异常处理器:

public class Thread implements Runnable {
    /**
     * Interface for handlers invoked when a <tt>Thread</tt> abruptly
     * terminates due to an uncaught exception.
     * <p>When a thread is about to terminate due to an uncaught exception
     * the Java Virtual Machine will query the thread for its
     * <tt>UncaughtExceptionHandler</tt> using
     * {@link #getUncaughtExceptionHandler} and will invoke the handler's
     * <tt>uncaughtException</tt> method, passing the thread and the
     * exception as arguments.
     * If a thread has not had its <tt>UncaughtExceptionHandler</tt>
     * explicitly set, then its <tt>ThreadGroup</tt> object acts as its
     * <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
     * has no
     * special requirements for dealing with the exception, it can forward
     * the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
     * default uncaught exception handler}.
     *
     * @see #setDefaultUncaughtExceptionHandler
     * @see #setUncaughtExceptionHandler
     * @see ThreadGroup#uncaughtException
     * @since 1.5
     */
    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         */
        void uncaughtException(Thread t, Throwable e);
    }
}

当线程因未捕获的异常而将要被终止时,则jvm首先查询当前线程是否有UncaughtExceptionHandler处理器,如果有则使用该处理器的uncaughtException()来处理,并将当前线程及其异常作为参数传递过去;

如果没有该处理器,则查看当前线程所在线程组是否设置了UncaughtExceptionHandler,如果已经设置则使用该线程组的UncaughtExceptionHandler来处理;

否则,通过getUncaughtExceptionHandler获取默认处理器:

    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }

如果上述处理器都不存在,那么jvm将直接在console中打印Exception的StackTrace信息。

线程的停止

线程停止不能使用Thread.stop(),因为该方法是暴力停止,如果业务执行到一半被stop()终止,则可能会导致数据的不一致。

正确的停止方式应该由实际业务决定,例如:

class ThreadDemo0 implements Runnable {

    private static volatile boolean stop = false;
    private static volatile int i = 0;

    public void run() {
        while(!stop) {
            System.out.println(i++);
        }
    }

    public static void main(String[] args) {
        new Thread(new ThreadDemo0()).start();
        while(true) {
            if (i == 5) {
                stop = true;
            }
        }
    }
}

实际上Thread提供了线程中断相关的API:

public void Thread.interrupt();

public boolean Thread.isInterrupted();

public static boolean Thread.interrupted();

interrupt()用于通知线程中断,也就是设置中断标志位,中断标志位表示当前线程已经被中断了。isInterrupted()用于判断当前线程是否被中断。静态方法interrupted()也是用于判断当前线程的中断状态,但同时会清除当前线程的中断标志位。

class ThreadDemo0 implements Runnable {

    private static volatile int i = 0;

    public void run() {
        while(!Thread.currentThread().isInterrupted()) {
            System.out.println(i++);
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new ThreadDemo0());
        thread.start();

        while(true) {
            if (i == 5) {
                thread.interrupt();
                break;
            }
        }
    }
}

当发生异常时,interrupt标志位会被复位。

线程等待和通知

wait()和notify()、notifyAll()用于多线程之间的协作,这两个方法都属于Object类,这意味着任何对象都能调用这两个方法。

当在一个对象A调用wait()方法后,当前线程就会在这个对象上等待,直到其他线程调用了对象A的notify()或notifyAll()为止。

如果一个线程调用了object.wait(),那么该线程则进入object对象的等待队列。这个队列中可能会存在多个线程,当object.notify()被调用时,它就会从这个等待队列中随机选择一个线程并将其唤醒。如果执行object.notifyAll()则唤醒队列中所有线程。

需要注意的是,wait()、notify()、notifyAll()必须在synchronized代码块中使用,方法执行前都必须获取目标对象的监视器,当wait()执行时会释放当前的监视器并使得当前线程进入阻塞队列,当notify()被调用时,首先要获取到object的监视器,然后才能去唤醒其他线程。

示例:

class ThreadDemo {

    final static Object OBJECT = new Object();

    public static class T1 extends Thread {
        public void run() {
            synchronized (OBJECT) {
                try {
                    System.out.println("t1 start");
                    OBJECT.wait();
                    System.out.println("t1 end");
                } catch (InterruptedException e) {}
            }
        }
    }
    
    public static class T2 extends Thread {
        public void run() {
            synchronized (OBJECT) {
                System.out.println("t2 start");
                OBJECT.notify();
                System.out.println("t2 end");
                try {
                    Thread.sleep(2000);
                } catch (Exception e) {}
            }
        }
    }

    public static void main(String[] args) {
        new T1().start();
        new T2().start();;
    }
}

上述代码执行时,t1被唤醒后并不能立即执行,因为t2线程sleep了2秒,在这2s内t2并未释放object的监视器,所以在t2线程sleep了2s后,才会输出t1 end

这三个方法必须在synchronized代码块中执行,因为这些操作都和监视器相关,wait必须要知道获取谁的监视器,而notify需要知道去唤醒等待在哪里的线程,而synchronized可以提供这个监视器。

监视器(monitor)和锁(lock)的关系:

在jvm中,锁的实现方式就是monitor;
entermonitor就是获得某个对象的lock(owner是当前线程);
leavemonitor就是释放某个对象的lock。

简单的认为,在object中,monitor就是lock。

线程挂起和继续

线程的挂起和继续执行的方法分别是suspend()resume(),被挂起的线程,必须要等到resume()操作后才能继续执行。但这两个方法已经被废弃了。

因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它占用的锁时,都会被阻塞。直到对应的线程上进行了resume()操作,被挂起的线程才能继续。但是,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且它所占用的锁也不会被释放。

而且,对于被挂起的线程,从它的线程状态上看,居然还是Runnable,这也会严重影响我们对系统当前的判断。

线程join和yield

join和yield的部分源码:

public class Thread implements Runnable {
    /**
     * Waits for this thread to die.
     */
    public final void join() throws InterruptedException {
        join(0);
    }
    
    /**
     * Waits at most {@code millis} milliseconds for this thread to
     * die. A timeout of {@code 0} means to wait forever.
     */
    public final synchronized void join(long millis)
    throws InterruptedException {
        ...
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                ...
                wait(delay);
                ...
            }
        }
    }
    
    /**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     */
    public static native void yield();
}

join

当前线程调用其他线程的join方法,会阻塞当前线程,直到其他线程执行完毕,才会继续执行。

join()表示无限期的等待,而join(long millis)则指定等待的最大时间,如果超过最大时间则不再等待该线程,继续执行。

从源码中可以发现,join()方法是通过wait()实现的,而join(long millis)则是通过wait(long timeout)实现的。当millis为0 时,会进入while(isAlive())循环,并且只要子线程是活跃的,宿主线程就不停的等待。

join方法会让宿主线程交出CPU执行权,并放弃占有的锁。

yield

调用yield()会让当前线程交出CPU资源,但是交出CPU资源后,该线程仍然会参与争夺下一轮CPU的使用权。但是,yield()不能控制具体的交出CPU的时间。

调用yield()方法并不会让线程进入阻塞状态,也不会释放锁,而是让线程重回就绪状态,它只需要等待重新得到CPU的执行权就又能继续执行了。

参考:java高并发程序设计

网友评论

登录后评论
0/500
评论
~信~仰~
+ 关注