文章目录

  • 内容简介
    • 3.1 等待/通知机制
      • 3.1.1 不使用等待/通知机制实现线程间的通信
      • 3.1.3 等待/通知机制的实现
      • 3.1.4 方法wait()锁释放与notify()锁不释放
      • 3.1.5 当interrupt方法遇到wait方法
      • 3.1.6 只通知一个线程与通知所有线程
      • 3.1.8 方法wait(long)的使用
      • 3.1.9 等待wait的条件发生变化。
      • 3.1.11 生产者/消费者模式的实现。
        • 3.1.11.1 一生产与一消费:操作值
        • 3.1.11.2 多生产与多消费:假死
        • 3.1.11.3 一生产与一消费:操作栈
        • 3.1.11.4 一生产与多消费-操作栈:解决wait条件改变与假死
        • 3.1.11.4 多生产与多消费-操作栈:解决wait条件改变与假死
      • 3.1.12 通过管道进行线程间通信:字节流
      • 3.1.13通过管道进行线程间通信:字符流
    • 3.2 join方法的使用
      • 3.2.1 join(long)与sleep(long)的区别
      • 3.2.2 方法join()后面的代码提前运行
    • 3.3 ThreadLocal的使用
      • 3.3.1 验证线程变量的隔离性
    • 3.4 InheritableThreadLocal的使用
  • 参考文献

内容简介

本章需要重点掌握的技术点如下:
1)使用wait/notify实现线程间的通信。
2)生产者/消费者模式的实现。
3)方法join的使用。
4)ThreadLocal类的使用。

3.1 等待/通知机制

3.1.1 不使用等待/通知机制实现线程间的通信

public class MyList {private List list = new ArrayList();public void add(){list.add("");}public int size(){return list.size();}
}
public class ThreadA extends Thread {private MyList myList;public ThreadA(MyList myList) {this.myList = myList;}@Overridepublic void run() {try {for (int i = 0; i < 10; i++) {myList.add();System.out.println("添加了" + (i + 1) + "个元素");Thread.sleep(1000);}} catch (Exception e) {e.printStackTrace();}}
}
public class ThreadB extends Thread {private volatile MyList myList;public ThreadB(MyList myList) {this.myList = myList;}@Overridepublic void run() {try {while (true) {// System.out.print("");if (myList.size() == 5) {System.out.println("==5,线程b退出来");throw new InterruptedException();}}} catch (Exception e) {e.printStackTrace();}}
}
public class Run {public static void main(String[] args) {MyList myList = new MyList();ThreadA a = new ThreadA(myList);a.setName("a");a.start();ThreadB b = new ThreadB(myList);b.setName("b");b.start();}
}
添加了1个元素
添加了2个元素
添加了3个元素
添加了4个元素
添加了5个元素
==5,线程b退出来
java.lang.InterruptedExceptionat part6.ThreadB.run(ThreadB.java:18)
添加了6个元素
添加了7个元素
添加了8个元素
添加了9个元素
添加了10个元素

虽然两个线程实现了通信,但是是通过while语句轮询检测实现的,轮询时间过小浪费CPU资源,轮询时间过大有可能会取不到想要的数据,这时候就可以用到wait/notify机制。

3.1.3 等待/通知机制的实现

wait()的作用是使当前执行代码的线程进行等待。wait()是Object类的方法, 该方法用来将当前线程置入“预执行队列”中,在调用wait()所在代码行处停止执行,直到接到通知或被中断 。在调用wait()之前,线程必须获取该对象的对象级别锁,只能在同步方法或同步代码块中调用wait()方法。执行wait()之后,当前线程释放锁。如果调用wait()时没有持有适当的锁,则会抛出IllegalMonitorStateException

方法notify(),也要在同步方法或同步代码块中调用,用来通知那些等待该对象锁的其他线程,如果多个线程等待,则由线程规划器随机挑选一个呈wait状态的线程,对其发起通知。在执行notify()方法后,当前线程不会马上释放对象锁,当前线程释放该对象锁之后,呈wait状态的线程才可以获取锁。如果调用notify()时没有持有适当的锁,则会抛出IllegalMonitorStateException

public class Test {public static void main(String[] args) throws InterruptedException {String lock = new String();System.out.println("sync上面");synchronized (lock){System.out.println("sync第一行");lock.wait();System.out.println("wait下面");}System.out.println("end");}
}
sync上面
sync第一行

根据上面的执行结果来看,线程进入wait状态,不会继续向下运行,让它继续执行,可以使用notify()notifyAll()

public class Mythread1 extends Thread {private Object lock;public Mythread1(Object lock) {this.lock = lock;}@Overridepublic void run() {try {synchronized (lock) {System.out.println("start wait time =" + System.currentTimeMillis());lock.wait();System.out.println("end wait time   =" + System.currentTimeMillis());}} catch (InterruptedException e) {e.printStackTrace();}}
}
public class Mythread2 extends Thread {private Object lock;public Mythread2(Object lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {System.out.println("start notify time =" + System.currentTimeMillis());lock.notify();System.out.println("end   notify time =" + System.currentTimeMillis());}}
}
public class Run {public static void main(String[] args) throws InterruptedException {Object lock = new Object();Mythread1 mythread1 = new Mythread1(lock);mythread1.start();Thread.sleep(3000);Mythread2 mythread2 = new Mythread2(lock);mythread2.start();}
}
start wait time =1588821820237
start notify time =1588821823237
end   notify time =1588821823237
end wait time   =1588821823237

从上面打印信息来看,Mythread1先执行进入wait状态,3秒后被notify唤醒。

public class MyList {private static List list = new ArrayList();public static void add() {list.add("");}public static int size() {return list.size();}
}
public class ThreadA extends Thread {private Object lock;public ThreadA(Object lock) {this.lock = lock;}@Overridepublic void run() {try {synchronized (lock) {if (MyList.size() != 5) {System.out.println("wait begin " + System.currentTimeMillis());lock.wait();System.out.println("wait   end " + System.currentTimeMillis());}}} catch (InterruptedException e) {e.printStackTrace();}}
}
public class ThreadB extends Thread {private Object lock;public ThreadB(Object lock) {this.lock = lock;}@Overridepublic void run() {try {synchronized (lock) {for (int i = 0; i < 10; i++) {MyList.add();System.out.println("添加了" + (i + 1) + "个元素");if (MyList.size() == 5) {lock.notify();System.out.println("notify end");}Thread.sleep(1000);}}} catch (InterruptedException e) {e.printStackTrace();}}
}
public class Run {public static void main(String[] args) throws InterruptedException {Object object = new Object();ThreadA a = new ThreadA(object);a.start();Thread.sleep(50);ThreadB b = new ThreadB(object);b.start();}
}
wait begin 1588829402319
添加了1个元素
添加了2个元素
添加了3个元素
添加了4个元素
添加了5个元素
notify end
添加了6个元素
添加了7个元素
添加了8个元素
添加了9个元素
添加了10个元素
wait end 1588829412382

日志wait end…在最后输出,也验证了我们之前的结论:noitfy()方法执行完之后不会立即释放锁。

synchronized可以将任何一个Object对象作为同步对象来对待,而Java为每个Object都实现了wait()notify(),他们必须用在被synchronized同步的Object的临界区内。
wait()可以使处于临界区内的线程进入等待状态,同时释放被同步的对象的锁。
notify()可以唤醒一个因调用了wait()操作而处于阻塞状态中的线程进入就绪状态。被重新唤醒的线程会试图重新获取临界区的控制权。

1)new Thread()后,调用start(),系统会为此线程分配CPU资源,使其处于Runnable状态,当线程抢占到CPU资源,此线程就处于Running状态。
2)Runnable状态和Runnable可相互切换,线程在Running状态时,调用yield()使当前线程重新回到Runnable状态。
3)Blocked是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

