Java 并发编程基础 ① - 线程

原文地址: Java 并发编程基础 ① - 线程
转载请注明出处!

1、什么是线程

进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源java

操做系统在分配资源时是把资源分配给进程的,可是CPU资源比较特殊,它是被分配到线程的,由于真正要占用CPU运行的是线程,因此也说线程是CPU分配的基本单位编程

以Java 为例,咱们启动一个main函数时,实际上就是启动了一个JVM 的进程main函数所在的线程就是这个进程的一个线程,也称为主线程。一个JVM进程中有多个线程,多个线程共享进程的堆和方法区资源,可是每一个线程有本身的程序计数器和栈区域tomcat

2、线程建立和运行

Java 线程建立有3种方式:多线程

  1. 继承 Thread 类而且重写 run 方法
  2. 实现 Runnable接口的 run 方法
  3. 使用FutureTask方式
具体代码示例不详述,太基础,会感受在水。

说下 FutureTask 的方式,这种方式的本事也是实现了Runnable 接口的 run 方法,看它的继承结构就能够知道。并发

前两种方式都没办法拿到任务的返回结果,可是 Futuretask 方式能够。ide

3、线程通知与等待

3.1 wait() 方法

wait() 方法的效果就是该调用线程被阻塞挂起,直到发生如下几种状况才会调起:函数

  • 其余线程调用了该共享对象的 notify() 方法或者 notifyAll() 方法(继续往下走)
  • 其余线程调用了该线程的 interrupt() 方法,该线程会 InterruptedException 异常返回

若是调用 wait() 方法的线程没有事先获取该对象的监视器锁,调用线程会抛出IllegalMonitorStateException 异常。当线程调用wait() 以后,就已经释放了该对象的监视器锁测试

那么,一个线程如何才能获取一个共享变量的监视器锁?网站

  1. 执行synchronized 同步代码块,使用该共享变量做为参数。this

    synchronized(共享变量) {
        // TODO
    }
  2. 调用该共享变量的同步方法(synchronized 修饰)

    synchronized void sum(int a, int b) {
        // TODO
    }

3.2 notify() / notifyAll()

一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait(...) 系列方法后被挂起的线程。

值得注意的是:

  • 一个共享变量上可能会有多个线程在等待,notify() 具体唤醒哪一个等待的线程是随机的
  • 被唤醒的线程不能立刻从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才能够返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不必定会获取到共享对象的监视器锁,这是由于该线程还须要和其余线程一块儿竞争该锁,只有该线程竞争到了共享变量的监视器锁后才能够继续执行

notifyAll() 方法则会唤醒全部在该共享变量上因为调用wait系列方法而被挂起的线程。

3.3 实例

比较经典的就是生产者和消费者的例子

public class NotifyWaitDemo {

    public static final int MAX_SIZE = 1024;
    // 共享变量
    public static Queue queue = new Queue();

