Java并发编程学习之路(八)多线程编程例子

本系列文章:
  Java并发编程学习之路(一)并发编程三要素、Thread、Runnable、interrupted、join、sleep、yield
  Java并发编程学习之路(二)线程同步机制、synchronized、CAS、volatile、final、Lock、AQS
  Java并发编程学习之路(三)ReentrantLock、ReentrantReadWriteLock、死锁、原子类
  Java并发编程学习之路(四)线程池、FutureTask
  Java并发编程学习之路(五)wait/notify/notifyAll、await/signal/signalAll、生产者消费者问题
  Java并发编程学习之路(六)ThreadLocal、BlockingQueue、CopyOnWriteArrayList、ConcurrentHashmap
  Java并发编程学习之路(七)CountDownLatch、CyclicBarrier、Semaphore、Exchanger、Phaser
  Java并发编程学习之路(八)多线程编程例子

一、交替输出1A2B3C4D5E…

  用两个线程,一个输出数字,一个输出字母,最后的结果是交替输出1A2B3C4D5E…

1.1 synchronized/wait/notify

  说到线程的运用,最先想到的可能就是“synchronized/wait/notify”的组合:

  1. 用synchronized锁住一个对象,达到同一时间只能有一个线程输出的目的;
  2. 用wait和notify进行线程间的通信,达到交替输出的目的。

  上面的思路总体上来说没有问题,不过两个线程同时启动时,获得CPU时间片的次序是不固定的,因此需要用一个变量来控制先输出数字。示例:

public class ThreadTest {
    
    
    
	private static volatile boolean t2Started = false;
    final Object lock = new Object();
    char[] arrI = "1234567".toCharArray();
    char[] arrC = "ABCDEFG".toCharArray();

    public static void main(String[] args) {
    
    
		new ThreadTest().testSyncWaitNofiy();
	}
    
