技术开发 频道

JAVA线程:如何解决资源共享冲突(下)

  上面对程序的比较可以引出下面的知识,有些知识前面已经讲过,这里就再强调下了,知识点如下:

  ①线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

  ②同步:同步是可以确定访问一组变量的所有线程都将拥有对这些变量的独占访问权(原子性),并且其他线程获得该锁定时,将可以看到对这些变量的更改(可见性)。同步就是java里确保线程安全性的机制。

  ③原子操作:在上篇文章里我说道,我们在java里使用同步往往是以方法或者说是以操作为单位,线程调度就是切分这些操作进行的,但是java里有些操作是不可中断,也就是说这样的操作要不成功,要不就全部失败,和数据库的事务很像,这样的操作叫做原子操作,理论上这些原子操作是线程安全的。

  学到这里我有了一个自己的理解:

  我们在程序里所做的线程安全操作就是在把我们设计的操作进行原子化操作。其实想想还真的很搞笑,我觉得线程就像社会学里讲的自由和约束的关系:自由需要约束保护而约束又会制约自由。为了让程序有并发的效果计算机技术因此摆脱以前程序单一的运行模式而引入了线程的概念,程序员们设计了一个线程调度算法:以随机的让某一个线程获得CPU计算资源,而不同的线程随机的抢夺这种获取计算的能力,但是这种随机导致我们意向不到的错误结果,如是我们又把这种随意性规范起来,怎么规范了?把有些操作合并起来,而这些按一定规则合并的操作又按照一定的顺序进行,这不是又回到了按顺序执行代码的计算流程吗,但是这样的随机和并发的结合居然真的解决了我们以前单一程序流程的难题了,产生了我们现在绚丽多彩的程序世界了

  这里我还要抛出一个概念,线程安全性。

  ④线程安全性:类要成为线程安全的,首先必须在单线程环境中有正确的行为。如果一个类实现正确(这是说它符合规格说明的另一种方式),那么没有一种对这个类的对象的操作序列(读或者写公共字段以及调用公共方法)可以让对象处于无效状态,观察到对象处于无效状态、或者违反类的任何不可变量、前置条件或者后置条件的情况。此外,一个类要成为线程安全的,在被多个线程访问时,不管运行时环境执行这些线程有什么样的时序安排或者交错,它必须仍然有如上所述的正确行为,并且在调用的代码中没有任何额外的同步。其效果就是,在所有线程看来,对于线程安全对象的操作是以固定的、全局一致的顺序发生的。正确性与线程安全性之间的关系非常类似于在描述 ACID(原子性、一致性、独立性和持久性)事务时使用的一致性与独立性之间的关系:从特定线程的角度看,由不同线程所执行的对象操作是先后(虽然顺序不定)而不是并行执行的。

  线程的理念和事务很像,这就是我想好好研究线程的重要原因之一,因为前不久有人问我关于事务的问题难住了我,回家细想后我发现这个问题的答案很接近线程的理念。

  我想上面的知识点里除了“原子操作”其他概念大多童鞋都很熟悉吧,下面我重点讲讲原子操作

  原子操作是不能被中断的操作,连线程调度机制也不能中断原子操作,java里以下操作是原子操作

  ①基本数据类型(不包括long和double类型);

  ②对所有加有voliatile关键字的数据类型。

  原子操作主要是针对单个数据的读取和赋值操作,具有原子性功能的变量只有在简单赋值或者简单的返回值操作时候才能算作原子操作。基本数据类型是具有原子操作的功能,为什么long和double除外了?在java中,long和double类型通常都是64位,而其他的基本类型都是32位表示的,而且对象引用本身的指针机制也是32位的,java里只对32位的数据做原子操作。我们写的EvenGenerator类使用的是基本数据类型int,但是任然有线程冲突的问题,也就是线程并不安全,那么原子操作到底是不是线程安全的操作呢?回答是的,但是如果它和线程放到一起就会有问题,大家可以看下面的代码:

class RealTimeClock 
  {
   private int clkID;
   public int clockID()
   {
   return clkID;
   }
   public void setClockID(int id)
   {
   clkID = id;
   }
  //...
  }

  我们构建一个RealTimeClock的对象,然后启动两个线程调T1、T2用这个对象的两个方法setClockID和clockID,大家看看下面的过程(测试代码不写了,这种代码比较普遍,看完我后面解释,大家可以自己试试):

  T1 调用setClockID(5)
  T1将5放入自己的私有工作内存
  T2调用setClockID(10)
  T2将10放入自己的私有工作内存
  T1调用clockID,它返回5
  5是从T1的私有工作内存返回的

  对clockID的调用应该返回10,因为这是被T2设置的,然而返回的是5,因为读写操作是对私有工作内存的而非主存。赋值操作当然是原子的,但是因为JVM允许这种行为,因此线程安全不是一定的,同时,JVM的这种行为也不是被保证的。

0
相关文章