为什么会产生线程不安全问题?
下面从一个简单地代码示例说明一下为什么会产生线程不安全问题。
package cn.xmx.ioc.threadtest;
public class TicketDemo implements Runnable{
private int tickets = 10; //记录剩余票数
private int num = 0 ; //记录买到的票数
public void run() {
while (true) {
//没有余票时跳出循环
if (tickets > 0) {
tickets--;
num++;
try {
Thread.sleep(500);//模拟网络延迟
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "抢到第"+num+"张票,剩余" + tickets+"张票。");
}
}
}
public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
Thread t1 = new Thread(ticketDemo,"张三");
Thread t2 = new Thread(ticketDemo,"李四");
Thread t3 = new Thread(ticketDemo,"王五");
Thread t4 = new Thread(ticketDemo,"康宁");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
在main方法中,创建四个线程模拟四个人开始抢票,并启动线程,运行的结果如图:
从运行结果中会发现:1.多人抢到同一张票,2.不是从第一张票开始,3.有些票号没有被抢到。这就是线程的安全问题。
总结得出:
当多个线程操作一个有多条线程代码的共享数据时,有可能会导致线程安全问题的。
注意:这只是有可能,也可能线程安全问题不会显示出来(在每个线程刚好运行完后才被别的线程抢夺到cpu资源的使用权的情况)
解决思路
什么是线程同步?
从示例代码中,我们能想到,要解决这类问题,就需要保证一个人在抢票过程未结束前,不允许其他人同时抢票。
所以从编程上来说,需要将这些操作共享数据的代码封装起来,当有线程在执行这些代码的时候,其他线程不能参与运算。
只有在当前线程执行完后,才允许其他的线程参与运算。
我们将这个方法称为线程同步。
线程同步的实现
1.同步方法
使用synchronized修饰的方法来控制对类成员变量的访问。
每一个类实例对应一把锁,方法一旦执行,就独占该锁,知道从该方法返回时才将锁释放,以后其他的线程才能获得该锁,重新进入可执行的状态。
这种机制确保了同一时刻对应一个实例,从而有效的避免了类成员变量的访问冲突。
同步方法的格式:
访问修饰符 synchronized 返回类型 方法名 (参数列表){
//省略方法体
}
其代码修改如下:
public void run() {
while (true) {
sale();
}
}
public synchronized void sale(){
if (tickets > 0) {
tickets--;
num++;
try {
Thread.sleep(500);//模拟网络延迟
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "抢到第"+num+"张票,剩余" + tickets+"张票。");
}
}
执行结果如下图:
2.同步代码块
同步代码块的使用格式:
synchronized(syncObject)
{
需要被同步的代码 ;
}
需要注意三点:
- 其中syncObject指的是锁对象,该锁对象可以是任意对象。
(同步函数使用的锁是 this。静态的同步函数使用的锁是该函数所属 字节码文件对象 ,可以用 getClass()方法获取,也可以用 当前类名.class 表示。
同步函数和同步代码块的区别:同步函数的锁是固定的this。同步代码块的锁是任意的对象。
建议使用同步代码块。) - 必须保证多线程使用的锁对象是同一个。
- 锁对象的作用:把同步代码块锁住,只能让一个线程在同步代码中执行。
//使用同步代码块的线程类
public class TicketDemo implements Runnable{
private int tickets = 10; //记录剩余票数
private int num = 0 ; //记录买到的票数
public void run() {
while (true) {
synchronized(TicketDemo.class){ //同步代码块
if (tickets > 0) {
tickets--;
num++;
try {
Thread.sleep(500);//模拟网络延迟
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "抢到第"+num+"张票,剩余" + tickets+"张票。");
}
}
}
原理:
线程1抢到的cpu资源的使用权,执行run方法,遇到了synchronized代码块,这时线程1会检测代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步方法中执行。
然后线程2抢到了cpu的使用权,执行run方法,遇到了synchronized代码块,这时线程2会检测代码块没有锁对象,这时线程2会进入到阻塞状态,会一直等待线程1还锁对象。
单例设计模式中的线程安全问题
//饿汉式
class Single
{
private static final Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s;
}
}
//懒汉式
/*
*加入同步是为了解决多线程安全问题。
*
*加入双重判断不用每次都判断是否上锁,是为了解决效率问题。
**/
class Single
{
private static Single s = null;
private Single(){}
//线程安全问题---synchronized ,double check
public static Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
s = new Single();
}
}
return s;
}
}
开发用饿汉式,没有线程安全问题。——饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。
面试懒汉式,记住如何解决线程安全问题。