java线程安全问题以及同步的几种方式


一、线程并发同步概念

线程同步其核心就在于一个“同”。所谓“同”就是协同、协助、配合,“同步”就是协同步调昨,也就是按照预定的先后顺序进行运行,即“你先,我等, 你做完,我再做”。

线程同步,就是当线程发出一个功能调用时,在没有得到结果之前,该调用就不会返回,其他线程也不能调用该方法。

就一般而言,我们在说同步、异步的时候,特指那些需要其他组件来配合或者需要一定时间来完成的任务。在多线程编程里面,一些较为敏感的数据时不允许被多个线程同时访问的,使用线程同步技术,确保数据在任何时刻最多只有一个线程访问,保证数据的完整性。

二、多线程中可能存在安全隐患

用生活中的场景来举例:小明去银行开个银行账户,银行给他 一张银行卡和一张存折,明用银行卡和存折来搞事情:

银行卡疯狂存钱,存完一次就看一下余额;同时用存折子不停地取钱,取一次钱就看一下余额;

具体代码实现如下:

先弄一个银行账户对象,封装了存取插钱的方法:


package com.demo.thread.synchronize.no;

/**
 * 银行账户 提供存钱和取钱的方法
 * 线程不安全
 * @author 进击的菜鸟
 * @date: 2018年7月12日 上午10:23:00
 */
public class Account {
	private int count = 0;

	/**
	 * 存钱
	 * 
	 * @param money
	 */
	public void addAccount(String name, int money) {
		// 存钱
		count += money;
		System.out.println(name + "...存入:" + money + "..." + Thread.currentThread().getName());
		SelectAccount(name);
	}

	/**
	 * 取钱
	 * 
	 * @param name
	 * @param money
	 */
	public void subAccount(String name, int money) {
		// 先判断账户现在的余额是否够取钱金额
		if (count - money < 0) {
			System.out.println("账户余额不足!");
			return;
		}
		// 取钱
		count -= money;
		System.out.println(name + "...取出:" + money + "..." + Thread.currentThread().getName());
		SelectAccount(name);
	}

	/**
	 * 查询余额
	 */
	public void SelectAccount(String name) {
		System.out.println(name + "...账户余额:" + count);
	}

}

编写银行卡对象:

package com.demo.thread.synchronize.no;

/**
 * 银行卡账户 负责存钱 线程
 * 
 * @author 进击的菜鸟
 * @date: 2018年7月12日 上午10:27:36
 */
public class Card implements Runnable {
	private String name;
	private Account account = new Account();

	public Card(String name, Account account) {
		this.account = account;
		this.name = name;
	}

	@Override
	public void run() {
		while (true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			account.addAccount(name, 100);
		}
	}
}

编写存折对象(和银行卡方法几乎一模一样,就是名字不同而已):

package com.demo.thread.synchronize.no;

/**
 * 存折账号 负责取钱 线程
 * @author 进击的菜鸟
 * @date: 2018年7月12日 上午10:42:07
 */
public class Paper implements Runnable {

	private String name;
	private Account account = new Account();

	public Paper(String name, Account account) {
		this.account = account;
		this.name = name;
	}

	@Override
	public void run() {
		while (true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			account.subAccount(name, 50);
		}

	}

}

主方法测试,演示银行卡疯狂存钱,存折疯狂取钱:

package com.demo.thread.synchronize.no;

/**
 * 这样模拟 存钱取钱的线程安全问题 这样查看到数据明显是有问题的
 * @author 进击的菜鸟
 * @date: 2018年7月12日 上午10:49:34
 */
public class ACPTest {
    public static void main(String[] args) {
        
        // 开个银行帐号
        Account account = new Account();
        // 开银行帐号之后银行给张银行卡
        Card card = new Card("card",account);
        // 开银行帐号之后银行给张存折
        Paper paper = new Paper("存折",account);
        
        Thread thread1 = new Thread(card);
        Thread thread2 = new Thread(paper);
        
        thread1.start();
        thread2.start();            
    }
}

结果显示:从中可以看出 bug

java线程安全问题以及同步的几种方式_第1张图片

从上面的例子里就可以看出,银行卡存钱和存折取钱的过程中使用了 sleep() 方法,这只不过是模拟“系统卡顿”现象:银行卡存钱之后,还没来得及查余额,存折就在取钱,刚取完钱,银行卡这边“卡顿”又好了,查询一下余额,发现钱存的数量不对!当然还有“卡顿”时间比较长,存折在卡顿的过程中,把钱全取了,等银行卡这边“卡顿”好了,一查发现钱全没了的情况可能。

