如何保证单线程安全_第2页回答

2021-04-19 09:30发布

 如何保证单线程安全


 如何保证单线程安全


24条回答
722
2楼 · 2021-04-20 09:50

一、线程安全在三个方面体现


1.原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);


2.可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);


3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。


接下来,依次分析。


二、原子性---atomic


JDK里面提供了很多atomic类,AtomicInteger,AtomicLong,AtomicBoolean等等。


它们是通过CAS完成原子性。


我们一次来看AtomicInteger,AtomicStampedReference,AtomicLongArray,AtomicBoolean。


(1)AtomicInteger


先来看一个AtomicInteger例子:


public class AtomicIntegerExample1 {

    // 请求总数

    public static int clientTotal = 5000;

    // 同时并发执行的线程数

    public static int threadTotal = 200;

    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {

        ExecutorService executorService = Executors.newCachedThreadPool();//获取线程池

        final Semaphore semaphore = new Semaphore(threadTotal);//定义信号量

        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

        for (int i = 0; i < clientTotal>

            executorService.execute(() -> {

                try {

                    semaphore.acquire();

                    add();

                    semaphore.release();

                } catch (Exception e) {

                    log.error("exception", e);

                }

                countDownLatch.countDown();

            });

        }

        countDownLatch.await();

        executorService.shutdown();

        log.info("count:{}", count.get());

    }

 

    private static void add() {

        count.incrementAndGet();

    }

}

我们可以执行看到最后结果是5000是线程安全的。


那么看AtomicInteger的incrementAndGet()方法:




再看getAndAddInt()方法:




这里面调用了compareAndSwapInt()方法:




它是native修饰的,代表是java底层的方法,不是通过java实现的 。


再重新看getAndAddInt(),传来第一个值是当前的一个对象 ,比如是count.incrementAndGet(),那么在getAndAddInt()中,var1就是count,而var2第二个值是当前的值,比如想执行的是2+1=3操作,那么第二个参数是2,第三个参数是1 。


变量5(var5)是我们调用底层的方法而得到的底层当前的值,如果没有别的线程过来处理我们count变量的时候,那么它正常返回值是2。


因此传到compareAndSwapInt方法里的参数是(count对象,当前值2,当前从底层传过来的2,从底层取出来的值加上改变量var4)。


compareAndSwapInt()希望达到的目标是对于var1对象,如果当前的值var2和底层的值var5相等,那么把它更新成后面的值(var5+var4).


compareAndSwapInt核心就是CAS核心。


关于count值为什么和底层值不一样:count里面的值相当于存在于工作内存的值,底层就是主内存。


(2)AtomicStampedReference


接下来我们看一下AtomicStampedReference。



关于CAS有一个ABA问题:开始是A,后来改为B,现在又改为A。解决办法就是:每次变量改变的时候,把变量的版本号加1。


这就用到了AtomicStampedReference。


我们来看AtomicStampedReference里的compareAndSet()实现:




而在AtomicInteger里compareAndSet()实现:




可以看到AtomicStampedReference里的compareAndSet()中多了 一个stamp比较(也就是版本),这个值是由每次更新时来维护的。


(3)AtomicLongArray


这种维护数组的atomic类,我们可以选择性地更新其中某一个索引对应的值,也是进行原子性操作。这种对数组的操作的各种方法,会多处一个索引。


比如,我们看一下compareAndSet():




(4)AtomicBoolean


看一段代码:


public class AtomicBooleanExample {

 

    private static AtomicBoolean isHappened = new AtomicBoolean(false);

    // 请求总数

    public static int clientTotal = 5000;

    // 同时并发执行的线程数

    public static int threadTotal = 200;

    public static void main(String[] args) throws Exception {

        ExecutorService executorService = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(threadTotal);

        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

        for (int i = 0; i < clientTotal>

            executorService.execute(() -> {

                try {

                    semaphore.acquire();

                    test();

                    semaphore.release();

                } catch (Exception e) {

                    log.error("exception", e);

                }

                countDownLatch.countDown();

            });

        }

        countDownLatch.await();

        executorService.shutdown();

        log.info("isHappened:{}", isHappened.get());

    }

    private static void test() {

        if (isHappened.compareAndSet(false, true)) {

            log.info("execute");

        }

    }

}

执行之后发现,log.info("execute");只执行了一次,且isHappend值为true。


原因就是当它第一次compareAndSet()之后,isHappend变为true,没有别的线程干扰。


通过使用AtomicBoolean,我们可以使某段代码只执行一次。


三、原子性---synchronized


synchronized是一种同步锁,通过锁实现原子操作。


JDK提供锁分两种:一种是synchronized,依赖JVM实现锁,因此在这个关键字作用对象的作用范围内是同一时刻只能有一个线程进行操作;另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性的是ReentrantLock。