    public void testSyncWaitNofiy() {
    
    
        new Thread(() -> {
    
    
            synchronized (lock) {
    
    
                while (!t2Started) {
    
    
                    try {
    
    
                    	lock.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                for (char c : arrC) {
    
    
                    System.out.print(c);
                    try {
    
    
                    	lock.notify();
                    	lock.wait();//让出锁
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                lock.notify();//必须,否则无法终止程序
            }
        }, "t1").start();
        
        new Thread(() -> {
    
    
            synchronized (lock) {
    
    
                for (char c : arrI) {
    
    
                    System.out.print(c);
                    t2Started = true;
                    try {
    
    
                    	lock.notify();
                    	lock.wait();//让出锁
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                lock.notify();//必须,否则无法终止程序
            }
        }, "t2").start();
    }
}

  以上是经典的交替输出写法,以上代码有两种执行顺序:

  • 1、t1先执行
      假如t1先获得CPU时间片,会走入以下逻辑:

      接着t1线程被暂停,t2线程获得锁,将t2Started变量置为true,然后打印数字,唤醒t1,t2线程自身被暂停。
      这时t1线程获得锁,开始打印字母,然后唤醒t2,自己暂停 ------> t2线程获得锁,开始打印数组,然后唤醒t1,自己暂停…
      最后,t2输出数字"7",然后唤醒t1,自己暂停。t1输出字母"G",然后唤醒t2,自己暂停。这时t1线程中第二个notify的作用就体现出来了。假如t1中没有第二个notify,在t1获得锁后,什么也不做,然后释放锁。但是由于没有唤醒其他被暂停的线程,t2将会一直被暂停!
      所以即便t1线程的数字数组arrI已经不打印任何字母了,还是需要将暂停的t2线程唤醒,达到两个线程都停止的目的。
  • 2、t2先执行
      如果t2线程先获得时间片,相当于直接进入打印数字的逻辑,t1中while循环相关的代码不会被执行。

1.2 Condition/await/signal

  用Lock和synchronized的逻辑,总体上是一样的,不过是将隐式锁换成了显式锁而已。示例:

public class ThreadTest {
    
    
    
	private static volatile boolean t2Started = false;
    char[] arrI = "1234567".toCharArray();
    char[] arrC = "ABCDEFG".toCharArray();

    public static void main(String[] args) {
    
    
		new ThreadTest().testLockCondition();
	}
    
    public void testLockCondition() {
    
    
        Lock lock = new ReentrantLock();
        Condition conditionT1 = lock.newCondition();
        Condition conditionT2 = lock.newCondition();
        new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                while (!t2Started) {
    
    
                    try {
    
    
                    	conditionT1.await();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                for (char c : arrI) {
    
    
                    System.out.print(c);
                    conditionT2.signal();
                    conditionT1.await();
                }
                conditionT2.signal();
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t1").start();
        
        new Thread(() -> {
    
    
            try {
    
    
                lock.lock();
                for (char c : arrC) {
    
    
                    System.out.print(c);
                    t2Started = true;
                    conditionT1.signal();
                    conditionT2.await();
                }
                conditionT1.signal();
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }, "t2").start();
    }
}

二、生产者–消费者问题

  生产者–消费者问题在之前的文章章已经介绍过,这里再写这个例子,是为了可以将这个例子和交替输出字符串的例子放在一起对比看:交替输出字符串的问题要达到交替输出(两个线程按顺序获取锁)的目的,所以在各自的线程中在暂停自身前先唤醒对方;而生产者消费者问题不需要两个线程交替执行,所以没有在暂停自身前唤醒对方的操作

2.1 synchronized/wait/notify

  此种方式指的是:通过配合调用Object对象的wait方法和notify方法或notifyAll方法来实现线程间的通信,简单来说可以分为两个步骤:

  • 在线程中调用wait方法,将阻塞当前线程;
  • 其他线程调用了调用notify方法或notifyAll方法进行通知之后,当前线程才能从wait方法返回,继续执行下面的操作。

  假设有一个箱子,最多只能装10个苹果,箱子里没苹果时不能消费(简单理解为从箱子里往外取苹果),箱子里苹果的数量为10时不能生产(简单理解为向箱子里放苹果)。要实现这一效果,至少有三个点需要关注:

  1. 要达到阻塞生产者和消费者两种效果,需要用不同的条件判断+wait实现
  2. 要达到通知生产者和消费者正常运行下去的效果,需要用notify/notifyAll实现(具体用哪种通知方法看需求,notify是随机通知一个,notifyAll是通知所有);
  3. 存放共享数据,需要选择合适的集合类(该例子比较简单,并没有用集合类,而是用了一个变量来实现)。

  示例:

//箱子,存量苹果的容器
public class Box {
    
    
	//箱子中目前苹果的数量
    int num;

    public synchronized void put() {
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        
        //10是箱子中能存放苹果的最大数量
        while (num == 10) {
    
                 
            try {
    
    
                System.out.println("箱子满了,生产者暂停");
                //等待消费者消费一个才能继续生产,所以要让出锁
                this.wait();                       
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        
        num++;
        System.out.println("箱子未装满,开始生产,生产后箱子中的苹果数量:"+num);
        //唤醒可能因为没苹果而等待的消费者
        this.notify();     
    }
    
    public synchronized void take() {
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        
        while (num == 0) {
    
              
            try {
    
    
                System.out.println("箱子空了,消费者暂停");
                //等待生产者生产一个才能继续消费,所以要让出锁
                this.wait();                      
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        
        num--;
        System.out.println("箱子中有了苹果,开始消费,消费后箱子中的苹果数量:"+num);
        //唤醒可能因为苹果满了而等待的生产者
        this.notify();                    
    }
}

//消费者
public class Consumer implements Runnable {
    
    
	 
    private Box box;
 
    public Consumer(Box box) {
    
    
        this.box= box;
    }
 
    @Override
    public void run() {
    
    
        while (true){
    
    
            box.take();
        }
    }
}

//生产者
public class Producer implements Runnable {
    
    
	 
    private Box box;
 
    public Producer(Box box) {
    
    
        this.box= box;
    }
 
    @Override
    public void run() {
    
    
        while (true){
    
    
            box.put();
        }
    }
}

public class ConsumerAndProducer {
    
    
	 
    public static void main(String[] args) {
    
    
        Box box = new Box();
        //生产者线程
        Producer p1 = new Producer(box);  
        //消费者线程
        Consumer c1 = new Consumer(box);    
 
        new Thread(p1).start();
        new Thread(c1).start();
    }
}

  结果示例:

2.2 Condition/await/signal

  该种方式实现生产者消费者,和之前一种方式原理是一样的,不过是将隐式锁换成了显式锁而已。以上个小节的功能为例,修改Box.java代码即可,示例:

//箱子,存量苹果的容器
public class Box {
    
    
	//箱子中目前苹果的数量
    int num;
    Lock lock = new ReentrantLock();
    Condition full = lock.newCondition();
    Condition empty = lock.newCondition();

    public void put() {
    
    
    	lock.lock();
    	try {
    
    
	        try {
    
    
	            Thread.sleep(1000);
	        } catch (InterruptedException e) {
    
    
	            e.printStackTrace();
	        }
	        
	        //10是箱子中能存放苹果的最大数量
	        while (num == 10) {
    
                 
	            try {
    
    
	                System.out.println("箱子满了,生产者暂停");
	                //等待消费者消费一个才能继续生产,所以要让出锁
	                full.await();                       
	            } catch (InterruptedException e) {
    
    
	                e.printStackTrace();
	            }
	        }
	        
	        num++;
	        System.out.println("箱子未装满,开始生产,生产后箱子中的苹果数量:"+num);
	        //唤醒可能因为没苹果而等待的消费者
	        empty.signal(); 
    	}finally {
    
    
    		lock.unlock();
		}
    }
    
    public synchronized void take() {
    
    
    	lock.lock();
    	try {
    
    
	        try {
    
    
	            Thread.sleep(1000);
	        } catch (InterruptedException e) {
    
    
	            e.printStackTrace();
	        }
	        
	        while (num == 0) {
    
              
	            try {
    
    
	                System.out.println("箱子空了,消费者暂停");
	                //等待生产者生产一个才能继续消费,所以要让出锁
	                empty.await();                      
	            } catch (InterruptedException e) {
    
    
	                e.printStackTrace();
	            }
	        }
	        
	        num--;
	        System.out.println("箱子中有了苹果,开始消费,消费后箱子中的苹果数量:"+num);
	        //唤醒可能因为苹果满了而等待的生产者
	        full.signal();    
    	}finally {
    
    
    		lock.unlock();
		}
    }
}

结果示例:

猜你喜欢

转载自blog.csdn.net/m0_37741420/article/details/110225520
今日推荐