synchronized 锁不住 Integer ?

文章目录

  • 1、synchronized 锁住的是什么?
  • 2、多线程同步的前提
  • 3、当锁对象为 Integer 时
    • 3.1 Integer 的对象自减(or 其他运算)后还是同一个对象?
  • 4、基本数据类型和包装类
    • 4.1 自动装箱和拆箱
    • 4.2 valueOf() 缓存池
  • 更新记录

1、synchronized 锁住的是什么?

首先:
synchronized(obj) { /* 同步代码块 */} 中的obj相当于一个锁,它可以是任意对象。当线程任务中需要多个同步时,必须通过锁来区分。举个栗子:

  • 线程 A 和线程 B ,A 和 B 共同操作一段数据共享的代码。当 A 和 B 并发修改同一个共享数据时,如果 CPU 恰好在 A 没有执行完该任务就已经切换到了正在修改此数据的 B ,当在切换回 A 时,就会造成最终结果异常。

  • 这种问题的原因在于,A 和 B 在同时操作同一个数据,也即是,同时执行任务。那么,解决思路就是,让 A 彻底执行完含有共享数据的代码后,再让 CPU 切换到 B 去执行。

  • 这便引入了synchronized

  • synchronized { /* 同步代码块 */}中,用大括号将对共享数据操作的代码单独围起来,当 A 和 B 进入到 {} 时,A 和 B 开始执行这段任务。

  • 怎么让 A 在执行任务还没有结束的时候,B 不会进去插一杠呢?

  • 这便引入了对象锁obj

  • obj像一个监视者,所有的线程都要在门口接受它的“盘查”。当 A 进入{}之前,先获取“监视者”obj给予的进入资格,只要 A 没执行结束,就全程盯着 A。这样,当 B 想进来的时候,因为obj去盯着 A 了,没空管 B ,B 就获取不到进入{}的资格,当然只能等obj盯完梢回来后再给予 B 进门的资格了,这时候,A 已经执行完对共享数据的操作了,B 再进去的时候,就不会出现上述问题了。

  • 这就是多线程的同步。

  • 任何人都可以成为“监视者”(obj可以是任意对象),但“监视者”只能有一个(多线程来访时,面对的必须是同一个obj对象)。试想一下,如果 A 进去时,已经去了一个“监视者”,B 在 A 没有出来的时候又遇见了一个“监视者”,它给予了 B 进入的权利,这样,A 和 B不就同时进去操作了嘛。

    public class ThreadSafeDemo1 {
    
        public static void main(String[] args) {
            Demo1 d = new Demo1();
            
            Thread thread1 = new Thread(d);
            Thread thread2 = new Thread(d);
    
            thread1.start();
            thread2.start();
        }
    }
    class Demo1 implements Runnable {
    
        private int a = 200;
        // 比如,这里自定义创建一个锁对象为 object
        private Object object = new Object();
    
        @Override
        public void run() {
            while (true) {
                /**
                 * 堆内存中只创建了一个object对象,
                 * 无论如何,synchronized锁住的都是object这一个对象,
                 * 所有的线程进来都要被这同一个对象“监察”
                 * 
                 * 最终输出的结果符合预期结果
                 */
                synchronized (obj) {
                    if (a > 0) {
                        try {
                            // 让线程在这里稍停,模拟问题的发生
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "---" + a--);
                    }
                }
            }
        }
    }
    
    public class ThreadSafeDemo2 {
    
        public static void main(String[] args) {
            Demo2 d = new Demo2();
            
            Thread thread1 = new Thread(d);
            Thread thread2 = new Thread(d);
    
            thread1.start();
            thread2.start();
        }
    }
    class Demo2 implements Runnable {
    
        private int a = 200;
    //    private Object object = new Object();
    
        @Override
        public void run() {
            while (true) {
                /**
                 * 每个run()方法都new了一个object,
                 * 相当于每个线程来的时候都创建了各自的一把锁,
                 * 多个线程进来的时候并没有进入到一个同步中去
                 * 
                 * 最终输出的结果不符合预期
                 */
                synchronized (new Object()) {
                    if (a > 0) {
                        try {
                            // 让线程在这里稍停,模拟问题的发生
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "---" + a--);
                    }
                }
            }
        }
    }
    

综上,obj的目的就是为了阻止一个以上的线程对同一个共享资源进行并发访问。

2、多线程同步的前提

多线程同步前提:多个线程在同步中,使用的是同一个锁。

好比在公共场合排队上厕所:

  • 如果只有一个厕所(一个锁),那么所有的人(所有的线程)都只会排成一队(一个同步)老老实实等前一个人完事儿了,锁释放后才能进入——多线程同步。
  • 如果有多个厕所(多把锁),那么所有的人(所有的线程)都会排成多队(多个同步),每队之间互不影响,这个厕所的锁释放与否并不影响其他厕所的人进人出——多线程没有同步。

如果明确多线程存在安全问题,但是加了同步后,数据错误问题依然存在,那么就要考虑多个线程是否用的同一个锁。

3、当锁对象为 Integer 时

