上篇提了一点: 由于内部锁是基于线程而非调用的,当一个线程执行increase方法时,已经拿到了counter对象引用的锁,那么其他线程执行同一个对象的increase或increase2会堵塞等待counter对象锁。
这里涉及到线程的重进入机制,如果线程A进入了increase,那么它已经拿到了counter的对象锁,那么该线程也可以进入increase2,因为这里的increase和increase2是同一把锁。
重进入的原理 :JVM记录了锁所属的线程和该线程对锁的计数。比如我们在下面的increase方法中再调用同样对this加锁的increase2,那么这个线程在 increase方法执行时如果获得了锁,那么它可以进入increase2中,该锁的计数会变成2.当它推出increase2时,锁的计数值减1.当 一个锁的计数值为0时,认为该锁没有被任何线程占有。
如果一个类的成员变量在很多地方需要访问,不仅需要把访问的代码加入到synchronized块中,在任何地方,每次访问相同的变量时,需要同一个锁。
下面修改了一些方法,
package com.zyp.test.concurrent;
public class Counter {
private int count = 0;
public int increase(){
synchronized (this) {
System.out.print("[increase]"+Thread.currentThread().getName()+" count="+count+" \n");
increase2();
return count++;
}
}
public synchronized int increase2(){
System.out.print("[increase2]"+Thread.currentThread().getName()+" count="+count+" \n");
return count++;
}
public int increase3(){
System.out.print("[increase3]"+Thread.currentThread().getName()+" count="+count+" \n");
return count++;
}
public synchronized void increase4(){
System.out.print("[increase4]"+Thread.currentThread().getName()+" start");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.print("[increase4]"+Thread.currentThread().getName()+" end");
}
public int getCount() {
return count;
}
}
package com.zyp.test.concurrent.thread;
import com.zyp.test.concurrent.Counter;
public class CounterThread extends Thread{
private Counter counter;
public CounterThread(Counter counter){
this.counter = counter;
}
public void run(){
//此处测试三种increase方法
counter.increase4();
counter.increase3();
// counter.increase2();
// counter.increase3();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args){
Counter counter = new Counter();
for(int i = 0;i<20;i++){
CounterThread t = new CounterThread(counter);
t.start();
if(i%3==0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
截取一点打印的结果,对increase4加synchronized,对increase3不加时,出现了下面红色部分,线程Thread-3 拿到了counter的对象锁,但是Thread-1 在increase4执行的中途运行了,(Thread-3执行的是increase3而非increase4,increase3的方法并不需要锁,它当然可以执行)
******************************************
[increase4]Thread-1 start[increase4]Thread-1 end[increase4]Thread-3 start[increase3]Thread-1 count=1
[increase4]Thread-3 end [increase3]Thread-3 count=2
[increase4]Thread-2 start[increase4]Thread-2 end[increase3]Thread-2 count=3
***********************************************
把调用increase3的改为调用increase2(即加上synchronized)后,就没有线程能在increase4执行的中途执行了。
因为要执行increase2的线程的对象锁与increase4是相同的,当一个线程占用后,其他的线程都无法执行increase4和increase2了。
***********************************************
[increase4]Thread-0 start[increase4]Thread-0 end[increase4]Thread-3 start[increase4]Thread-3 end[increase2]Thread-3 count=0
[increase4]Thread-1 start[increase4]Thread-1 end[increase2]Thread-1 count=1
[increase4]Thread-6 start[increase4]Thread-6 end[increase2]Thread-6 count=2
************************************************
java有两种并发的控制机制,一种是synchronized代码块,一种是可见性(volatile)
synchronized代码块 ,它包含了两部分:锁对象引用及被保护的代码块。
如increase方法,它的锁对象是当前的对象,保护的代码块是synchronized{}中的内容。
如increase2方法,它是将整个方法的代码作为了被保护的代码块。并且锁对象的引用则是调用该方法的对象。
因此此处的increase方法与increase2方法的意义是相同的 ,锁住的代码是相同的,使用的锁都是当前调用方法的对象。
每个java对象都可以作为锁,这种this锁又叫内部锁(Intrinsic locks),或叫监控锁(monitor locks) 。要获取这种锁唯一的方法就是进入synchronized代码块或者synchronized申明的方法。正常或异常退出synchronized代码块或synchronized申明的方法后,锁被自动释放掉。内部锁在java中是排他锁,同一时刻最多只有一个线程可以持有该锁,其他想要获得该所的线程,要么等待,要么堵塞。
成员变量的状态跟锁没有内在联系。被当做锁的使用的对象,并不影响其他线程访问对该对象。把该对象作为锁唯一做的一件事情,就是防止其他线程也对该对象加锁。 事实上每个对象都有个java内部创建的内部锁的原因,只是为了方便开发人员不用每次去显示的创建锁对象。
我在代码中通过List lst = Collections.synchronizedList(new ArrayList())创建了一个线程安全的List,可以查看Collections类中有一个内部类SynchronizedCollection,这个SynchronizedCollection类专门创建了Object mutex,把mutex作为锁,把List的非线程安全的方法用mutex加锁重写了一遍:
public boolean equals(Object o) {
synchronized(mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized(mutex) {return list.hashCode();}
}
public E get(int index) {
synchronized(mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized(mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized(mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized(mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
synchronized(mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized(mutex) {return list.lastIndexOf(o);}
}
有一种开发习惯是将在多线程中不稳定的成员变量封装到一个对象中,再在该对象中用该对象的内部锁加锁操作,如Vector的一些方法,就直接用了内部锁。
public synchronized int capacity() {
return elementData.length;
}
public synchronized int size() {
return elementCount;
}
public synchronized boolean isEmpty() {
return elementCount == 0;
}
这种开发习惯并没有得到JDK特别的支持,只是习惯而已。甚至这种线程安全的控制模式是很容易被破坏掉的,比如继承Vectory类,并提供一个没有synchronized关键字的方法来读取上面代码中的elementCount变量。
在上述两例JDK的代码中可以看到,一个变量被一个锁保护了,那么所有对该变量的访问都需要持有这个锁,并且要保证在同一时刻只有一个线程能访问该变量。所以我上例中的increase3,在无获得锁的情况下,访问了被内部锁保护的count,造成了线程不安全的问题,是不对的,并且此处的increase3方法要执行应该要获得同其他increase方法同样的锁,才是正确的。
复合操作(Compount Action)
除了这种count++,需要加锁,对于if doSometing这种复合操作也需要加锁,比如下面虽然getCount(),increase()都加了锁,但是当他们复合操作需要原子性,则需要对这个代码块加锁
if(getCount()>0){
increase();
}
static synchronized使用的锁是调用该方法的类。
volatile 只能降低并发造成错误的几率,不能根本消除问题,对变量加volatile关键字后,线程在操作该变量时,不会将该变量复制到自己的线程的内存区中操作,而是直接操作所有线程共享的内存区域数据。相当于所有线程操作一个数据。
比如count++操作:
它有三步,如果不加volatile关键字,先读取内存的count值到线程工作区的内存,把线程的内存区的count值加1,把线程区结果值复制到内存区。
如果用synchronized锁住count++,那么可以保证多个线层执行时,只有一个线程完成了这三步后其他的才能继续count++。而使用volatile则没有了第一步和第三部,直接在内存中将count++了。这样减少了线程并发出错的概率,但是不能保证,当count是1时,线程1正在将其加一,此时线程2又来执行count++,线程2把执行完后将count设置为了2,此时线程1执行完了,再把count设置成了2,而我们希望的count应该是3。
——————————————————————————
《Java Concurrency Program》:
Every shared, mutable variable should be guarded by exactly one lock.
Make it clear to maintainers which lock that is.