Java基础复习之旅(3)-线程篇

1. 基本概念

1.1 线程、进程、协程是什么?

首先要知道,一个进程可以有多个线程,一个线程可以有多个协程。
先说说线程跟进程:

  • 进程是资源分配的最小单元,线程是CPU调度的最小单位。所有与进程相关的资源,均被记录在PCB(印刷电路板)中。
  • 线程隶属于某一个进程,共享进程的资源。线程只由堆栈寄存器,程序计数器和栈堆指针组成。
  • 进程有独立的地址空间,彼此之间相互不影响,可以看做一个独立的应用。而线程只是进程的执行路径。
  • 总结成一句话就是:对操作系统来说,进程是最小的资源管理单元,线程的最小的执行单元。

协程平时听的比较少,所以这里引用维基百科的解释:

  • 协程是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。
  • 协程是一种比线程更加轻量级的存在,并且协程与线程最大的区别就在于。线程是抢占式多任务的,协程是协作式多任务的。这就意味着协程提供并发性而非并行性,所以不需要加锁的操作。

1.2 run()方法与start()方法的区别?

\quad start()方法是运行多线程的主方法,在start()方法内部,通过调用run()方法启动这个线程。run()方法用以启动一个线程,然后主线程立刻返回。该启动的线程不会马上执行,而是在等待队列中等待CPU的调度。

1.3 进程的状态与线程的状态

\quad我们知道在操作系统中,进程有五种状态: 新建、运行、就绪、阻塞、终止。注意这只是比较广泛的说法,不同的操作系统对进程状态定义也有不同。

但是在Java中,Thread类定义了六种线程状态,千万不要混淆了:

  • 1.初始(NEW):新创建了一个线程对象,但是还没有调用start()方法。
  • 2.可运行(RUNNABLE):分为就绪状态与运行中状态。
    在调用start()方法之后线程就进入就绪状态,等待CPU的调度。注意:线程在睡眠和挂起中恢复的时候也会进入就绪状态。
    就绪状态的线程得到了CPU的时间片,线程被设置为当前线程,开始执行run()方法,线程进入运行中状态。
  • 3.阻塞(BLOCKED):表示线程由于锁的原因造成了阻塞。
  • 4.等待(WAITING)比如使用Thread.sleep()方法,CPU不会给线程分配时间,使得线程处于等待状态。处于该状态的线程需要被期待对象所唤醒,否则会处于无限期的等待状态。
  • 5.超时等待(TIME_WAITTING):与等待转态的区别就在于,他不会无限期的等待,当达到一定的时间之后,他们会被自动的唤醒。
  • 6.终止状态(TERMINATED):当线程的run()方法执行完毕时,或者主线程的main()执行完毕时,我们就认为这个线程终止了。注意:线程一旦终止了,就不能调用start()方法。

补充: 进入Thread.getState()方法可以发现在JVM中也定义了六种线程的状态:

public State getState() {
        // get current thread state
        return sun.misc.VM.toThreadState(threadStatus);
    }
复制代码

sun.misc包中的VM类中的线程状态:

private static final int JVMTI_THREAD_STATE_ALIVE = 1;
    private static final int JVMTI_THREAD_STATE_TERMINATED = 2;
    private static final int JVMTI_THREAD_STATE_RUNNABLE = 4;
    private static final int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 1024;
    private static final int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 16;
    private static final int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 32;
复制代码

1.4 Synchronized与ReentrantLock的相同点与不同点

  • 相同点在于,二者都是采用加锁的方式实现同步,而且都是阻塞式的同步。
  • 区别在于,Synchronized是Java的关键字,需要JVM去实现。而ReentrantLock是JDK1.5之后提供的API层面的互斥锁,需要lock(),unlock()方法以及try/catch去实现。

1.5 新建线程的几种方法

  • 通过继承Thread类,重写run()方法。
  • 通过实现Callable接口。
  • 通过实现Runnable接口。

