java多线程编程的常见陷阱

学习笔记( 11 ) - 解决:finalshell连接虚拟机(ubuntu)超时,主机ping不通虚拟机,主机没有VMnet8网卡, Connection refused: connect 解决:finalshell连接虚拟机(ubuntu)超时,主机ping不通虚拟机,主机没有VMnet8网卡, Connection refused: connect 阅读详情

1、在构造函数中启动线程
     我在很多代码中都看到这样的问题,在构造函数中启动一个线程,类似这样:

public class A{
   
public A(){
      
this.x=1;
      
this.y=2;
      
this.thread=new MyThread();
      
this.thread.start();
    }
   
}

   这个会引起什么问题呢?如果有个类B继承了类A,依据java类初始化的顺序,A的构造函数一定会在B的构造函数调用前被调用,那么thread线程也将 在B被完全初始化之前启动,当thread运行时使用到了类A中的某些变量,那么就可能使用的不是你预期中的值,因为在B的构造函数中你可能赋给这些变量 新的值。也就是说此时将有两个线程在使用这些变量,而这些变量却没有同步。
   解决这个问题有两个办法:将A设置为final,不可继承;或者提供单独的start方法用来启动线程,而不是放在构造函数中。

2、不完全的同步
  
都知道对一个变量同步的有效方式是用synchronized保护起 来,synchronized可能是对象锁,也可能是类锁,看你是类方法还是实例方法。但是,当你将某个变量在A方法中同步,那么在变量出现的其他地方, 你也需要同步,除非你允许弱可见性甚至产生错误值。类似这样的代码:

class A{
  
int x;
  
public int getX(){
     
return x;
   }
  
public synchronized void setX(int x)
   {
     
this.x=x;
   }
}

    x的setter方法有同步,然而getter方法却没有,那么就无法保证其他线程通过getX得到的x是最新的值。事实上,这里的setX的同步是没有 必要的,因为对int的写入是原子的,这一点JVM规范已经保证,多个同步没有任何意义;当然,如果这里不是int,而是double或者long,那么 getX和setX都将需要同步,因为double和long都是64位,写入和读取都是分成两个32位来进行(这一点取决于jvm的实现,有的jvm实 现可能保证对long和double的read、write是原子的),没有保证原子性。类似上面这样的代码,其实都可以通过声明变量为volatile 来解决。
  
3、在使用某个对象当锁时,改变了对象的引用,导致同步失效。
   这也是很常见的错误,类似下面的代码:

synchronized(array[0])
{
   ......
   array[0]=new A();
   ......
}

   同步块使用array[0]作为锁,然而在同步块中却改变了array[0]指向的引用。分析下这个场景,第一个线程获取了array[0]的锁,第二个 线程因为无法获取array[0]而等待,在改变了array[0]的引用后,第三个线程获取了新的array[0]的锁,第一和第三两个线程持有的锁是 不一样的,同步互斥的目的就完全没有达到了。这样代码的修改,通常是将锁声明为final变量或者引入业务无关的锁对象,保证在同步块内不会被修改引用。

4、没有在循环中调用wait()。

    wait和notify用于实现条件变量,你可能知道需要在同步块中调用wait和notify,为了保证条件的改变能做到原子性和可见性。常常看见很多代码做到了同步,却没有在循环中调用wait,而是使用if甚至没有条件判断:

