`
he_wen
  • 浏览: 238754 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

线程安全

阅读更多

一、本文主要描述线程安全:

      1.1: 什么是线程安全

      1.2: 保持同步的原子性

      1.3: 锁机制

      1.4: 用锁保护状态

      1.5: 激活性和性能

 

二、线程安全的类一般是无状态的对象、或者类里面的变量是不可变的,下面举例说明什么是无状态类

 

public class StatelessFactorizer implements Servlet {
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }
}


 

 

跟线程模型有关系,因为每个线程都有自己的变量,并且变量放在自己的线程堆栈中(除了共享变量),所以说无论多个线程怎么访问,该线程类都是安全。

 

三、也许有人会问是什么样的操作是原子性的呢?

 

      那么举个例子  count++;

   这个操作时原子性的吗?好像单道程序中该操作时原子性,但在早多线程中该操作不是原子性;该操作分为读、修改、写入,因此该操作就会在多个线程中执行时会别切换,也就说明该操作会在执行过程中呗切换给下一个线程。

 

下面还是举上面的那个例子:

 

public class UnsafeCountingFactorizer implements Servlet {
    private long count = 0;

    public long getCount() { return count; }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count;
        encodeIntoResponse(resp, factors);
    }

}


 

 

该例子有一个私有的但是多个线程共享的变量,而在service方法中该方法体里面执行了++count,所以该类不是线程安全的。

  类失去原子性操作的另一个典型是:检查在运行

下面就为大家举个单例模式中的懒加载问题:

 

public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)
            instance = new ExpensiveObject();
        return instance;
    }
}

 

大家可能会一下子知道这个没有什么问题,但是在多线程中运行该getInstance方法时,有可能两个或以上的线程会判断instance为空。。。所以说要尽量在判断和新建对象的时候不要被线程切换,也就是说保持操作的原子性的时候那么该类就是线程安全。

 

下面为大家阐明上面出现的两个问题的解决方案:

 

   第一:要想解决原子性的操作就需要给这些操作加锁

  第二:要让每个线程知道,当有个线程修改对象或者修改变量时,其他线程都要可见,这个关系到JVM的类存模型

 

第一个问题read-modify-write解决方案就是:

    我们引用了JDK包中java.lang.concurrent.atomic 该包里面可以让变量保持原子性操作,具体类的简单介绍如:

 

 

AtomicBoolean 可以用原子方式更新的 boolean 值。
AtomicInteger 可以用原子方式更新的 int 值。
AtomicIntegerArray 可以用原子方式更新其元素的 int 数组。
AtomicIntegerFieldUpdater<T> 基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。
AtomicLong 可以用原子方式更新的 long 值。
AtomicLongArray 可以用原子方式更新其元素的 long 数组。
AtomicLongFieldUpdater<T> 基于反射的实用工具,可以对指定类的指定 volatile long 字段进行原子更新。
AtomicMarkableReference<V> AtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。
AtomicReference<V> 可以用原子方式更新的对象引用。
AtomicReferenceArray<E> 可以用原子方式更新其元素的对象引用数组。
AtomicReferenceFieldUpdater<T,V> 基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。
AtomicStampedReference<V> AtomicStampedReference 维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。

具体用法可参照文档的用法:

 

 

public class CountingFactorizer implements Servlet {
    private final AtomicLong count = new AtomicLong(0);

    public long getCount() { return count.get(); }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();
        encodeIntoResponse(resp, factors);
    }
}

 

第二个问题解决方案就是加上关键字sychronized.

 

四、锁

 

下面为大家引入前面的第一个例子,然后又为该类添加一个变量(该变量用来缓存servlet最近计算的结果),而且该变量可以保持原子性操作,那么想一想两个变量都是保持原子性操作的,那么该类是否是线程安全的呢?

 

答案:否

 

这个例子的代码:

public class UnsafeCachingFactorizer implements Servlet {
     private final AtomicReference<BigInteger> lastNumber
         = new AtomicReference<BigInteger>();
     private final AtomicReference<BigInteger[]>  lastFactors
         = new AtomicReference<BigInteger[]>();

     public void service(ServletRequest req, ServletResponse resp) {
         BigInteger i = extractFromRequest(req);
         if (i.equals(lastNumber.get()))
             encodeIntoResponse(resp,  lastFactors.get() );
         else {
             BigInteger[] factors = factor(i);
             lastNumber.set(i);
             lastFactors.set(factors);
             encodeIntoResponse(resp, factors);
         }
     }
}

 看起来这个类是没有问题的,但是大家用多线程的思维看待这个类,就会发现该类存在着竞争条件:

   lastNumber.set(i);

   lastFactors.set(factors);

 这两个操作一定要是原子性操作,否则别的线程就会在执行这两个操作时,切换掉,以至于别的线程达不到想要的数据。

 

总结:一个类中虽然每个变量是线程安全的,但是如果组合起来就不是线程安全,以为变量之间会相互依赖,只有变量不互相依赖,那么这个类仍然是线程安全的。换一句话:为了保持类的状态的一致性,那么就要把更新的相关变量在一个原子操作中完成。

接下来说明两个锁:内部锁和可重入锁

 

内部锁用sychronized关键字,为了保持原子的一致性,在并发编程中要善于的用这个关键字,因为滥用的话:导致死锁

、竞争条件更加激烈、并发性并单一性更加慢。

 

可重入锁:

When a thread requests a lock that is already held by another thread, the requesting thread blocks. But because intrinsic locks are reentrant, if a thread tries to acquire a lock that it already holds, the request succeeds. Reentrancy means that locks are acquired on a per-thread rather than per-invocation basis. [7] Reentrancy is implemented by associating with each lock an acquisition count and an owning thread. When the count is zero, the lock is considered unheld. When a thread acquires a previously unheld lock, the JVM records the owner and sets the acquisition count to one. If that same thread acquires the lock again, the count is incremented, and when the owning thread exits the synchronized block, the count is decremented. When the count reaches zero, the lock is released

有高人可以解释一下这段话说的是什么,说实话这个不是很清楚

下面有一个例子:

 

public class Widget {
    public synchronized void doSomething() {
        ...
    }
}

public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}

 

如果没有可重入锁就会死锁。

 

五:用锁保护类的状态

 


人们通常犯的错误是认为:只有在写入共享变量的时候才会用到同步。下面总结一下:当多个线程访问每个可变的变量,访问整个变量时候一定要拥有同一个锁。

 

大家也许会问同步关键字能够解决所有的竞争条件那么为什么不用同步来全部解决呢?

 这是因为滥用同步会使得应用程序构建的同步代码块过多或者过少,所以要善于考虑这个问题。仅仅把同步关键字放在方法前面(同步集合时这么干的,同步集合扩大了同步代码,是的在并发性能上非常弱),那么也不会解决复合行为(put-if-absent)如:

 

if (!vector.contains(element))
    vector.add(element);


六、激活性和性能

 

在解决servlet缓存原子性操作时加同步关键字,这样会引起很大的性能问题,如图所示:

 



 

 


因此就要考虑:尝试在程序中运行时间比较久的操作放在同步代码块外面但有不影响类的共享状态

public class CachedFactorizer implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    @GuardedBy("this") private long hits;
    @GuardedBy("this") private long cacheHits;

    public synchronized long getHits() { return hits; }
    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized (this) {
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if (factors == null) {
            factors = factor(i);
            synchronized (this)  {
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
        encodeIntoResponse(resp, factors);
    }
}

 

在上面的类中四个变量都没有用到AtomicLong 类来保持原子一致性,这是因为方法中用了两个同步代码块,来协调保持变量的同步性。

那么大家肯定会想,在方法中加同步关键字和在方法里面分解出耗时且不影响类状态的改变的同步代码块,这两个解决方案如何选择呢?

那么必须要编程者清晰的知道类中的变量和竞争条件。所以编程者要切记在方法中添加关键字,虽然简单但是会影响并发性能,特别是在方法里面含有耗时的操作,如:I/O,网络连接、等等。

  • 大小: 15.9 KB
分享到:
评论

相关推荐

    servlet线程安全问题

    Servlet 线程安全问题 Servlet 线程安全问题是指在使用 Servlet 编程时,如果不注意多线程安全性问题,可能会导致难以发现的错误。Servlet/JSP 技术由于其多线程运行而具有很高的执行效率,但这也意味着需要非常...

    c# 线程安全队列的用法原理及使用示例

    什么是线程安全? 答:线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等...

    C#多线程List的非线程安全性

    本文将深入探讨在多线程环境中使用List时遇到的非线程安全问题,并提供相应的解决方案和最佳实践。 List是.NET框架中常用的一个动态数组,它提供了方便的增删改查操作。然而,List并未设计为线程安全的容器,这意味...

    CVI 线程锁、线程安全变量实例

    在计算机编程领域,尤其是涉及到实时系统和并发编程时,线程锁和线程安全变量是至关重要的概念。LabWindows/CVI是一种流行的交互式C开发环境,特别适合于开发科学和工程应用。本实例将深入探讨如何在LabWindows/CVI...

    关于如何解决HashMap线程安全问题的介绍

    但是需要注意,虽然这个方法可以保证基本的线程安全,但迭代仍然是非线程安全的,即不能在遍历过程中修改Map。 2. 使用ConcurrentHashMap:Java从1.5版本开始引入了ConcurrentHashMap,它是线程安全且高并发性能的...

    C++日志库-线程安全

    线程安全的日志库在多线程环境下尤为重要,因为不正确的日志操作可能会导致数据竞争和同步问题。本文将详细讨论如何在C++中实现一个基于Win32接口的线程安全日志库,并关注其核心概念和技术。 首先,我们需要理解...

    浅谈C#跨线程调用窗体控件(比如TextBox)引发的线程安全问题

    浅谈C#跨线程调用窗体控件引发的线程安全问题 C#跨线程调用窗体控件时可能会引发线程安全问题,例如当多个线程操作同一个控件时,该控件可能会进入不一致的状态,出现争用情况和死锁等问题。因此,确保以线程安全...

    C# 高效线程安全,解决多线程写txt日志类.zip

    在IT行业中,尤其是在开发高并发应用时,线程安全是一个至关重要的问题。"C# 高效线程安全,解决多线程写txt日志类.zip" 提供了一个专门用于多线程环境下写入txt日志文件的解决方案,确保了在并发写入时的数据一致性...

    servlet与Struts action线程安全问题分析

    Servlet和Struts Action是两种常见的Java Web开发组件,它们在多线程环境下运行时可能存在线程安全问题。线程安全是指在多线程环境中,一个类或者方法能够正确处理多个线程的并发访问,保证数据的一致性和完整性。 ...

    浅议单例模式之线程安全(转)

    在多线程环境下,线程安全的单例模式尤为重要,因为如果不正确实现,可能会导致多个线程同时创建多个实例,违反了单例模式的基本原则。 在Java中,单例模式通常有以下几种实现方式: 1. 饿汉式(静态常量): ...

    操作系统课设-线程安全的双向链表

    操作系统课程设计中实现线程安全的双向链表是一项重要的实践任务,这涉及到多线程编程、数据结构以及并发控制等核心知识点。在这个项目中,我们主要关注如何在多线程环境下构建一个能够正确操作(如插入、删除)而不...

    易语言线程安全之原子锁与读写锁

    在IT行业中,线程安全是多线程编程中的一个重要概念,确保多个线程并发执行时,数据的正确性和完整性不会受到影响。线程安全通常通过同步机制来实现,其中包括原子操作和锁机制。本文将深入探讨易语言中的原子锁与...

    C# 高效线程安全,解决多线程写txt日志类

    在C#编程中,线程安全是多线程应用程序中至关重要的一个方面,尤其是在处理共享资源如文本日志文件时。本主题将深入探讨如何在C#中创建一个高效的线程安全日志类,用于在多线程环境中安全地写入txt日志。 首先,...

    java线程安全测试

    Java线程安全是多线程编程中的一个关键概念,它涉及到多个线程访问共享资源时可能出现的问题。在Java中,线程安全问题通常与并发、内存模型和可见性有关。Java内存模型(JMM)定义了如何在多线程环境下共享数据的...

    C# VS2013 收串口数据 线程安全

    在处理串口通信时,线程安全是一个关键的概念,特别是当程序需要同时处理多个任务或者从外部设备(如串口)持续接收数据时。本主题将深入探讨如何在VS2013中使用C#实现线程安全的串口数据接收。 串口通信是计算机...

    hiredis的c++封装, 线程安全

    本文将深入探讨如何使用C++进行hiredis的封装,以实现线程安全的Redis客户端操作。 首先,hiredis是Redis官方提供的一个纯C语言的简单协议解析库,它专注于处理Redis命令和响应,而忽略了更高级别的抽象。为了在C++...

    使用C++11实现线程安全的单例模式

    线程安全的单例模式在多线程环境下尤其重要,因为不正确的实现可能导致多个线程创建多个实例,这违反了单例模式的基本原则。C++11引入了新的特性,如std::mutex和std::call_once,使得实现线程安全的单例模式变得...

    Java多线程安全集合

    在Java编程中,多线程安全集合是程序员在并发环境下处理数据共享时必须考虑的关键概念。这些集合确保了在多个线程访问时的数据一致性、完整性和安全性,避免了竞态条件、死锁和其他并发问题。Java提供了一系列的线程...

    局部变量线程安全测试

    在编程领域,线程安全是多线程编程中的一个重要概念,尤其在Java、C++等支持并发编程的语言中。线程安全通常指的是当多个线程访问一个对象时,如果对象的状态始终保持一致,那么我们就说这个对象是线程安全的。这里...

Global site tag (gtag.js) - Google Analytics