1 什么是并发问题。
多个进程或线程同时(或着说在同一段时间内)访问同一资源会产生并发问题。
银行两操作员同时操作同一账户就是典型的例子。比如A、B操作员同时读取一余额为1000元的账户,A操作员为该账户增加100元,B操作员同时为该账户减去 50元,A先提交,B后提交。 最后实际账户余额为1000-50=950元,但本该为 1000+100-50=1050。这就是典型的并发问题。如何解决?可以用锁。
2 java中synchronized的用法
- 用法1
public class Test{ public synchronized void print(){ ....; } }
某线程执行print()方法,则该对象将加锁。其它线程将无法执行该对象的所有synchronized块。
- 用法2
public class Test{ public void print(){ synchronized(this){//锁住本对象 ...; } } }
同用法1, 但更能体现synchronized用法的本质。
- 用法3
public class Test{ private String a = "test"; public void print(){ synchronized(a){//锁住a对象 ...; } } public synchronized void t(){ ...; //这个同步代码块不会因为print()而锁定. } }
执行print(),会给对象a加锁,注意不是给Test的对象加锁,也就是说 Test对象的其它synchronized方法不会因为print()而被锁。同步代码块执行完,则释放对a的锁。
为了锁住一个对象的代码块而不影响该对象其它 synchronized块的高性能写法:
public class Test{ private byte[] lock = new byte[0]; public void print(){ synchronized(lock){ ...; } } public synchronized void t(){ ...; } }
- 静态方法的锁
public class Test{ public synchronized static void execute(){ ...; } }
效果同
public class Test{ public static void execute(){ synchronized(TestThread.class){ ...; } } }
3 使用Executors、Executor、ExecutorService、ThreadPoolExecutor
可以使用线程管理任务。还可以使用jdk1.5提供的一组类来更方便的管理任务。从这些类里我们可以体会一种面向任务的思维方式。这些类是:
- Executor接口。使用方法:
Executor executor = anExecutor;//生成一个Executor实例。 executor.execute(new RunnableTask1());
用意:使用者只关注任务执行,不用操心去关注任务的创建、以及执行细节等这些第三方实现者关心的问题。也就是说,把任务的调用执行和任务的实现解耦。
实际上,JDK1.5中已经有该接口出色的实现。够用了。
- Executors是一个如同Collections一样的工厂类或工具类,用来产生各种不同接口的实例。
- ExecutorService接口它继承自Executor. Executor只管把任务扔进 executor()里去执行,剩余的事就不管了。而ExecutorService则不同,它会多做点控制工作。比如:
class NetworkService { private final ServerSocket serverSocket; private final ExecutorService pool; public NetworkService(int port, int poolSize) throws IOException { serverSocket = new ServerSocket(port); pool = Executors.newFixedThreadPool(poolSize); } public void serve() { try { for (;;) { pool.execute(new Handler(serverSocket.accept())); } } catch (IOException ex) { pool.shutdown(); //不再执行新任务 } } } class Handler implements Runnable { private final Socket socket; Handler(Socket socket) { this.socket = socket; } public void run() { // read and service request } }
ExecutorService(也就是代码里的pool对象)执行shutdown后,它就不能再执行新任务了,但老任务会继续执行完毕,那些等待执行的任务也不再等待了。
- 任务提交者与执行者通讯
public static void main(String args[])throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); Callable<String> task = new Callable<String>(){ public String call()throws Exception{ return "test"; } }; Future<String> f = executor.submit(task); String result = f.get();//等待(阻塞)返回结果 System.out.println(result); executor.shutdown(); }
Executors.newSingleThreadExecutor()取得的Executor实例有以下特性:
- 任务顺序执行. 比如:
executor.submit(task1); executor.submit(task2);
必须等task1执行完,task2才能执行。
- task1和task2会被放入一个队列里,由一个工作线程来处理。即:一共有2个线程(主线程、处理任务的工作线程)。
- 任务顺序执行. 比如:
- 其它的类请参考Java Doc
- 简单例子1
package concurrent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestCachedThreadPool {
public static void main(String[] args) {
// ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService = Executors.newFixedThreadPool(5);
// ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executorService.execute(new TestRunnable());
System.out.println("************* a" + i + " *************");
}
executorService.shutdown();
}
}
class TestRunnable implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + "线程被调用了。");
while (true) {
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
例子2import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class CallableDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> resultList = new ArrayList<Future<String>>();
//创建10个任务并执行
for (int i = 0; i < 10; i++) {
//使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中
Future<String> future = executorService.submit(new TaskWithResult(i));
//将任务执行结果存储到List中
resultList.add(future);
}
//遍历任务的结果
for (Future<String> fs : resultList) {
try {
System.out.println(fs.get()); //打印各个线程(任务)执行的结果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
//启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则调用没有其他作用。
executorService.shutdown();
}
}
}
}
class TaskWithResult implements Callable<String> {
private int id;
public TaskWithResult(int id) {
this.id = id;
}
/**
* 任务的具体过程,一旦任务传给ExecutorService的submit方法,则该方法自动在一个线程上执行。
*
* @return
* @throws Exception
*/
public String call() throws Exception {
System.out.println("call()方法被自动调用,干活!!! " + Thread.currentThread().getName());
//一个模拟耗时的操作
for (int i = 999999; i > 0; i--) ;
return "call()方法被自动调用,任务的结果是:" + id + " " + Thread.currentThread().getName();
}
}
4 并发3定律
- Amdahl定律. 给定问题规模,可并行化部分占12%,那么即使把并行运用到极致,系统的性能最多也只能提高1/(1-0.12)=1.136倍。即:并行对提高系统性能有上限。
- Gustafson定律. Gustafson定律说Amdahl定律没有考虑随着cpu的增多而有更多的计算能力可被使用。其本质在于更改问题规模从而可以把Amdahl定律中那剩下的88%的串行处理并行化,从而可以突破性能门槛。本质上是一种空间换时间。
- Sun-Ni定律. 是前两个定律的进一步推广。其主要思想是计算的速度受限于存储而不是CPU的速度. 所以要充分利用存储空间等计算资源,尽量增大问题规模以产生更好/更精确的解.