synchronized(lock)
{
  
if(isEmpty()
      lock.wait();
   
}

    对条件的判断是使用if,这会造成什么问题呢?在判断条件之前可能调用notify或者notifyAll,那么条件已经满足,不会等待,这没什么问题。 在条件没有满足,调用了wait()方法,释放lock锁并进入等待休眠状态。如果线程是在正常情况下,也就是条件被改变之后被唤醒,那么没有任何问题, 条件满足继续执行下面的逻辑操作。问题在于线程可能被意外甚至恶意唤醒,由于没有再次进行条件判断,在条件没有被满足的情况下,线程执行了后续的操作。意外唤醒的情况,可能是调用了notifyAll,可能是有人恶意唤醒,也可能是很少情况下的自动苏醒(称为“伪唤醒”)。因此为了防止这种条件没有满足就执行后续操作的情况,需要在被唤醒后再次判断条件,如果条件不满足,继续进入等待状态,条件满足,才进行后续操作。

synchronized(lock)
{
   
while(isEmpty()
      lock.wait();
   
}

    没有进行条件判断就调用wait的情况更严重,因为在等待之前可能notify已经被调用,那么在调用了wait之后进入等待休眠状态后就无法保证线程苏醒过来。

5、同步的范围过小或者过大。

    同步的范围过小,可能完全没有达到同步的目的;同步的范围过大,可能会影响性能。同步范围过小的一个常见例子是误认为两个同步的方法一起调用也是将同步的,需要记住的是Atomic+Atomic!=Atomic。

   Map map=Collections.synchronizedMap(new HashMap());
  
if(!map.containsKey("a")){
             map.put(
"a", value);
   }

   这是一个很典型的错误,map是线程安全的,containskey和put方法也是线程安全的,然而两个线程安全的方法被组合调用就不一定是线程安全的 了。因为在containsKey和put之间,可能有其他线程抢先put进了a,那么就可能覆盖了其他线程设置的值,导致值的丢失。解决这一问题的方法 就是扩大同步范围,因为对象锁是可重入的,因此在线程安全方法之上再同步相同的锁对象不会有问题。

       Map map = Collections.synchronizedMap(new HashMap());
      
synchronized (map) {
            
if (!map.containsKey("a")) {
                 map.put(
"a", value);
             }
         }

   注意,加大锁的范围,也要保证使用的是同一个锁,不然很可能造成死锁。 Collections.synchronizedMap(new HashMap())使用的锁是map本身,因此没有问题。当然,上面的情况现在更推荐使用ConcurrentHashMap,它有putIfAbsent方法来达到同样的目的并且满足线程安全性。

    同步范围过大的例子也很多,比如在同步块中new大对象,或者调用费时的IO操作(操作数据库,webservice等)。不得不调用费时操作的时候,一 定要指定超时时间,例如通过URLConnection去invoke某个URL时就要设置connect timeout和read timeout,防止锁被独占不释放。
同步范围过大的情况下,要在保证线程安全的前提下,将不必要同步的操作从同步块中移出。

6、正确使用volatile
在jdk5修正了volatile的语义后,volatile作为一种轻量级的同步策略就得到了大量的使用。volatile的严格定义参考jvm spec,这里只从volatile能做什么,和不能用来做什么出发做个探讨。

volatile可以用来做什么?

1)状态标志,模拟控制机制。常见用途如控制线程是否停止:

private volatile boolean stopped;
public void close(){
    stopped
=true;
}

public void run(){

   
while(!stopped){
      
//do something
    }
   
}
  


    前提是do something中不会有阻塞调用之类。volatile保证stopped变量的可见性,run方法中读取stopped变量总是main memory中的最新值。

2)安全发布,如修复DLC问题,具体参考http://www.javaeye.com/topic/260515和http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html

    private volatile IoBufferAllocator instance;
    
public IoBufferAllocator getInsntace(){
        
if(instance==null){
            
synchronized (IoBufferAllocator.class) {
                
if(instance==null)
                     instance
=new IoBufferAllocator();
             }
         }
        
return instance;
     }


3)开销较低的读写锁

public class CheesyCounter {
    private volatile int value;

    
public int getValue() { return value; }

    
public synchronized int increment() {
        
return value++;
     }
}


synchronized保证更新的原子性,volatile保证线程间的可见性。

volatile不能用于做什么?
1)不能用于做计数器

    public class CheesyCounter {
        
private volatile int value;

        
public int getValue() { return value; }

        
public int increment() {
            
return value++;
         }
     }


    因为value++其实是有三个操作组成的:读取、修改、写入,volatile不能保证这个序列是原子的。对value的修改操作依赖于value的最新值。解决这个问题的方法可以将increment方法同步,或者使用AtomicInteger原子类。

2)与其他变量构成不变式
   一个典型的例子是定义一个数据范围,需要保证约束lower<upper。

public class NumberRange {
    
private volatile int lower, upper;

    
public int getLower() { return lower; }
    
public int getUpper() { return upper; }

    
public void setLower(int value) {
        
if (value > upper)
            
throw new IllegalArgumentException();
         lower
= value;
     }

    
public void setUpper(int value) {
        
if (value < lower)
            
throw new IllegalArgumentException();
         upper
= value;
     }
}


    尽管讲lower和upper声明为volatile,但是setLower和setUpper并不是线程安全方法。假设初始状态为(0,5),同时调用 setLower(4)和setUpper(3),两个线程交叉进行,最后结果可能是(4,3),违反了约束条件。修改这个问题的办法就是将 setLower和setUpper同步:

