首页 > 其他分享 >线程安全

线程安全

时间:2023-04-30 23:45:49浏览次数:43  
标签:同步 synchronized 代码 list 安全 线程

1、前言   先看看下面的代码输出是什么:

public class TestSync implements Runnable {
    int b = 100;         
    synchronized void m1() throws InterruptedException {
        b = 1000;
        Thread.sleep(500); //6
        System.out.println("b=" + b);
    }
    synchronized void m2() throws InterruptedException {
        Thread.sleep(250); //5
        b = 2000;
    }
    public static void main(String[] args) throws InterruptedException {
        TestSync tt = new TestSync();
        Thread t = new Thread(tt);  //1
        t.start(); //2
        tt.m2(); //3
        System.out.println("main thread b=" + tt.b); //4
    }
    @Override
    public void run() {
        try {
            m1();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
 
输出可能是:
b = 1000
main thread b= 2000
or
main thread b= 2000
b = 1000
or
main thread b=2000
b=1000
这个题涉及了两个点:
  • synchronized
  • 线程的几个状态:new,runnable(thread.start()),running,blocking(Thread.Sleep())
  如果对这几个不熟悉的同学不要着急下面我都会讲,下面我解释一下整个流程:   1. 新建一个线程t, 此时线程t为new状态。 2. 调用t.start(),将线程至于runnable状态。 3. 这里有个争议点到点是t线程先执行还是tt.m2()先执行呢,我们知道此时线程t还是runnable状态,此时还没有被cpu调度,但是我们的tt.m2()是我们本地的方法代码,此时一定是tt.m2()先执行。这里修改:tt.m2有可能比新线程后执行,所以有第三种结果。 4. 执行tt.m2()进入synchronized同步代码块,开始执行代码,这里的sleep()没啥用就是混淆大家视野的,此时b=2000。 5. 在执行tt.m2()的时候。有两个情况:   情况A:有可能t线程已经在执行了,但是由于m2先进入了同步代码块,这个时候t进入阻塞状态,然后主线程也将会执行输出,这个时候又有一个争议到底是谁先执行?是t先执行还是主线程,这里有小伙伴就会把第3点拿出来说,肯定是先输出啊,t线程不是阻塞的吗,调度到CPU肯定来不及啊?很多人忽略了一点,synchronized其实是在1.6之后做了很多优化的,其中就有一个自旋锁,就能保证不需要让出CPU,有可能刚好这部分时间和主线程输出重合,并且在他之前就有可能发生,b先等于1000,这个时候主线程输出其实就会有两种情况。2000 或者 1000。   情况B:有可能t还没执行,tt.m2()一执行完,他刚好就执行,这个时候还是有两种情况。b=2000或者1000   6.在t线程中不论哪种情况,最后肯定会输出1000,因为此时没有修改1000的地方了。   整个流程如下面所示: 2、线程安全 我们用《java concurrency in practice》中的一句话来表述:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其它的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。 从上我们可以得知:
  1. 在什么样的环境:多个线程的环境下。
  2. 在什么样的操作:多个线程调度和交替执行。
  3. 发生什么样的情况: 可以获得正确结果。
  4. 谁 : 线程安全是用来描述对象是否是线程安全。
2.1、线程安全性 我们可以按照java共享对象的安全性,将线程安全分为五个等级:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立:

2.2.1不可变

在java中Immutable(不可变)对象一定是线程安全的,这是因为线程的调度和交替执行不会对对象造成任何改变。同样不可变的还有自定义常量,final及常池中的对象同样都是不可变的。 在java中一般枚举类,String都是常见的不可变类型,同样的枚举类用来实现单例模式是天生自带的线程安全,在String对象中你无论调用replace(),subString()都无法修改他原来的值

2.2.2绝对线程安全

我们来看看Brian Goetz的《Java并发编程实战》对其的定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替进行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的。 周志明在<<深入理解java虚拟机>>中讲到,Brian Goetz的绝对线程安全类定义是非常严格的,要实现一个绝对线程安全的类通常需要付出很大的、甚至有时候是不切实际的代价。同时他也列举了Vector的例子,虽然Vectorget和remove都是synchronized修饰的,但还是展现了Vector其实不是绝对线程安全。简单介绍下这个例子:
public  Object getLast(Vector list) {
    return list.get(list.size() - 1);
}
public  void deleteLast(Vector list) {
    list.remove(list.size() - 1);
}

  

如果我们使用多个线程执行上面的代码,虽然remove和get是同步保证的,但是会出现这个问题有可能已经remove掉了最后一个元素,但是list.size()这个时候已经获取了,其实get的时候就会抛出异常,因为那个元素已经remove。

2.2.3相对安全

周志明认为这个定义可以适当弱化,把“调用这个对象的行为”限定为“对对象单独的操作”,这样一来就可以得到相对线程安全的定义。其需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的操作,但是对于一些特定的顺序连续调用,需要额外的同步手段。我们可以将上面的Vector的调用修改为:
public synchronized Object getLast(Vector list) {
    return list.get(list.size() - 1);
}
public synchronized void deleteLast(Vector list) {
    list.remove(list.size() - 1);
}

  

这样我们作为调用方额外加了同步手段,其Vector就符合我们的相对安全。

2.2.4线程兼容

线程兼容是指其对象并不是线程安全,但是可以通过调用端正确地使用同步手段,比如我们可以对ArrayList进行加锁,一样可以达到Vector的效果。

2.2.5线程对立

线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。 对于解决线程安全一般来说有几个办法:互斥阻塞(悲观,加锁),非阻塞同步(类似乐观锁,CAS),不需要同步(代码写得好,完全不需要考虑同步)
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程(或是一些,使用信号量的时候)线程使用。

2.3、线程安全实现方法 2.3.1 互斥同步

互斥是一种悲观的手段,因为他担心他访问的时候时刻有人会破坏他的数据,所以他需要通过某种手段进行将这个数据在这个时间段给占为独有,不能让其他人有接触的机会。临界区(CriticalSection)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。在Java中一般用ReentrantLock和synchronized 实现同步。 而实际业务当中,推荐使用synchronized,在第一节的代码其实也是使用的synchronized ,为什么推荐使用synchronized 的呢?
  • 如果我们显示的使用lock我们得手动的进行解锁unlock()调用,但是很多人在实际开发过程其实有可能出现忘记,所以推荐使用synchronized ,在易于编程方面Lock败。
  • synchronized 在jdk1.6之后对其进行了优化会从偏向锁,轻量级锁,自旋适应锁,最后才到重量级锁。而Lock一来就是重量锁。在未来的jdk版本中,重点优化的也是synchronized。在性能方便Lock也败。
如果你在业务中需要等待可中断,等待超时,公平锁等功能的话,那你可以选择这个ReentrantLock。 当然在我们的Mysql数据库中排他锁其实也是互斥同步的实现,当加上排他锁,其他事务都不能进行访问其数据。

2.3.2 非阻塞同步

非阻塞同步是一种乐观的手段,在乐观的手段中他会先去尝试操作,如果没有人在竞争,就成功,否则就进行补偿(一般就是死循环重试或者循环多次之后跳出),在互斥同步最重要的问题就是进行线程阻塞和唤醒所带来的性能问题,而乐观同步策略解决了这一问题。 但是上面就有个问题操作和检测是否有人竞争这两个操作一定得保证原子性,这就需要我们硬件设备的支持,例如我们java中的cas操作其实就是操作的硬件底层的指令。 在JDK1.5之后,Java程序中才可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS之类,没有方法调用的过程,或者可以认为是无条件内联进去了

2.3.3 无同步

要保证线程安全,并不一定就要进行同步,两者没有因果关系。同步只是保障共享数据争用时的正确性手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是现场安全的。 一般分为两类:
  • 可重入代码:可重入代码也叫纯代码,可以随时中断,恢复控制权之后程序依然不会出任何错误,可重入代码的结果一般来说是可预测的:
public int sum(){
    return 1+2;
}
例如这种代码就是可重入代码,但是在我们自己的代码中其实出现得很少
  • 线程本地存储:而这个一般来说是我们用得比较多的手段,我们可以通过保证类是无状态的,所有的变量都存在于我们的方法之中,或者通过ThreadLocal来进行保存。
2.4、线程安全实现的一些经验 上面写得都比较官方,下面说说从一些真实的经验中总结出来的:
  • 在使用某些对象作为单例的时候,需要确定这个对象是否是线程安全的: 比如我们使用SimpleDateFormate的时候,很多初学者都不注意将其作为单例一个工具类来使用,导致了我们的业务异常。
  • 如果发现其不是单例,需要进行替换,比如HashMap用ConcurrentHashMap,queue用ArrayBlockingQueue进行替换。
  • 注意死锁,如果使用锁一定记得释放锁,同时使用锁的顺序一定要注意,这里不仅仅说的是单机的锁,也要说分布式锁,一定要注意:一个线程先锁A后锁B,另一个线程先锁B后锁A这个情况。所以一般来说分布式锁会加上超时时间,避免由于网络问题释放锁失败,而导致死锁。
  • 锁的粒度:同样的不仅仅是说单机的锁,也包括了分布式锁,不要图方便直接从入口方法,不加分析的就开始加锁,这样会严重影响性能。同样的也不能过于细粒度,单机的锁会增加上下文的切换,分布式锁会增加网络调用,都会导致我们性能的下降。
  • 适当引入乐观锁:比如我们有个需求是给用户扣款,为了防止多扣,这个时候会用悲观锁进行锁,但是效率比较低,因为用户扣款其实同时扣的情况是比较少的,我们就可以使用乐观锁,在用户的账户表里面添加version字段,首先查询version,然后更新的时候看看当前version和数据库的version是否一致,一致就更新不一致就证明已经扣过了。
  • 如果想要在多线程环境下使用非线程安全对象,数据可以放在ThreadLocal,或者只在方法里面进行创建,我们的ArrayList虽然不是线程安全的,但是一般我们使用的时候其实都是在方法里面进行List list = new ArrayList()使用,用无同步的方式也保证了线程安全。 

标签:同步,synchronized,代码,list,安全,线程
From: https://www.cnblogs.com/liujiarui/p/17365996.html

相关文章

  • 七、Kubernetes安全性
    官方指导文档NFS的动态供应;Pod;pvc---自动创建pvk8s会认为每个Pod也可以是操作集群的一个用户。给这个用户会给一个ServiceAccount(服务账号)权限控制流程:用户携带令牌或者证书给k8s的api-server发送请求要求修改集群资源k8s开始认证。认证通过k8s查询用户的授权(有哪些权限)......
  • 从CPU的视角看 多线程代码为什么那么难写!
      当我们提到多线程、并发的时候,我们就会回想起各种诡异的bug,比如各种线程安全问题甚至是应用崩溃,而且这些诡异的bug还很难复现。我们不禁发出了灵魂拷问“为什么代码测试环境运行好好的,一上线就不行了?”。为了解决线程安全的问题,我们的先辈们在编程语言中引入了各种各样新名......
  • cocoa线程同步synchronized
    synchronized关键字   1.synchronized方法:通过在方法声明中加入synchronized关键字来声明synchronized方法。如:publicsynchronizedvoidaccessVal(intnewVal);synchronized方法控制对类成员变量的访问:每个类实例对应一把锁,每个synchroni......
  • Qt中Socket跨线程通讯问题
    对于一个QTcpServer服务器来说,每当有新客户端连接时,系统会为其分配一个新的QTcpSocket对象进行管理。默认情况下,在incomingConnection函数中创建的QTcpSocket对象将在应用程序主线程中运行,而不是连接所需的处理线程中运行。如果开发者需要确保收到数据的顺序以及避免线程竞争,可以......
  • Android开发多线程断点续传下载器
    111Android开发多线程断点续传下载器分类: Android 2011-10-0123:14931人阅读 评论(8) 收藏 举报使用多线程断点续传下载器在下载的时候多个线程并发可以占用服务器端更多资源,从而加快下载速度,在下载过程中记录每个线程已拷贝数据的数......
  • 阿里云服务器无法使用udp连接,防火墙安全组全开仍然无法连接(已解决)
    我使用的是阿里云的ecs服务器,想使用openvpn的udp连接,但是始终无法连接到,最终确定问题到udp连接失败上,期间使用nc测试,客户端能ping通服务器,但是就是不能使用udp连接到,也换过网络换过软件测试,消息都不能发到服务器今天问阿里的售后工程师,才知道他们的安全组分了很多个区域,设置安全......
  • 04、安全管理
    一、安全管理功能从广义上讲,安全管理功能需要在高级负责人的指导下建立、实施和监控信息安全程序(InformationSecurityProgram)。安全管理涉及多个层次,不同级别的管理利用各类专家的专业知识、权威和资源为整体安全计划做出贡献。首席信息安全官(CISO):全面负责企业的信息安全程序,......
  • 线程常用方法join 和threadLocal
     从源码中可以得知,如果想要join方法正常生效,调用join方法的线程对象必须已经调用了start()方法并且未进入终止状态。扩展:从join方法的源码来看,join方法的本质调用的是Object中的wait方法实现线程的阻塞,wait方法的实现原理在后续的文章中在说详细阐述。**但是我们需要知道的是......
  • Ubuntu 发行版更新 Linux 内核,修复 17 个安全漏洞
    IT之家近日消息,Canonical于今天面向所有处于支持状态的Ubuntu发行版,发布了 Linux 内核安全更新,累计修复了17个安全漏洞。IT之家近日消息,Canonical于今天面向所有处于支持状态的Ubuntu发行版,发布了Linux内核安全更新,累计修复了17个安全漏洞。本次更新适用于......
  • Ubuntu 发行版更新 Linux 内核,修复 17 个安全漏洞
    IT之家近日消息,Canonical于今天面向所有处于支持状态的Ubuntu发行版,发布了 Linux 内核安全更新,累计修复了17个安全漏洞。IT之家近日消息,Canonical于今天面向所有处于支持状态的Ubuntu发行版,发布了Linux内核安全更新,累计修复了17个安全漏洞。本次更新适用于......