线程进入Runnable状态大致有以下5中情况:
1.调用sleep()方法后经过的时间查过了指定的休眠时间。
2.线程调用的阻塞IO已经返回,阻塞方法执行完毕。
3.线程成功获得视图同步的监视器。
4.线程正在等待某个通知,其他线程发出了通知。
5.处于挂起状态的线程调用了resume()。
线程进入Blocked状态大致有以下5中情况:
1.线程调用sleep(),主动放弃占用的处理器资源。
2.线程调用的阻塞IO方法,在该方法返回前,该线程被阻塞。
3.线程视图获得一个同步监视器,但同步监视器正被其他线程持有。
4.线程等待某个通知。
5.调用suspend()将该线程挂起。

3.1.4 方法wait()锁释放与notify()锁不释放

下面我们进行试验wait锁释放,代码如下:

public class Service {public void testMethod(Object lock){try {synchronized (lock){System.out.println("begin wait()");lock.wait();System.out.println("  end wait()");}}catch (Exception e){e.printStackTrace();}}
}
public class ThreadA extends Thread {private Object lock;public ThreadA(Object lock) {this.lock = lock;}@Overridepublic void run() {Service service = new Service();service.testMethod(lock);}
}
public class ThreadB extends Thread {private Object lock;public ThreadB(Object lock) {this.lock = lock;}@Overridepublic void run() {Service service = new Service();service.testMethod(lock);}
}
public class Test {public static void main(String[] args) {Object lock = new Object();ThreadA a = new ThreadA(lock);a.start();ThreadB b = new ThreadB(lock);b.start();}
}

执行结果如下:

begin wait()
begin wait()
结论:当wait()被执行后,锁被自动释放。

下面来验证:notify()被执行后不释放锁。

public class Service {public void testMethod(Object lock) {try {synchronized (lock) {System.out.println("begin wait() ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());lock.wait();System.out.println("  end wait() ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());}} catch (Exception e) {e.printStackTrace();}}public void synNotifyMethod(Object lock) {try {synchronized (lock) {System.out.println("begin notify() ThreadName=" + Thread.currentThread().getName() + "  time=" + System.currentTimeMillis() + " time=" + System.currentTimeMillis());lock.notify();Thread.sleep(5000);System.out.println("  end notify() ThreadName=" + Thread.currentThread().getName() + "  time=" + System.currentTimeMillis() + " time=" + System.currentTimeMillis());}} catch (Exception e) {e.printStackTrace();}}
}
public class ThreadA extends Thread {private Object lock;public ThreadA(Object lock) {this.lock = lock;}@Overridepublic void run() {Service service = new Service();service.testMethod(lock);}
}
public class NotifyThread extends Thread {private Object lock;public NotifyThread(Object lock) {this.lock = lock;}@Overridepublic void run() {Service service = new Service();service.synNotifyMethod(lock);}
}
public class SynNotifyMethodThread extends Thread {private Object lock;public SynNotifyMethodThread(Object lock) {this.lock = lock;}@Overridepublic void run() {Service service = new Service();service.synNotifyMethod(lock);}
}
public class Test {public static void main(String[] args) {Object lock = new Object();ThreadA a = new ThreadA(lock);a.start();NotifyThread notifyThread = new NotifyThread(lock);notifyThread.start();SynNotifyMethodThread syn = new SynNotifyMethodThread(lock);syn.start();}
}
begin wait() ThreadName=Thread-0 time=1590474000435
begin notify() ThreadName=Thread-2  time=1590474000436 time=1590474000436end notify() ThreadName=Thread-2  time=1590474005436 time=1590474005436end wait() ThreadName=Thread-0 time=1590474005436
begin notify() ThreadName=Thread-1  time=1590474005436 time=1590474005436end notify() ThreadName=Thread-1  time=1590474010436 time=1590474010436
结论:必须执行完notify方法所在的同步synchronized代码块之后才释放锁。

3.1.5 当interrupt方法遇到wait方法

当线程呈wait状态时,调用线程对象的interrupt方法时,会出现InterruptException异常。
public class Service {public void testMethod(Object lock){try {synchronized (lock){System.out.println("begin wait");lock.wait();System.out.println("  end wait");}}catch (Exception e){}}
}
public class ThreadA extends Thread {private Object lock;public ThreadA(Object lock){this.lock = lock;}@Overridepublic void run() {Service service = new Service();service.testMethod(lock);}
}
public class Test {public static void main(String[] args) {try {Object lock = new Object();ThreadA a = new ThreadA(lock);a.start();Thread.sleep(5000);a.interrupt();} catch (Exception e) {e.printStackTrace();}}
}
begin wait
java.lang.InterruptedExceptionat java.lang.Object.wait(Native Method)