    public static void main(String[] args) {
        // 生产者
        Thread producer = new Thread(() -> {
            synchronized (queue) {
                while (true) {
                    // 挂起当前线程(生产者线程)
                    // 而且,释放经过queue的监视器锁,让消费者对象获取到锁,执行消费逻辑
                    if (queue.size() == MAX_SIZE) {
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 空闲则生成元素,而且通知消费线程
                    queue.add();
                    queue.notifyAll();
                }
            }
        });
        // 消费者
        Thread consumer = new Thread(() -> {
            synchronized (queue) {
                while (true) {
                    // 挂起当前线程(消费者线程)
                    // 而且,释放经过queue的监视器锁,让生产者对象获取到锁,执行生产逻辑
                    if (queue.size() == 0) {
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 空闲则消费元素,而且通知生产线程
                    queue.take();
                    queue.notifyAll();
                }
            }
        });
        producer.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        consumer.start();
    }

    static class Queue {

        private int size = 0;

        public int size() {
            return this.size;
        }

        public void add() {
            // TODO
            size++;
            System.out.println("执行add 操做,current size: " +  size);
        }

        public void take() {
            // TODO
            size--;
            System.out.println("执行take 操做,current size: " +  size);
        }
    }
}

3.4 wait()/notify()/notifyAll() 为何定义在 Object 类中?

因为Thread类继承了Object类,因此Thread也能够调用者三个方法,等待和唤醒必须是同一个锁。而锁能够是任意对象,因此能够被任意对象调用的方法是定义在object类中。

4、join() - 等待线程执行终止

适用场景:须要等待某几件事情完成后才能继续往下执行,好比多个线程加载资源,须要等待多个线程所有加载完毕再汇总处理。

public static void main(String[] args){
    ...
    thread1.join();
    thread2.join();
    System.out.println("all child thread over!");
}

主线程首先会在调用thread1.join() 后被阻塞,等待thread1执行完毕后,调用thread2.join(),等待thread2 执行完毕(有可能),以此类推,最终会等全部子线程都结束后main函数才会返回。若是其余线程调用了被阻塞线程的 interrupt() 方法,被阻塞线程会抛出 InterruptedException 异常而返回。

4.1 实例

给出一个实例帮助理解。

public class JoinExample {

    private static final int TIMES = 100;

    private class JoinThread extends Thread {

        JoinThread(String name){
           super(name);
        }

        @Override
        public void run() {
            for (int i = 0; i < TIMES; i++) {
                System.out.println(getName() + " " + i);
            }
        }
    }

    public static void main(String[] args) {
        JoinExample example = new JoinExample();
        example.test();
    }

    private void test() {
       for (int i = 0; i < TIMES; i++) {
           if (i == 20) {
               Thread jt1 = new JoinThread("子线程1");
               Thread jt2 = new JoinThread("子线程2");
               jt1.start();
               jt2.start();
               // main 线程调用了jt线程的join()方法
               // main 线程必须等到 jt 执行完以后才会向下执行
               try {
                   jt1.join();
                   jt2.join();
                   // join(long mills) - 等待时间内 被join的线程还没执行,再也不等待
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
           System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

5、线程睡眠

sleep()会使线程暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度但不会释放锁

指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,而后参与CPU的调度,获取到CPU资源后就能够继续运行了。

若是在睡眠期间其余线程调用了该线程的 interrupt() 方法中断了该线程,则该线程会在调用sleep方法的地方抛出 InterruptedException 异常而返回。

/**
 * 帮助理解 sleep 不会让出监视器资源
 * 
 * 在线程A睡眠的这10s内那个独占锁lock仍是线程A本身持有
 * 线程B会一直阻塞直到线程A醒来后执行unlock释放锁。
 */
public class ThreadSleepDemo {

    // 独占锁
    private static final Lock LOCK = new ReentrantLock();

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            // 获取独占锁
            LOCK.lock();
            try {
                System.out.println("child thread A is in sleep");
                Thread.sleep(10000);
                System.out.println("child thread A is awake");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // release lock
                LOCK.unlock();
            }
        });

        Thread threadB = new Thread(() -> {
            // 获取独占锁
            LOCK.lock();
            try {
                System.out.println("child thread B is in sleep");
                Thread.sleep(10000);
                System.out.println("child thread B is awake");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // release lock
                LOCK.unlock();
            }
        });

        threadA.start();
        threadB.start();
    }
}

6、让出CPU执行权 - yield()

线程调用yield 方法时,其实是暗示线程调度器当前线程请求让出本身的CPU使用(告诉线程调度器能够进行下一轮的线程调度),但线程调度器能够无条件忽略这个暗示

咱们知道操做系统是为每一个线程分配一个时间片来占有CPU的,正常状况下当一个线程把分配给本身的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread类的静态方法 yield 时,是在告诉线程调度器本身占有的时间片中尚未使用完的部分本身不想使用了,这暗示线程调度器如今就能够进行下一轮的线程调度。

通常不多使用这个方法,在调试或者测试时这个方法或许能够帮助复现因为并发竞争条件致使的问题,其在设计并发控制时或许会有用途,在java.util.concurrent.locks包里面的锁时会看到该方法的使用

sleep与yield方法的区别在于:当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield 方法时,线程只是让出本身剩余的时间片,并无被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

7、线程中断

不少人看到 interrupt() 方法,认为“中断”线程不就是让线程中止嘛。实际上, interrupt() 方法实现的根本就不是这个效果, interrupt()方法更像是发出一个信号,这个信号会改变线程的一个标识位属性(中断标识),对于这个信号如何进行响应则是没法肯定的(能够有不一样的处理逻辑)。不少时候调用 interrupt() 方法非但不是为了中止线程,反而是为了让线程继续运行下去

官方一点的表述:

Java中的线程中断是一种线程间的协做模式, 经过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理

7.1 void interrupt()

设置线程的中断标志为true并当即返回,但线程实际上并无被中断而会继续向下执行;若是线程由于调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,其余线程调用该线程的interrupt()方法会使该线程抛出InterruptedException异常而返回。

7.2 boolean isInterrupted()

检测线程是否被中断,是则返回true,不然返回false。

public boolean isInterrupted() {
        // 传递 false 说明不清除中断标志
        return isInterrupted(false);
    }

7.3 boolean interrupted()

检测当前线程是否被中断,返回值同上 isInterrupted() ,不一样的是,若是发现当前线程被中断,会清除中断标志;该方法是static方法,内部是获取当前调用线程的中断标志而不是调用interrupted()方法的实例对象的中断标志

public static boolean interrupted() {
        // static 方法
        // true - 清除终端标志
        // currentThread
        return currentThread().isInterrupted(true);
    }

8、线程上下文切换

咱们都知道,在多线程编程中,线程个数通常都大于CPU 个数,而每一个CPU同一时刻只能被一个线程使用。

为了让用户感受多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是每一个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态而且让出CPU让其余线程占用,这就是线程的上下文切换

9、线程死锁

死锁是指两个或两个以上的线程在执行过程当中,因争夺资源而形成的互相等待的现象,在无外力做用的状况下,这些线程会一直相互等待而没法继续运行下去。

就如上图,线程A持有资源2,同时想申请资源1,线程B持有资源1,同时想申请资源2,两个线程相互等待就造成了死锁状态。

死锁的产生有四个条件:

  • 互斥条件:指线程对已经获取到的资源进行排他性使用,即该资源同时只由一个线程占用。若是此时还有其余线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其余线程占有,因此当前线程会被阻塞,但阻塞的同时并不释放本身已经获取的资源。
  • 不可剥夺条件:指线程获取到的资源在本身使用完以前不能被其余线程抢占,只有在本身使用完毕后才由本身释放该资源。
  • 环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0, T1, T2, …, Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源。

10、守护线程与用户线程

Java 线程分为两类,

  • daemon 线程(即守护线程)
  • user 线程 (用户线程)

在JVM 启动时会调用 main 函数,main 函数所在的线程就是一个用户线程,同时在JVM 内部也启动了不少守护线程,好比 GC 线程。

守护线程和用户线程的区别在于,守护线程不会影响JVM 的退出,当最后一个用户线程结束时,JVM 会正常退出。

因此,若是你但愿在主线程结束后JVM进程立刻结束,那么在建立线程时能够将其设置为守护线程,若是你但愿在主线程结束后子线程继续工做,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程

举例:好比在Tomcat 的NIO 实现NioEndpoint 类中,会开启一组接受线程来接受用户的链接请求,以及一组处理线程负责具体处理用户请求。

/**
     * Start the NIO endpoint, creating acceptor, poller threads.
     */
    @Override
    public void startInternal() throws Exception {

        if (!running) {
            // ... 省略

            // Start poller threads 处理线程
            pollers = new Poller[getPollerThreadCount()];
            for (int i=0; i<pollers.length; i++) {
                pollers[i] = new Poller();
                Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                pollerThread.setPriority(threadPriority);
                pollerThread.setDaemon(true); // 
                pollerThread.start();
            }
            // 启动接收线程
            startAcceptorThreads();
        }
    }

    protected final void startAcceptorThreads() {
        int count = getAcceptorThreadCount();
        acceptors = new Acceptor[count];

        for (int i = 0; i < count; i++) {
            acceptors[i] = createAcceptor();
            String threadName = getName() + "-Acceptor-" + i;
            acceptors[i].setThreadName(threadName);
            Thread t = new Thread(acceptors[i], threadName);
            t.setPriority(getAcceptorThreadPriority());
            t.setDaemon(getDaemon()); // 默认值是true 
            t.start();
        }
    }

在如上代码中,在默认状况下,接受线程和处理线程都是守护线程,这意味着当tomcat收到shutdown命令后而且没有其余用户线程存在的状况下tomcat进程会立刻消亡,而不会等待处理线程处理完当前的请求。

小结

本篇讲了有关Java 线程的一些基础知识。下一篇,我计划是写一下 Java 线程的生命周期、线程的各个状态和以及各个状态的流转。由于关于这部分我感受国内网站上大部分的文章都没有讲清楚,我会尝试用图片、文字和具体的代码结合的方式写一篇。有兴趣的小伙伴能够关注一下。

若是本文有帮助到你,但愿能点个赞,这是对个人最大动力🤝🤝🤗🤗。

参考

  • 《Java 并发编程之美》
相关文章
相关标签/搜索