Java线程锁总结

今天复习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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Ticket implements Runnable {
private int num = 100;

public Ticket() {
}

public void run() {
while(true) {
if(this.num > 0) {
try {
Thread.sleep(10L);
System.out.println(Thread.currentThread().getName() + ".....sale..." + this.num--);
} catch (InterruptedException var5) {
Thread.currentThread().interrupt();
}
}
}
}
}

这是一个模拟卖票的多线程程序,是学习多线程的一个经典问题。这个时候没有对多线程做任何安全保障,因为四个线程同时访问一个Ticket对象,所以会发生多线程的数据异常。
先来看看最后的结果:
result1

可以看到,线程0和1访问Ticket对象,出现了两次为100的情况,这显然是有问题的。

为什么会出现这种情况呢?因为线程0和线程1同时访问同一个Ticket对象,当线程0首先执行后还没有执行到num–时线程1就已经开始执行了run内代码,这时候由于num变量的值没有更新,所以线程0和线程1同时拥有一份值为100的num的拷贝,这样就导致后面的逻辑全部出问题。

这种情况就是典型的线程安全处理不当导致的问题。

那么如何解决这个问题呢?

Java中针对线程安全有以下几种解决办法:

(1)Synchronized关键字。

Java中的每个对象都内置了一个锁。当一个方法前加上Synchronized关键字后它在同一时间就只能被一个线程执行。

例如:

1
2
3
Synchronized function(){
a++;
}
1
2
3
4
5
lock();
function(){
a++;
}

unlock();

前者将会被编译器编译成类似后者的样子。

这样上锁后对象将保护这个方法,当上锁后解锁前任何对象都不能再执行这个方法。

同时,Synchronized也可以对代码块上锁。

用法如下:

1
2
3
4
Object obj = new Object();
synchronized(obj){
//需要同步的代码
}

这里的对象可以是任何对象,因为我们刚才提到了synchronized实际是使用对象的内部锁,这个其实就是需要一个锁。

(2)volatile(错误方法)

在以前我一直以为volatile是对一个变量值上锁,今天自己动手试了一下,居然不对!果然还是实践出真知。

我们先来看看结果:

我首先修改private volatile num=100;把num变量设置为volatile变量。

result2

这是结果,可以很明显的看到两个88。这是什么原因呢?我查了一些资料:

volatile Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量 的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示JVM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。 使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。 由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。 就跟C中的一样 禁止编译器进行优化.

注意:如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对longdouble的简单操作之外,volatile并不能提供原子性。 所以,就算你将一个变量修饰为volatile,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生。

volatile并不是真正的锁住了一个变量,它强制要求线程使用对象的变量时不进行拷贝,而是直接修改这个对象的变量值,但是因为修改变量值这个操作本身不是一个原子操作,所以仍然会出现问题!

实践出真知。

(3)使用Lock。

Java在早期加锁操作只有synchronized关键字,但是后来又加入了使用Lock对象方法。

Lock是一个接口,它的实现类有以下几种:

hier

多的不说了,我现在也只了解一个实现类RentrantLoock。

简单看一下用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Created by zuliangwang on 16/11/20.
*/

public class Ticket implements Runnable {

private Lock ticketLock = new ReentrantLock();
private volatile int num = 100;
@Override
public void run() {

while (true){
ticketLock.lock();
if (num>0){
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+".....sale..."+num--);
}
catch (InterruptedException e){
Thread.currentThread().interrupt();
}
finally {
ticketLock.unlock();
}

}
}
}
}

修改了Ticket中的代码,对里面的一部分代码上锁。

Lock的原理和synchronized差不多,只不过因为我们可以手动上锁解锁,因此灵活度更高,需要注意的是合理使用,避免死锁。

基本内容就到这里,更深入的等以后碰上生产环境再说。