关于java线程(2)----访问共享数据:竞争与协作
?
A.共享对象
一个线程在它的生命周期内都只会访问它自己的局部变量,那么他是无状态的,它永远是线程安全的,这是最好的状态,代码和非并发模式下没有什么不同。但是在高并发情况下,经常用同时访问一个共享数据,比如:
?
?
1.集合的CRU操作、一些符复合操作
?
2.某些关键资源的初始化,检查再运行(check-then-act)
?
?
如果不能很好的控制这些共享资源,那么就会有非线程安全的风险,进入预料之外的结果!
?
?
?
B.同步、可见性和原子性(atomicity)、重排
?
?
考虑下面的程序:
?
public class VisableTest {private static boolean ready;private static int number;private static class ReaderThread extends Thread{public void run(){while(!ready)Thread.yield();System.out.println(number);}}public static void main(String[] args){new ReaderThread().start();number=42;ready=true;}}
?
?
VisableTest 可能永远保持循环,对于读线程来说,ready值可能永远不可见,甚至有可能会打印出0!这是因为“重排序(reordering)”,ready会在number之前写入,并且对读线程可见!
?在没有同步的情况下,编译器、处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生的内存动作,是不靠谱的!
?
?
public void test(){ int i=12; synchronized(this){ i++; }}
?
?
?
?
?
public class WaitTest {public static void main(String[] args) throws Exception{WaitTest waitTest=new WaitTest();waitTest.doo();}Public synchronized void ttttt(final ReaderThread tt,final int value){new Thread(){public void run(){try {if(value==2)System.out.println("going to ready queue,"+Thread.currentThread().getName());tt.test(value);} catch (InterruptedException e) {}}}.start();}public synchronized void doo() throws InterruptedException{final ReaderThread tt=new ReaderThread(1);tt.start();ttttt(1);ttttt(1);ttttt(0);ttttt(2);ttttt(2);}}class ReaderThread extends Thread{int i=0;public ReaderThread(int i){this.i=i;}public void run(){try {test(i);} catch (InterruptedException e) {}}public synchronized void test(int i) throws InterruptedException{if(i==0){System.out.println("i am .."+Thread.currentThread().getName());TimeUnit.SECONDS.sleep(5);this.notify();}else if(i==1){System.out.println("going to wait queue,"+Thread.currentThread().getName());this.wait();System.out.println("i am weakup,"+Thread.currentThread().getName());}else if(i==2){System.out.println("i am here,"+Thread.currentThread().getName());}}}?
?
?
?
如果将notify换成notifyAll,会发现会先唤醒所有的等待线程,如果只是notify会唤醒一个等待线程,但是不知道为什么,到后面,其他的等待线程也都被唤醒了!
?
?
锁主要提供了两种特性:互斥性和可见性。互斥一次只允许一个线程持有某个特定的锁,因此可以使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据;可见性和java内存模型有关,她确保释放锁之前对共享数据作出的修改对于随后获得该锁的另一个线程是可见的。
对于一些“简单的变量”,可见性可以考虑使用volatile;一些变量自增操作的原子性,可以通过JUC的Atomic原子变量操作;
?
?
D.Volatile
Volatile相对域锁来说是更加轻量级的同步,使用volatile修饰的变量能够保证可见性,不过不能像锁一样保证原子性!
?
使用volatile修饰的时,不会将变量缓存也不会参与重排序,所以,读取一个volatile变量的时候,总会返回某一个线程写入的值
?
使用volatile的典型场景就是检查标记:
If(xxxx)
........
?
volatile?操作不会像锁一样造成阻塞,因此,在能够安全使用?volatile?的情况下,volatile?可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile?变量通常能够减少同步的性能开销。
?
?
?
?
?
?
F.协作?wait和notify
?
当使用多个线程来同时运行多个任务的时候,可以使用锁来同步两个任务的行为,这样保证不会相互干扰。但是,有些任务可能需要线程之间协作解决,这不再是彼此之间的干涉,而是彼此之间的协调!
?
?
让这些线程协作,关键就是握手,这可以通过基础特性:互斥,可以确保只有一个任务可以响应某个信号,在互斥的基础上,还有一个途径,可以将自己挂起,直到外部条件发生变化!
这可以用Object的wait和notify方法来安全的实现,另外,JAVA5还提供了具有await和signal方法的Condition对象!
?
?
Wait可以让你等待某个条件变化,这个变化由另一个任务来改变。它和sleep有两个显著的不同:
?
1.Wait期间锁是释放的
2.可以通过notify/notifyAll或者时间到期,让wait恢复执行
?
?
?
前面已经说过,获得锁有一个等待区域,每一个同步锁lock下面都挂了几个线程队列,包括就绪(Ready)队列,等待(Waiting)队列等。当线程A因为得不到同步锁lock,从而进入的是lock.ReadyQueue(就绪队列),一旦同步锁不被占用,JVM将自动运行就绪队列中的线程而不需要任何notify()的操作。但是当线程A被wait()了,那么将进入lock.WaitingQuene(等待队列),同时如果占据的同步锁也会放弃。而此时如果同步锁不唤醒等待队列中的进程(lock.notify()),这些进程将永远不会得到运行的机会。
?
?
?
?