出现场景: 多个线程同时操作一个对象,如果该对象没有线程安全的控制,便会出现线程安全问题。例如:我们有一个类A
public class A{
int count=0;
public void add1000(){
for(int i=0;i<1000;i++){
count++;
System.out.println(count);
}
}
}
如上代码所示,A中有一个成员变量count,有一个让count加1000的方法,并打印。如果声明A一个对象,一个线程访问没问题,会从打印0~999。但当多个线程操作A的同一个对象,就可能发生一个现象:
public static void main(String[] args) {
Run run = new Run();
new Thread(new Runnable() {
@Override
public void run() {
run.add1000("A");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
run.add1000("B");
}
}).start();
}
从上面的代码可以看到,启动了两个线程,每个线程都只是想让A的count打印从0到999,但由于线程之间的运行方式是轮着运行的(对线程运行方法不了解,可自行搜索学习),会导致这两个线程都操作count,count的值也是在一直变的,就不能保证add1000这个方法的原子性。
解决办法: 对于解决办法,java特别提供了保证线程安全的类和关键字,同时java的类中也有好多是线程安全和不安全两个版本。
首先最简单的解决办法就是使用synchronized关键字
public synchronized void add1000(String s) {
for (int i = 0; i < 1000; i++) {
count++;
System.out.println(s + count);
}
}
在add1000方法前添加synchronized关键字便可保证该方法变成线程安全的,原理大概是,当某个线程调用此方法时,会获取该实例的对象锁,方法未结束之前,其他线程只能去等待。当这个方法执行完时,才会释放对象锁。其他线程才有机会去抢占这把锁,去执行该方法。同时该关键字也能同步代码块。
使用Lock类;lock是java concurrent包下的一个类,该类可以很自由的给任意代码段添加锁及释放锁。
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Run run = new Run(lock);
new Thread(new Runnable() {
@Override
public void run() {
run.add1000("A");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
run.add1000("B");
}
}).start();
}
public static class Run {
int count = 0;
Lock lock;
public Run(Lock lock){
this.lock=lock;
}
// count加1000
public synchronized void add1000(String s) {
for (int i = 0; i < 1000; i++) {
try{
lock.lock();
count++;
System.out.println(s + count);
}finally{
lock.unlock();
}
}
}
}
如上代码便是使用Lock进行加锁,如果你把finally的释放锁代码注释掉,第二个线程就无法执行该,程序一直卡在这,因为线程一一直持有该段代码的锁,线程二一直等待获取锁,便发生了死锁堵塞。