资讯专栏INFORMATION COLUMN

Java并发编程——线程基础查漏补缺

luqiuwen / 1381人阅读

摘要:告诉当前执行的线程为线程池中其他具有相同优先级的线程提供机会。不能保证会立即使当前正在执行的线程处于可运行状态。当达到超时时间时,主线程和是同样可能的执行者候选。下一篇并发编程线程安全性深层原因

Thread

使用Java的同学对Thread应该不陌生了,线程的创建和启动等这里就不讲了,这篇主要讲几个容易被忽视的方法以及线程状态迁移。

wait/notify/notifyAll

首先我们要明白这三个方法是定义在Object类中,他们起到的作用就是允许线程就资源的锁定状态进行通信。这里所说的资源一般就是指的我们常说的共享对象了,也就是说针对共享对象的锁定状态可以通过wait/notify/notifyAll来进行通信。我们先看下如何使用的,并对相应原理进行展开。

wait

wait方法告诉调用线程放弃锁定并进入休眠状态,直到其他某个线程进入同一个监视器(monitor)并调用notify方法。wait方法在等待之前释放锁,并在wait方法返回之前重新获取锁。wait方法实际上和同步锁紧密集成,补充同步机制无法直接实现的功能。
需要注意到wait方法在jdk源码中是final并且是native的本地方法,我们无法去覆盖此方法。
调用wait一般的方式如下:

synchronized(lockObject) {
    while(!condition) {
        lockObject.wait();
    }
    // 这里进行相应处理;
}

注意这里使用while进行条件判断而没有使用if进行条件判断,原因是这里有个很重要的点容易被忽视,下面来自官方的建议:

应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误的警报和伪唤醒,如果不在循环条件中等待,程序就会在没有满足结束条件的情况下退出。
notify

notify方法唤醒了同一个对象上调用wait的线程。这里要注意notify并没有放弃对资源的锁定,他告诉等待的线程可以唤醒,但是作用在notify上synchronized同步块完成之前,实际上是不会放弃锁。因此,如果通知线程在同步块内,调用notify方法后,需要在进行10s的其他操作,那么等待的线程将会再至少等待10s。
notify一般的使用方式如下:

synchronized(lockObject) {
    // 确定条件
    lockObject.notify();
    // 如果需要可以加任意代码
}
notifyAll

notifyAll会唤醒在同一个对象上调用wait方法的所有线程。在大多数情况下优先级最高的线程将被执行,但是也是无法完全保证会是这样。其他的与notify相同。

使用例子

下面的代码示例实现了队列空和满时线程阻塞已经非空非满时的通知:

生产者:

class Producer implements Runnable {
    private final List taskQueue;
    private final int MAX_CAPACITY;

    public Producer(List sharedQueue, int size) {
        this.taskQueue = sharedQueue;
        this.MAX_CAPACITY = size;
    }

