java多线程小探

递增共享计数器

https://www6.software.ibm.com/developerworks/cn/education/java/j-threads/tutorial/j-threads-6-5.html


通常,如果正在保护一个基本变量(如一个整数),有时只使用 volatile 就可以侥幸过关。但是,如果变量的新值派生自以前的值,就必须使用同步。为什么?考虑这个类:


public class Counter {
private int counter = 0;

public int get()      { return counter; }
public void set(int n) { counter = n; }
public void increment() {
    set(get() + 1);
}
}
         

当我们要递增计数器时,会发生什么?请看 increment() 的代码。它很清楚,但不是线程安全的。如果两个线程试图同时执行 increment(),会发生什么?计数器也许会增加 1,也许增加 2。令人惊奇的是,把 counter 标记成 volatile 没有帮助,使 get() 和 set() 都变成 synchronized 也没有帮助。

设想计数器是零,而两个线程同时执行递增操作代码。这两个线程会调用 Counter.get(),并且看到计数器是零。现在两个线程都对它加一,然后调用 Counter.set()。如果我们的计时不太凑巧,那么这两个线程都看不到对方的更新,即使 counter 是 volatile,或者 get() 和 set() 是 synchronized。现在,即使计数器递增了两次,得到的值也许只是一,而不是二。

要使递增操作正确运行,不仅 get() 和 set() 必须是 synchronized,而且 increment() 也必需是 synchronized!否则,调用 increment() 的线程可能会中断另一个调用 increment() 的线程。如果您不走运,最终结果将会是计数器只增加了一次,不是两次。同步 increment() 防止了这种情况的发生,因为整个递增操作是原子的。

当循环遍历 Vector 的元素时,同样如此。即使同步了 Vector 的方法,但在循环遍历时,Vector 的内容仍然会更改。如果要确保 Vector 的内容在循环遍历时不更改,必须同步整个代码块。

=======================================================================================================
不变性和 final 字段


许多 Java 类,包括 String、Integer 和 BigDecimal,都是不可改变的:一旦构造之后,它们的状态就永远不会更改。如果某个类的所有字段都被声明成 final,那么这个类就是不可改变的。(实际上,许多不可改变的类都有非 final 字段,用于高速缓存以前计算的方法结果,如 String.hashCode(),但调用者看不到这些字段。)

不可改变的类使并发编程变得非常简单。因为不能更改它们的字段,所以就不需要担心把状态的更改从一个线程传递到另一个线程。在正确构造了对象之后,可以把它看作是常量。

同样,final 字段对于线程也更友好。因为 final 字段在初始化之后,它们的值就不能更改,所以当在线程之间共享 final 字段时,不需要担心同步访问。

=======================================================================================================
什么时候不需要同步


在某些情况中,您不必用同步来将数据从一个线程传递到另一个,因为 JVM 已经隐含地为您执行同步。这些情况包括:

由静态初始化器(在静态字段上或 static{} 块中的初始化器)初始化数据时 
访问 final 字段时 
在创建线程之前创建对象时 
线程可以看见它将要处理的对象时
=======================================================================================================
同步准则


当编写 synchronized 块时,有几个简单的准则可以遵循,这些准则在避免死锁和性能危险的风险方面大有帮助:

使代码块保持简短。Synchronized 块应该简短 ― 在保证相关数据操作的完整性的同时,尽量简短。把不随线程变化的预处理和后处理移出 synchronized 块。


不要阻塞。不要在 synchronized 块或方法中调用可能引起阻塞的方法,如 InputStream.read()。


在持有锁的时候,不要对其它对象调用方法。这听起来可能有些极端,但它消除了最常见的死锁源头。
=======================================================================================================

每个 Java 程序都使用线程,不论您知道与否。如果您正在使用 Java UI 工具箱(AWT 或 Swing)、Java Servlet、RMI、JavaServer Pages 或 Enterprise JavaBeans 技术,您可能没有意识到您正在使用线程。

在许多情况中,您可能想要显式地使用线程以提高程序的性能、响应速度或组织。这些情况包括:

在执行耗时较长的任务时,使用户界面的响应速度更快 
利用多处理器系统以并行处理多个任务 
简化仿真或基于代理的系统的建模 
执行异步或后台处理 
虽然线程 API 很简单,但编写线程安全的程序并不容易。在线程之间共享变量时,必须非常小心,以确保正确同步了对它们的读写访问。当写一个可能接下来由另一个线程读取的变量,或者读取可能由另一个线程写过的变量时,必须使用同步以确保对数据的更改在线程之间是可见的。

当使用同步保护共享变量时,必须确保不仅使用了同步,而且读取器和写入器在同一个监控器上同步。而且,如果依赖对象的状态在多个操作中保持相同,或者依赖多个变量互相保持一致(或者,与它们过去的值一致),那么必须使用同步来强制实现这一点。但简单地同步一个类中的每一个方法并不能使它变成线程安全的 ― 只会使它更容易发生死锁。

