为什么需要线程同步
对于以下代码:两个线程对同一个变量分别进行100000次加一和减一操作,但是每次运行的输出基本都是不同的(注意线程的join操作保证了两个线程都运行完之后才执行System.out.println)
import org.junit.Test;public class ReviewSynchronize {@Testpublic void test() throws InterruptedException {DEC dec = new DEC();INC inc = new INC();dec.start();inc.start();dec.join();inc.join();System.out.println(Counter.count);}}
class Counter{public static int count = 0;
}class INC extends Thread{@Overridepublic void run() {for (int i = 0; i < 100000; i++){Counter.count ++;}}
}class DEC extends Thread{@Overridepublic void run() {for (int i = 0; i < 100000; i++){Counter.count --;}}
}
两个线程的加一或减一操作可以大致拆分为以下三步:
step | DEC | INC |
---|---|---|
1 | 从内存读取count | 从内存读取count |
2 | count=count-1 | count=count+1 |
3 | count的值写入内存 | count的值写入内存 |
假设执行顺序为DEC1 -> INC1 -> DEC2-> INC2 -> INC3 -> DEC3,count的初值为0。那么线程和内存中count的变化为:DEC线程中的count=0 -> INC线程中的count=0 -> DEC线程中的count=-1 -> INC线程中的count=1 -> 内存中的count=1(INC线程写入) -> 内存中的count = -1(DEC线程写入)。
可以看出:最终内存中的count值为-1,INC线程写入的值被DEC写入的值覆盖了。
主内存和工作内存
可以认为主内存是线程共享的,而工作内存是线程私有的。在上述线程操作中,DEC和INC线程都是将count从主内存读入到各自的工作内存中;各个线程对count执行操作时,实际上是在对工作内存中的count变量进行操作;结束操作后,各个线程将工作内存中的count值写入主内存中。
主内存与工作内存中的变量值不是时时刻刻都一致的,线程修改变量值之后需要写入主内存才能保证一致,因此线程之间的变量修改不是立即可见的(A线程修改var值之后需要同步到主内存,并且之后B线程读取主内存才能“看到”A线程中的var值),这就会出现INC线程已经修改了主内存中的count值,而DEC线程的工作内存中的count还是旧的值,并没有从主内存中读取新的count值。
线程同步
1.synchronized: 一个容易想到的解决上述问题的做法是,让线程独占式的访问临界资源,例如在DEC1 -> DEC2 ->DEC3的过程中不允许其他线程改变Counter对象。在 Java 中,我们可以使用synchronized关键字来实现线程同步。
import org.junit.Test;public class ReviewSynchronize {@Testpublic void test() throws InterruptedException {DEC dec = new DEC();INC inc = new INC();long start = System.currentTimeMillis();dec.start();inc.start();dec.join();inc.join();System.out.println(Counter.count);System.out.println(System.currentTimeMillis() - start);}
}class Counter{public static int count = 0;public static final Object lock = new Object();
}class INC extends Thread{@Overridepublic void run() {for (int i = 0; i < 100000; i++){synchronized (Counter.lock) {Counter.count ++;}}}
}class DEC extends Thread{@Overridepublic void run() {for (int i = 0; i < 100000; i++){synchronized (Counter.lock) {Counter.count --;}}}
}
在上述代码中,使用了一个静态的Object对象lock作为锁。在INC和DEC线程的run方法中,使用synchronized块来保证在同一时刻只有一个线程可以访问和修改Counter.count变量。这样就避免了多个线程同时访问和修改共享资源导致的数据不一致问题。
- volatile+CAS:除了使用synchronized关键字,Java 还提供了基于volatile+CAS的线程同步机制,例如ReentrantLock、Semaphore、CountDownLatch、原子类等。例如:
import org.junit.Test;
import java.util.concurrent.locks.ReentrantLock;public class ReviewSynchronize {@Testpublic void test() throws InterruptedException {DEC dec = new DEC();INC inc = new INC();long start = System.currentTimeMillis();dec.start();inc.start();dec.join();inc.join();System.out.println(Counter.count);System.out.println(System.currentTimeMillis() - start);}
}class Counter{public static int count = 0;public static final ReentrantLock lock = new ReentrantLock();
}class INC extends Thread{@Overridepublic void run() {for (int i = 0; i < 100000; i++){Counter.lock.lock();try {Counter.count ++;} finally {Counter.lock.unlock();}}}
}class DEC extends Thread{@Overridepublic void run() {for (int i = 0; i < 100000; i++){Counter.lock.lock();try {Counter.count --;} finally {Counter.lock.unlock();}}}
}
在上述代码中,使用了ReentrantLock来实现线程同步。ReentrantLock提供了比synchronized关键字更灵活的锁机制,可以实现公平锁、可中断锁等。实际执行代码的用时也比synchronized方式更短。