    @Override
    public void run() {
        int counter = 0;
        while (true) {
            try {
                produce(counter++);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }

    private void produce(int i) throws InterruptedException {
        synchronized (taskQueue) {
            while (taskQueue.size() == MAX_CAPACITY) {
                System.out.println("队列已满,线程" + Thread.currentThread().getName() + "进入等待,队列长度:" + taskQueue.size());
                taskQueue.wait();
            }

            Thread.sleep(1000);
            taskQueue.add(i);
            System.out.println("生产:" + i);
            taskQueue.notifyAll();
        }
    }
}

消费者:

class Consumer implements Runnable {
    private final List taskQueue;

    public Consumer(List sharedQueue) {
        this.taskQueue = sharedQueue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                consume();
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }

    private void consume() throws InterruptedException {
        synchronized (taskQueue) {
            while (taskQueue.isEmpty()) {
                System.out.println("队列已空,线程" + Thread.currentThread().getName() + "进入等待,队列长度:" + taskQueue.size());
                taskQueue.wait();
            }
            Thread.sleep(1000);
            int i = (Integer) taskQueue.remove(0);
            System.out.println("消费:" + i);
            taskQueue.notifyAll();
        }
    }
}

测试代码:

public class ProducerConsumerExampleWithWaitAndNotify {
    public static void main(String[] args) {
        List taskQueue = new ArrayList<>();
        int MAX_CAPACITY = 5;
        Thread tProducer = new Thread(new Producer(taskQueue, MAX_CAPACITY), "Producer");
        Thread tConsumer = new Thread(new Consumer(taskQueue), "Consumer");
        tProducer.start();
        tConsumer.start();
    }
}

部分输出如下:

生产:0
生产:1
生产:2
生产:3
生产:4
队列已满,线程Producer进入等待,队列长度:5
消费:0
消费:1
消费:2
消费:3
消费:4
队列已空,线程Consumer进入等待,队列长度:0
yield/join yield

从字面意思理解yield可以是谦让、放弃、屈服、投降的意思。一个要“谦让”的线程其实是在告诉虚拟机他愿意让其他线程安排到他的前面,这表明他没有说明重要的事情要做了。注意了这只是个提示,并不能保证能起到任何效果。
yield在Thread.java中定义如下:

/**
 * 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.
 *  * 

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. * *

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();

从这里面我们总结出一些重点(有关线程状态后面会讲到):

yield方法是一个静态的并且是native的方法。

yield告诉当前执行的线程为线程池中其他具有相同优先级的线程提供机会。

不能保证yield会立即使当前正在执行的线程处于可运行状态。

他只能使得线程从运行状态变成可运行状态,而无法做其他状态改变。

yield使用例子:

public class YieldExample {
   public static void main(String[] args) {
      Thread producer = new Producer();
      Thread consumer = new Consumer();
       
      producer.setPriority(Thread.MIN_PRIORITY); // 最低优先级
      consumer.setPriority(Thread.MAX_PRIORITY); // 最高优先级
       
      producer.start();
      consumer.start();
   }
}
 
class Producer extends Thread {
   public void run() {
      for (int i = 0; i < 5; i++) {
         System.out.println("生产者 : 生产 " + i);
         Thread.yield();
      }
   }
}
 
class Consumer extends Thread {
   public void run() {
      for (int i = 0; i < 5; i++) {
         System.out.println("消费者 : 消费 " + i);
         Thread.yield();
      }
   }
}

当注释两个“Thread.yield();”时输出:

消费者 : 消费 0
消费者 : 消费 1
消费者 : 消费 2
消费者 : 消费 3
消费者 : 消费 4
生产者 : 生产 0
生产者 : 生产 1
生产者 : 生产 2
生产者 : 生产 3
生产者 : 生产 4

当不注释两个“Thread.yield();”时输出:

生产者 : 生产 0
消费者 : 消费 0
生产者 : 生产 1
消费者 : 消费 1
生产者 : 生产 2
消费者 : 消费 2
生产者 : 生产 3
消费者 : 消费 3
生产者 : 生产 4
消费者 : 消费 4
join

join方法用于将线程当前执行点连接到另一个线程的执行结束,这样这个线程就不会开始运行直到另一个线程结束。在Thread实例上调用join,则当前运行的线程将会阻塞,直到这个Thread实例完成执行。
简要摘抄Thread.java源码中join的定义:

// Waits for this thread to die.
public final void join() throws InterruptedException

join还有可以传入时间参数的重载方法,这个可以时join的效果在特定时间后无效。当达到超时时间时,主线程和taskThread是同样可能的执行者候选。但是join和sleep一样,依赖于OS进行计时,不应该假定刚好等待指定的时间。
join和sleep一样也通过InterruptedException来响应中断。
join使用示例:

public class JoinExample {
   public static void main(String[] args) throws InterruptedException {
      Thread t = new Thread(new Runnable() {
          public void run() {
              System.out.println("第一个任务启动");
              System.out.println("睡眠2s");
              try {
                  Thread.sleep(2000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("第一个任务完成");
          }
      });
      Thread t1 = new Thread(new Runnable() {
          public void run() {
              System.out.println("第二个任务完成");
          }
      });
      t.start();
      t.join();
      t1.start();
   }
}

输出结果:

第一个任务启动
睡眠2s
第一个任务完成
第二个任务完成
join原理分析

join在Thread.java中有三个重载方法:

public final void join() throws InterruptedException

public final synchronized void join(long millis) throws InterruptedException

public final synchronized void join(long millis, int nanos) throws InterruptedException

查看源码可以得知最终的实现核心部分都在join(long millis)中,我们来分析下这个方法源码:

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;
        }
    }
}

首先可以看到这个方法是使用synchronized修饰的同步方法,从这个方法的源码可以看出join的核心就是使用wait来实现的,而外部条件就是isAlive(),可以断定,在非isAlive()时会进行notify。

线程状态

在Thread.java的源代码中就体现出了六种状态:

   /**
     * A thread state.  A thread can be in one of the following states:
     * 
    *
  • {@link #NEW}
    * A thread that has not yet started is in this state. *
  • *
  • {@link #RUNNABLE}
    * A thread executing in the Java virtual machine is in this state. *
  • *
  • {@link #BLOCKED}
    * A thread that is blocked waiting for a monitor lock * is in this state. *
  • *
  • {@link #WAITING}
    * A thread that is waiting indefinitely for another thread to * perform a particular action is in this state. *
  • *
  • {@link #TIMED_WAITING}
    * A thread that is waiting for another thread to perform an action * for up to a specified waiting time is in this state. *
  • *
  • {@link #TERMINATED}
    * A thread that has exited is in this state. *
  • *
* *

* 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. * * @since 1.5 * @see #getState */ 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: *

    *
  • {@link Object#wait() Object.wait} with no timeout
  • *
  • {@link #join() Thread.join} with no timeout
  • *
  • {@link LockSupport#park() LockSupport.park}
  • *
* *

A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called Object.wait() * on an object is waiting for another thread to call * Object.notify() or Object.notifyAll() on * that object. A thread that has called Thread.join() * 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: *

    *
  • {@link #sleep Thread.sleep}
  • *
  • {@link Object#wait(long) Object.wait} with timeout
  • *
  • {@link #join(long) Thread.join} with timeout
  • *
  • {@link LockSupport#parkNanos LockSupport.parkNanos}
  • *
  • {@link LockSupport#parkUntil LockSupport.parkUntil}
  • *
*/ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; }

一般我们用如下图来表示状态迁移,注意相关方法。(注意:其中RUNNING和READY是无法直接获取的状态。)

下一篇:Java并发编程——线程安全性深层原因

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72737.html

相关文章

  • Java并发编程——线程安全性深层原因

    摘要:线程安全性深层原因这里我们将会从计算机硬件和编辑器等方面来详细了解线程安全产生的深层原因。类似这种不影响单线程语义的乱序执行我们称为指令重排。通过线程安全性深层原因我们能更好的理解这三大性质的根本性原因。上一篇并发编程线程基础查漏补缺 线程安全性深层原因 这里我们将会从计算机硬件和编辑器等方面来详细了解线程安全产生的深层原因。 缓存一致性问题 CPU内存架构 随着CPU的发展,而因为C...

    Faremax 评论0 收藏0
  • 作为我的的第一门语言,学习Java时是什么感受?

    摘要:作为技术书籍或者视频,讲解一门语言的时候都是从最底层开始讲解,底层的基础有哪些呢首先是整个,让我们对这门语言先混个脸熟,知道程序的基本结构,顺带着还会说一下注释是什么样子。 2018年新年刚过,就迷茫了,Java学不下去了,不知道从哪里学了。 那么多细节的东西,我根本记不住,看完就忘。 刚开始学习的时候热情万丈,持续不了几天就慢慢退去。 作为技术书籍或者视频,讲解一门语言的时候都是...

    isaced 评论0 收藏0
  • 【面试篇】JS基础知识查漏补缺

    摘要:因为在页面加载完成后,引擎维护着两个队列,一个是按页面顺序加载的执行队列,还有一个空闲队列,使用定时函数就是将回调函数加入到空闲队列中,故和其他定时器是并发执行的。 1.window.onload和$(document).ready()的区别: ①执行时间:window.onload会在所有元素,包括图片,引用文件加载完成之后执行,而$(document).ready()则会在HTML...

    myeveryheart 评论0 收藏0
  • 【推荐】最新200篇:技术文章整理

    摘要:作为面试官,我是如何甄别应聘者的包装程度语言和等其他语言的对比分析和主从复制的原理详解和持久化的原理是什么面试中经常被问到的持久化与恢复实现故障恢复自动化详解哨兵技术查漏补缺最易错过的技术要点大扫盲意外宕机不难解决,但你真的懂数据恢复吗每秒 作为面试官,我是如何甄别应聘者的包装程度Go语言和Java、python等其他语言的对比分析 Redis和MySQL Redis:主从复制的原理详...

    BicycleWarrior 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<