public class ThreadSafeDemo3 {

    public static void main(String[] args) {
        Demo3 d = new Demo3();
        
        Thread thread1 = new Thread(d);
        Thread thread2 = new Thread(d);

        thread1.start();
        thread2.start();
    }
}
class Demo3 implements Runnable {

    private Integer a = 200;

    @Override
    public void run() {
        while (true) {
            synchronized (a) {
                if (a > 0) {
                    try {
                        // 让线程在这里稍停,模拟问题的发生
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 输出的结果表明并没有实现多线程安全
                    System.out.println(Thread.currentThread().getName() + "---" + a--);
                }
            }
        }
    }
}

输出的结果表明并没有实现多线程安全,那么,根据多线程同步安全的前提,原因就是在同步中,多线程并没有使用同一个锁。也就是,问题出在Integer声明的变量上。

Integer类型是引用类型,Demo3中在输出的结果的同时,a也在做自减操作。

3.1 Integer 的对象自减(or 其他运算)后还是同一个对象?

当声明一个Integer类型的变量时,例如Integer a = 200;

  • 当对 a 进行运算时,如果其结果值总是在 [-128, 127] 之间时,这个值会直接从一个缓存数组中取出,这时取出来的都是同一个对象,而不会重新创建一个新的对象。

  • 如果运算后的结果超出了这个范围,就会每次重新创建一个对象出来,新创建的两个对象也肯定是不一样的。

    (ps:关于这两点的解释,在后面的第四节 – 基本数据类型和包装类 中单独描述)

因此,在Demo3中,Integer的对象a从 200 自减,会创建出多个对象,违背了多线程同步安全的前提,最终结果输出是存在并发问题的。

4、基本数据类型和包装类

Java 的 8 种基本数据,每种都有一个与之对应的继承自 Object 类的包装类,实现基本数据类型的变量无法处理的操作,比如基本数据类型和字符串数据的转换:Integer.parseInt("str");String.valueOf(666);等,或是将int这样的基本类型转换为对象使用:声明一个Integer对象的列表:ArrayList list = new ArrayList<>();等。

4.1 自动装箱和拆箱

  • 装箱:将基本数据类型转换为包装类对象。如果一个基本类型值出现在需要对象的环境中,编译器会将基本类型值进行自动装箱。(编译器在生成类的字节码时,插入必要的方法调用。然后虚拟机去执行这些字节码)。
    /**
     * 声明一个Integer对象的列表,其add方法中的参数时Integer类型:list.add(Integer e)
     * list.add(10)会自动被编译器变换为 list.add(Integer.valueOf(10))
     */
    ArrayList<Integer> list = new ArrayList<>();
    list.add(10);
    
  • 拆箱:将包装类对象转换为基本数据类型。如果在需要基本类型值的环境中,编译器会将对象进行自动拆箱。(编译器在生成类的字节码时,插入必要的方法调用。然后虚拟机去执行这些字节码)
    /**
     * 声明一个Integer对象的列表,其get方法返回的是Integer类型对象,因此,这里是将Integer对象赋给一个int值
     * int a = list.get(0)会自动被编译器变换为 int a = list.get(0).intValue()
     */
    ArrayList<Integer> list = new ArrayList<>();
    int a = list.get(0);
    
  • 自动装箱要求booleanbytechar ≤ 127,介于 -128 ~ 127 之间的shortint被包装到固定的对象中。也就是说,当基本类型数据值处于这个范围时,数据值从内存中取出后都会包装到同一个对象中;当超过这个范围时,就会重新创建一个对象出来。例如:
    Integer a = 100;
    Integer b = 100;
    Integer c = 1000;
    Integer d = 1000;
    // 输出结果:true
    System.out.println(a == b);
    // 输出结果:false
    System.out.println(c == d);
    

4.2 valueOf() 缓存池

Integer.valueOf()为例:

  • Integer a = new Integer(100)Integer a = 100的区别在于,new Integer(100)每次都会新建一个对象,而Integer a = 100会经历自动装箱的过程,由编译器自动翻译成Integer a = Integer.valueOf(100)
  • valueOf() 方法的实现,就是先判断值是否在缓存池中,如果在的话就直接使用缓存池的内容。从源码中,可以看到,这个缓存池的大小为 -128~127。因此多次使用Integer.valueOf(100)会取得同一个对象的引用。
    /**
     * 以下为Integer类的部分源码:
     *   将int类型转换为Integer对象时,会自动调用Integer的valueOf(int i)方法,
     *   该方法返回Integer对象,并总是从缓存中取-128至127之间的数据
     * 
     */
    public static Integer valueOf(int i) {
    	// IntegerCache.low = -128
    	// IntegerCache.high <= 127
    	// IntegerCache.cachep[]:缓存数组
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    // valueOf()方法减少创建对象次数和节省内存
    
  • 因此当基本类型数据值在缓存池范围中并且相同时,多个包装类实例通过自动装箱,就会引用相同的对象。

更新记录

暂无

喜欢你就点个赞吧~~(*/ω\*)

你可能感兴趣的:(Java,多线程)