1.6 实现线程安全的方法

  • 使用不可变的类:Integer/String
  • 使用synchronized同步块同步对象

\quad synchronized({Object})可以用以锁住一个对象,同样synchronized还可以用在方法签名上。那么问题来了,对于非静态方法,synchronized很明显是锁住调用这个方法的实体,那么对于静态方法,锁住的是什么?答案是锁住的是类在JVM中所存储的Class对象。

  • 使用ReentrantLock,配合lock(),unLock()使用。相比于synchronized,ReentrantLock更灵活,可以在这个方法中加锁,在其他方法中将锁释放。
  • 使用java.util.concurrent包下面的方法去替换在多线程情况下不安全的实现,比如HashMap可以替换为ConcurrentHashMap。
  • 使用Collections.synchronized{Colleation}
    private static List<Integer> list = Collections.synchronizedList(new ArrayList<>());
复制代码
  • 使用原子操作:AtomicInteger/AtomicLong/AtomicBoolean

1.7 wait(),notify(),notifyAll()方法

首先,他们都是Object类中的方法,接下来看下面这个例子:

public static void main(String[] args) {
        new Thread(new Thread1()).start();
        new Thread(new Thread2()).start();
        new Thread(new Thread3()).start();
    }

    static class Thread1 extends Thread {
        @Override
        public void run() {
            System.out.println("Thread1");
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            System.out.println("Thread2");
        }
    }

    static class Thread3 extends Thread {
        @Override
        public void run() {
            System.out.println("Thread3");
        }
    }
复制代码

这里我开辟了三个线程,分别去实现自己的方法。结果很明显,就是按顺序打印结果:

Thread1
Thread2
Thread3
复制代码

但是当我对Thread1使用wait()方法时,

static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Thread1");
        }
    }
复制代码

Thread会交出自己所持有的锁,让其他进程去争夺这个锁,而自己就一直等待着其他线程的唤醒。

他就一直在这等啊,直到我们在其他线程中使用notify()方法将其唤醒:

static class Thread3 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                lock.notify();
            }
            System.out.println("Thread3");
        }
    }
复制代码

这时候,执行的顺序也改变了:

Thread2
Thread3
Thread1
复制代码

那么notifyAll()就是唤醒其他正在等待的线程,然后让他们之间重新去争夺锁:

static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Thread1");
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Thread2");
        }
    }

    static class Thread3 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                lock.notifyAll();
            }
            System.out.println("Thread3");
        }
    }
复制代码

结果:

Thread1
Thread2
Thread3
复制代码

注意:使用Object的wait(),notify(),notifyAll()方法都需要持有这个对象的监视器,就是代码中的synchronized()。java doc中的原话:

* This method should only be called by a thread that is the owner
* of this object's monitor.
复制代码

否则会报出IllegalMonitorStateException异常。

8.什么是线程池?

\quad 由于Java的线程调度完全依赖于操作系统,所以每个线程都会占用一定的资源,进而我们不可能随心所欲的去开辟线程。所以,这时候就需要线程池了,线程池就是预先在内存中开辟一块资源,专门用于线程的调度。当需要线程执行任务时,就通过线程池管理器来实现线程的分配。

9.有哪些创建线程池的方法,他们之间的区别在哪?

\quad java.util.concurrent包中的Executors类中,封装了四种开辟线程池的方法。

  • newFixedThreadPool—数量固定的线程池
    \quad 规定最大的数量,超过这个数量后进来的线程任务放入等待数列中。
  • newSingleThreadExecutor—只有一个线程的线程池
    \quad 一次只能执行一个任务。
  • newCachedThreadPool-缓存型线程池
    \quad 缓存型即在核心线程达到最大值之前,有新的任务就在线程池中加入新的线程,即使有空闲的线程也不复用。达到最大线程后,再复用空闲的线程。没有空余的线程则新建临时线程,用于处理大量短时间工作的线程池。
  • new ScheduledThreadPool—计划型线程池
    \quad 可以设置指定的眼时或定期执行任务。

