1.三个概念
1.1.原子性
原子性是指几个操作要么全部执行,要么全部不执行。比如在支付问题中,往A给B转1000元,那么A账户减去1000和B账户加上1000就是一个原子操作,如果A减去1000成功了,但是B加上1000失败了,那么应当回滚。
1.2.可见性
可见性是指当前线程A对于共享变量所做的更改对于线程B是可见的。
1.3.有序性
在正常的单线程中,对于下面一段代码的执行:
int i = 10 //语句1
int j = 20 //语句2
i = i + 10 //语句3
j = j*j //语句4
在cpu中指令的执行是会重排序的,比如上面的一段代码可能的一段执行的顺序就是:
语句2-->语句1-->语句3-->语句4
但是不可能是:语句2-->语句3-->语句1-->语句4
因为语句3的执行依赖于语句1。
但是如果是在多线程中就不一定了,比如下面这个例子:
//线程1的代码
p = new Person() //语句1
b = true //语句2
//线程2的代码
while(b){ //语句3
p do something
}
由于语句1 和 2没有依赖关系,可能被倒置,那么多线程中可能出现的执行顺序是:
语句2 --> 语句3 --> 语句1
那么在语句3中使用变量p做一些事情,就会报空指针了,因为p的实例化在后面。
1.4.总结
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
2.validate和synchronized以及lock和以上三个性质的关系
2.1.如何保证原子性
synchronized和lock关键字保证了同一时刻只有一个线程能够访问被锁起来的代码块。故该代码块内部可以保证原子性。
那么validate可以保证原子性吗?
看下面着一段代码:
public class ValidateTest {
private volatile int inc = 0;
private void increase(String threadName) {
System.out.println("当前线程为:" + threadName+"当前inc为:"+inc);
inc++;
}
public static void main(String[] args) {
final ValidateTest test = new ValidateTest();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase(this.getName());
}
}.start();
}
while(Thread.activeCount()>2) { //保证前面的线程都执行完
System.out.println("当前线程数:" + Thread.activeCount());
Thread.yield();
}
System.out.println("最终结果为:" + test.inc);
}
}
按照逻辑,这段代码的执行结果为10000,但是由于increase自增操作并不是一个原子操作,所以执行的结果有时候是小于10000的。可见validate关键字是没有原子性的
2.2.如何保证可见性
validate可以保证变量的可见性,使用validate修饰的共享变量,当被修改时,会立即更新到计算机主存中,其他的线程读取时会去内存中读取(关于主存和内存如何存取值,可以参考这篇文章),synchronized和lock也可以保证可见性,同一时刻只有一个线程可以访问同步代码块,并且在锁被释放的时候会立即将共享变量修改的值刷新过去。
2.3.如何保证有序性
synchronized和lock显然能够保证有序性,validate可以一定程度的保证有序性。为什么说是一定程度呢?首先validate可以禁止指令重排序。那么是如何进行的呢,看下面一个例子:
int x; //语句1
int y; //语句2
validate boolean b = false; //语句3
int z = 1; //语句4
y = 2; //语句5
以前5个语句,由于validate修饰了b,所以语句1、2只能在3前面,4、5只能在3后面,但是1、2和4、5语句执行顺序不定。所以是一定程度上保证时序性。
那么对于之前的例子:
//线程1的代码
p = new Person() //语句1
b = true //语句2
//线程2的代码
while(b){ //语句3
p do something
}
如果给p加上一个validate修饰,就不会出现问题了。
3.目前中常用的一些锁使用及优缺点
3.1 synchronizd
原理:Java中的每一个对象都可以作为一个锁
synchronized 获取锁常见有两种,一种是实例化锁也就是对象锁,一种是字节码锁,可以参考下面这段代码:
public class SynTest {
private static List list = new ArrayList();
//当前实例的锁
public synchronized void add1(String s){
list.add(s);
}
//SynTest.class 锁
public static synchronized void add2(String s){
list.add(s);
}
//SynTest.class 锁
public void add3(String s){
synchronized(SynTest.class){
list.add(s);
}
}
//当前实例的锁
public void add4(String s){
synchronized(this){
list.add(s);
}
}
}
上面的方法中,1、4是同一把锁,2、3是同一把锁,只有同一把锁才可以实现锁互斥。所谓互斥就是同时只有一个线程可以访问1和4。
3.2. lock
为什么会出现lock呢,想象一下这种情况,如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
那么lock如何使用呢?
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
和synchronized相比就是需要自己手动去释放锁,稍微麻烦一些。
END
参考
【Java线程】锁机制:synchronized、Lock、Condition
Java并发编程:volatile关键字解析
深入理解synchronized
Java并发编程:Lock