2451 2018-04-11 2020-06-25

前言:多线程一直是Java里面的一个重点,一个难点,这次我们来简单看下。

一、线程的实现

多线程的实现可以通过两种方式,一是继承Thread类,二是实现Runable接口,这两者的本质是一样的,区别在于是否需要规避Java中的单继承机制。

1、Runnable接口

查看代码,如下

public interface Runnable {
    // 只有一个抽象方法
    public abstract void run();
}

2、Thread类

public class Thread implements Runnable {
    private static native void registerNatives();
    static {
        registerNatives();
    }

    private volatile String name;
    private int            priority;
    private Thread         threadQ;
    private long           eetop;
    // 省略大部分代码

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

    public Thread() {
        // 首先会执行init方法,初始化线程
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        this.name = name;
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */
            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }
            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();
        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        g.addUnstarted();
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
}

从上面的代码中,我们就可以看出,当把一个Runnable接口作为参数传进Thread类的构造方法时,Thread内部还是会把当作一个Thread看待,两者只是形式不同。

二、线程的生命周期

线程是程序执行的顺序控制流,它与进程一样,具有生命周期。在Java中,它有6种状态。

1、NEW

初始状态,当创建一个Thread类或其子类的对象时,新生成的线程对象就处于新建状态。这是系统还没有为线程对象分配资源,线程尚未启动。

处于新建状态的线程只能被启动或终止,若试图调用线程的其他处理方法则会提示失败。

2、RUNNABLE

运行状态,运行状态的线程,可在规定时间内(时间片)执行run()方法中的任务。当使用完系统分配的时间后,系统会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。如果任务还没完成,则该线程进入等待状态,等待下次获得CPU资源;如果已完成线程任务,则该线程进入终止状态;如果正在执行的线程遇到特殊情况,也可进入阻塞状态。

3、WAITING

等待状态,当线程对象调用了start()方法后,就进入了等待状态。处于该状态的线程进入就绪线程队列排队等待CPU的使用权。

4、BLOCKED

阻塞状态,处于运行状态的线程在某些特殊情况下,会主动放弃被动剥夺CPU的使用权,进入阻塞状态。下面列出了会导致运行线程进入阻塞状态的几种情况

  • 调用sleep(long millis)方法
  • 当在一个线程中调用另一个线程的join方法时,也会使线程进入阻塞状态
  • 当线程调用wait()方法时,线程进入阻塞状态
  • 当线程调用一个阻塞式的IO方法时,线程会进入阻塞状态
  • 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程进入阻塞状态

进入阻塞状态的线程不能进入就绪队列排队,只有当引起阻塞的原因被消除时,线程才可以转入就绪状态,重新进到线程队列中排队等待CPU的使用权,以便从原来终止处开始继续运行。由于进入阻塞状态的不同,从阻塞状态进入就绪状态也存在下面几种情况。

  • 因执行sleep(long millis)方法引起的阻塞,只需等待线程休眠时间到了以后,线程就会自动进入就绪状态。
  • 因调用join()方法引起的阻塞,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。
  • 因调用wait()方法引起的阻塞,需要调用notify()方法唤醒该线程,进入就绪状态。
  • 因调用阻塞式IO方法引起的阻塞,只需等到该IO方法结束,就会进入就绪状态。
  • 因同步锁被占用而引起的阻塞,只需获取其他线程所持有的锁,就会进入就绪状态。

5、TIME_WAITING

超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的。

6、TERMINATED

终止状态,线程的run()方法正常执行完毕,或者线程抛出一个为捕获的异常(Exception)、错误(Error),被提前强制性地终止,线程就进入终止状态。处于终止状态的线程不具有继续运行的能力,也不能转换到其他状态。

三、线程的调度

1、优先级

Java中默认采用基于抢占式的分时调度策略即首先让优先级高的线程优先占用CPU,对优先级相同的线程按照分时策略进行调度

在Java中定义了3个默认的优先级别及设置优先级别的方法,如下

public final static int MIN_PRIORITY = 1;

public final static int NORM_PRIORITY = 5;

public final static int MAX_PRIORITY = 10;

public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}

需要补充的是,优先级高仅仅是获取CPU使用权的机会更大些,并不是一定的。

2、线程休眠与让步

  • 线程休眠:是指让线程放弃执行的权利,进入阻塞状态,给其他线程运行的机会,指定时间后,再次进入就绪队列排队
  • 线程让步:是指让线程放弃执行的权利,进入等待状态,给相同或更高优先级的线程提供运行的机会

