JAVA 线程安全
线程同步
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。在恢复执行之后,面临共享变量被其他线程所改变的情况,将会导致数据不一致。因此多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待。程序上称为同步。
public class Counter {
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public void add() {
this.num++;
}
}
public class Async {
public static void main(String[] args) {
Counter counter = new Counter();
counter.setNum(0);
Thread[] threds = new Thread[2];
for (int i = 0; i < 2; i++) {
threds[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 2000; j++) {
counter.add();
}
}
});
}
threds[0].start();
threds[1].start();
try {
threds[0].join();
threds[1].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num:" + counter.getNum());
}
}
执行结果:
num:2938 #随机出现数值,并非想要的结果4000
Counter 中的 num 为共享变量,被两个线程同时操作,便出现线程安全问题。对于共享变量不安全问题可以采用同步解决。最常用的方式是对共享变量操作指令加锁,保证一组指令以原子方式执行。即在同一时间点,只能有一个线程拿到锁,操作共享变量,然后释放锁,其他线程再争抢锁。
synchronized
在 Java 语言中,每一个对象都会内置一把锁(监视器锁),这是一种互斥锁,每一时间片只能有一个线程获取该锁,其他线程进入等待,直到原先线程释放锁。 线程可以使用 synchronized 关键字来获取对象上的锁。synchronized 关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。
synchronized 方法
当用 synchronized 修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
public class Counter {
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public synchronized void add() {
this.num++;
}
}
public class Sync {
public static void main(String[] args) {
Counter counter = new Counter();
counter.setNum(0);
Thread[] threds = new Thread[2];
for (int i = 0; i < 2; i++) {
threds[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 2000; j++) {
counter.add();
}
}
});
}
threds[0].start();
threds[1].start();
try {
threds[0].join();
threds[1].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num:" + counter.getNum());
}
}
执行结果:
num:4000
把 Counter 计数程序中 add 方法添加 synchronized 关健字,便完成共有变量 num 的读写同步。synchronized 修饰普通类方法,那么锁住的是 this,即谁调用该方法便锁住谁。以上例子锁住的是 Counter 实例。 需要注意的是:synchronized 锁住的必须是同一个对象。
synchronized 代码块
被 synchronized 关键字修饰的语句块会自动被加上内置锁,从而实现同步。
public class Counter {
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public void add() {
synchronized (this){
this.num++;
}
}
}
public class Sync {
public static void main(String[] args) {
Counter counter = new Counter();
counter.setNum(0);
Thread[] threds = new Thread[2];
for (int i = 0; i < 2; i++) {
threds[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 2000; j++) {
counter.add();
}
}
});
}
threds[0].start();
threds[1].start();
try {
threds[0].join();
threds[1].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num:" + counter.getNum());
}
}
执行结果:
num:4000
在代码段 num++ 中添加 synchronized 修饰,便完成共有变量 num 的读写同步。synchronized 修饰代码块,那么锁住的是 synchronized 修饰的对象,即在括号中的值。
synchronized 静态方法
public class Counter {
private static int num;
public static int getNum() {
return num;
}
public synchronized static void add() {
num++;
}
}
public class Async {
public static void main(String[] args) {
Thread[] threds = new Thread[2];
for (int i = 0; i < 2; i++) {
threds[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 2000; j++) {
Counter.add();
}
}
});
}
threds[0].start();
threds[1].start();
try {
threds[0].join();
threds[1].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num:" + Counter.getNum());
}
}
执行结果:
num:4000
当 synchronized 修饰 static 方法,它锁住的是该类的 Class 对象,而不是某一个具体对象,在程序执行过程中,类的Class对象只有一份,同样可以达到同步效果。
synchronized 何时释放锁
synchronized 内部机制并不需要使用者关心何时获取锁何时释放锁,但是释放锁还是值得一提,总共有三种情况:
- 占有锁的线程执行完了该代码块,然后释放对锁的占有;
- 占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
- 占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。
明白释放锁,那么便可以知道 synchronized 的使用局限性:
- 占有锁的线程如遇IO读写阻塞,便无法释放锁 ,其他线程都在等待;
- 不能自定义某些线程需要拿锁操作共享变量,某些线程不需要锁操作变量;如某共享变量写操作需要同步,读不需要同步;
- 使用者不能感知是否获取锁,是否释放锁。
ReentrantLock
在 JavaSE5.0 中新增了一个 java.util.concurrent 包来支持同步。ReentrantLock 类是可重入、互斥、实现了Lock接口的锁, 它与使用 synchronized 方法和快具有相同的基本行为和语义,并且扩展了其能力。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ATM {
private int money;
Lock lock = new ReentrantLock();
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public void add() {
try {
lock.lock();
this.money ++;
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class TestThread {
static ATM atm = new ATM();
public static void main(String[] args) throws InterruptedException {
atm.setMoney(0);
Thread thread1 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 2000000; i++) {
atm.add();
}
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 2000000; i++) {
atm.add();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("atm现在的金额是:" + atm.getMoney());
}
}
执行结果:
atm现在的金额是:4000000