结论:
执行同步代码块的过程中,执行锁所属对象的wait()方法后,这个线程会释放对象锁,而此线程对象会进入线程等待池中,等待被唤醒,如果在这个过程中断该线程时,会出现InterruptException异常。

3.1.6 只通知一个线程与通知所有线程

下面来验证:调用notify()时,一次只随机通知一个线程进行唤醒。

public class Service extends Thread {public void testMethod(Object lock) {try {synchronized (lock) {System.out.println("begin wait ThreadName=" + Thread.currentThread().getName());lock.wait();System.out.println("  end wait ThreadName=" + Thread.currentThread().getName());}} catch (Exception e) {e.printStackTrace();}}
}
public class ThreadA extends Thread {private Object lock;public ThreadA(Object lock) {this.lock = lock;}@Overridepublic void run() {Service service = new Service();service.testMethod(lock);}
}
public class ThreadB extends Thread {private Object lock;public ThreadB(Object lock) {this.lock = lock;}@Overridepublic void run() {Service service = new Service();service.testMethod(lock);}
}
public class ThreadC extends Thread {private Object lock;public ThreadC(Object lock) {this.lock = lock;}@Overridepublic void run() {Service service = new Service();service.testMethod(lock);}
}
public class NotifyThread extends Thread {private Object lock;public NotifyThread(Object lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {lock.notify();}}
}
public class Test {public static void main(String[] args) throws InterruptedException {Object lock = new Object();ThreadA a = new ThreadA(lock);a.start();ThreadB b = new ThreadB(lock);b.start();ThreadC c = new ThreadC(lock);c.start();Thread.sleep(1000);NotifyThread notifyThread = new NotifyThread(lock);notifyThread.start();}
}
begin wait ThreadName=Thread-0
begin wait ThreadName=Thread-3
begin wait ThreadName=Thread-1end wait ThreadName=Thread-0
结论:调用notify方法后,随机通知一个等待该对象的对象锁的其他线程。

那么如何一次性唤醒所有等待该对象的对象锁的其他线程呢?修改NotifyThread.java代码如下:

public class NotifyThread extends Thread {private Object lock;public NotifyThread(Object lock) {this.lock = lock;}@Overridepublic void run() {synchronized (lock) {lock.notifyAll();}}
}
begin wait ThreadName=Thread-0
begin wait ThreadName=Thread-1
begin wait ThreadName=Thread-2end wait ThreadName=Thread-2end wait ThreadName=Thread-1end wait ThreadName=Thread-0

3.1.8 方法wait(long)的使用

wait(long)方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。
public class MyRunnable {static private Object lock = new Object();static private Runnable runnable = new Runnable() {public void run() {try {synchronized (lock) {System.out.println("wait begin timer=" + System.currentTimeMillis());lock.wait(5000);System.out.println("wait   end timer=" + System.currentTimeMillis());}} catch (Exception e) {e.printStackTrace();}}};static private Runnable runnable2 = new Runnable() {public void run() {synchronized (lock){System.out.println("notify begin timer=" + System.currentTimeMillis());lock.notify();System.out.println("notify   end timer=" + System.currentTimeMillis());}}};public static void main(String[] args) throws InterruptedException {Thread t = new Thread(runnable);t.start();}
}
wait begin timer=1590475975460
wait   end timer=1590475980460

3.1.9 等待wait的条件发生变化。

在使用wait/notify模式时,还需要注意另一正情况,也就是wait的等待条件发生变化,容易造成程序逻辑的混乱。

public class ValueObject {public static List<String> list = new ArrayList<String>();
}
public class Add {private String lock;public Add(String lock) {this.lock = lock;}public void add() {synchronized (lock) {ValueObject.list.add("lalala");lock.notifyAll();}}
}
public class AddThread extends Thread {public Add add;public AddThread(Add add) {this.add = add;}@Overridepublic void run() {add.add();}
}
public class Subtract {private String lock;public Subtract(String lock) {this.lock = lock;}public void subtract() {try {synchronized (lock) {if (ValueObject.list.size() == 0) {System.out.println("wait begin ThreadName=" + Thread.currentThread().getName());lock.wait();System.out.println("wait   end ThreadName=" + Thread.currentThread().getName());}System.out.println("do remove threadName=" + Thread.currentThread().getName());ValueObject.list.remove(0);System.out.println("do remove end threadName=" + Thread.currentThread().getName());}} catch (InterruptedException e) {e.printStackTrace();}}
}
public class SubtractThread extends Thread {public Subtract subtract;public SubtractThread(Subtract subtract) {this.subtract = subtract;}@Overridepublic void run() {subtract.subtract();}
}
public class Run {public static void main(String[] args) throws InterruptedException {String lock = "";Add add = new Add(lock);Subtract subtract = new Subtract(lock);SubtractThread subtractThread = new SubtractThread(subtract);subtractThread.setName("sub1");subtractThread.start();SubtractThread subtractThread2 = new SubtractThread(subtract);subtractThread2.setName("sub2");subtractThread2.start();Thread.sleep(1000);AddThread addThread = new AddThread(add);addThread.setName("add");addThread.start();}
}
wait begin ThreadName=sub2
wait begin ThreadName=sub1
wait   end ThreadName=sub1
do remove threadName=sub1
do remove end threadName=sub1
wait   end ThreadName=sub2
do remove threadName=sub2
Exception in thread "sub2" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0at java.util.ArrayList.rangeCheck(ArrayList.java:657)at java.util.ArrayList.remove(ArrayList.java:496)

出现上述问题的原因是Thread.sleep()之前,sub1,sub2线程都都执行了wait方法,进入WAITING状态,在add线程执行完成之后通过notifyAdd()唤醒两个线程,在一个线程执行完remove之后,list的size为零,这时候,另一个线程再进行remove操作的时候就抛出了IndexOutOfBoundsException。
那么下面我们修改Subtract.java代码如下:

public class Subtract {private String lock;public Subtract(String lock) {this.lock = lock;}public void subtract() {try {synchronized (lock) {while (ValueObject.list.size() == 0) {System.out.println("wait begin ThreadName=" + Thread.currentThread().getName());lock.wait();System.out.println("wait   end ThreadName=" + Thread.currentThread().getName());}System.out.println("do remove threadName=" + Thread.currentThread().getName());ValueObject.list.remove(0);System.out.println("do remove end threadName=" + Thread.currentThread().getName());}} catch (InterruptedException e) {e.printStackTrace();}}
}
wait begin ThreadName=sub1
wait begin ThreadName=sub2
wait   end ThreadName=sub2
do remove threadName=sub2
do remove end threadName=sub2
wait   end ThreadName=sub1
wait begin ThreadName=sub1