=======================================================================================================
例子:

(一)即时显示当前时间:

import java.util.*;
import java.text.*;
import java.applet.Applet;
import java.awt.Graphics;

public class Clock extends Applet implements Runnable {

Thread clockThread;

public void start() {
   if (clockThread == null) {
    clockThread = new Thread(this, "Clock");
    clockThread.start();
   }
}

public void run() {
   while (clockThread != null) {
    repaint();
    try {
     clockThread.sleep(1000);
    } catch (InterruptedException e) {
    }
   }
}

public void paint(Graphics g) {
   //Date now = new Date();
   // now.getHours()+now.getMinutes()+now.getSeconds()(过时的方法)
   DateFormat df = new SimpleDateFormat("EEEE-MMMM-dd-yyyy HH:mm:ss");
   Calendar c = Calendar.getInstance();
   g.drawString(df.format(c.getTime()), 5, 10);

   g.drawString(DateFormat.getTimeInstance().format(c.getTime()), 5, 30);
   g.drawString(DateFormat.getTimeInstance(DateFormat.LONG, Locale.CHINA)
     .format(c.getTime()), 5, 50);
   g.drawString(DateFormat.getInstance().format(c.getTime()), 5, 70);
   g.drawString(DateFormat.getDateTimeInstance(DateFormat.FULL,
     DateFormat.FULL).format(c.getTime()), 5, 90);
   g.drawString(DateFormat.getDateTimeInstance().format(c.getTime()), 5,
     110);
}

public void stop() {
   clockThread.stop();
   clockThread = null;
}

public static void main(String[] args) {
   new Clock().start();
}

}

(二)生产者、消费者问题

class CubbyHole {
private int seq;

private boolean available = false; // 信号量

/**
* 用synchronized来标识的区域或方法即为对象互斥锁锁住的部分。

* 如果一个程序内有两个或以上的方法使用synchronized标志,

* 则它们 在同一个对象互斥锁管理之下。

* 一般情况下,都使用synchronized关键字在方法的层次上实现对共享资源操作的同步,

* 很少使用volatile关键字声明共享变量。
*/
public synchronized int get() {
   while (available == false) {
    try {
     // wait()方法的作用是让当前线程释放其所持有的对象互斥锁,进入wait队列(等待队列)
     wait();
     // waits for notify() call from Producer
    } catch (InterruptedException e) {
    }
   }

   available = false;
   // notify()/notifyAll()方法的作用是唤醒一个或所有正在等待队列中等待的线程,
   // 并将它(们)移入等待同一个对象互斥锁的队列。
   notify();
   // 需要指出的是: notify()/notifyAll()方法和wait()方
   // 法都只能在被声明为synchronized的方法或代码段中调用。

   return seq;
}

public synchronized void put(int value) {
   while (available == true) {
    try {
     wait();
     // waits for notify() call from consumer
    } catch (InterruptedException e) {
    }
   }

   seq = value;
   available = true;
   notify();
}

}

class Producer extends Thread {
private CubbyHole cubbyhole;

private int number;

public Producer(CubbyHole c, int number) {
   cubbyhole = c;
   this.number = number;
}

public void run() {
   for (int i = 0; i < 10; i++) {
    cubbyhole.put(i);
    System.out.println("Producer @" + this.number + " put: " + i);
    try {
     sleep((int) (Math.random() * 100));
    } catch (InterruptedException e) {
    }
   }
}
}

class Consumer extends Thread {
private CubbyHole cubbyhole;

private int number;

public Consumer(CubbyHole c, int number) {
   cubbyhole = c;
   this.number = number;
}

public void run() {
   int value = 0;
   for (int i = 0; i < 10; i++) {
    value = cubbyhole.get();
    System.out.println("Consumer #" + this.number + " got: " + value);
   }
}
}

public class ProducerConsumerTest {
public static void main(String args[]) {
   CubbyHole c = new CubbyHole();
   Producer p1 = new Producer(c, 1);
   Consumer c1 = new Consumer(c, 1);
   //Consumer c2 = new Consumer(c, 2);

   p1.setPriority(Thread.MAX_PRIORITY);
   c1.setPriority(Thread.NORM_PRIORITY);
   //c2.setPriority(Thread.NORM_PRIORITY);

   p1.start();
   c1.start();
   //c2.start();
}

}

输出结果:

Producer @1 put: 0
Consumer #1 got: 0
Producer @1 put: 1
Consumer #1 got: 1
Producer @1 put: 2
Consumer #1 got: 2
Producer @1 put: 3
Consumer #1 got: 3
Producer @1 put: 4
Consumer #1 got: 4
Producer @1 put: 5
Consumer #1 got: 5
Producer @1 put: 6
Consumer #1 got: 6
Producer @1 put: 7
Consumer #1 got: 7
Producer @1 put: 8
Consumer #1 got: 8
Producer @1 put: 9
Consumer #1 got: 9

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值