Java并发编程实践读书笔记_线程安全性(一)

一、什么是线程安全性

   在线程安全性中,最核心的就是正确性。
正确性含义:某个类的行为与其规范完全一致。

   线程安全: 当多个线程访问某个类时,不管运行数时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

注意:在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。

   无状态对象一定是线程安全的。比如,Servlet框架中的类StatelessFactorizer是无状态的:它既不包含任何域,也不包含任何对其他类中域的引用。这个类计算过程中的临时状态仅存于线程栈上的局部变量中,并且只能由正在执行的线程访问。
   访问一个StatelessFactorizer的线程不会影响另一个访问同一个StatelessFactorizer的线程的计算结果,因为这两个线程并没有共享状态,就像它们在访问不同的实例。由于线程访问无状态对象不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。

   大多数Servlet都是无状态的,只有当Servlet在处理请求时需要保存一些信息,线程安全性才会成为一个问题。

二、原子性

举例:没有同步的情况下统计给Value赋值的次数。

public class Sequence {
    private int Value;

    public int getValue(){
        return Value;
    }
    
    public void fz(boolean p){
        if(p){
            ++Value;
        }
    }
}

   这个类在单线程能够正常执行。但在多线程中很可能会丢失一些更新操作。虽然递增操作++Value是一种紧凑的语法,使其看上去只是一个操作,但这个操作并非是原子的,因而他并不会作为一个不可分割的操作来执行。实际上,它包含了三种操作,读取count的值,将值加1,然后将值写入count。这是一个“读取-修改-写入”的操作序列,并且结果依赖之前的状态。

   假设两个线程在没有同步的情况下对它进行操作,很可能发生这样的情况:此时Value的值为6,A线程正在修改Value的值,而B线程在A线程修改Value操作开始后但没有执行完的时间段调用getValue方法,由于此时A线程还没有来得及将修改的新的Value值写回,B线程就读取的修改前Value的值,即6,而实际上B应该读取Value的值为7。

   在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它由一个正式的名字:竞态条件

三、竞态条件阐述

   竞态条件中的一类是“先检查后执行”:首先观察到某个条件为真,然后根据这个观察结果采取相应的动作,但事实上,在你观察到这个结果到你开始执行相应的操作之间,观察结果可能变得无效,从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏等)。可以举个例子,假设你和朋友约定10点在奶茶店见面,你已经到了并且等待了10分钟,当你判断朋友鸽你并且向前门走出时,你的朋友刚好出现在后门,(你此时得到朋友鸽你的信息就变得无效了)此后你得到错误信息 朋友爽约,而其实朋友是来了的,然后就会导致你和朋友的矛盾。

   要避免竞态条件,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改完成之前或之后读取和修改状态,而不是在修改状态的过程中。

   假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。

   为了确保线程安全“先检查后执行”和读取-修改-写入操作必须是原子的。我们将“先检查后执行”和读取-修改-写入统称为复合操作。

    在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。通过用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。

   AtomicLong是一种替代long类型整数的线程安全类,类似地,AtomicReference是一种替代对象引用的线程安全类。
   要保证状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

四、内置锁

   Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)同步代码块包括两部分:一个作为锁的对象引用,一个作为有这个锁保护的代码块。

   每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论通过正常的控制路径退出,还是通过代码块中抛异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

   Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。当线程A尝试获取由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B获得这个锁。如果线程B永远不释放锁,那么A也将永远地等待下去。

   由于每次只能有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。并发环境中的原子性与事务应用程序中的原子性有着相同的含义。一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行同一个锁保护的同步代码块。

五、重入

   当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。在程序运行时会发生这样一种情况:

package xiancheng;

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

   由于每个dosomething方法在执行前都会获取Widget上的锁。然而,如果内置锁不是可重入的,那么在调用super.doSomething时将无法获得Widget上的锁,因为这个锁已经被持有,从而线程将永远停顿下去,等待一个永远无法获得的锁。重入则避免了这种死锁情况的发生。

   重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1.如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应的递减。当计数值为0时,这个锁将被释放。

六、用锁来保护状态

   对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称这个变量是由这个锁保护的。

   1.每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。

   2.对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

   3.如果只是将每个方法都作为同步方法,例如Vector,那么并不足以确保Vector上复合操作都是原子的:

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

   虽然contains和add等方法都是原子方法,但在上面这个“如果不存在则添加”的操作中仍然存在竞态条件。虽然synchronized方法可以确保单个操作的原子性,但如果要把多个操作合并为一个复合操作,还需要额外的加锁机制。此外,将每个方法都作为同步方法还可能会导致活跃性问题或性能问题。

七、活跃性与性能

   使用两种不同的同步机制不仅会带来混乱,也不会在性能或者安全性上带来热河好处,因此在这里不使用原子变量。

   无论是执行计算密集的操作,还是在执行某个可能阻塞的操作,如果持有锁的时间过长,那么都会带来活跃性问题或性能问题。

   当执行 时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值