public class NumberRange {
    
private volatile int lower, upper;

    
public int getLower() { return lower; }
    
public int getUpper() { return upper; }

    
public synchronized void setLower(int value) {
        
if (value > upper)
            
throw new IllegalArgumentException();
         lower
= value;
     }

    
public synchronized void setUpper(int value) {
        
if (value < lower)
            
throw new IllegalArgumentException();
         upper
= value;
     }
}
四旋翼滑模控制:从理论到Simulink实现 本文深入探讨了四旋翼滑模控制的理论与Simulink实现,对比了其相较于PID控制在抗干扰和鲁棒性方面的显著优势。文章详细介绍了从精确动力学建模、内外环滑模控制器设计到指令解算的完整流程,并分享了Simulink仿真搭建、参数调试及抗扰性能验证的实战经验,帮助工程师掌握在复杂环境下提升四旋翼稳定性的进阶技能。 阅读详情

相关推荐

SFR算法实现详解:从边缘检测到MTF计算的完整流程

本文详细解析了SFR算法从边缘检测到MTF计算的完整流程,包括边缘检测、ESF超采样、LSF差分等关键步骤。通过代码示例和实战经验,帮助开发者高效实现成像系统解析力评估,优化MTF测量精度。适用于摄像头模组调试、工业检测等场景。

Black的专栏 1009

并发类 AtomicInteger 使用入门及源码详解