通过上面打印信息可以看出,当两个线程同时被唤醒之后,两个SubtractThread线程其中一个执行完remove之后,另一个线程执行完wait()方法之后的代码,因满足ValueObject.list.size() == 0条件,重新进入WAITING状态。

3.1.11 生产者/消费者模式的实现。

等待/通知模式的经典案例就是生产者/消费者模式。

3.1.11.1 一生产与一消费:操作值

public class ValueObject {public static String value = "";
}
public class Producer {private String lock;public Producer(String lock) {this.lock = lock;}public void setValue() {try {synchronized (lock){if (!"".equals(ValueObject.value)) {lock.wait();}String value = System.currentTimeMillis() + "_" + System.nanoTime();System.out.println("put value = " + value);ValueObject.value = value;lock.notify();}} catch (Exception e) {e.printStackTrace();}}
}
public class ProducerThread extends Thread {private Producer producer;public ProducerThread(Producer producer) {this.producer = producer;}@Overridepublic void run() {while (true) {producer.setValue();}}
}
public class Consumer {private String lock;public Consumer(String lock) {this.lock = lock;}public void getValue() {synchronized (lock) {if ("".equals(ValueObject.value)) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("get Value = " + ValueObject.value);ValueObject.value = "";lock.notify();}}
}
public class ConsumerThread extends Thread {private Consumer consumer;public ConsumerThread(Consumer consumer) {this.consumer = consumer;}@Overridepublic void run() {while (true) {consumer.getValue();}}
}
public class Run {public static void main(String[] args) {String lock = "";Producer producer = new Producer(lock);Consumer consumer = new Consumer(lock);ProducerThread producerThread = new ProducerThread(producer);ConsumerThread consumerThread = new ConsumerThread(consumer);producerThread.start();consumerThread.start();}
}
put value = 1590487918891_263135745700279
get Value = 1590487918891_263135745700279put value = 1590487918891_263135745708299
get Value = 1590487918891_263135745708299put value = 1590487918891_263135745716318
get Value = 1590487918891_263135745716318put value = 1590487918891_263135745729149
get Value = 1590487918891_263135745729149put value = 1590487918891_263135745736848
get Value = 1590487918891_263135745736848put value = 1590487918891_263135745744547
get Value = 1590487918891_263135745744547···

当前只是一个生产者与一个消费者进行的数据交互。但是如果在此实验的基础上,出现多个生产与消费者,那么在运行的时候极有可能出现“假死”的情况,也就是所有的线程都呈WAITING状态。

3.1.11.2 多生产与多消费:假死

