同步与异步
对于同步与异步在很多地方都听过,但是理解这2个概念却是不容易啊。在学习操作系统的时候,才把这2个概念理解:
同步:如果有2个进程,如果进程A的任务要在进程B完成一个任务之后才可以进程,那A就会一直等待,直到B完成了之后,A才继续做它的任务。
异步:A不会等待B,A去做其他的事,当B完成了之后,通知A,然后A在做返回做原来的任务。
举个栗子:
- 在Android里面,从网上请求数据,主线程一直等待直到线程A请求到了数据,这就是同步。而异步,主线程做其他的事,当A请求到数据了之后,通知主线程,然后主线程才操作这些数据;所以对于异步请求,我们会经常看到一个Callback,这个Callback就是用于通知主线程的。
- 对于2个进程同时操作硬盘上的数据的时候。一个进程必须等待另一个进程操作完成之后,才可以继续进行操作,这就是同步问题(所以有一个同步锁啦)。如果这里用异步操作的话就GG了。。。
对于第一个栗子的代码部分(Retrofit,因为最近在学习这个。。。):
//同步操作:
List<Contributor> contributors =
gitHubService.repoContributors("square", "retrofit");
//异步操作:
service.repoContributors("square", "retrofit", new Callback<List<Contributor>>() { //CallBack hear
@Override void success(List<Contributor> contributors, Response response) {
// ...
}
@Override void failure(RetrofitError error) {
// ...
}
});
内存模型(JMM)
Java Memory Model:The Java memory model describes how threads in
the Java programming language interact through memory. Together with
the description of single-threaded execution of code, the memory model
provides the semantics of the Java programming language.
然后我们要了解2个概念:
- 可见性:一个线程对共享变量的修改,能够及时的被其他线程看到。
- 共享变量:如果一个变量在多个线程的工作内存都存在副本,那么这个变量就是共享变量。
JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory,Thread Local Allocation Buffer(TLAB)),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
synchronized
synchronized
关键字就是上面说的同步锁,首先注意synchronized
操作是对象,而不是代码块,这点非常重要。不然后面都理解不了啦。
宏观来说:对于synchronized
所操作的对象,在某一个时间间隔里面有且只有一个线程可以对它操作,其他线程必须等那个同步锁的线程释放掉这个同步锁之后才可以继续拿到这个同步锁,然后才可以对这个对象操作。
微观来说:
JMM对synchronized
有2条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存里面。
- 线程加锁时,先把工作内存共享变量的值清空,从而使用共享变量时需要从主内存重新读取最新的值。
也就是线程执行互斥代码有下面过程:
- 获得互斥锁
- 清空工作内存
- 从主内存拷贝最新副本到工作内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
下面我们看看synchronized
是怎么实现的:
方式一:最基本的方式
/**
* Created by WQH on 2016/2/2.
* 上锁的是类实例对象 即Example aExample = new Example(); 上锁aExample
* 每一个对象都有一个lock 当一个线程访问一个同步方法时 其他线程不能访问这个对象所有的同步方法
* 2个线程访问不同的2个类实例对象 不受影响 是并发的
*/
class BasicLock {
private int x = 0;
/**
* 等同于这个,这样写可以改变锁的粒度,
* synchronized(this)
* {
* for (int i = 0; i < 10; ++i) {
* x = x + 1;
* System.out.println(name+ " " + x);
* }
* }
*/
public synchronized void execute(String name) {
for (int i = 0; i < 10; ++i) {
x = x + 1;
System.out.println(name + " " + x);
}
}
public synchronized void execute2(String name) {
for (int i = 0; i < 10; ++i) {
x = x - 1;
System.out.println(name + " " + x);
}
}
}
方式二:
/**
* Created by WQH on 2016/2/2.
* 锁定类实例
* 这种情况下,是实现代码块锁定,锁定的对象是 变量 a 或 b; (注意,a 、b 都是非static的)如果有一个 类实例对象: demo = new BlockLock(),
* 另外有两个线程: thread1,thread2,都调用了demo 对象,那么,在同一时间,如果 thread1调用BlockLock.m1(),thread2调用BlockLock.m2()是可以得;但不能同时访问 BlockLock.m1().
*/
public class BlockLock {
Object a = new Object();
Object b = new Object();
public void m1() {
synchronized (a) {
}
}
public void m2() {
synchronized (b) {
}
}
}
方式三:
/**
* Created by WQH on 2016/2/2.
* 上锁的是类对象
* 以上4个方法中实现的效果都是一样的,其锁定的对象都是类StaticLock,而不是类实例对象 ,即在多线程中,其共享的资源是属于类的,而不是属于类对象的。
* 在这种情况下,如果thread1 访问了这4个方法中的任何一个, 在同一时间内其它的线程都不能访问 这4个方法。
*/
public class StaticLock {
private static Object o = new Object();
public static synchronized void m1() {
//....
}
public static void m2() {
//...
synchronized (StaticLock.class) {
//.....
}
//.....
}
public static void m3() throws ClassNotFoundException {
synchronized (Class.forName("StaticLock")) {
}
}
public static void m4() {
synchronized (o) {
}
}
}
ReentrantLock
java.util.concurrent.lock
中的 Lock
框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。ReentrantLock
类实现了 Lock
,它拥有与 synchronized
相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
其使用代码如下:
Lock lock = new ReentrantLock();
lock.lock();
try {
// update object state
}
finally {
lock.unlock(); // 必须要在finally中释放
}
其更多内容见:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html
volatile
volatile
可以保证可见性,但是不能保证原子性。为什么呢?在微观上来说:线程写volatile
变量的过程如下:
- 改变线程工作内存中volatile
变量副本的值。
- 将改变的副本值从工作内存刷新到主内存。
线程读volatile
变量的过程如下:
- 从主内存读取volatile
变量最新值到线程的工作内存。
- 从工作内存读取变量的副本。
上述过程就会导致操作失去原子性(因为没有锁啊,也就是丢失更新的问题的出现)。
看一哈代码,就知道了:
package wqh;
/**
* Created on 2016/8/1.
*
* @author 王启航
* @version 1.0
*/
public class VolatileDemo extends Thread {
private static int number = 0;
public void increase() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
VolatileDemo.number++;
}
@Override
public void run() {
super.run();
increase();
}
public static void main(String args[]) throws InterruptedException {
Thread threads[] = new Thread[100];
for (int i = 0; i < threads.length; i++)
threads[i] = new VolatileDemo();
for (int i = 0; i < threads.length; i++)
threads[i].start();
// 在这里最好不要使用 Thread.activeCount(),因为activeCount()显示的是an estimate of the number of active threads in the current thread's thread group.
//也就是估计值
for (int i = 0; i < threads.length; i++)
threads[i].join();
System.out.println(" n= " + VolatileDemo.number);
}
}
其运行结果可能为100,可能为<100的数。造成了数据的丢失更新,所以这是很棘手的问题。
那么对于基本数据类型的原子操作,可以将变量声明为volatile
,因为其比synchronized
消耗较少的资源。
wait
与notify
对于Java来说,wait()
notify()
是Object
类里面的方法。
下面进行基本的解释:
wait()
当一个线程已经得到一个对象的同步锁的时候,但是由于某个条件不满足,这个对象就会把自己加入阻塞队列里面,然后释放同步锁,等待条件满足。
notify(),notifyAll()
:和wait()
相对,当一个线程得到某个对象的线程锁后,产生了某个资源(暂且叫做资源吧),就会通知原来wait
的对象,让它继续执行。(当然当前调用notify
的线程退出synchronized
代码块后才会切换,因为有同步锁啦)然后wait
就会在阻塞的地方继续执行。
注意:
- 2个方法都必须得到同步锁之后才可以执行,从语意来说
wait
notify
要在synchronized
的{}
里面啦 - 这2 个方法在
Object
里面。所以每个对象都会有个同步锁,这从逻辑上也是合理的。
生产者与消费者问题
经典问题:有一个容器,生产者生产东西加进去,消费者取出东西消费掉。当容器为0时,消费者就会等待生产者生产;不为0时,生产者就会等待消费者消费。
/**
* Created on 2016/4/22.
*
* @author 王启航
* @version 1.0
*/
public class Consumer implements Runnable {
Container mContainer;
public Consumer(Container mContainer) {
this.mContainer = mContainer;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
// Waiting for producer products
synchronized (this) {
while (mContainer.list.size() == 0) {
wait();
}
}
// Notify the producer to products
synchronized (mContainer.producer) {
mContainer.list.clear();
System.out.println("Consumer");
mContainer.producer.notifyAll();
}
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* Created on 2016/4/22.
*
* @author 王启航
* @version 1.0
*/
public class Producer implements Runnable {
Container mContainer;
public Producer(Container mContainer) {
this.mContainer = mContainer;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
// Waiting for Consumer to consume
synchronized (this) {
while (mContainer.list.size() != 0) {
wait();
}
}
// Notify the consumer to consume
synchronized (mContainer.consumer) {
mContainer.list.add("WQH");
System.out.println("Producer");
mContainer.consumer.notifyAll();
}
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created on 2016/4/22.
*
* @author 王启航
* @version 1.0
*/
public class Container {
List<String> list = new ArrayList<>();
final Consumer consumer = new Consumer(this);
final Producer producer = new Producer(this);
public Container() {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(consumer);
executorService.execute(producer);
}
public static void main(String args[]) throws InterruptedException {
new Container();
}
}
由于代码都有一些说明,这里注意几个问题:
Thread.interrupted()
判断线程是否阻塞,这里没有阻塞,所以会一直运行下去。- 有2中实现方法,另一种把
wait``notify
放在Container
里面,不过这样不是很合理,因为容器类具有单一的职责,只负责单纯的拿和放操作,不需要其他的功能。 - 每个
run
都有2个同步锁的地方,注意2个同步的对象是不同的了。
下一篇将会对消费者-生产者进行优化。