关于如何在Integer上实现synchronized

多线程中Integer类型计数问题:为何同步失败及解决方案
本文探讨了在多线程环境下,使用全局静态Integer变量计数时同步失败的现象,分析了原因——对象地址改变。通过Integer.valueOf()方法固定对象地址并提供了两种解决策略:对象缓存和方法同步。

一、产生了什么问题(What)

设想这样一个场景,我定义了一个全局静态变量count来统计次数,在多线程的情景下,我们的一个解决方案就是对count对象操作时进行同步。然后我们写出了这样一段代码:

public class Main {
   static Integer count=0;



    public static void main(String[] args) {

        Runnable run=()->{
            //在count对象上进行同步
            synchronized (count){
                System.out.println(Thread.currentThread().getName()+"进入");
                count++;
                try {
                    //为了演示效果,阻塞线程
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println(Thread.currentThread().getName()+"退出");
                }
            }
        };

        Thread t1=new Thread(run,"thread-1");
        Thread t2=new Thread(run,"thread-2");
        t1.start();

        try {
            //主线程休眠100ms,以保证线程t1修改count
            Thread.sleep(1000);
            t2.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在理想情况下,允许结果应该为:

//0s后
thread-1进入
//5s后
thread-1退出
//5s后
thread-2进入
//10s后
thread-2退出

然后,实际允许结果为:

//0s后
thread-1进入
//1s后
thread-2进入
//5s后
thread-1退出
//6s后
thread-2退出

可见,在Integer类型的变量count上同步失败。

二、问题怎么产生的(Why)

在count上同步失败,说明t1和t2线程拿到的不是同一个锁,也就是说count对象的地址发生了改变。下面借助joI-core来验证地址是否发生改变:
测试代码:

import org.openjdk.jol.vm.VM;

public class Main {
   static Integer count=0;
    public static void main(String[] args) {
        System.out.println(VM.current().addressOf(count));
        count++;
        System.out.println(VM.current().addressOf(count));
    }
}

运行结果:

3590308504
3590308520

count++操作使对象地址发生了改变,因此导致同步失败。事实上对count对象做运算都会导致对象地址改变。类似的还有String以及基本类型的包装类。

三、如何解决(How)

3.1 通过Integer.valueOf()获取到[-128,127]的对象。

以下是Integer.valueOf()方法的源码

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

如果i在[-128,127]范围内,是直接返回cache中的对象的,自身的count对象不会地址不会发生改变。
这个方法虽然可行,但最大只能计数到127。

3.1 将++操作封装成一个方法,对方法同步。

将count++操作封装为一个同步的方法:

   static Integer count=0;
    public static synchronized void add(){
        count++;
    }

Runable调用add()方法:

  Runnable run=()->{
          //调用同步方法
          add();
        };
在高并发情况下,`synchronized` 具有以下特点、问题和相应的解决方案: ### 特点 - **简单易用**:`synchronized` 是 Java 内置的关键字,使用起来非常方便,无需额外创建锁对象,直接在方法或代码块上使用即可。例如,在方法上添 `synchronized` 关键字: ```java public synchronized void someMethod() { // 同步的代码逻辑 } ``` - **可重入性**:同一个线程可以多次获取同一个 `synchronized` 锁,避免了死锁的发生。当一个线程已经持有了某个对象的 `synchronized` 锁,它可以再次请求该锁而不会被阻塞。 - **保证原子性和可见性**:`synchronized` 可以保证在同一时刻只有一个线程能够执行被同步的代码块,从而保证了操作的原子性。同时,它也能保证一个线程修改的变量对其他线程是可见的。 ### 问题 - **性能开销大**:在高并发场景下,`synchronized` 会导致大量线程竞争同一把锁,造成线程的阻塞和唤醒操作,这些操作会带来较大的性能开销。例如,当多个线程频繁访问一个被 `synchronized` 修饰的方法时,会出现大量的线程等待,导致系统响应时间变长。 - **锁粒度问题**:`synchronized` 的锁粒度通常较大,如果使用不当,会将一些不必要同步的代码也包含在同步块中,降低了并发性能。例如,在一个方法中只有部分代码需要同步,但整个方法都被 `synchronized` 修饰,会影响其他线程对该方法中其他非同步部分的访问。 - **无法中断和超时**:当一个线程获取 `synchronized` 锁失败后,会一直处于阻塞状态,无法主动中断等待,也不能设置等待超时时间。这可能会导致线程长时间阻塞,影响系统的整体性能。 ### 解决方案 - **使用细粒度锁**:尽量缩小 `synchronized` 的作用范围,只对需要同步的代码进行同步。例如,将一个大的同步方法拆分成多个小的同步代码块,减少锁的持有时间,提高并发性能。 ```java public void someMethod() { // 非同步代码 synchronized (this) { // 需要同步的代码 } // 非同步代码 } ``` - **使用可重入锁 `ReentrantLock`**:`ReentrantLock` 提供了更灵活的锁机制,支持中断和超时功能。可以使用 `tryLock(long timeout, TimeUnit unit)` 方法设置超时时间,避免线程长时间阻塞。 ```java import java.util.concurrent.locks.ReentrantLock; public class LockExample { private final ReentrantLock lock = new ReentrantLock(); public void someMethod() { try { if (lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) { try { // 同步的代码逻辑 } finally { lock.unlock(); } } else { // 处理获取锁失败的情况 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } ``` - **使用并发容器**:对于一些共享数据的操作,可以使用 Java 提供的并发容器,如 `ConcurrentHashMap` 等,这些容器内部已经实现了线程安全的机制,不需要使用 `synchronized` 进行同步。 ```java import java.util.concurrent.ConcurrentHashMap; public class ConcurrentMapExample { private final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); public void addValue(String key, int value) { map.put(key, value); } public int getValue(String key) { return map.getOrDefault(key, 0); } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值