public class Producer {private Object lock;private List<String> list;public Producer(Object lock, List<String> list) {this.lock = lock;this.list = list;}public void setValue() {try {synchronized (lock) {if (list.size() != 0) {System.out.println("生产者 :" + Thread.currentThread().getName() + " 处于WAITING");lock.wait();}String value = System.currentTimeMillis() + "_" + System.nanoTime();System.out.println("生产者 :" + Thread.currentThread().getName() + " 处于RUNNABLE");list.add(value);lock.notify();}} catch (Exception e) {e.printStackTrace();}}
}
public class ProducerThread extends Thread {private Producer producer;public ProducerThread(Producer producer) {this.producer = producer;}@Overridepublic void run() {while (true) {producer.setValue();}}
}
public class Consumer {private Object lock;private List<String> list;public Consumer(Object lock, List<String> list) {this.lock = lock;this.list = list;}public void getValue() {try {synchronized (lock) {if (list.size() == 0) {System.out.println("消费者 :" + Thread.currentThread().getName() + " 处于WAITING");lock.wait();}System.out.println("消费者 :" + Thread.currentThread().getName() + " 处于RUNNABLE");list.remove(0);lock.notify();}} catch (Exception e) {e.printStackTrace();}}
}
public class ConsumerThread extends Thread {private Consumer consumer;public ConsumerThread(Consumer consumer) {this.consumer = consumer;}@Overridepublic void run() {while (true) {consumer.getValue();}}
}
public class Run {public static void main(String[] args) throws InterruptedException {Object lock = new Object();List<String> list = new ArrayList<String>();Consumer consumer = new Consumer(lock, list);Producer producer = new Producer(lock, list);ConsumerThread[] consumerThread = new ConsumerThread[2];ProducerThread[] producerThread = new ProducerThread[2];for (int i = 0; i < 2; i++) {consumerThread[i] = new ConsumerThread(consumer);consumerThread[i].setName("消费者" + (i + 1));producerThread[i] = new ProducerThread(producer);producerThread[i].setName("生产者" + (i + 1));consumerThread[i].start();producerThread[i].start();}Thread.sleep(5000);// Thread.currentThread().getThreadGroup().activeCount() 活动线程数Thread[] threadArray = new Thread[Thread.currentThread().getThreadGroup().activeCount()];// 每个活动线程的线程组及其子组复制到指定的数组中Thread.currentThread().getThreadGroup().enumerate(threadArray);for (Thread thread : threadArray) {System.out.println(thread.getName() + " " + thread.getState());}}
}
···
生产者 :生产者2 处于RUNNABLE
生产者 :生产者2 处于WAITING
生产者 :生产者1 处于WAITING
消费者 :消费者1 处于RUNNABLE
消费者 :消费者1 处于WAITING
消费者 :消费者2 处于RUNNABLE
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0at java.util.ArrayList.rangeCheck(ArrayList.java:657)at java.util.ArrayList.remove(ArrayList.java:496)at part17.Consumer.getValue(Consumer.java:20)at part17.ConsumerThread.run(ConsumerThread.java:11)
消费者 :消费者2 处于WAITING
main RUNNABLE
Monitor Ctrl-Break RUNNABLE
消费者1 WAITING
生产者1 WAITING
消费者2 WAITING
生产者2 WAITING

出现都在等待状态的原因是:代码中确实已经通过notify/wait进行通信了,但不保证notify唤醒的也许是同类(生产者唤醒生产者、消费者唤醒消费者),如果是按照这种情况运行,那么就会造成该情况的出现。解决方案就是使用notifyAll方法即可。

3.1.11.3 一生产与一消费:操作栈

生产者向堆栈List对象中放入数据,使消费者从List堆栈中取出数据。

public class MyStack {private List<String> list = new ArrayList();public synchronized void push() {System.out.println("do push threadName=" + Thread.currentThread().getName());try {if (list.size() == 1) {wait();}list.add("anyString=" + Math.random());notify();System.out.println("push size = " + list.size());} catch (InterruptedException e) {e.printStackTrace();}}public synchronized String pop() {String returnValue = "";System.out.println("do pop threadName=" + Thread.currentThread().getName());try {if (list.size() == 0) {System.out.println("pop操作中的:" + Thread.currentThread().getName() + "线程呈WAITING");wait();}returnValue = "" + list.get(0);list.remove(0);notify();System.out.println("pop=" + list.size());} catch (InterruptedException e) {e.printStackTrace();}return returnValue;}
}
public class Producer {private MyStack myStack;public Producer(MyStack myStack) {this.myStack = myStack;}public void pushService() {myStack.push();}
}
public class ProducerThread extends Thread {private Producer producer;public ProducerThread(Producer producer) {this.producer = producer;}@Overridepublic void run() {while (true) {producer.pushService();}}
}
public class Consumer {private MyStack myStack;public Consumer(MyStack myStack) {this.myStack = myStack;}public void popService() {myStack.pop();}
}
public class ConsumerThread extends Thread {private Consumer consumer;public ConsumerThread(Consumer consumer) {this.consumer = consumer;}@Overridepublic void run() {while (true){consumer.popService();}}
}
public class Run {public static void main(String[] args) {MyStack myStack = new MyStack();Producer producer = new Producer(myStack);Consumer consumer = new Consumer(myStack);ProducerThread producerThread = new ProducerThread(producer);ConsumerThread consumerThread = new ConsumerThread(consumer);producerThread.start();consumerThread.start();}
}
···
do push threadName=Thread-0
push size = 1
do push threadName=Thread-0
do pop threadName=Thread-1
pop=0
do pop threadName=Thread-1
pop操作中的:Thread-1线程呈WAITING
push size = 1
do push threadName=Thread-0
pop=0
do pop threadName=Thread-1
pop操作中的:Thread-1线程呈WAITING
push size = 1
do push threadName=Thread-0
pop=0
do pop threadName=Thread-1
···

3.1.11.4 一生产与多消费-操作栈:解决wait条件改变与假死

使用上一节的代码,修改Run方法如下:

public class Run {public static void main(String[] args) {MyStack myStack = new MyStack();Producer producer = new Producer(myStack);Consumer consumer1 = new Consumer(myStack);Consumer consumer2 = new Consumer(myStack);Consumer consumer3 = new Consumer(myStack);Consumer consumer4 = new Consumer(myStack);Consumer consumer5 = new Consumer(myStack);ProducerThread producerThread = new ProducerThread(producer);producerThread.start();ConsumerThread consumerThread1 = new ConsumerThread(consumer1);ConsumerThread consumerThread2 = new ConsumerThread(consumer2);ConsumerThread consumerThread3 = new ConsumerThread(consumer3);ConsumerThread consumerThread4 = new ConsumerThread(consumer4);ConsumerThread consumerThread5 = new ConsumerThread(consumer5);consumerThread1.start();consumerThread2.start();consumerThread3.start();consumerThread4.start();consumerThread5.start();}
}
do push threadName=Thread-0
push size = 1
do push threadName=Thread-0
do pop threadName=Thread-5
pop=0
do pop threadName=Thread-5
pop操作中的:Thread-5线程呈WAITING
do pop threadName=Thread-4
pop操作中的:Thread-4线程呈WAITING
push size = 1
do push threadName=Thread-0
pop=0
do pop threadName=Thread-5
pop操作中的:Thread-5线程呈WAITING
Exception in thread "Thread-4" do pop threadName=Thread-3
pop操作中的:Thread-3线程呈WAITING
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0at java.util.ArrayList.rangeCheck(ArrayList.java:657)at java.util.ArrayList.get(ArrayList.java:433)at com.ykc.m.MyStack.pop(MyStack.java:31)at com.ykc.m.Consumer.popService(Consumer.java:9)at com.ykc.m.ConsumerThread.run(ConsumerThread.java:11)
do pop threadName=Thread-2
pop操作中的:Thread-2线程呈WAITING
do pop threadName=Thread-1
pop操作中的:Thread-1线程呈WAITING

由于条件发生改变没有及时得到响应,所以多个呈wait状态的线程被唤醒,执行remove方法时出现异常,我们可以吧if改成while即可,代码如下:

public class MyStack {private List<String> list = new ArrayList();public synchronized void push() {System.out.println("do push threadName=" + Thread.currentThread().getName());try {while (list.size() == 1) {wait();}list.add("anyString=" + Math.random());notify();System.out.println("push size = " + list.size());} catch (InterruptedException e) {e.printStackTrace();}}public synchronized String pop() {String returnValue = "";System.out.println("do pop threadName=" + Thread.currentThread().getName());try {while (list.size() == 0) {System.out.println("pop操作中的:" + Thread.currentThread().getName() + "线程呈WAITING");wait();}returnValue = "" + list.get(0);list.remove(0);notify();System.out.println("pop=" + list.size());} catch (InterruptedException e) {e.printStackTrace();}return returnValue;}
}
···
do pop threadName=Thread-5
pop操作中的:Thread-5线程呈WAITING
do pop threadName=Thread-1
pop操作中的:Thread-1线程呈WAITING
do pop threadName=Thread-2
pop操作中的:Thread-2线程呈WAITING
do pop threadName=Thread-4
pop操作中的:Thread-4线程呈WAITING
push size = 1
do push threadName=Thread-0
pop=0
do pop threadName=Thread-3
pop操作中的:Thread-3线程呈WAITING
pop操作中的:Thread-1线程呈WAITING
pop操作中的:Thread-5线程呈WAITING

异常的问题是解决了,却又出现了“假死”,之前我们有验证过,把notify方法,改为notifyAll即可,有疑问的可以重新看3.1.11.2小节。

3.1.11.4 多生产与多消费-操作栈:解决wait条件改变与假死

仍然使用上一章节代码,修改我们的Run.java的代码如下:

public class Run {public static void main(String[] args) {MyStack myStack = new MyStack();Producer producer1 = new Producer(myStack);Producer producer2 = new Producer(myStack);Producer producer3 = new Producer(myStack);Producer producer4 = new Producer(myStack);Producer producer5 = new Producer(myStack);Consumer consumer1 = new Consumer(myStack);Consumer consumer2 = new Consumer(myStack);Consumer consumer3 = new Consumer(myStack);Consumer consumer4 = new Consumer(myStack);Consumer consumer5 = new Consumer(myStack);ProducerThread producerThread1 = new ProducerThread(producer1);ProducerThread producerThread2 = new ProducerThread(producer2);ProducerThread producerThread3 = new ProducerThread(producer3);ProducerThread producerThread4 = new ProducerThread(producer4);ProducerThread producerThread5 = new ProducerThread(producer5);producerThread1.start();producerThread2.start();producerThread3.start();producerThread4.start();producerThread5.start();ConsumerThread consumerThread1 = new ConsumerThread(consumer1);ConsumerThread consumerThread2 = new ConsumerThread(consumer2);ConsumerThread consumerThread3 = new ConsumerThread(consumer3);ConsumerThread consumerThread4 = new ConsumerThread(consumer4);ConsumerThread consumerThread5 = new ConsumerThread(consumer5);consumerThread1.start();consumerThread2.start();consumerThread3.start();consumerThread4.start();consumerThread5.start();}
}
do push threadName=Thread-0
push size = 1
do push threadName=Thread-0
do push threadName=Thread-2
do push threadName=Thread-4
do pop threadName=Thread-8
pop=0
do pop threadName=Thread-8
pop操作中的:Thread-8线程呈WAITING
push size = 1
do push threadName=Thread-4
do pop threadName=Thread-5
pop=0
do pop threadName=Thread-5
pop操作中的:Thread-5线程呈WAITING
push size = 1
do push threadName=Thread-2
pop=0
do pop threadName=Thread-5
pop操作中的:Thread-5线程呈WAITING
push size = 1
do pop threadName=Thread-9
pop=0
···

3.1.12 通过管道进行线程间通信:字节流

PipeStream是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据输出管道,另一个线程从输入管道中读取数据。
1)PipedInputStreamPipedOutputStream
2)PipedReaderPipedWriter

