自助建个人网站哪个好,黑龙江建设厅官网,新闻最新消息10条,重庆高考征集志愿网站线程安全问题一直是系统亘古不变的痛点。这不#xff0c;最近在项目中发了一个错误使用线程同步的案例。表面上看已经使用了同步机制#xff0c;一切岁月静好#xff0c;但实际上线程同步却毫无作用。关于线程安全的问题#xff0c;基本上就是在挖坑与填坑之间博弈#xf… 线程安全问题一直是系统亘古不变的痛点。这不最近在项目中发了一个错误使用线程同步的案例。表面上看已经使用了同步机制一切岁月静好但实际上线程同步却毫无作用。关于线程安全的问题基本上就是在挖坑与填坑之间博弈这也是为什么面试中线程安全必不可少的原因。下面就来给大家分析一下这个案例。有隐患的代码 先看一个脱敏的代码实例。代码要处理的业务逻辑很简单就是多线程访问一个单例对象的成员变量对其进行自增处理。SyncTest类实现了Runnable接口run方法中处理业务逻辑。在run方法中通过synchronized来保证线程安全问题在main方法中创建一个SyncTest类的对象两个线程同时操作这一个对象。public class SyncTest implements Runnable {private Integer count 0;Overridepublic void run() {synchronized (count) {System.out.println(new Date() 开始休眠 Thread.currentThread().getName());count;try {Thread.sleep(10000);System.out.println(new Date() 结束休眠 Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {SyncTest test new SyncTest();new Thread(test).start();Thread.sleep(100);new Thread(test).start();}
}
在上述代码中两个线程访问SyncTest的同一个对象并对该对象的count属性进行自增操作。由于是多线程那就要保证count的线程安全。代码中使用了synchronized来锁定代码块进行同步处理。为了演示效果在处理完业务逻辑对线程进行睡眠。理想的状况是第一个线程执行完毕然后第二个线程才能进入并执行。表面上看一切都很完美下面我们来执行一下程序看看结果。执行验证 执行main方法打印结果如下Fri Jul 23 22:10:34 CST 2021 开始休眠Thread-0
Fri Jul 23 22:10:34 CST 2021 开始休眠Thread-1
Fri Jul 23 22:10:44 CST 2021 结束休眠Thread-0
Fri Jul 23 22:10:45 CST 2021 结束休眠Thread-1
正常来说由于使用了synchronized来进行同步处理那么第一个线程进入run方法之后会进行锁定。先执行“开始休眠”然后再执行“结束休眠”最后释放锁之后第二个线程才能够进入。但分析上面的日志会发现两个线程同时进入了“开始休眠”状态也就是说锁并未起效线程安全依旧存在问题。下面我们就针对synchronized失效原因进行逐步分析。synchronized知识回顾 在分析原因之前我们先来回顾一下synchronized关键字的使用。synchronized关键字解决并发问题时通常有三种使用方式同步普通方法锁的是当前对象同步静态方法锁的是当前Class对象同步块锁的是()中的对象很显然上面的场景中使用的是第三种方式进行锁定处理。synchronized实现同步的过程是JVM通过进入、退出对象监视器(Monitor)来实现对方法、同步块的同步的。代码在编译时编译器会在同步方法调用前加入一个monitor.enter指令在退出方法和异常处插入monitor.exit的指令。其本质就是对一个对象监视器(Monitor)进行获取而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。原因分析 经过上面基础知识的铺垫我们就来排查分析一下上述代码的问题。其实对于这个问题IDE已经能够给出提示了。如果你使用的IDE带有代码检查的插件synchronized (count)的count上会有如下提示Synchronization on a non-final field xxx Inspection info: Reports synchronized statements where the lock expression is a reference to a non-final field. Such statements are unlikely to have useful semantics, as different threads may be locking on different objects even when operating on the same object.很多人可能会忽视掉这个提示但它已经明确指出此处代码有线程安全问题。提示的核心是“同步处理应用在了非final修饰的变量上”。对于synchronized关键字来说如果加锁的对象是一个可变的对象那么当这个变量的引用发生了改变不同的线程可能锁定不同的对象进而都会成功获得各自的锁。用一个图来回顾一下上述过程在上图中Thread0在①处进行了锁定但锁定的对象是Integer(0)Thread1中②处也进行锁定但此时count已经进行自增导致Thread1锁定的是对象Integer(1)也就是说两个线程锁定的对象不是同一个也就无法保证线程安全了。解决方案 既然找到了问题的原因我们就可以有针对性的进行解决这里用的count属性很显然不可能用final进行修饰不然就无法进行自增处理。这里我们采用对象锁的方式来进行处理也就锁对象为当前this或者说是当前类的实例对象。修改之后的代码如下public class SyncTest implements Runnable {private Integer count 0;Overridepublic void run() {synchronized (this) {System.out.println(new Date() 开始休眠 Thread.currentThread().getName());count;try {Thread.sleep(10000);System.out.println(new Date() 结束休眠 Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}}// ...
}在上述代码中锁定了当前对象而当前对象在这个示例中是同一个SyncTest的对象。再次执行main方法打印日志如下Fri Jul 23 23:13:55 CST 2021 开始休眠Thread-0
Fri Jul 23 23:14:05 CST 2021 结束休眠Thread-0
Fri Jul 23 23:14:05 CST 2021 开始休眠Thread-1
Fri Jul 23 23:14:15 CST 2021 结束休眠Thread-1
可以看到第一个线程完全执行完毕之后第二个线程才进行执行达到预期的同步处理目标。上面锁定当前对象还是有一个小缺点大家在使用时需要注意比如该类有其他方法也使用了synchronized (this)那么由于两个方法锁定的都是当前对象其他方法也会进行阻塞。所以通常情况下建议每个方法锁定各自定义的对象。比如单独定义一个private的变量然后进行锁定public class SyncTest implements Runnable {private Integer count 0;private final Object locker new Object();Overridepublic void run() {synchronized (locker) {System.out.println(new Date() 开始休眠 Thread.currentThread().getName());count;try {Thread.sleep(10000);System.out.println(new Date() 结束休眠 Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}}
}
synchronized使用小常识 在使用synchronized时我们首先要搞清楚它锁定的是哪个对象这能帮助我们设计更安全的多线程程式。在使用和设计锁时我们还要了解一下知识点对象建议定义为private的然后通过getter方法访问。而不是定义为public/protected否则外界能够绕过同步方法的控制而直接取得对象并改变它。这也是JavaBean的标准实现方式之一。当锁定对象为数组或ArrayList等类型时getter方法获得的对象仍可以被改变这时就需要将get方法也加上synchronized同步并且只返回这个private对象的clone()。这样调用端得到的就是对象副本的引用了。无论synchronized关键字加在方法上还是对象上取得的锁都是对象而不是把一段代码或函数当作锁。同步方法很可能还会被其他线程的对象访问每个对象只有一个锁lock和之相关联实现同步是要很大的系统开销作为代价的甚至可能造成死锁所以尽量避免无谓的同步控制小结 通过本文的实践案例主要为大家输出两个关键点第一不要忽视IDE对代码的提示信息某些提示真的很有用如果深挖还能发现很多性能问题或代码bug第二对于多线程的运用不仅要全面了解相关的基础知识点还需要尽可能的进行压测这样才能让问题事先暴露出来。
往期推荐
SpringBoot时间格式化的5种方法SpringBoot 如何统一后端返回格式老鸟们都是这样玩的绝Java 中创建对象的 5 种方法