我们知道,在多线程访问一个共享变量的时候会发生安全问题。
首先看下面例子:
public class Counter {
private int count;
public void add(){
try{
for (int i = 0;i<200;i++){
Thread.sleep(100);
this.count++;
System.out.println(this.count);
}
}catch (Exception e ){
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) {
final Counter counter = new Counter();
new Thread(new Runnable() {
public void run() {
counter.add();
}
}).start();
new Thread(new Runnable() {
public void run() {
counter.add();
}
}).start();
new Thread(new Runnable() {
public void run() {
counter.add();
}
}).start();
}
}
运行结果如下:
如果没发生线程对数据的赃操作,3个线程执行,结果会累加到600的,看上面结果明显没有,并且出现一下重复的数据。这是因为存在3个线程同时操作同一个数字,导致输出重复数字。
解决办法:
1.在方法上加上synchronized关键字。如下图:
虽然结果不会出现重复,但是synchronized效率及其低下,运行速度明显被拖慢。原因是,for循环中的i变量是每个线程都有独自的局部变量,各不影响,但是每个线程都要排队进入这个方法,排队睡觉,这样就导致效率低下
2.在竞争条件上加synchronized,我们知道各个线程其实竞争的是count这个成员变量。因此在此地方加即可。如下图:
这样运行效率比方法一快了很多,因为省去了排队进入方法,排队睡觉。只需要排队取count值即可,这样效率比方法一快。
注意上图中,输出语句并不是竞争条件,并不一定要放在synchroized里面,这里放在里面是为了让线程取到值自增后立即输出,这样输出就不会发生混论,不发生抢占输出问题,一样能累加到600,
如果把输出放在synchronized外面会出现值有重复现象,因为累加后的值并没有立即输出,这样导致输出混乱,但仍然能加到600.知识输出不安全罢了。
3.使用原子类型,比如将上面的代码的int count类型改成AtomicInteger count类型,我们知道获取count的值然后再自加个1是可能会出现问题的,也就是结果出现重复数字。AtomicInteger类型是以同步的方法解决这个问题的。如下图:
结果如下图:
可以看到数字的输出没有严格的排队,但是数据确实给你保证的了,就是完整的加到600.这也恰恰因为不是严格的进行排队,才是的这种方法比前面两种方法的效率大大改进。
理论总结:synchronized是一种内部锁,就是所对象内部给我们提供的,因为每一个对象有一个状态变量,相当于一个锁,进入同步块,改变这个变量。别的线程进入之后就要判断这个变量有没有改变。
一个线程获取它本生已经持有的锁,这是可以成功的。我们知道多个线程同时抢占同一个锁它们是失败的。因为它们之间是互斥的。但是呢,一个线程再次获取一个自己已经拿过的锁是可以成功的,那么它是能够成功的。
看如下例子:
public class Widget {
public synchronized void doSth(){
}
}
public class ChildWidget extends Widget {
@Override
public synchronized void doSth() {
super.doSth();
}
}
public class Test {
public static void main(String[] args) {
Widget w = new ChildWidget();
w.doSth();
}
}
子类调用自己的方法的那个synchroized那个锁是w对象的这个锁,而在子类方法中的super.doSth()父类中的方法的synchronized的锁也是w对象的锁。因此不要被子类继承父类的方法中的锁所迷惑了。
因此这种方式叫做内部锁的可重入机制,也叫可重入锁。