class WriteData {void writeMethod(PipedOutputStream out) throws IOException {try {System.out.println("write");for (int i = 0; i < 10; i++) {String outData = "" + (i + 1);out.write(outData.getBytes());System.out.print(outData);}System.out.println();} catch (Exception e) {e.printStackTrace();} finally {out.close();}}
}
public class WriteThread extends Thread {private WriteData write;private PipedOutputStream out;public WriteThread(WriteData write, PipedOutputStream out) {this.write = write;this.out = out;}@Overridepublic void run() {try {write.writeMethod(out);} catch (IOException e) {e.printStackTrace();}}
}
public class ReadData {public void readMethod(PipedInputStream input) throws IOException {try {System.out.println("read");byte[] byteArray = new byte[20];int readLength = input.read(byteArray);while (readLength != -1) {String newData = new String(byteArray, 0, readLength);System.out.print(newData);readLength = input.read(byteArray);}} catch (Exception e) {e.printStackTrace();} finally {input.close();}}
}
public class ReadThread extends Thread {private ReadData read;private PipedInputStream in;public ReadThread(ReadData read, PipedInputStream in) {this.read = read;this.in = in;}@Overridepublic void run() {try {read.readMethod(in);} catch (IOException e) {e.printStackTrace();}}
}
public class Run {public static void main(String[] args) throws IOException, InterruptedException {WriteData writeData = new WriteData();ReadData readData = new ReadData();PipedInputStream inputStream = new PipedInputStream();PipedOutputStream outputStream = new PipedOutputStream();outputStream.connect(inputStream);WriteThread writeThread = new WriteThread(writeData, outputStream);writeThread.start();Thread.sleep(1000);ReadThread readThread = new ReadThread(readData, inputStream);readThread.start();}
}
write
12345678910
read
12345678910

3.1.13通过管道进行线程间通信:字符流

public class WriteData {public void writeMethod(PipedWriter out) {try {System.out.print("write :");for (int i = 0; i < 11; i++) {String outData = "" + (i + 1);System.out.print(outData);out.write(outData);}System.out.println();out.close();} catch (Exception e) {e.printStackTrace();}}
}
public class WriteThread extends Thread {private WriteData write;private PipedWriter out;public WriteThread(WriteData write, PipedWriter out) {this.write = write;this.out = out;}@Overridepublic void run() {write.writeMethod(out);}
}
public class ReadData {public void readMethod(PipedReader input) {try {System.out.print(" read :");char[] byteArray = new char[20];int readLength = input.read(byteArray);while (readLength != -1) {String newData = new String(byteArray, 0, readLength);System.out.print(newData);readLength = input.read(byteArray);}input.close();} catch (Exception e) {e.printStackTrace();}}
}
public class ReadThread extends Thread {private ReadData read;private PipedReader in;public ReadThread(ReadData read, PipedReader in) {this.read = read;this.in = in;}@Overridepublic void run() {read.readMethod(in);}
}
public class Run {public static void main(String[] args) {try {WriteData writeData = new WriteData();ReadData readData = new ReadData();PipedReader inputStream = new PipedReader();PipedWriter outputStream = new PipedWriter();outputStream.connect(inputStream);WriteThread writeThread = new WriteThread(writeData, outputStream);writeThread.start();Thread.sleep(100);ReadThread readThread = new ReadThread(readData, inputStream);readThread.start();} catch (Exception e) {e.printStackTrace();}}
}
write :1234567891011read :1234567891011

3.2 join方法的使用

主线程创建并启动子线程,如果子线程需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束,这时,如果主线程想要等待子线程执行完成之后再结束,就要用到join()了。join()的作用是等待线程对象销毁。

public class MyThread extends Thread {@Overridepublic void run() {int secondValue = (int) (Math.random() * 10000);System.out.println(secondValue);}
}
public class Test {public static void main(String[] args) {MyThread threadTest = new MyThread();threadTest.start();System.out.println("想在线程运行结束后打印···");}
}
想在线程运行结束后打印···
9607