————————————————

版权声明:本文为CSDN博主「阿梨若苦」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/weixin_40459875/article/details/80290875


20200921文 - 做更棒的自己!
3楼 · 2021-04-20 10:31

Servlet并不是单例,只是容器只实例化它一次,表现出来单例的效果而已。
但需要注意的是,Servlet是web容器来控制实例化的,并不是你自己用你编写的代码来实例化,即使你自己编写代码实例化你的servlet,Web服务器也不会直接调用你的实例化的Servlet对象的。

香蕉牛油果酸奶
4楼 · 2021-04-20 16:26

了解了HashMap为什么线程不安全,那现在看看如何线程安全的使用HashMap。这个无非就是以下三种方式:
Hashtable
ConcurrentHashMap
Synchronized Map

希希
5楼 · 2021-04-20 19:40

要理解多线程种的锁机制我们得先了解线程的五大状态:

创建状态:当线程类编写完毕,我们创建这个线程类的对象的时候,当前创建的线程就处于创建状态。

就绪状态:当线程创建完毕,调用start()方法,该线程进入就绪状态,等等cpu分配资源运行的时间片。

运行状态:当cpu分配给该线程时间片的时候,线程就可以运行现在的内容, 那么线程记进入运行状态。

阻塞状态:当线程在运行的时候,可能被休眠或者其他方式让该线程让出cpu的使用资源,那么当前线程就进入阻塞状态。当阻塞时间完毕,线程再次进入就绪状态,等待cpu分配资源。


寂静的枫林
6楼 · 2021-04-21 11:40

要理解多线程种的锁机制我们得先了解线程的五大状态:

创建状态:当线程类编写完毕,我们创建这个线程类的对象的时候,当前创建的线程就处于创建状态。

就绪状态:当线程创建完毕,调用start()方法,该线程进入就绪状态,等等cpu分配资源运行的时间片。

运行状态:当cpu分配给该线程时间片的时候,线程就可以运行现在的内容, 那么线程记进入运行状态。

阻塞状态:当线程在运行的时候,可能被休眠或者其他方式让该线程让出cpu的使用资源,那么当前线程就进入阻塞状态。当阻塞时间完毕,线程再次进入就绪状态,等待cpu分配资源。

死亡状态:当线程该执行的所有内容执行完毕之后,线程就虎进入死亡状态。


小新没有蜡笔
7楼 · 2021-04-21 15:02

要理解多线程种的锁机制我们得先了解线程的五大状态:


创建状态:当线程类编写完毕,我们创建这个线程类的对象的时候,当前创建的线程就处于创建状态。

就绪状态:当线程创建完毕,调用start()方法,该线程进入就绪状态,等等cpu分配资源运行的时间片。

运行状态:当cpu分配给该线程时间片的时候,线程就可以运行现在的内容, 那么线程记进入运行状态。

阻塞状态:当线程在运行的时候,可能被休眠或者其他方式让该线程让出cpu的使用资源,那么当前线程就进入阻塞状态。当阻塞时间完毕,线程再次进入就绪状态,等待cpu分配资源。

死亡状态:当线程该执行的所有内容执行完毕之后,线程就虎进入死亡状态。


杨晓春
8楼 · 2021-04-21 15:59

一、线程安全等级


       之前的博客中已有所提及“线程安全”问题,一般我们常说某某类是线程安全的,某某是非线程安全的。其实线程安全并不是一个“非黑即白”单项选择题。按照“线程安全”的安全程度由强到弱来排序,我们可以将java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。


          


1、不可变


       在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如final关键字修饰的数据不可修改,可靠性最高。


2、绝对线程安全


      绝对的线程安全完全满足Brian GoetZ给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的代价。


3、相对线程安全


      相对线程安全就是我们通常意义上所讲的一个类是“线程安全”的。


       它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。


    在java语言中,大部分的线程安全类都属于相对线程安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保证的集合。


4、线程兼容


      线程兼容就是我们通常意义上所讲的一个类不是线程安全的。


      线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境下可以安全地使用。Java API中大部分的类都是属于线程兼容的。如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。


5、线程对立


      线程对立是指无论调用端是否采取了同步错误,都无法在多线程环境中并发使用的代码。由于java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是很少出现的。


      一个线程对立的例子是Thread类的supend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都有死锁风险。正因此如此,这两个方法已经被废弃啦。




