A.共享对象
一个线程在它的生命周期内都只会访问它自己的局部变量,那么他是无状态的,它永远是线程安全的,这是最好的状态,代码和非并发模式下没有什么不同。但是在高并发情况下,经常用同时访问一个共享数据,比如:
1.集合的CRU操作、一些符复合操作
2.某些关键资源的初始化,检查再运行(check-then-act)
如果不能很好的控制这些共享资源,那么就会有非线程安全的风险,进入预料之外的结果!
B.同步、可见性和原子性(atomicity)、重排
原子性:的操作在一定的临界区内一起完成,不会被其他线程的影响,一旦操作开始,那么他一定可以在可能发生的“上下文切换”之前执行完毕;但是对java来说,除了要保证原子性还要保证可见性
可见性:这是java内存模型决定的,虽然A线程原子的完成了变量的修改,但是B线程不一定看得到相应的修改,这时候就需要恰当的同步!
指令重排:
考虑下面的程序:
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之前写入,并且对读线程可见!
在没有同步的情况下,编译器、处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生的内存动作,是不靠谱的!
C.内置锁
锁是保证原子性和可见性的有效工具,java内置的synchronized块就是内置锁的支持,每个java对象内部都有一个:
public void test(){
int i=12;
synchronized(this){
i++;
}
}
先将12压到栈顶
istore_1将栈顶存到局部变量区索引为1的位置
aload_0将索引0压到栈顶(一般索引为0的就是当前对象)
dup指令将当前栈顶的再拷贝一份压入栈顶,
astore_2将栈顶的引用存入索引为2的位置
monitorenter将当前栈顶的引用所指向的对象加锁
iinc 1,1将索引为1的元素+1
aload_2将索引为2的对象压入栈顶
Monitorexit将栈顶引用指向的对象释放锁
后面的指令确保在出现异常的时候锁的释放!
锁可以确保一个线程以可预见的方式看到另一个线程的修改,它不仅仅是关于同步和互斥的,也是关于内存可见的,为了保证所有线程都能看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步!
当前线程A得到锁以后,会在锁中记录下当前线程为占有者,当有其他线程调用该方法的时候,会将该线程放入锁对象的就绪(Ready)队列,当对象调用wait方法的时候,会将对应的线程放入等待(wait)队列!下面写了个简单的程序,发现会优先取等待队列中的线程:(发现notify也唤醒了所有等待的线程,why?)
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());
}
}
}
结果
going to wait queue,Thread-0
going to wait queue,Thread-1
going to wait queue,Thread-2
i am ..Thread-3
going to ready queue,Thread-4
going to ready queue,Thread-5
i am weakup,Thread-0
i am here,Thread-5
i am here,Thread-4
i am weakup,Thread-2
i am weakup,Thread-1
如果将notify换成notifyAll,会发现会先唤醒所有的等待线程,如果只是notify会唤醒一个等待线程,但是不知道为什么,到后面,其他的等待线程也都被唤醒了!
D.互斥性与可见性的保证
锁主要提供了两种特性:互斥性和可见性。互斥一次只允许一个线程持有某个特定的锁,因此可以使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据;可见性和java内存模型有关,她确保释放锁之前对共享数据作出的修改对于随后获得该锁的另一个线程是可见的。
对于一些“简单的变量”,可见性可以考虑使用volatile;一些变量自增操作的原子性,可以通过JUC的Atomic原子变量操作;
D.Volatile
Volatile相对域锁来说是更加轻量级的同步,使用volatile修饰的变量能够保证可见性,不过不能像锁一样保证原子性!
使用volatile修饰的时,不会将变量缓存也不会参与重排序,所以,读取一个volatile变量的时候,总会返回某一个线程写入的值
使用volatile的典型场景就是检查标记:
If(xxxx)
........
volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。
F.ThreadLocal
另外一种防止共享资源上产生冲突的方式就是根除对变来那个的共享,使用线程本地存储的方式。当让ThreadLocal还有另外一个好处就是可以在多个方法之间传递变量,不用使用参数的形式。
ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。
一个ThreadLocal并非只能存放一个对象,网上有人讨论说:
每个ThreadLocal当然只能放一个对象。要是需要放其他的对象,就再new 一个新的ThreadLocal出来,这个新的ThreadLocal作为key,需要放的对象作为value,放在ThreadLocalMap中。。。。
甚至还有人通过某些方式提供了一种一个ThreadLocal存放更丰富的对象比如Map,不用实例化太多thread local的方法,但是看了源码之后,我们会发现
初始化多次TheadLocal并没有多大的问题,也没有什么资源的浪费,一些人的误解可能是因为对ThreadLocalMap的误解:以为是一个ThreadLocal对应一个ThreadLocalMap,其实,是一个Thread对应一个ThreadLocalMap,所以在一个线程中创建多个ThredLocal实例的开销只有:set的时候,要将threadLocalHashCode 来计算哈希值,并将其放到线程实例唯一的ThreadLocalMap中,或者说是哈希函数计算后,放到数组中,其他的开销,都在第一次set的时候就做了,就是创建一个线程唯一的ThreadLocalMap。
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()),这些进程将永远不会得到运行的机会。