修改Test.java代码如下:

public class Test {public static void main(String[] args) throws InterruptedException {MyThread threadTest = new MyThread();threadTest.start();threadTest.join();System.out.println("想在线程运行结束后打印···");}
}
5277
想在线程运行结束后打印···

join()的作用是使所属线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后,再继续执行线程z后面的代码。在join过程中,如果当前线程对象被中断,则当前会出现InterruptedException。

join()具有使线程排队运行的作用,有些类似同步的运行效果。join与synchronized的区别是:join()在内部使用wait()方法进行等待,而synchronized使用的是对象监视器来实现同步。

3.2.1 join(long)与sleep(long)的区别

join(long)的功能在内部是使用wait(long)来实现的,所以join(long)方法具有释放锁的特点。join(long)源码如下:

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

3.2.2 方法join()后面的代码提前运行

public class ThreadB extends Thread {@Overridesynchronized public void run() {try {System.out.println("begin B threadName=" + Thread.currentThread().getName() + " " + System.currentTimeMillis());Thread.sleep(5000);System.out.println("  end B threadName=" + Thread.currentThread().getName() + " " + System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();}}
}
public class ThreadA extends Thread {private ThreadB threadB;public ThreadA(ThreadB threadB) {this.threadB = threadB;}@Overridepublic void run() {synchronized (threadB) {try {System.out.println("begin A threadName = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());Thread.sleep(5000);System.out.println("  end A threadName = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class Run {public static void main(String[] args) {try {ThreadB threadB = new ThreadB();ThreadA threadA = new ThreadA(threadB);threadA.start();threadB.start();threadB.join(2000);System.out.println("main end " + System.currentTimeMillis());} catch (InterruptedException e) {e.printStackTrace();}}
}
begin A threadName = Thread-1 1590631573130end A threadName = Thread-1 1590631578130
main end 1590631578130
begin B threadName=Thread-0 1590631578130end B threadName=Thread-0 1590631583130

那么下面我们来分析一下问题出现的原因,修改Run.java代码如下:

public class Run {public static void main(String[] args) {ThreadB threadB = new ThreadB();ThreadA threadA = new ThreadA(threadB);threadA.start();threadB.start();System.out.println("main end " + System.currentTimeMillis());}
}
main end 1590631921788
begin A threadName = Thread-1 1590631921788end A threadName = Thread-1 1590631926790
begin B threadName=Thread-0 1590631926790end B threadName=Thread-0 1590631931790

从执行结果来看,“main end ···”每次都是第一个打印的。那么针对上次的执行结果来看,join(2000)几乎是每次都是先运行的,也就是最先抢到ThreadB的锁,然后释放。基本的过程如下:
1)threadB.join(2000)先抢到ThreadB的锁,然后释放。
2)ThreadA抢到ThreadB的锁,执行synchronized代码块内的代码,进行sleep(5000),5s结束后打印end。
3)join(2000)和ThreadB再次争抢锁,时间已过2000ms,释放锁,然后打印“main end”。
4)ThreadB获得锁之后执行run方法。

3.3 ThreadLocal的使用

ThreadLocal是除了加锁这种同步方式之外,保证多线程访问不会出现非线程安全问题,当我们在创建一个变量后,每个线程对其进行访问的时,访问的都是线程自己的变量这样就不会存在线程不安全问题。
Map里面存储线程本地对象(key)和线程的变量副本(value)
但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
下面我看几个常用方法如下:

