今天复习Java的多线程基本知识,总结一下线程安全方面的内容。
首先来看一段程序:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* Created by zuliangwang on 16/11/20.
*/
public class TicketDemo {
public static void main(String args[]){
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
Thread thread4 = new Thread(ticket);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
1 | public class Ticket implements Runnable { |
这是一个模拟卖票的多线程程序,是学习多线程的一个经典问题。这个时候没有对多线程做任何安全保障,因为四个线程同时访问一个Ticket对象,所以会发生多线程的数据异常。
先来看看最后的结果:
可以看到,线程0和1访问Ticket对象,出现了两次为100的情况,这显然是有问题的。
为什么会出现这种情况呢?因为线程0和线程1同时访问同一个Ticket对象,当线程0首先执行后还没有执行到num–时线程1就已经开始执行了run内代码,这时候由于num变量的值没有更新,所以线程0和线程1同时拥有一份值为100的num的拷贝,这样就导致后面的逻辑全部出问题。
这种情况就是典型的线程安全处理不当导致的问题。
那么如何解决这个问题呢?
Java中针对线程安全有以下几种解决办法:
(1)Synchronized关键字。
Java中的每个对象都内置了一个锁。当一个方法前加上Synchronized关键字后它在同一时间就只能被一个线程执行。
例如:
1 | Synchronized function(){ |
1 | lock(); |
前者将会被编译器编译成类似后者的样子。
这样上锁后对象将保护这个方法,当上锁后解锁前任何对象都不能再执行这个方法。
同时,Synchronized也可以对代码块上锁。
用法如下:
1 | Object obj = new Object(); |
这里的对象可以是任何对象,因为我们刚才提到了synchronized实际是使用对象的内部锁,这个其实就是需要一个锁。
(2)volatile(错误方法)
在以前我一直以为volatile是对一个变量值上锁,今天自己动手试了一下,居然不对!果然还是实践出真知。
我们先来看看结果:
我首先修改private volatile num=100;
把num变量设置为volatile变量。
这是结果,可以很明显的看到两个88。这是什么原因呢?我查了一些资料:
volatile
Java
语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量 的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而volatile
关键字就是提示JVM
:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。 使用建议:在两个或者更多的线程访问的成员变量上使用volatile
。当要访问的变量已在synchronized
代码块中,或者为常量时,不必使用。 由于使用volatile
屏蔽掉了JVM
中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。 就跟C
中的一样 禁止编译器进行优化.注意:如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对
long
和double
的简单操作之外,volatile
并不能提供原子性。 所以,就算你将一个变量修饰为volatile
,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生。
volatile并不是真正的锁住了一个变量,它强制要求线程使用对象的变量时不进行拷贝,而是直接修改这个对象的变量值,但是因为修改变量值这个操作本身不是一个原子操作,所以仍然会出现问题!
实践出真知。
(3)使用Lock。
Java在早期加锁操作只有synchronized关键字,但是后来又加入了使用Lock对象方法。
Lock是一个接口,它的实现类有以下几种:
多的不说了,我现在也只了解一个实现类RentrantLoock。
简单看一下用法:
1 | /** |
修改了Ticket中的代码,对里面的一部分代码上锁。
Lock的原理和synchronized差不多,只不过因为我们可以手动上锁解锁,因此灵活度更高,需要注意的是合理使用,避免死锁。
基本内容就到这里,更深入的等以后碰上生产环境再说。