美文网首页
线程交替打印数字

线程交替打印数字

作者: mark_x | 来源:发表于2019-10-06 14:03 被阅读0次

    线程交替打印数字是一道典型的面试题,主要考察对象锁的使用、锁状态、wait和notify的使用,下面记录一下我做这道题的时发现的一些问题。
    第一遍的思路是这样,定义一个变量count,既作为计数器,又作为对象锁。
    之所以觉得这样可行,见博文《多线程同步与通信》,里面的示例就是把引用对象既作为共享数据,又作为对象锁,事实证明,那个例子这样做可以,这个Integer类型的count不行,这篇文章主要就说这个事——锁对象不能改变。

    然后分别写两个线程,在同步代码块内部进行条件判断,对于线程A如果count是偶数,就使用wait()让线程等待,线程A处于等待状态,不会再竞争锁,此时线程B拿到了对象锁,打印完偶数后唤醒线程A。
    等待和唤醒都在同步代码块中进行的,应该不会有问题。

    代码如下:

    package cn.demo.demo01;
    
    public class AlterPrint {
        // 用于交替打印的数字, 同时作为锁对象
        private static Integer count = 0;
    
    
        public static void main(String[] args) {
            // 线程1打印偶数
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (count < 100) {
                        synchronized (count) {
                            if (count % 2 == 1) {
                                try {
                                    count.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
    
                            // count为偶数
                            System.out.println(Thread.currentThread().getName() + "打印: " + count);
                            count++;
    
                            count.notify();
                        }
                    }
                }
            });
    
            // 线程2打印奇数
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (count < 100) {
                        synchronized (count) {
                            if (count % 2 == 0) {
                                try {
                                    count.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
    
                            // count为偶数
                            System.out.println(Thread.currentThread().getName() + "打印: " + count);
                            count++;
                            count.notify();
                        }
                    }
                }
            });
    
    
            t1.start();
            t2.start();
    
    
        }
    }
    
    
    

    但是结果抛异常

    Thread-0打印: 0
    Exception in thread "Thread-0" Thread-1打印: 1
    Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
        at java.lang.Object.notify(Native Method)
        at cn.demo.demo01.AlterPrint$1.run(AlterPrint.java:27)
        at java.lang.Thread.run(Thread.java:748)
    java.lang.IllegalMonitorStateException
        at java.lang.Object.notify(Native Method)
        at cn.demo.demo01.AlterPrint$2.run(AlterPrint.java:50)
        at java.lang.Thread.run(Thread.java:748)
    

    从结果可以看出,线程A和线程B在调用notify时都出现了IllegalMonitorStateException异常,而出现该异常的原因往往当前线程不是对象锁的持有者。可是明明是在一个同步代码块中,既然能进入同步代码块,说明当前线程持有count,但是这里又说不持有,只能是count发生了改变。

    开始我以为Integer这些包装类型跟一般的引用类型一样,其count的值只是该对象的一个实例域,改变count的值不会改变count对象的引用,而实际上在count++前后,通过debug可以看到count的地址发生了改变,也就是说++后的count是一个新的对象,该线程显然不是这个新对象的锁的持有者,因此会报异常。

    既然知道了原因,可以使用单独的一个引用对象,大部分答案也是这么处理的:
    private static Object object = new Object();
    private static Integer count = 0;
    使用object的内部锁进行同步,而object从开始到结束都没有改变。

    代码如下:

    package cn.demo.demo01;
    
    
    public class demo04 {
        // 用于交替打印的数字, 同时作为锁对象
        private static Object object = new Object();
        private static Integer count = 0;
    
    
        public static void main(String[] args) {
            // 线程1打印偶数
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (count < 100) {
                        synchronized (object) {
                            if (count % 2 == 1) {
                                try {
                                    object.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
    
                            // count为偶数
                            System.out.println(Thread.currentThread().getName() + "打印: " + count);
                            count++;
                            object.notify();
                        }
                    }
                }
            });
    
            // 线程2打印奇数
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (count < 100) {
                        synchronized (object) {
                            if (count % 2 == 0) {
                                try {
                                    object.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
    
                            // count为偶数
                            System.out.println(Thread.currentThread().getName() + "打印: " + count);
                            count++;
                            object.notify();
                        }
                    }
                }
            });
    
            t1.start();
            t2.start();
        }
    }
    

    番外:那么这些包装类对象到底是什么样的存在呢?通过查阅资料发现,以Integer为例,在首次使用Integer对象时,Integer有一段静态代码块会创建一个池,数值范围是-127~128,当创建的对象的值在这个,从池中选择对应的对象返回给引用,如果再次创建相同值的引用,就会返回相同的对象引用。

        private static class IntegerCache {
            static final int low = -128;
            static final int high;
            static final Integer cache[];
    
            static {
                // high value may be configured by property
                int h = 127;
                String integerCacheHighPropValue =
                    sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
                if (integerCacheHighPropValue != null) {
                    try {
                        int i = parseInt(integerCacheHighPropValue);
                        i = Math.max(i, 127);
                        // Maximum array size is Integer.MAX_VALUE
                        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                    } catch( NumberFormatException nfe) {
                        // If the property cannot be parsed into an int, ignore it.
                    }
                }
                high = h;
    
                cache = new Integer[(high - low) + 1];
                int j = low;
                for(int k = 0; k < cache.length; k++)
                    cache[k] = new Integer(j++);
    
                // range [-128, 127] must be interned (JLS7 5.1.7)
                assert IntegerCache.high >= 127;
            }
    
            private IntegerCache() {}
        }
    
    指向同一个对象

    相关文章

      网友评论

          本文标题:线程交替打印数字

          本文链接:https://www.haomeiwen.com/subject/pnjupctx.html