10.Runnable()与Callable()的区别?

  • Runnable()是没有返回值的,而Callable()允许有返回值。
  • Runnable()不能声明异常,Callable()可以。 代码演示:
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1;
            }
        });
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(" ");
            }
        });
        System.out.println(submit.get());
        //线程池使用完毕后,必须关闭
        executorService.shutdown();
复制代码

11. 守护线程与非守护线程的区别?

\quad 程序允许完毕,jvm会等待非守护线程完成后关闭,但是jvm不会等待守护线程关闭,守护线程最典型的例子就是GC进程。

12. 什么是乐观锁?什么是悲观锁?

  • 乐观锁:乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
  • 悲观锁:悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

2. 死锁问题

2.1 如何产生死锁?

像下面这种情况锁与锁之间相互竞争的情况:

public class MultiplyThreadTest {
    private static final Object lock = new Object();
    private static final Object lock1 = new Object();

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

    static class ThreadClass1 extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("Thread1");
                }
            }
        }
    }

    static class ThreadClass2 extends Thread {
        @Override
        public void run() {
            synchronized (lock1) {
                synchronized (lock) {
                    System.out.println("Thread2");
                }
            }
        }
    }
}
复制代码

其实我刚开始写出的来的时候,我自己都有疑问:

  • 1.不加Thread.sleep(0)这两个线程是不会产生死锁的,为什么?
  • 2.就算加了Thread.sleep(0),那也只是让线程睡0毫秒,加了跟没加不是一样嘛,为什么加上了就能产生死锁呢?

\quad其实这地方,涉及到一个冷门的知识点,可以参考stackoverflow的回答,那就是绝大部分的操作系统的时间精度都在10ms,所以看上去是sleep(0),但由于精度问题,实际上不是睡0ms。另外一个,不加Thread.sleep(0)就不会产生死锁的原因在于,线程执行这两个简单的方法的过程其实是非常短暂的,所以锁之间根本来不及相互竞争。

2.2 如何排查及预防死锁?

  • 使用jsp命令查看哪些进程正在执行:
  • 找到运行的类对应的进程编号,显示详细的调用栈信息,导出到记事本中:
jstack 17424 > ~/Desktop/1.txt
复制代码

可以看到日志中,有详细的死锁信息。

  • 预防死锁的原则就是:保证每个线程都已想同的顺序拿到资源的锁。像这个例子中两个线程拿到锁的顺序就相互矛盾,所以容易产生死锁。

3.三种方式实现生产者,消费者模型

3.1 使用wait(),notify()实现

public class ProducerConsumer1 {

    private static final Object lock = new Object();
    private static Optional<Integer> optional = Optional.empty();

    public static void main(String[] args) throws InterruptedException {
        Container container = new Container(optional);
        Producer producer = new Producer(container);
        Consumer consumer = new Consumer(container);

        producer.start();
        consumer.start();

        producer.join();
        producer.join();
    }

    public static class Producer extends Thread {
        Container container;

        public Producer(Container container) {
            this.container = container;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                synchronized (lock) {
                    while (container.getOptional().isPresent()) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    Integer integer = new Random().nextInt();
                    System.out.println("Product:" + integer);
                    container.setOptional(Optional.of(integer));
                    lock.notify();
                }
            }
        }
    }

    public static class Consumer extends Thread {
        Container container;

        public Consumer(Container container) {
            this.container = container;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                synchronized (lock) {
                    while (!container.getOptional().isPresent()) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Consume:" + container.getOptional().get());
                    container.setOptional(Optional.empty());
                    lock.notify();
                }
            }
        }
    }
}
复制代码

\quad虽然看起来代码挺多的,但其实就是一个思想,就是生产者在生产之前,先检查容器中是否为空,如果不为空就等待,为空才生产。同样,消费者也需要在容器中有值的情况下才会消费,否则就等待。

3.2 使用lock,condition实现