因此多个线程一起访问共享的数据的时候,就会可能出现数据不同步的问题,本来一个存钱的时候不允许别人打断我(当然实际中可以存在刚存就被取了,有交易记录在,无论怎么动这个帐号,都是自己的银行卡和存折在动钱。小明这个例子里,要求的是存钱和查钱是一个完整过程,不可以拆分开),但从结果来看,并没有实现小生想要出现的效果,这破坏了线程“原子性”。

三、线程同步中可能存在安全隐患的解决方法

3.1 同步代码块:

使用 synchronized() 对需要完整执行的语句进行“包裹”,synchronized(Obj obj) 构造方法里是可以传入任何类的对象,

  但是既然是监听器就传一个唯一的对象来保证“锁”的唯一性,因此一般使用共享资源的对象来作为 obj 传入 synchronized(Obj obj) 里:

  只需要锁 Account 类中的存钱取钱方法就行了:


package com.demo.thread.synchronize.yes;

/**
 * 银行账户 提供存钱和取钱的方法 线程安全
 * 
 * @author 进击的菜鸟
 * @date: 2018年7月12日 上午10:23:00
 */
public class Account {
	private int count = 0;

	/**
	 * 存钱
	 * synchronized 同步代码块
	 * @param money
	 */
	public void addAccount(String name, int money) {
		synchronized (this) {
			// 存钱
			count += money;
			System.out.println(name + "...存入:" + money + "..." + Thread.currentThread().getName());
			SelectAccount(name);
		}
	}

	/**
	 * 取钱
	 * synchronized 同步代码块
	 * @param name
	 * @param money
	 */
	public void subAccount(String name, int money) {
		synchronized (this) {

			// 先判断账户现在的余额是否够取钱金额
			if (count - money < 0) {
				System.out.println("账户余额不足!");
				return;
			}
			// 取钱
			count -= money;
			System.out.println(name + "...取出:" + money + "..." + Thread.currentThread().getName());
			SelectAccount(name);
		}
	}

	/**
	 * 查询余额
	 */
	public void SelectAccount(String name) {
		System.out.println(name + "...账户余额:" + count);
	}

}

3.2 同步方法

者在方法的申明里申明 synchronized 即可:

package com.demo.thread.synchronize.yes2;

/**
 * 银行账户 提供存钱和取钱的方法 线程安全 
 * 
 * @author 进击的菜鸟
 * @date: 2018年7月12日 上午10:23:00
 */
public class Account {
	private int count = 0;

	/**
	 * 存钱
	 * synchronized 同步方法
	 * @param money
	 */
	public synchronized void addAccount(String name, int money) {
		// 存钱
		count += money;
		System.out.println(name + "...存入:" + money + "..." + Thread.currentThread().getName());
		SelectAccount(name);
	}

	/**
	 * 取钱
	 * synchronized 同步方法
	 * @param name
	 * @param money
	 */
	public synchronized void subAccount(String name, int money) {
		// 先判断账户现在的余额是否够取钱金额
		if (count - money < 0) {
			System.out.println("账户余额不足!");
			return;
		}
		// 取钱
		count -= money;
		System.out.println(name + "...取出:" + money + "..." + Thread.currentThread().getName());
		SelectAccount(name);
	}

	/**
	 * 查询余额
	 */
	public void SelectAccount(String name) {
		System.out.println(name + "...账户余额:" + count);
	}

}

3.3 使用同步锁:

account 类创建私有的 ReetrantLock 对象,调用 lock() 方法,同步执行体执行完毕之后,需要用 unlock() 释放锁。


package com.demo.thread.lock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 银行账户 提供存钱和取钱的方法 线程安全
 * 
 * @author 进击的菜鸟
 * @date: 2018年7月12日 上午10:23:00
 */
public class Account {
	private int count = 0;

	private ReentrantLock lock = new ReentrantLock();

	/**
	 * 存钱
	 * 
	 * @param money
	 */
	public void addAccount(String name, int money) {
		try {
			lock.lock();
			// 存钱
			count += money;
			System.out.println(name + "...存入:" + money + "..." + Thread.currentThread().getName());
			SelectAccount(name);
		} finally {
			lock.unlock();
		}
	}

	/**
	 * 取钱
	 * 
	 * @param name
	 * @param money
	 */
	public void subAccount(String name, int money) {
		try {
			lock.lock();
			// 先判断账户现在的余额是否够取钱金额
			if (count - money < 0) {
				System.out.println("账户余额不足!");
				return;
			}
			// 取钱
			count -= money;
			System.out.println(name + "...取出:" + money + "..." + Thread.currentThread().getName());
			SelectAccount(name);
		} finally {
			lock.unlock();
		}
	}

	/**
	 * 查询余额
	 */
	public void SelectAccount(String name) {
		System.out.println(name + "...账户余额:" + count);
	}

}

运行效果如下

java线程安全问题以及同步的几种方式_第2张图片




你可能感兴趣的:(java基础)