一、介绍
1、线程安全:
指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存、cpu是不是够用即可。
2、线程不安全:
反过来,线程不安全就意味着线程的调度顺序会影响最终结果。
举例:
public class GetMoney implements Runnable {
private int money= 100;//你的账户余额
private int smoney = 0;//一共取钱的总额
public void run() {
while(money>0){
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
smoney += 10;
money -= 10;
System.out.println(Thread.currentThread().getName()+"取了一次10元,一共取了" + smoney+"元");
}
}
public static void main(String[] args) {
GetMoney gm = new GetMoney ();
new Thread(gm).start();
new Thread(gm).start();
new Thread(gm).start();
}
}
这是一次运行结果:
可以发现,金额是100,可是取出来的钱却可以多于100。因为CPU在一个时间点只能执行一个线程,CPU速度快,多个线程是同时运行的,至于谁先执行就看哪个线程先抢到了CPU的时间片,谁先抢到谁先执行,且只执行一次,执行结束再去抢时间片。“钱”这个资源在几个窗口取的时候可以同时被访问,这就造成了可以多取的现象。我们肯定要做到资源在同一时间只能有一个线程访问。
Thread-0取了一次10元,一共取了20元
Thread-1取了一次10元,一共取了20元
Thread-2取了一次10元,一共取了20元
Thread-1取了一次10元,一共取了30元
Thread-2取了一次10元,一共取了40元
Thread-0取了一次10元,一共取了50元
Thread-1取了一次10元,一共取了60元
Thread-2取了一次10元,一共取了70元
Thread-0取了一次10元,一共取了80元
Thread-1取了一次10元,一共取了90元
Thread-2取了一次10元,一共取了100元
Thread-0取了一次10元,一共取了110元
Thread-1取了一次10元,一共取了120元
二、线程不安全解决方法
1、使用安全类
比如 Java. util. concurrent 下的类。
2、使用自动锁synchronized同步:
java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。在保证结果准确的同时,提高性能,线程安全的优先级高于性能。
(1)同步代码块
synchronized 关键字可以用于方法中的某个区块中(临界区),表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){
需要同步操作的代码
}
(2)同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。(满足解除死锁的条件:不可剥夺条件)
public synchronized void method(){
可能会产生线程安全问题的代码
}
demo:
模拟存钱取钱:下面的代码创建了两个线程,一个是存钱线程,一个是取钱线程,前者run()不断存钱,后者run()不断取钱。整个程序只有两个线程运行,两个线程在Account对象实例上发生资源共享/竞争。
class DrawMoneyThread extends Thread { // 取钱线程
private Account account;
private double amount; // 取款数额
public DrawMoneyThread(String threadName, Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
}
public void run() {
for (int i = 0; i < 100; i++)
account.draw(amount, i); // 取100次钱
}
}
class DepositeMoneyThread extends Thread { // 存钱线程
private Account account;
private double amount; // 存款数额
public DepositeMoneyThread(String threadName, Account account, double amount) {
super(threadName);
this.account = account;
this.amount = amount;
}
public void run() {
for (int i = 0; i < 100; i++)
account.deposite(amount, i); // 存100次钱
}
}
public class Account {
private String accountNo;
private double balance;
public Account() {}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
... //省略getter和setter,
// 存钱同步方法
public synchronized void deposite(double depositeAmount, int i) {
setBalance(balance + depositeAmount);
notifyAll(); // 唤醒取钱线程
System.out.println(Thread.currentThread().getName() + " 存钱执行完毕,当前余额为:" + getBalance());
}
// 取钱同步方法
public synchronized void draw(double drawAmount, int i) {
if (getBalance() - drawAmount < 0) { // 账户中还没人存钱进去,此时当前线程需要等待阻塞
try {
System.out.println(Thread.currentThread().getName() + " 余额不足,执行wait操作,当前余额为:"+getBalance());
wait();
System.out.println(Thread.currentThread().getName() + " 执行了wait操作,线程被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
setBalance(getBalance() - drawAmount);
System.out.println(Thread.currentThread().getName() + " 取钱执行完毕,当前余额为:" + getBalance());
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Account account = new Account("123456", 0);
// 两个线程都是对同一个Account对象进行操作
Thread drawMoneyThread = new DrawMoneyThread("取钱线程", account, 700);
Thread depositeMoneyThread = new DepositeMoneyThread("存钱线程", account, 700);
drawMoneyThread.start();
depositeMoneyThread.start();
}
}
结果如下,若账号没钱就会阻塞取钱线程,并释放锁。存钱线程获得锁后就可以存钱,同时唤醒取钱线程,直到时间片消耗完。之后切换取钱线程就可以继续取钱了。
- 取钱线程 余额不足,执行wait操作,当前余额为:0.0
+ 存钱线程 存钱执行完毕,当前余额为:700.0
+ 存钱线程 存钱执行完毕,当前余额为:1400.0
- 取钱线程 执行了wait操作,线程被唤醒
- 取钱线程 取钱执行完毕,当前余额为:700.0
- 取钱线程 取钱执行完毕,当前余额为:0.0
- 取钱线程 余额不足,执行wait操作,当前余额为:0.0
+ 存钱线程 存钱执行完毕,当前余额为:700.0
+ 存钱线程 存钱执行完毕,当前余额为:1400.0
+ 存钱线程 存钱执行完毕,当前余额为:2100.0
+ 存钱线程 存钱执行完毕,当前余额为:2800.0
+ 存钱线程 存钱执行完毕,当前余额为:3500.0
+ 存钱线程 存钱执行完毕,当前余额为:4200.0
+ 存钱线程 存钱执行完毕,当前余额为:4900.0
+ 存钱线程 存钱执行完毕,当前余额为:5600.0
+ 存钱线程 存钱执行完毕,当前余额为:6300.0
+ 存钱线程 存钱执行完毕,当前余额为:7000.0
+ 存钱线程 存钱执行完毕,当前余额为:7700.0
+ 存钱线程 存钱执行完毕,当前余额为:8400.0
- 取钱线程 执行了wait操作,线程被唤醒
- 取钱线程 取钱执行完毕,当前余额为:7700.0
- 取钱线程 取钱执行完毕,当前余额为:7000.0
- 取钱线程 取钱执行完毕,当前余额为:6300.0
- 取钱线程 取钱执行完毕,当前余额为:5600.0
- 取钱线程 取钱执行完毕,当前余额为:4900.0
...
3、使用手动锁Lock
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock() :加同步锁。
public void unlock() :释放同步锁。
class X {
// Lock同步锁对象,此对象与共享资源具有一对一关系,此时共享资源将是X的一个实例对象
private final Lock lock = new ReentrantLock();
public void m(){
lock.lock();
try{
...
// 需要进行线程安全同步的代码
...
}catch(Execption e){
...
}finally{
lock.unlock();
}
}
}
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
lock.lock();
if(ticket>0){//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto‐generated catch block
e.printStackTrace();
}
//获取当前线程对象的名字
String appname = Thread.currentThread().getName();
System.out.println(appname+"正在卖:"+ticket‐‐);
}
lock.unlock();
}
}
}
Lock 和 synchronized 对比:
(1)synchronized 是 JVM 层面的技术,是 Java 内置的关键字,所以 获取锁/释放锁 都是由虚拟机管理的;而 Lock 是一个 Java 类,我们需要手动管理锁。
(2)出现异常时,JVM 会自动释放 synchronized 同步锁,但是 Lock 不会,所以使用 Lock 的时候我们需要搭配 try…catch…finally 代码块使用,并且将锁的释放放到 finally ,确保出现异常的时候可以释放锁。
(3)Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;
(4)通过Lock可以知道有没有成功获取锁,而 synchronized 却无法办到。
(5)Lock 可以提高多个线程进行读操作的效率。
4、使用线程变量ThreadLocal
synchronzed或者lock控制线程对临界区资源的同步顺序从而解决线程安全的问题,这种加锁的方式会让未获取到锁的线程进行阻塞等待,时间效率并不是很好。线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,如果每个线程都使用自己的“共享资源”,各自使用各自的,又互相不影响到彼此即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。ThreadLocal是一种“空间换时间”的方案,每个线程都会都拥有自己的“共享资源”无疑内存会大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待的情况从而提高的时间效率。
5、使用Redis实现分布式锁
以上都是基于单节点下的,如果是多节点集群模式,仍然不能保证整个系统的线程安全问题,如
用户A、B访问节点一,C、D访问节点二,两个节点本身通过以上方式实现了线程安全,假设A、C各自成功加锁,这样对于整个系统而言有两个用户都获得了锁,不符合需求。这时可以将节点一、节点二都配置到同一个redis连接,利用redis的setNx原子操作来实现锁的功能,如果set Key成功认为获取了锁,使用delete key实现解锁的功能。这个是实际应用中常用的。
(1)分布式锁是指在分布式环境下的多个节点之间控制并发访问的一种机制,而synchronized是在单个进程的线程之间进行同步控制;
(2)分布式锁一般通过Redis等分布式数据库实现,可以在多个应用服务器之间共享;而synchronized则只能在单个应用进程内起作用。
(3)分布式锁需要考虑分布式环境下的数据一致性问题,保证多个节点之间的数据同步;而synchronized只需要考虑单个进程内的数据同步问题。
(3)Redis等分布式数据库提供的分布式锁机制可以实现比较灵活的锁定方式,如设置超时时间、可重入等功能;而synchronized没有这些灵活的操作。