java笔记:熟练掌握线程技术---基础篇之解决资源共享的问题(中)--下篇

  终于到了这个小系列的最后一篇了,写这个主题的博文感觉很奇特,虽然我尽全力去深入理解这些知识点所蕴含的深意,最终也感觉似乎懂了,但略微冷静下来后又感到自己的理解还是有点似是而非。我想原因还是在于自己知识面还不够宽阔,有些底层的计算机技术我并不熟悉,解决这些不熟悉的计算机技术就是我以后学习的目标之一。开始进入正题之前,我还是想再强调下,线程技术真的很重要,熟练掌握了线程技术一定能解决我们在编程时候碰到的各种难以解决的问题。

  上篇博文里最后我们写了一个SynchronizedEvenGenerator类:

package cn.com.sxia;

public class SynchronizedEvenGenerator implements Invariant {

private int i;

public synchronized void next(){
i++;
i++;
}

public synchronized int getValue(){
return i;
}

@Override
public InvariantState invariant() {
int val = getValue();
if (val % 2 == 0)
return new InvariantOK();
else
return new InvariantFailure(new Integer(val));
}

public static void main(String[] args) throws InterruptedException {
SynchronizedEvenGenerator gen = new SynchronizedEvenGenerator();
new InvariantWatcher(gen,4000);
while(true){
gen.next();
}
}

}

  前面我们还写了个EvenGenerator类:

package cn.com.sxia;

public class EvenGenerator implements Invariant {

private int i;

public void next(){
i++;
i++;
}

public int getValue(){
return i;
}

@Override
public InvariantState invariant() {
int val = i;
if (val % 2 == 0)
return new InvariantOK();
else
return new InvariantFailure(new Integer(val));
}

public static void main(String[] args){
EvenGenerator gen = new EvenGenerator();
new InvariantWatcher(gen);
while(true){
gen.next();
}
}

}

  两个类很相似,但还是有区别:

  1. SynchronizedEvenGenerator类里的getValue和next方法前都加上了Synchronized关键字。
  2. EvenGenerator类里invariant方法里有这样一个语句int val = i;而SynchronizedEvenGenerator里面对应的语句是int val = getValue();

  两个类运行的结果是:EvenGenerator会执行失败,原因就是线程不安全了,而SynchronizedEvenGenerator线程安全所以会一直正常运行。

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

  1. 线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
  2. 同步:同步是可以确定访问一组变量的所有线程都将拥有对这些变量的独占访问权(原子性),并且其他线程获得该锁定时,将可以看到对这些变量的更改(可见性)。同步就是java里确保线程安全性的机制。
  3. 原子操作:在上篇文章里我说道,我们在java里使用同步往往是以方法或者说是以操作为单位,线程调度就是切分这些操作进行的,但是java里有些操作是不可中断,也就是说这样的操作要不成功,要不就全部失败,和数据库的事务很像,这样的操作叫做原子操作,理论上这些原子操作是线程安全的。
  学到这里我有了一个自己的理解:

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

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

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

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

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

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

  1. 基本数据类型(不包括long和double类型);
  2. 对所有加有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的这种行为也不是被保证的。

  为什么会有上面的结果,因为java的线程允许线程在自己的内存区保存变量的副本,而线程运行时候是使用本地的变量副本操作的,副本是一个临时变量,最终某个时刻都会和主存保持同步,但是过程中使用临时备份那么程序的性能就会越好。而上面的问题正是因为两个线程里的拷贝和主存不一致。而让线程的临时备份保持和主存的一致一般使用两个办法:

1、 变量使用volatile声明

2、 被访问的变量处于同步方法或者同步块中

   上面的解释道出了java里volatile关键字的作用:让线程里的临时拷贝的内存和主存保持一致

  原子操作只有在对基本类型进行读取或赋值时候才被认为是线程安全的,不过正如EvenGenerator中所见,原子操作也很容易访问到对象处在不稳定时候的数值,这种不稳定让使用volatile变量实现同步变的不是很可靠,就算用它做出了安全的线程同步程序员付出的代价也是很大。最安全的也最简单的线程安全做法可以使用下面的方法:

  1. 如果要对类中的某个方法进行同步控制,最好同步所有的方法,假如有被漏掉的方法,结果就很难被我们把控,我们的程序也会带有线程随机调度的特点了。
  2. 假如我们要真的去掉某个方法前的同步标记(这种情况往往是性能的考虑,线程同步总会消耗系统宝贵的计算资源),要非常小心,除非万不得已,要不千万别这么做。

  线程同步会消耗我们宝贵的计算资源。我们写软件总希望它是很快很快再快些,这也是非专业用户觉得你软件做的好的最重要指标之一。但是为了保证线程安全我们在很多方法前加上synchronized关键字导致方法变慢,这个代价虽然我们不得不去承受,但是能不能有变通的方案呢?

  当然有,一个方法里真正需要线程的可能只有一部分,而多个线程也只是需要访问这一部分代码,而不是访问整个方法,那么我们就可以把这段代码分离出来,java语言给这样的代码起了一个名字:临界区(critical section)。同步块的模式如下:

    synchronied(syncObject){
//This code can be accessed
//by only one thread at a time
}

  同步块使用synchronized关键字定义,synchronized关键字会告诉java语言获得syncObject对象的锁,而对象的锁被用来对花括号内的代码进行同步控制。哈哈,只是同步一部分代码而不是方法里所有代码,这样的做法效率一定会有显著的提高。

  下面我将写一段程序,这段程序里会比较两种同步控制。这段代码还会演示如何把一个非保护类型的类在其他类的保护和控制之下应用到多线程的环境里:

