synchronized关键字的使用

/ 多线程 / 没有评论 / 371浏览

在编写多线程时主要有两种实例变量的使用,一种是共享实例变量,一种是不共享实例变量。下面我们看一下在编写多线程时这两种方式的主要区别是什么。

不共享实例变量

/**
* 商品信息
*
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-13 11:51
* @since 1.0.0
*/
public class Goods extends Thread {

/**
* 商品库存
*/
private int stock = 5;

public Goods(String name) {
super(name);
}

@Override
public void run() {
while (stock > 0) {
stock--;
System.out.println(String.format("购买用户: %s 当前库存: %s", Thread.currentThread().getName(), stock));
}
}
}

测试:

/**
* 用户购买
*
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-13 11:57
* @since 1.0.0
*/
public class GookdsTest {
public static void main(String[] args) {
Goods ming = new Goods("小明");
Goods bai = new Goods("小白");
ming.start();
bai.start();
}

}

输出:

购买用户: 小明 当前库存: 4
购买用户: 小白 当前库存: 4
购买用户: 小明 当前库存: 3
购买用户: 小白 当前库存: 3
购买用户: 小明 当前库存: 2
购买用户: 小白 当前库存: 2
购买用户: 小明 当前库存: 1
购买用户: 小白 当前库存: 1
购买用户: 小明 当前库存: 0
购买用户: 小白 当前库存: 0

下面我们来分析一下上述的代码。 我们创建了两个线程对象并且开启了两个线程。抛开线程对象不说,简单点就是我们创建了两个对象,这时JVM会为我们在堆内存上创建两个独立的内存空间。所以它们两个线程彼此独立,它们相互之间访问不了。所以也就不会有所谓的线程安全问题。看输出信息这两个线程是交替输出的。这是因为这里面有一个概念就是CPU时间片,简短概述就是CPU分给不同线程所执行的时间。CPU就是这靠这种动态切换的方式来支持同时执行多个程序的。这样我们就明白了为什么会输出这样的结果了。

共享实例变量

/**
* 商品信息
*
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-13 11:51
* @since 1.0.0
*/
public class Goods extends Thread {

/**
* 商品库存
*/
private int stock = 5;

@Override
public void run() {
stock--;
System.out.println(String.format("购买用户: %s 当前库存: %s", Thread.currentThread().getName(), stock));
}
}
/**
* 用户购买
*
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-13 11:57
* @since 1.0.0
*/
public class GookdsTest {
public static void main(String[] args) {
Goods goods = new Goods();
Thread ming = new Thread(goods, "小明");
Thread bai = new Thread(goods, "小白");
ming.start();
bai.start();
}

}
购买用户: 小白 当前库存: 3
购买用户: 小明 当前库存: 3

我们继续分析上述代码。这回我们并没有创建两个线程对象而是只创建了一个线程对象但我们创建了两个线程去调用它。因为我们只实例化了一次Goods对象,所以JVM只会在堆内存中分配一份它的内存。因为我们有两个线程去操作同一个内存地址所以这也就是上述我说的共享实例变量。就是因为有多个线程去操作同一个对象也就是同一个实例变量也就会出现所谓的线程安全问题。那我们继续分析,到底是哪里有线程安全问题呢?

1.jpg

我们看一下红色标识的代码。我们知道在Java中这样的写法是简写的,完整的下法应该是:

stock = stock - 1;

那么我们用完整的写法来分析一下造成此问题的原因。

1.jpg

我们知道当线程调用start()方法时并不会立刻执行线程类的run()方法,只有当前线程获取到CPU执行权时才会执行。那么我们先看一下线程进入run()方法时所要执行的步骤都是什么。首先它要获取实例变量stock然后进行减1操作然后在赋值给stock实例变量。假如当第一个线程进入run()方法进行减1操作时,另一个线程也进入的run()方法。因为前一个线程并没有将减1操作后的值赋值给stock所以此时当前线程获取到的stock也是原先的值,于是它将进行减1操作然后赋值,如果执行顺利的话(CPU此时不切换)那么当前线程就执行完了。然后CPU开始切换到上一个线程。因为上一个线程已经完成了计算也就是减1操作。所以虽然实例变量已经被后一个线程修改了,但它不会在重新获取,而是直接执行赋值的操作。所以,就会两个线程出同一种库存的信息可能了。(备注:上述输出的结果需多次执行才有可能出现)

那我们怎么解决出现的线程安全问题呢。在Java中解决的办法有很多今天我们只介绍一个那就是用synchronized关键字。我们首先看一下代码然后在通过代码具体分析。

/**
* 商品信息
*
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-13 11:51
* @since 1.0.0
*/
public class Goods extends Thread {

/**
* 商品库存
*/
private int stock = 5;

@Override
public synchronized void run() {
stock--;
System.out.println(String.format("购买用户: %s 当前库存: %s", Thread.currentThread().getName(), stock));
}
}
/**
* 用户购买
*
* @author Sama
* @author admin@jilinwula.com
* @date 2017-03-13 11:57
* @since 1.0.0
*/
public class GookdsTest {
public static void main(String[] args) {
Goods goods = new Goods();
Thread ming = new Thread(goods, "小明");
Thread bai = new Thread(goods, "小白");
ming.start();
bai.start();
}

}
购买用户: 小白 当前库存: 4
购买用户: 小明 当前库存: 3

我们只做了一处修改就是将线程类中的run()方法添加了synchronized关键字。那为什么添加这个关键字就能解决线程的安全问题呢。synchronized关键字也叫synchronized同步锁。也就是说当有线程调用synchronized修饰的run()方法时,当前线程要获取到这个对象的同步锁,然后在执行run()方法里的具体代码。如果此时有另一个线程也要执行run()方法时,因为第一个线程已经获取到了当前对象锁,所以此时这个新线程就会阻塞。也就是说它会在这里排队等着,只有当前一个线程执行完释放锁时,这个新线程才可以执行run()方法。举个简单点的例子就是好比我们要去卫生间。首我们要先找到这个卫生间的把手,也就是说要先获取到线程对象的锁,然后我们进入卫生间,也就相当于我们进行了run()方法,然后我们用门的把手给卫生间的门给锁上。这时,如果有其它人也要在去卫生间时,因为当前卫生间有人,也就是有一个线程正在执行run()方法,这个后来的人就不无法用把手来打开这个门也就是无法获取这个线程对象的锁。所以它只能等着里面的人出来后,才可以重新用把手获取到锁。但synchronized关键字要慎用,因为在用synchronized修饰时每一个线程在执行时都先要判断锁获取锁,所以会影响多线程的性能。这篇只是简单介绍synchronized的使用,如果在不考虑性能问题时,添加synchronized关键字的确会解决多线程程序的安全问题,并且synchronized关键字可以在任意对象和方法上加锁,给开发人员更多的选择。