    /*** Returns the value in the current thread's copy of this* thread-local variable.  If the variable has no value for the* current thread, it is first initialized to the value returned* by an invocation of the {@link #initialValue} method.** @return the current thread's value of this thread-local*/public T get() {// 获取当前线程对象Thread t = Thread.currentThread();// 获取当前线程对象的ThreadLocalMapThreadLocalMap map = getMap(t);// 判断map是否存在if (map != null) {// 当前ThreadLocal实例对象为key,获取之前存储的实体ThreadLocalMap.Entry e = map.getEntry(this);// 判断当前实体是否存在if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 若map不存在则进行初始化值return setInitialValue();}/*** Get the map associated with a ThreadLocal. Overridden in* InheritableThreadLocal.** @param  t the current thread* @return the map*/ThreadLocalMap getMap(Thread t) {// 返回当前线程的ThreadLocalMap return t.threadLocals;}/*** Variant of set() to establish initialValue. Used instead* of set() in case user has overridden the set() method.** @return the initial value*/private T setInitialValue() {// nullT value = initialValue();// 获取当前线程Thread t = Thread.currentThread();// 获取当前线程对象的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null)// map不为空时,初始化存储对象实体value为nullmap.set(this, value);else// map为空时,以当前线程的threadLocals为keycreateMap(t, value);return value;}protected T initialValue() {return null;}/*** Create the map associated with a ThreadLocal. Overridden in* InheritableThreadLocal.** @param t the current thread* @param firstValue value for the initial entry of the map*/void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}/*** Sets the current thread's copy of this thread-local variable* to the specified value.  Most subclasses will have no need to* override this method, relying solely on the {@link #initialValue}* method to set the values of thread-locals.** @param value the value to be stored in the current thread's copy of*        this thread-local.*/public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}/*** Removes the current thread's value for this thread-local* variable.  If this thread-local variable is subsequently* {@linkplain #get read} by the current thread, its value will be* reinitialized by invoking its {@link #initialValue} method,* unless its value is {@linkplain #set set} by the current thread* in the interim.  This may result in multiple invocations of the* {@code initialValue} method in the current thread.** @since 1.5*/public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}

注意点:在线程run() 方法中不显示的调用remove() 清理与线程相关的ThreadLocal 信息,线程复用会产生脏数据(线程池会重用Thread对象,如果先一个线程不调用set() 设置初始值,那么与Thread绑定的类静态属性也会被重用)。

3.3.1 验证线程变量的隔离性

public class ThreadLocalTools {public static ThreadLocal threadLocal = new ThreadLocal();
}
public class ThreadA extends Thread {@Overridepublic void run() {try {for (int i = 0; i < 100; i++) {ThreadLocalTools.threadLocal.set("ThreadA " + i);System.out.println("ThreadA get Value = " + ThreadLocalTools.threadLocal.get());Thread.sleep(200);}} catch (InterruptedException e) {e.printStackTrace();}}
}
public class ThreadB extends Thread {@Overridepublic void run() {try {for (int i = 0; i < 100; i++) {ThreadLocalTools.threadLocal.set("ThreadB " + i);System.out.println("ThreadB get Value = " + ThreadLocalTools.threadLocal.get());Thread.sleep(200);}} catch (InterruptedException e) {e.printStackTrace();}}
}
public class Run {public static void main(String[] args) throws InterruptedException {ThreadA a = new ThreadA();ThreadB b = new ThreadB();a.start();b.start();for (int i = 0; i < 100; i++) {ThreadLocalTools.threadLocal.set("main thread " + i);System.out.println("main thread getValue " + ThreadLocalTools.threadLocal.get());Thread.sleep(200);}}
}
···
main thread getValue main thread 97
ThreadA get Value = ThreadA 97
ThreadB get Value = ThreadB 97
main thread getValue main thread 98
ThreadA get Value = ThreadA 98
ThreadB get Value = ThreadB 98
main thread getValue main thread 99
ThreadA get Value = ThreadA 99
ThreadB get Value = ThreadB 99

3.4 InheritableThreadLocal的使用

使用InheritableThreadLocal可以再子线程中取得父线程继承下来的值。

public class InheritableThreadLocalExt extends InheritableThreadLocal {@Overrideprotected Object initialValue() {return System.currentTimeMillis();}
}
public class Tools {public static InheritableThreadLocalExt t1 = new InheritableThreadLocalExt();
}
public class MyThread extends Thread {@Overridepublic void run() {System.out.println("MyThread获取的值 = " + Tools.t1.get());}
}
public class Main {public static void main(String[] args) throws InterruptedException {System.out.println(" main线程取值    = " + Tools.t1.get());Thread.sleep(1000);MyThread myThread = new MyThread();myThread.start();}
}
 main线程取值    = 1590647232214
MyThread获取的值 = 1590647232214

参考文献

《Java多线程编程核心技术》高红岩

一文详解wait与notify相关推荐

  1. asterisk配置文详解

    asterisk配置文详解 Configuration GuideYou've  installed Asterisk and verified that it will  start up.Now ...

  2. 一文详解JavaBean 看这篇就够了

    一文详解JavaBean 看这篇就够了 JavaBean的历史渊源 JavaBean的定义(通俗版) JavaBean应用 < jsp:useBean > < jsp:getProp ...

  3. 【卷积神经网络结构专题】一文详解AlexNet(附代码实现)

    关注上方"深度学习技术前沿",选择"星标公众号", 资源干货,第一时间送达! [导读]本文是卷积神经网络结构系列专题第二篇文章,前面我们已经介绍了第一个真正意义 ...

  4. 一文详解 YOLO 2 与 YOLO 9000 目标检测系统

    一文详解 YOLO 2 与 YOLO 9000 目标检测系统 from 雷锋网 雷锋网 AI 科技评论按:YOLO 是 Joseph Redmon 和 Ali Farhadi 等人于 2015 年提出 ...

  5. 一文详解决策树算法模型

    AI有道 一个有情怀的公众号 上文我们主要介绍了Adaptive Boosting.AdaBoost演算法通过调整每笔资料的权重,得到不同的hypotheses,然后将不同的hypothesis乘以不 ...

  6. 「软件项目管理」一文详解软件配置管理计划

    一文详解软件配置管理计划 前言 一.配置管理概述 1. 配置管理(SCM)定义 2. 软件配置项目(SCI) 3. 基线 4. 软件配置控制委员会(SCCB) 二.软件配置管理过程 1. 管理过程 2 ...

  7. 「软件项目管理」一文详解软件项目质量计划

    一文详解软件项目质量计划

  8. 「软件项目管理」一文详解软件项目管理概述

    一文详解软件项目管理概述

  9. OpenCV-Python实战(12)——一文详解AR增强现实

    OpenCV-Python实战(12)--一文详解AR增强现实 0. 前言 1. 增强现实简介 2. 基于无标记的增强现实 2.1 特征检测 2.2 特征匹配 2.3 利用特征匹配和单应性计算以查找对 ...

最新文章

  1. JVM-05垃圾收集Garbage Collection(中)【垃圾收集算法】
  2. 汇编语言——输入两位数比较大小
  3. python的def函数_Python 学习之 def 函数
  4. 阿里开源Canal--①简介
  5. C#通过COM组件操作IE浏览器(三):了解IHTMLDocument2
  6. 她看:2021年95后女性人群洞察与媒体消费趋势前瞻.pdf(附下载链接)
  7. 监控zabbix 服务并在异常时python 邮件报警
  8. java 控制语句_java两个控制语句(转)
  9. 扁平化设计的几个规律
  10. intel无线网卡日志服务器,不定期找不到Intel N 2230无线网卡
  11. 企业全面运营管理沙盘模拟心得_你要的企业沙盘模拟心得来了!!!
  12. WPS导入SQLSERVER的数据
  13. 林子雨 慕课答案2021新版
  14. 十大经典排序算法-堆排序算法详解
  15. 第1章第26节:如何通过幻灯片母版统一管理相同类型的幻灯片2 [PowerPoint精美幻灯片实战教程]
  16. JavaScript prototype原型实现继承
  17. 几何公差基础知识之圆度
  18. vue源码解析:vue生命周期方法$mount方法的实现原理
  19. RTKLIB学习总结(五)后处理函数调用流程、postpos、execses_b、execses_r、execses、procpos、rtkpos
  20. 有没有好奇过路由器宽带拨号的mtu值为什么是1492呢?了解MTU与IP分片

热门文章

  1. python数字类型分为三类_Python | 数据类型
  2. 潮流计算中,已知末端功率和首端电压,手算方法
  3. Python3 生成和识别二维码
  4. 扎克伯格、元宇宙和性
  5. js调用websocket接口示例代码
  6. 基于FPGA+MPU+MCU全自动血细胞分析仪解决方案
  7. 87金融汇小柒:信用卡有8大陷阱
  8. 集线器,路由器,交换机,网关设备
  9. 主机虚拟服务器 域名如何绑定,西部数码虚拟主机绑定域名教程
  10. windows环境和linux环境,多加了正斜杠导致结果不同