借鉴Condition接口中的提示:

 *   public void put(Object x) throws InterruptedException {
 *     <b>lock.lock();
 *     try {</b>
 *       while (count == items.length)
 *         <b>notFull.await();</b>
 *       items[putptr] = x;
 *       if (++putptr == items.length) putptr = 0;
 *       ++count;
 *       <b>notEmpty.signal();</b>
 *     <b>} finally {
 *       lock.unlock();
 *     }</b>
 *   }
复制代码

代码如下:

public class ProducerConsumer2 {
    private static final Lock lock = new ReentrantLock();
    private static final Condition notConsumer = lock.newCondition();
    private static final Condition notProduct = lock.newCondition();

    private static Optional<Integer> optional = Optional.empty();

    public static void main(String[] args) throws InterruptedException {
        Container container = new Container(optional);
        Producer producer = new Producer(container);
        Consumer consumer = new Consumer(container);

        producer.start();
        consumer.start();

        producer.join();
        producer.join();
    }

    public static class Producer extends Thread {
        private Container container;

        public Producer(Container container) {
            this.container = container;
        }

        @Override
        public void run() {
            lock.lock();
            try {
                while (container.getOptional().isPresent()) {
                    try {
                    //注意:是await()而不是wait()
                        notConsumer.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                int random = new Random().nextInt();
                System.out.println("Product" + random);
                container.setOptional(Optional.of(random));
                notProduct.signal();
            } finally {
                lock.unlock();
            }
        }
    }

    public static class Consumer extends Thread {

        private Container container;

        public Consumer(Container container) {
            this.container = container;
        }

        @Override
        public void run() {
            try {
                lock.lock();
                while (!container.getOptional().isPresent()) {
                    try {
                        notProduct.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(container.getOptional().get());
                container.setOptional(Optional.empty());
                notConsumer.notify();
            } finally {
                lock.unlock();
            }
        }
    }
}
复制代码

与第一种方法类似,这里增加了Condition条件判断,并用到了ReentrantLock。

3.3 使用BlockingQueue.take()/put()方法实现

public class ProducerConsumer3 {

    private static final BlockingQueue<Integer> queue = new LinkedBlockingDeque<>(1);
    private static final BlockingQueue<Integer> signal = new LinkedBlockingDeque<>(1);

    public static void main(String[] args) throws InterruptedException {
        Producer producer = new Producer(queue, signal);
        Consumer consumer = new Consumer(queue, signal);

        producer.start();
        consumer.start();

        producer.join();
        producer.join();
    }

    public static class Producer extends Thread {
        BlockingQueue<Integer> queue;
        BlockingQueue<Integer> signal;

        public Producer(BlockingQueue<Integer> queue, BlockingQueue<Integer> signal) {
            this.queue = queue;
            this.signal = signal;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                int random = new Random().nextInt();
                System.out.println("Product:" + random);
                try {
                    queue.put(random);
                    signal.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static class Consumer extends Thread {
        BlockingQueue<Integer> queue;
        BlockingQueue<Integer> signal;

        public Consumer(BlockingQueue<Integer> queue, BlockingQueue<Integer> signal) {
            this.queue = queue;
            this.signal = signal;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println("Consumer:" + queue.take());
                    signal.put(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
复制代码

\quad 这种方法就比较简单了,不用判断容器是否为空,因为BlockingQueue在内部为我们实现了判断,直接拿来用就可以了。这里需要注意一点,由于take()与put()方法几乎是同时执行的,所有需要加一个signal标志信息,避免乱序。

Container容器:

public class Container {
    Optional<Integer> optional;

    public Container(Optional<Integer> optional) {
        this.optional = optional;
    }

    public Optional<Integer> getOptional() {
        return optional;
    }

    public void setOptional(Optional<Integer> optional) {
        this.optional = optional;
    }
}
复制代码

参考资料

猜你喜欢

转载自juejin.im/post/5dec97d3e51d4557f42b62aa