package cn.com.sxia;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

/**
* 该类线程不安全
*
@author Administrator
*
*/
class Pair{
private int x,y;

public Pair(int x,int y){
this.x = x;
this.y = y;
}

public Pair(){
this(0,0);
}

public int getX() {
return x;
}

public int getY() {
return y;
}

public void incrementX(){
x++;
}

public void incrementY(){
y++;
}

@Override
public String toString() {
return "Pair [x=" + x + ", y=" + y + "]";
}

public void checkState(){
if (x!=y){
throw new PairValuesNotEqualException();
}
}


//异常内部类
public class PairValuesNotEqualException extends RuntimeException{
public PairValuesNotEqualException(){
super("Pair类的数值不相等:" + Pair.this);
}
}
}

/**
* PairManager是实现了线程安全的类,它将线程不安全的Pair类
* 置于其内部,是的Pair被保护起来
*
@author Administrator
*
*/
abstract class PairManager{
protected Pair p = new Pair();
private List<Object> storage = new ArrayList<Object>();

public synchronized Pair getPair(){
return new Pair(p.getX(),p.getY());
}

protected void store(){
storage.add(getPair());
}

public abstract void doTask();
}

class PairMangerMethod extends PairManager{

@Override
public synchronized void doTask() {
p.incrementX();
p.incrementY();
store();
}

}

/**
* 使用临界区,也就是同步块了
*
@author Administrator
*
*/
class PairManagerCriticalSetion extends PairManager{

@Override
public void doTask() {
synchronized (this) {
p.incrementX();
p.incrementY();
}
store();
}

}

class PairManipulator extends Thread{
private PairManager pm;
private int checkCounter = 0;

private class PairChecker extends Thread{
PairChecker(){
start();
}

public void run(){
while(true){
checkCounter++;
pm.getPair().checkState();
}
}
}

public PairManipulator(PairManager pm){
this.pm = pm;
start();
new PairChecker();
}

public void run(){
while(true){
pm.doTask();
}
}

@Override
public String toString() {
return "PairManipulator [pm=" + pm.getPair() + ", checkCounter=" + checkCounter
+ "]";
}


}

public class CriticalSection {

public static void main(String[] args) {
final PairManipulator
pm1 = new PairManipulator(new PairMangerMethod()),
pm2 = new PairManipulator(new PairManagerCriticalSetion());
new Timer(true).schedule(new TimerTask() {

@Override
public void run() {
System.out.println("pm1:" + pm1);
System.out.println("pm2:" + pm2);
System.exit(0);
}
}, 1000);
}

}

  结果如下:

pm1:PairManipulator [pm=Pair [x=727345, y=727345], checkCounter=696350]
pm2:PairManipulator [pm=Pair [x=631430, y=631430], checkCounter=1234099]

  代码里Pair类是线程不安全的,PairManage类里包含了Pair对象,而获取Pair对象数据的getPair方法,它是用到了同步标记,那么线程不安全的Pair对象就被线程安全的PairManage类保护起来了。

  大家可以看到在PairManagerCriticalSetion类的同步块必须在指定一个对象后才会进行同步,还有在PairMangerMethod类里我们覆盖了PairManager类里的doTask方法并且在覆盖的新方法前加上了synchronized关键字,这就说明java里父类的方法被子类覆盖或者是接口的方法被实现,如果原方法没有synchronized关键字,实现方法可以加上,这种做法是java语言可以接受的

  大家在看看下面的例子,在这个例子里我们混合使用了两种同步方式:

 

package cn.com.sxia;

class DualSynch{
private Object syncObject = new Object();

public synchronized void f(){
System.out.println("Inside f()");

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println("Leaving f()");
}

public void g(){
synchronized (syncObject) {
System.out.println("Inside g()");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Leaving g()");
}
}
}




public class SyncObject {

public static void main(String[] args) {
final DualSynch ds = new DualSynch();

new Thread(){
public void run(){
ds.f();
}
}.start();
ds.g();

}

}

  结果如下:

Inside g()
Inside f()
Leaving f()
Leaving g()

  我们不断运行这个main函数发现结果正常,表明两个同步方式可以混合使用,两种方式混合使用不会产生线程冲突。

  同步块比起方法的同步还有一个优势,大家回到我们写的CriticalSection类,我们再看看结果:

pm1:PairManipulator [pm=Pair [x=680723, y=680723], checkCounter=671226]
pm2:PairManipulator [pm=Pair [x=627127, y=627127], checkCounter=1276546]

  多次运行后同步块的结果记录的值都比同步方法大,这说明了同步块的方式让对象不加锁的时间更长,这就说明使用同步块进行线程同步时候,其他的线程能得到更多的访问资源的机会,这个特别可能会是编程的一大优势,大家可以记下来,说不定会用上啊。


  资源共享的冲突问题结束了,下面的文章继续线程之旅,我们要看看线程之间是如何进行协作的。



你可能感兴趣的:(java)