前言
如果一个共享资源被多个线程同时访问,可能会遭到破坏。
假设一个银行账户,有100个线程同时往这个账户里面添加一元钱。即创建一个只有一个线程的线程池,让100个线程一起访问这个线程池。
ExecutorService executor= Executors.newCachedThreadPool();
其结果是不可预测。
一个数据源被多个线程同时访问时,会遭到不可预测的破坏。
线程竞争
假设一个线程刚刚让账户余额+1,还没来得及更新账户的数据,第二个线程就马上来了,让账户余额+1,这样第一次线程的操作相当于被覆盖了。
这就是线程的竞争状态(race condition),如果没有竞争状态,那么称这个线程是安全的(thread-safe)。
package P1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AccountWithoutSync {
private static Account account=new Account();
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executor.execute(new AddAPennyTask());
}
executor.shutdown();
while (!executor.isTerminated()){
}
System.out.println("余额:" + account.getBalance());
}
private static class AddAPennyTask implements Runnable{
@Override
public void run() {
account.deposit(1);
}
}
private static class Account{
private int balance=0;
public int getBalance() {
return balance;
}
public void deposit(int amount){
int newBalance=balance+amount;
try {
Thread.sleep(5);
}catch(Exception e){
}
balance=newBalance;
}
}
}
错误效果
很明显这种结果并不是我们希望的,这是因为所有线程访问同一个数据源的时候,就会出现数据破坏的问题。
线程的竞争容易让一些操作被覆盖,得不到应有的效果,为了避免竞争状态,应该放置多个线程同时进入程序的某一特定部分。
synchronized
为了避免竞争状态,应该放置多个线程同时进入程序某一特定部分,这一部分成为临界区(critical region),最好使用synchronized关键字来同步方法
以存钱方法deposite为例:
public synchronized void deposit(double amount)
根据上述代码,我们只要在deposit方法前面增加一个synchronized关键字就可以达到预期的效果。
public synchronized void deposit(int amount)
正确效果
给线程加锁
通过synchronized关键字可知线程被强制同步,其实这里是对线程隐式的加锁。
Java也可以显式的加锁,给协调线程带来更多的控制功能
一个同步方法在执行前需要加锁,对于实例方法,要给调用该方法的对象加锁;对于静态方法,要给这个类加锁。
package P1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AccountWithoutSync {
private static Account account=new Account();
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executor.execute(new AddAPennyTask());
}
executor.shutdown();
while (!executor.isTerminated()){
}
System.out.println("余额:" + account.getBalance());
}
private static class AddAPennyTask implements Runnable{
@Override
public void run() {
account.deposit(1);
}
}
private static class Account{
private static Lock lock=new ReentrantLock();
private int balance=0;
public int getBalance() {
return balance;
}
public void deposit(int amount){
lock.lock();
try {
int newBalance=balance+amount;
Thread.sleep(5);
balance=newBalance;
}catch(Exception e){
}
finally {
lock.unlock();
}
}
}
}
效果
先创建一个锁,然后获取该锁,最后释放该锁。
如果一个线程调用一个对象上的实例方法或者静态方法,那么先给这个对象或者这个类加锁,然后再调用,最后解锁。
如果取钱方法deposit方法被同步化,那么就不会出现多个线程访问一个数据源导致数据源被破坏的情况了。