AtomicInterger 介绍 可以原子性更新的 Integer 值,当然这个类并不能完全替代 Integer 对象。 使用 使用起来还是很方便的。 比如说我们定义一个计数器,使用 AtomicInteger 可以同时兼顾性能与并发安全。 import java.util.concurrent.atomic.AtomicInteger; /** * @author binbin.hou * @since 1.0.0 */ public class Counter { private

719

ffmpeg 4.4 cenc-aes-ctr 加解密 MP4 工程性质分析

ffmpeg 4.4 cenc-aes-ctr 加解密 MP4 工程性质分析

Dream_Dynasty的博客 3045

JAVA多线程编程陷阱总结一

陷阱1:在构造函数中启动线程 在构造函数中启动一个线程,类似这样:         public class A         {            public A()            {               this.x=1;               this.y=2;               this.thread=new MyThread();

guomei的专栏 726

Cheat Engine实战:多级指针逆向破解游戏内存的5个关键步骤(附C++模拟代码)

本文详细解析了使用Cheat Engine进行多级指针逆向破解游戏内存的完整流程。通过5个关键步骤,从定位动态地址到逐层追踪指针链,最终构建稳定指针路径。文章结合C++模拟代码,深入剖析了指针链的底层汇编原理,并提供了高级排查技巧,帮助读者掌握逆向工程中的核心内存追踪技术。

weixin_29239279的博客 306

结合 Conda 手动安装 RepeatMasker

RepeatMasker 是一个广泛使用的生物信息学工具,用于识别和屏蔽(mask)基因组序列中的 重复序列(repeats) 。在使用过程中需要调用重复序列数据库(Repbase 和 Dfam),如果使用Conda安装的话会出现许多问题,因此推荐手动安装。在此记录我手动安装RepeatMasker的流程,希望对各位同学有所帮助。

m0_65437087的博客 2535

Java多线程编程:避免常见陷阱

以下是针对Java多线程编程中常见陷阱的深度解析与解决方案,结合典型错误案例和最佳实践,帮助开发者编写安全可靠的多线程代码:错误示例: 解决方案: 2. 可见性问题 错误场景: 修复方案: 二、死锁与资源竞争 1. 死锁形成条件 规避方法: 2. 活锁(Livelock) 典型场景: 线程不断重试失败操作(如消息处理失败后反复回滚)解决方案: 三、线程管理陷阱 1. 线程池滥用 错误用法: 正确方案: 2. 线程泄漏 典型场景: 规避方法: 四、并发工具误用 1. 错误使用ConcurrentHas

免费 1936

#智慧文旅#文创商城管理系统,一站式文旅商品营销管理平台

#智慧文旅#文创商城管理系统满足景区、运营公司或者全域文旅对文创店铺、商品的全流程管理需求,提供文创商品的线上线下产品销售、会员管理、促销管理等服务。包括文创店铺的基本信息管理、文创商品的分类管理、标签管理、上下架管理、库存案例、配送管理等功能。

数字赋能,开启智慧文旅新时代 1174

Java多线程编程的常见陷阱

本文介绍了Java多线程编程中的常见陷阱,如在构造函数中启动线程,不完全的同步,在使用某个对象当锁时,改变了对象的引用,导致同步失效等等。 AD: 1、在构造函数中启动线程 我在很多代码中都看到这样的问题,在构造函数中启动一个线程,类似这样: public class A{     public A(){        this.x=1;

novelly的专栏 637

ruoyi-nbcio-plus基于vue3的flowable服务任务的升级修改

ruoyi-nbcio-plus基于vue3的flowable服务任务的升级修改

宁波阿成的博客 847

使用 Python 爬虫获取中国证券市场公司公告数据

本文详细介绍了如何使用 Python 爬虫技术从中国证券市场(如巨潮资讯网)爬取公司公告数据。通过分析网页结构,编写爬虫脚本,抓取并存储公告信息,我们实现了从网页抓取数据的全过程。此外,本文还展示了如何应对反爬虫机制,并介绍了数据分析与可视化的基本方法。未来,我们可以通过机器学习等技术进一步挖掘公告中的潜在信息,例如,通过情感分析对公告内容进行情绪分类,或者预测某些公告类型对股市走势的影响等。希望本文能帮助你理解如何使用 Python 爬虫技术获取和处理证券市场的数据,并为你的数据分析项目提供帮助。

2201_76125261的博客 1887

Java多线程编程中的常见问题与陷阱汇总

多线程编程中,常见的线程安全问题包括竞态条件、死锁、线程饥饿、上下文切换开销等。竞态条件可通过synchronized或AtomicInteger解决;死锁可通过固定锁顺序或tryLock避免;线程饥饿可通过公平锁缓解;上下文切换开销可通过线程池优化。此外,线程间通信、线程中断、线程局部变量、线程组和线程优先级的使用也需谨慎处理,以避免资源耗尽、内存泄漏或程序行为不可预测等问题。合理使用线程池和工具类,如BlockingQueue和ThreadLocal,有助于编写健壮高效的多线程程序。

cesske的博客 348

广东省低应变题库

11.按《建筑地基基础检测规范》DBJ/T15-60-2019的规定,采用低应变法对混凝土预制桩进行检测时,地基基础设计等级为甲级的桩基工程抽检桩数不应少于总桩数的30%,其余桩基工程抽检桩数不应少于总桩数的20%,且不得少于10根;16.按《建筑地基基础检测规范》DBJ/T15-60-2019的规定,当对某桩的桩身完整性判定为III类时,可认为该桩不满足验收条件,不用进一步确定该桩桩身缺陷对桩身结构承载力的影响程度。2.地基基础检测所用计量器具均应在检定(校准、验证)的有效期内,性能符合相关技术要求。

ZShy001238的博客 319

如何快速掌握Java多线程编程:10个实用技巧帮你避开常见陷阱

想要在Java开发领域脱颖而出,多线程编程是必须攻克的难关!🚀 很多开发者在学习多线程时都会遇到各种坑,今天我们就来分享10个实用技巧,帮你快速掌握Java多线程编程,轻松避开那些让人头疼的陷阱。 ## 1. 理解线程生命周期是基础 在深入学习多线程之前,必须先搞清楚线程的6种状态:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIME

gitblog_00949的博客 730

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

allocator java_Java多线程编程的常见陷阱

1、在构造函数中启动线程我在很多代码中都看到这样的问题,在构造函数中启动一个线程,类似这样:public classA{publicA(){this.x=1;this.y=2;this.thread=newMyThread();this.thread.start();}}这个会引起什么问题呢?如果有个类B继承了类A,依据java类初始化的顺序,A的构造函数一定会在B的构造函数调用前被调用,那么th...

weixin_39627665的博客 199

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

【Java核心技术解析深入探讨多线程编程的最佳实践与常见陷阱】

掌握Java多线程编程要求开发者不仅理解基本的线程创建与同步,更要深入洞察并发的内在复杂性。最佳实践的核心在于:优先使用高级并发工具而非低级原语;面向接口编程,充分利用`java.util.concurrent`包;始终将线程安全作为设计时的首要考虑因素;并通过压力测试和代码审查来主动发现潜在的并发陷阱。唯有如此,才能构建出既高效又可靠的并发应用程序。

yservice的博客 747

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

java this 多线程_Java多线程编程的常见陷阱

1、在构造函数中启动线程我在很多代码中都看到这样的问题,在构造函数中启动一个线程,类似这样:publicclassA{publicA(){this.x=1;this.y=2;this.thread=newMyThread();this.thread.start();}}这个会引起什么问题呢?如果有个类B继承了类A,依据java类初始化的顺序,A的构造函数一定会在B的构造函数调用前被调用,那么thr...

weixin_39880490的博客 183
上一篇: java socket编程收藏之四(线程池)
下一篇: 线程同步 --转
hacktt
博客等级 码龄19年 21粉丝 31原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值