在Java中定义了相关的方法,如下

public static native void sleep(long millis) throws InterruptedException;

/**
 * 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.
 *
 * <p> Yield is a heuristic attempt to improve relative progression
 * between threads that would otherwise over-utilise a CPU. Its use
 * should be combined with detailed profiling and benchmarking to
 * ensure that it actually has the desired effect.
 *
 * <p> It is rarely appropriate to use this method. It may be useful
 * for debugging or testing purposes, where it may help to reproduce
 * bugs due to race conditions. It may also be useful when designing
 * concurrency control constructs such as the ones in the
 * {@link java.util.concurrent.locks} package.
 */
public static native void yield();

正如所见,都是native方法,这都是操作系统级别的操作。

3、线程插队

线程插队是指让一个线程插入到另外一个线程里面,直到该线程执行完毕,另外一个线程才会继续

在Java中定义了相关方法,如下

// 等待该线程终止的时间最长为 millis 毫秒,其底层还是依赖于wait
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.
 *
 * <p> This implementation uses a loop of {@code this.wait} calls
 * conditioned on {@code this.isAlive}. As a thread terminates the
 * {@code this.notifyAll} method is invoked. It is recommended that
 * applications not use {@code wait}, {@code notify}, or
 * {@code notifyAll} on {@code Thread} instances.
 *
 * @param  millis
 *         the time to wait in milliseconds
 *
 * @throws  IllegalArgumentException
 *          if the value of {@code millis} is negative
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          <i>interrupted status</i> of the current thread is
 *          cleared when this exception is thrown.
 */
public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

四、线程的同步

线程默认是不同步,因此会出现很多对于数据脏读或脏写的情况。Java提供了两种同步机制,来保证线程同步。

1、同步代码块

所谓的同步代码块,就是将处理共享资源的代码放置于一个代码块中,使用synchronized关键字修饰,外加一个锁对象。

2、同步方法

同步方法和同步代码块类似,只不过是用synchronized关键字来修饰方法。对于单个对象方法,锁对象就是对象本身;针对静态方法,锁对象就是Class对象;

五、示例代码

/**
 * 开三个线程,依次循环打印012 345 678,即a打印0,接b打印2,接c打印2,接a打印3....
 * 在调用wait、notify方法时,需要获取对象的监视器monitor——即先获取这个对象锁,否则将抛出IllegalMonitorStateException异常。
 */
public class WaitAndNotify {

    private static AtomicInteger i = new AtomicInteger(0);
    
    private static final int target = 7;

    public static void main(String[] args) throws InterruptedException {
        Object lockAB = new Object();
        Object lockBC = new Object();
        Object lockCA = new Object();
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                while (i.get() < target) {
                    synchronized (lockCA) {
                        try {
                            lockCA.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    synchronized (lockAB) {
                        System.out.println(Thread.currentThread() + ":" + i.getAndIncrement());
                        lockAB.notifyAll();
                    }
                }
            }
        }, "线程一");
        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                while (i.get() < target) {
                    synchronized (lockAB) {
                        try {
                            lockAB.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    synchronized (lockBC) {
                        System.out.println(Thread.currentThread() + ":" + i.getAndIncrement());
                        lockBC.notifyAll();
                    }
                }
            }
        }, "线程二");
        Thread c = new Thread(new Runnable() {
            @Override
            public void run() {
                while (i.get() < target) {
                    synchronized (lockBC) {
                        try {
                            lockBC.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    synchronized (lockCA) {
                        System.out.println(Thread.currentThread() + ":" + i.getAndIncrement());
                        lockCA.notifyAll();
                    }
                }

            }
        }, "线程三");
        c.start();
        b.start();
        a.start();
        Thread.sleep(500);
        synchronized (lockCA) {
            lockCA.notifyAll();
        }
    }
}
// 输出如下
Thread[线程一,5,main]:0
Thread[线程二,5,main]:1
Thread[线程三,5,main]:2
Thread[线程一,5,main]:3
Thread[线程二,5,main]:4
Thread[线程三,5,main]:5
Thread[线程一,5,main]:6
Thread[线程二,5,main]:7
Thread[线程三,5,main]:8

更多关于多线程的特性,请查看多线程系列后续章节,里面有更为细致的讨论。

总访问次数: 254次, 一般般帅 创建于 2018-04-11, 最后更新于 2020-06-25

进大厂! 欢迎关注微信公众号,第一时间掌握最新动态!