多线程, 并发编程知识总结

http://macrochen.iteye.com/blog/385608

引子 
一点体会:对并发和多线程的学习理解, 最好多作几个例子跟踪调试实战一把, 可能会理解的更深刻些. 比如像我这样 

Java内存模型 

共享数据保存在主存储器中, 计算机将数据中主存储器读到寄存器中, 然后进行操作, 一般情况下, 对寄存器的操作速度要比主存储器要快

Lock(锁机制) 
... 

synchronized 
当线程进入synchronized方法或者代码块的时候, 它必须重新将数据从主存储器中加载到当前线程的寄存器中.并对同步的对象加锁,从而阻塞其他线程的进入, 线程在离开之前再将寄存器中的数据同步到主存储器中并释放锁. 

synchronized只会对共享的内容有效, 如果不存在同时对共享内容进行操作, 则同步毫无意义.比如下面的测试代码: 

Java代码    收藏代码
  1. public class SyncTest {  
  2.     public void syncMethod1(SyncDomain domain) {  
  3.         synchronized (domain) {  
  4.             System.out.println(domain.getStr());  
  5.         }  
  6.     }  
  7.   
  8.     public static void main(String[] args) {  
  9.         final SyncTest test = new SyncTest();  
  10.         new Thread() {  
  11.             @Override  
  12.             public void run() {  
  13.                 SyncDomain domain = new SyncDomain();  
  14.                 domain.setStr("t1");  
  15.                 test.syncMethod1(domain);  
  16.             }  
  17.         }.start();  
  18.   
  19.         new Thread() {  
  20.             @Override  
  21.             public void run() {  
  22.                 SyncDomain domain = new SyncDomain();  
  23.                 domain.setStr("t2");  
  24.                 test.syncMethod1(domain);  
  25.             }  
  26.         }.start();  
  27.     }  

这里虽然使用到了synchronized块, 但是该方法不存在对公共的内容进行加锁, 而锁住的是两个不同的domain, 如果锁定的对象改成this则就回达到我们期望的结果 

synchronized修饰的方法和代码块, 一次只能有一个线程访问, 而一个类的多个方法被synchronized修饰, 则这些方法只能由一个线程访问, 但是这里有一个区别, 静态synchronized方法之间只允许一个线程访问, 非静态的synchronized方法之间只允许一个线程访问, 静态和非静态方法之间不存在互斥的情况.比如: 
Java代码    收藏代码
  1. public synchronized static void method1(){...}  
  2. public synchronized void method2(){...}  

这两个方法之间不会互为阻塞. 

在可能的情况下,一般尽量将要同步的代码最小化, 这样可以达到线程的阻塞最小化. 
对于下面两个同步方法: 
Java代码    收藏代码
  1. public synchronized void method1(){...}  
  2. public synchronized void method2(){...}  

如果线程a执行到method1()方法, 而线程b同时执行到method2()方法则会被阻塞, 直到线程a退出method1()方法为止. 

而对于下面的测试则是另外一种情况: 
Java代码    收藏代码
  1. public class SyncBlockAndNonBlockMethodTest {  
  2.     public synchronized  void syncMethod1(SyncDomain domain) {  
  3.         System.out.println(domain.getStr() + ": invoke before");  
  4.     }  
  5.     public void syncMethod2(SyncDomain domain) {  
  6.         // 如果同步块锁定的是this, 那么执行syncMethod2方法时, 访问syncMethod1方法的线程将被阻塞  
  7.         // 而如果同步块锁定的是domain, 那么两个同步方法互不影响  
  8.         synchronized(domain) {  
  9.             System.out.println(domain.getStr() + ": invoke before");  
  10.         }  
  11.     }  
  12.   
  13.     public static void main(String[] args) {  
  14.   
  15.         final SyncBlockAndNonBlockMethodTest test = new SyncBlockAndNonBlockMethodTest();  
  16.         final SyncDomain domain = new SyncDomain();  
  17.         new Thread() {  
  18.             @Override  
  19.             public void run() {  
  20.                 // 断点1  
  21.                 domain.setStr("t1");  
  22.                 test.syncMethod1(domain);  
  23.             }  
  24.         }.start();  
  25.   
  26.         new Thread() {  
  27.             @Override  
  28.             public void run() {  
  29.                 //SyncDomain domain = new SyncDomain();  
  30.                 // 断点2  
  31.                 domain.setStr("t2");  
  32.                 test.syncMethod2(domain);  
  33.             }  
  34.         }.start();  
  35.     }  
  36.   
  37. }  

这主要是因为如果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的一个例子: 
Java代码    收藏代码
  1. public class AtomicTest {  
  2.     private int x, y;  
  3.   
  4.     private enum State {  
  5.         NEW, INITIALIZING, INITIALIZED  
  6.     };  
  7.   
  8.     private final AtomicReference<State> init = new AtomicReference<State>(State.NEW);  
  9.       
  10.     public AtomicTest() {  
  11.     }  
  12.       
  13.     public AtomicTest(int x, int y) {  
  14.         initialize(x, y);  
  15.     }  
  16.   
  17.     private void initialize(int x, int y) {  
  18.         if (!init.compareAndSet(State.NEW, State.INITIALIZING)) {  
  19.             throw new IllegalStateException("initialize is error");  
  20.         }  
  21.         this.x = x;  
  22.         this.y = y;  
  23.         init.set(State.INITIALIZED);  
  24.     }  
  25.   
  26.     public int getX() {  
  27.         checkInit();  
  28.         return x;  
  29.     }  
  30.   
  31.     public int getY() {  
  32.         checkInit();  
  33.         return y;  
  34.     }  
  35.       
  36.     private void checkInit() {  
  37.         if (init.get() == State.INITIALIZED) {  
  38.             throw new IllegalStateException("uninitialized");  
  39.         }  
  40.     }  
  41.       
  42. }  

上面的例子比较容易懂, 不过貌似没什么价值, 而在实际的应用中, 我们一般采用下面的方式来使用atomic class: 
Java代码    收藏代码
  1. public class CounterTest {  
  2.     AtomicInteger counter = new AtomicInteger(0);  
  3.   
  4.     public int count() {  
  5.         int result;  
  6.         boolean flag;  
  7.         do {  
  8.             result = counter.get();  
  9.             // 断点  
  10.             // 单线程下, compareAndSet返回永远为true,  
  11.             // 多线程下, 在与result进行compare时, counter可能被其他线程set了新值, 这时需要重新再取一遍再比较,  
  12.             // 如果还是没有拿到最新的值, 则一直循环下去, 直到拿到最新的那个值  
  13.             flag = counter.compareAndSet(result, result + 1);  
  14.         } while (!flag);  
  15.   
  16.         return result;  
  17.     }  
  18.   
  19.     public static void main(String[] args) {  
  20.         final CounterTest c = new CounterTest();  
  21.         new Thread() {  
  22.             @Override  
  23.             public void run() {  
  24.                 c.count();  
  25.             }  
  26.         }.start();  
  27.   
  28.         new Thread() {  
  29.             @Override  
  30.             public void run() {  
  31.                 c.count();  
  32.             }  
  33.         }.start();  
  34.   
  35.         new Thread() {  
  36.             @Override  
  37.             public void run() {  
  38.                 c.count();  
  39.             }  
  40.         }.start();  
  41.     }  
  42. }  


类似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)的作用是创建、销毁并管理线程池,将工作线程放入线程池中;工作线程是一个可以循环执行任务的线程,在没有任务时进行等待;任务队列的作 用是提供一种缓冲机制,将没有处理的任务放在任务队列中;任务接口是每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执 行状态等,工作线程通过该接口调度任务的执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值