http://macrochen.iteye.com/blog/385608
引子
一点体会:对并发和多线程的学习理解, 最好多作几个例子跟踪调试实战一把, 可能会理解的更深刻些. 比如像我这样
Java内存模型
共享数据保存在主存储器中, 计算机将数据中主存储器读到寄存器中, 然后进行操作, 一般情况下, 对寄存器的操作速度要比主存储器要快.
Lock(锁机制)
...
synchronized
当线程进入synchronized方法或者代码块的时候, 它必须重新将数据从主存储器中加载到当前线程的寄存器中.并对同步的对象加锁,从而阻塞其他线程的进入, 线程在离开之前再将寄存器中的数据同步到主存储器中并释放锁.
synchronized只会对共享的内容有效, 如果不存在同时对共享内容进行操作, 则同步毫无意义.比如下面的测试代码:
- public class SyncTest {
- public void syncMethod1(SyncDomain domain) {
- synchronized (domain) {
- System.out.println(domain.getStr());
- }
- }
- public static void main(String[] args) {
- final SyncTest test = new SyncTest();
- new Thread() {
- @Override
- public void run() {
- SyncDomain domain = new SyncDomain();
- domain.setStr("t1");
- test.syncMethod1(domain);
- }
- }.start();
- new Thread() {
- @Override
- public void run() {
- SyncDomain domain = new SyncDomain();
- domain.setStr("t2");
- test.syncMethod1(domain);
- }
- }.start();
- }
这里虽然使用到了synchronized块, 但是该方法不存在对公共的内容进行加锁, 而锁住的是两个不同的domain, 如果锁定的对象改成this则就回达到我们期望的结果
synchronized修饰的方法和代码块, 一次只能有一个线程访问, 而一个类的多个方法被synchronized修饰, 则这些方法只能由一个线程访问, 但是这里有一个区别, 静态synchronized方法之间只允许一个线程访问, 非静态的synchronized方法之间只允许一个线程访问, 静态和非静态方法之间不存在互斥的情况.比如:
- public synchronized static void method1(){...}
- public synchronized void method2(){...}
这两个方法之间不会互为阻塞.
在可能的情况下,一般尽量将要同步的代码最小化, 这样可以达到线程的阻塞最小化.
对于下面两个同步方法:
- public synchronized void method1(){...}
- public synchronized void method2(){...}
如果线程a执行到method1()方法, 而线程b同时执行到method2()方法则会被阻塞, 直到线程a退出method1()方法为止.
而对于下面的测试则是另外一种情况:
- public class SyncBlockAndNonBlockMethodTest {
- public synchronized void syncMethod1(SyncDomain domain) {
- System.out.println(domain.getStr() + ": invoke before");
- }
- public void syncMethod2(SyncDomain domain) {
- // 如果同步块锁定的是this, 那么执行syncMethod2方法时, 访问syncMethod1方法的线程将被阻塞
- // 而如果同步块锁定的是domain, 那么两个同步方法互不影响
- synchronized(domain) {
- System.out.println(domain.getStr() + ": invoke before");
- }
- }
- public static void main(String[] args) {
- final SyncBlockAndNonBlockMethodTest test = new SyncBlockAndNonBlockMethodTest();
- final SyncDomain domain = new SyncDomain();
- new Thread() {
- @Override
- public void run() {
- // 断点1
- domain.setStr("t1");
- test.syncMethod1(domain);
- }
- }.start();
- new Thread() {
- @Override
- public void run() {
- //SyncDomain domain = new SyncDomain();
- // 断点2
- domain.setStr("t2");
- test.syncMethod2(domain);
- }
- }.start();
- }
- }
这主要是因为如果synchronized块中是domain的话, 那么synchronized方法锁住的是this对象, synchronized锁住的是domain对象, 因此两个synchronized互不影响.
volatile
volatile是一种轻量级的synchronized, synchronized是对代码块和方法加锁进行同步, 而volatile修饰的共享变量能保证在多线程状态下做到同步,通常情况下多线程在操作共享变量的时候, 都会在工作内存中将主存中的共享变量复制一份, 因此在一个线程中修改了共享变量, 可能还没有更新到主存中, 而另一个线程又访问了主存中的共享变量, 这样将导致另一个线程使用过期的数据, 使用volatile修饰的共享变量则能保证线程操作的都是主存中的共享变量, 工作变量不会在工作内存拷贝一份再处理, 这样就保证了多个线程访问到的共享变量时刻都是最新的, 因为都在主存中操作volatile共享变量, 因此保证对共享变量的修改是"原子"级的操作(不存在工作内存和主存之间的同步更新), 因此实现了线程安全. 比如说有一个共享变量i, 然后在一个方法中有这样的操作i++, 它实际上是一个包含了三个操作的复合操作:读, 改, 写. 如果不对i不加volatile, i又在另外一个线程中被修改, 那么可能出现i++操作读的时候是另外一个线程未更新前的值, 改的同时另一个线程的工作内存中的变量与主存在做同步更新, 最后写的时候就会将另外一个线程的修改覆盖掉. 从上面的例子中可以看出, 这种情况需要较高的压力与并发情况下, 才会出现. 同时这个例子即使使用volatile也无法完全保证线程安全, 必须将i++操作使用synchronized包装成一个原子操作, 或者使用jdk1.5的atomic原子包中的类实现原子操作.
volatile 变量仅能被安全地用在单一的载入或存储操作。这个限制导致volatile变量的使用是不常见的。
理论上每一个线程都有自己的寄存器来存放操作数据. 而使用volatile, 则可以保存每个线程操作的数据保存在主存储器中, 达到多个线程之间能够共享
原子变量(AtomicLong, AtomicInteger, AtomicReference)
J2SE 5.0提供了一组atomic class来帮助我们简化同步处理。基本工作原理是使用了同步synchronized的方法实现了对一个long, integer, 对象的增、减、赋值(更新)操作. 比如对于++运算符AtomicInteger可以将它持有的integer 能够atomic 地递增。在需要访问两个或两个以上 atomic变量的程序代码(或者是对单一的atomic变量执行两个或两个以上的操作)通常都需要被synchronize以便两者的操作能够被当作是一个atomic的单元。
对array atomic变量来说,一次只有一个索引变量可以变动,并没有功能可以对整个array做atomic化的变动。
关于Atomic的几个方法
getAndSet() : 设置新值,返回旧值.
compareAndSet(expectedValue, newValue) : 如果当前值(current value)等于期待的值(expectedValue), 则原子地更新指定值为新值(newValue), 如果更新成功,返回true, 否则返回false, 换句话可以这样说: 将原子变量设置为新的值, 但是如果从我上次看到的这个变量之后到现在被其他线程修改了(和我期望看到的值不符), 那么更新失败
从effective java (2)中拿来的一个关于AtomicReference的一个例子:
- public class AtomicTest {
- private int x, y;
- private enum State {
- NEW, INITIALIZING, INITIALIZED
- };
- private final AtomicReference<State> init = new AtomicReference<State>(State.NEW);
- public AtomicTest() {
- }
- public AtomicTest(int x, int y) {
- initialize(x, y);
- }
- private void initialize(int x, int y) {
- if (!init.compareAndSet(State.NEW, State.INITIALIZING)) {
- throw new IllegalStateException("initialize is error");
- }
- this.x = x;
- this.y = y;
- init.set(State.INITIALIZED);
- }
- public int getX() {
- checkInit();
- return x;
- }
- public int getY() {
- checkInit();
- return y;
- }
- private void checkInit() {
- if (init.get() == State.INITIALIZED) {
- throw new IllegalStateException("uninitialized");
- }
- }
- }
上面的例子比较容易懂, 不过貌似没什么价值, 而在实际的应用中, 我们一般采用下面的方式来使用atomic class:
- public class CounterTest {
- AtomicInteger counter = new AtomicInteger(0);
- public int count() {
- int result;
- boolean flag;
- do {
- result = counter.get();
- // 断点
- // 单线程下, compareAndSet返回永远为true,
- // 多线程下, 在与result进行compare时, counter可能被其他线程set了新值, 这时需要重新再取一遍再比较,
- // 如果还是没有拿到最新的值, 则一直循环下去, 直到拿到最新的那个值
- flag = counter.compareAndSet(result, result + 1);
- } while (!flag);
- return result;
- }
- public static void main(String[] args) {
- final CounterTest c = new CounterTest();
- new Thread() {
- @Override
- public void run() {
- c.count();
- }
- }.start();
- new Thread() {
- @Override
- public void run() {
- c.count();
- }
- }.start();
- new Thread() {
- @Override
- public void run() {
- c.count();
- }
- }.start();
- }
- }
类似i++这样的"读-改-写"复合操作(在一个操作序列中, 后一个操作依赖前一次操作的结果), 在多线程并发处理的时候会出现问题, 因为可能一个线程修改了变量, 而另一个线程没有察觉到这样变化, 当使用原子变量之后, 则将一系列的复合操作合并为一个原子操作,从而避免这种问题, i++=>i.incrementAndGet()
原子变量只能保证对一个变量的操作是原子的, 如果有多个原子变量之间存在依赖的复合操作, 也不可能是安全的, 另外一种情况是要将更多的复合操作作为一个原子操作, 则需要使用synchronized将要作为原子操作的语句包围起来. 因为涉及到可变的共享变量(类实例成员变量)才会涉及到同步, 否则不必使用synchronized
wait, notify, notifyAll, sleep, yield
wait暂停被当前同步锁当前线程的执行, 同时线程会释放其同步锁, 使用同一锁的线程将有机会通过notify或notifyAll被唤醒
notify唤醒当前暂停的线程进入执行队列等待执行
notifyAll会唤醒多个线程进入执行队列, 这些线程根据最终的竞争结果被继续执行
sleep暂停当前线程的执行, 但是不释放同步锁, 这样使用同一锁的线程将被阻塞
yield会将当前线程暂时让位一小段时间,让其它的线程有机会运行,过了这段时间后,该线程继承运行。上述功能也可以用Thread.sleep()方法实现。简单的说就是在线程执行过程中sleep(0)一下, 让其他等待的线程继续运行
线程安全
编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的、可变的状态。这里的状态就是对象的变量(静态变量和实例变量)
线程安全的前提是该变量是否被多个线程访问, 保证对象的线程安全性需要使用同步来协调对其可变状态的访问;若是做不到这一点,就会导致脏数据和其他不可预期的后果。无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。Java中首要的同步机制是synchronized关键字,它提供了独占锁。除此之外,术语“同步”还包括volatile变量,显示锁和原子变量的使用。
在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患。有3种方法修复它:
l 不要跨线程共享变量;
l 使状态变量为不可变的;或者
l 在任何访问状态变量的时候使用同步。
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
如果共享变量在多个方法中访问到, 当在多线程环境下, 仅仅对其中一个方法使用synchronized是不够的, 为了保证线程安全, 必须对多个方法都使用synchronized, 具体做法可以参看vector, vector相对于list来说, 是线程安全的, 它的所有公共的方法都是synchronized, 这也是synchronized的另一个作用, 保证所有方法使用的共享变量不是过期数据.
util.concurrent
map的数据结构, map内部是一个存放单向链接的Entry元素的一维数组(map称之为bucket), 首先根据每一个元素的key的hashcode按照一维数组的length取模, 得到的结果就是该元素存放在这个数组的位置, 存放在这个数组的元素可能有多个, 这些元素将通过单向链表的方式进行关联
HashTable是线程安全的, 因此它的所有方法都是synchronized的, 而HashMap是非线程安全的. 但是传统的HashTable在高性能的环境下性能很差, 因为每次只能有一个线程对HashTable进行读写, 而针对大量读, 少量写的应用场景来说, 只能一个线程能读将严重影响性能, 为了解决这个问题, 将原来对整个Map锁定改成对Map中的一个或几个bucket锁定(即减小锁的粒度)来实现能有多个线程能同时进行读,写操作. 对于大量的读操作(get), 少量的写操作(set), concurrent采用了copy-on-write策略, 即对于写操作, 将后台的数组复制一份, 然后对副本进行写操作, 完成之后, 替换原来的数组, 这样可以不影响读操作. 其实现有CopyOnWriteArrayList和CopyOnWriteArraySet
线程池是为发挥多线程的优点(并发),避免多线程的缺点(创建和销毁的时空开销)而出现的. 一个比较简单的线程池至少应包含线程池管理器、工作线程、任务队列、任务接口等部分。其中线程池管理器(ThreadPool Manager)的作用是创建、销毁并管理线程池,将工作线程放入线程池中;工作线程是一个可以循环执行任务的线程,在没有任务时进行等待;任务队列的作 用是提供一种缓冲机制,将没有处理的任务放在任务队列中;任务接口是每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执 行状态等,工作线程通过该接口调度任务的执行。