二、线程安全的实现方法


        保证线程安全以是否需要同步手段分类,分为同步方案和无需同步方案。


       


       1、互斥同步


        互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。


       在java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码质量,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。


        此外,ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。


       互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。


       2、非阻塞同步 


       随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。


        非阻塞的实现CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。


        CAS缺点:


       ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。


        ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3C。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。


       3、无需同步方案


        要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。


        1)可重入代码


       可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。


       可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入、不调用 非可重入的方法等。


       (类比:synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁)


      2)线程本地存储


       如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。


       符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的“一个请求对应一个服务器线程(Thread-per-Request)”的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。

息红泪
9楼 · 2021-04-21 18:18

单线程的话,不会出现线程安全问题,只有在多线程的情况下,操作同一个数据了才会出现线程安全的问题。

相关问题推荐

  • 回答 156

    对于每一位才开始接触JAVA的新手来说,先不要管算法和数据结构,大多数简单的程序不需要用到算法和数据结构,所以当你真正需要时再去学习。编程一段时间以后,你就会知道在哪些地方用到他们。这时知道算法的名字并了解它们的功能,然后动手去实践。当我们在去...

  • 回答 93

    2个都很好就业,更关键的是要学得到东西

  • 回答 12
    已采纳

    获取Map集合中所有的key可以通过map集合的keySet()方法获取例如:    Map map = new HashMap();    map.put(xx,xx); //存放数据    //.... 省略    Set set = map.keySet();    //可以通过迭代器进行测试    Iterator iter = set.iter...

  • 回答 56
    已采纳

    不同年龄,不同掌握程度,学历,找工作城市,面试能力这是一个多方面影响的结果,如果是平均值的话,全国平均薪资14k左右

  • 回答 38

    具体学多久,根据自己的学习力,自律性、解决问题能力来决定若系统性学习,跟着讲师的节奏走,大概半年左右,有专业的讲师把课程进行规划,尽心系统学习,有问题,讲师会帮忙解决,学习的效率很高,避免了自学中出现各种问题解决不了,而耽误很多时间,可能会...

  • 回答 23
    已采纳

    (1)idea启动时会有两个快捷方式,安装完后默认生成在桌面的是32位的idea的快捷方式,如果我们使用这个快捷方式运行大项目,一般都会很卡。解决方法是找到idea的安装目录,然后进入bin文件夹,找到名称为idea64的应用程序,右键他生成桌面快捷方式。以后每次...

  • Java方法的命名规则2021-04-06 19:07
    回答 31

    ava是一种区分字母的大小写的语言,所以我们在定义变量名的时候应该注意区分大小写的使用和一些规范,接下来我们简单的来讲讲Java语言中包、类、变量等的命名规范。(一)Package(包)的命名Package的名字应该都是由一个小写单词组成,例如com、xuetang9、compan...

  • BIO与NIO、AIO的区别2020-05-19 15:59
    回答 4
    已采纳

    IO的方式通常分为几种,同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO。一、BIO     在JDK1.4出来之前,我们建立网络连接的时候采用BIO模式,需要先在服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,默认情况下服务端需要...

  • 回答 2

    public class Point {    private int x;    private int y;    public int getX() {        return x;    }    public void setX(int x) {        this.x = x;    }    public int getY() {        return y;    } ...

  • 回答 6

    经典版单例模式public class Singleton {        private static Singleton uniqueInstance;//利用一个静态常量来记录singleton类的唯一实例。     private Singleton() {     }     public static  Singleton getInstance()...

  • 回答 3

    哈希表的长度一般是定长的,在存储数据之前我们应该知道我们存储的数据规模是多大,应该尽可能地避免频繁地让哈希表扩容。但是如果设计的太大,那么就会浪费空间,因为我们跟不用不到那么大的空间来存储我们当前的数据规模;如果设计的太小,那么就会很容易发...

  • 回答 14

    1. DOM(Document Object Model)        DOM是用与平台和语言无关的方式表示XML文档的官方W3C标准。DOM是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才...

  • 回答 19

    1)作用不同: throw用于程序员自行产生并抛出异常; throws用于声明在该方法内抛出了异常2) 使用的位置不同: throw位于方法体内部,可以作为单独语句使用; throws必须跟在方法参数列表的后面,不能单独使用。3)内容不同: throw抛出一个异常对象,且只能是...

  • 回答 11

    基本执行过程如下:1)程序首先执行可能发生异常的try语句块。2)如果try语句没有出现异常则执行完后跳至finally语句块执行;3)如果try语句出现异常,则中断执行并根据发生的异常类型跳至相应的catch语句块执行处理。4)catch语句块可以有多个,分别捕获不同类型...

  • 回答 20

    100-199 用于指定客户端应相应的某些动作。 200-299 用于表示请求成功。 300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。 400-499 用于指出客户端的错误。 400 语义有误,当前请求无法被服务器理解。 401 当前请求需要用户验证...

  • 回答 16

    异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译...

没有解决我的问题,去提问