您现在的位置是:首页 > 文章详情

多线程基础

日期:2019-08-20点击:494

需要了解的概念

并发和并行

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

单核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高并发程序设计

原文链接:https://yq.aliyun.com/articles/715277
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章