线程安全与线程同步

简介: 线程安全与线程同步

1.线程安全

线程安全问题

多个线程同时操作同一个共享资源且存在修改该资源的时候可能会出现业务安全问题,称为线程安全问题。

线程安全问题出现的原因

①存在多线程并发。

②同时访问共享资源。

③存在修改共享资源。

线程安全问题案例模拟-取钱业务

需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。

分析:

①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。

②:需要定义一个线程类,线程类可以处理账户对象。

③:创建2个线程对象,传入同一个账户对象。

④:启动2个线程,去同一个账户对象中取钱10万。

示例代码如下:

Account账户类

publicclassAccount {
privateStringcardId;
doublemoney; // 账户余额publicAccount() {
    }
publicAccount(StringcardId, doublemoney) {
this.cardId=cardId;
this.money=money;
    }
publicvoiddrawMoney(doubledrawMoney) throwsInterruptedException {
// 获取取钱人姓名StringuserName=Thread.currentThread().getName();
// 判断账户余额是否足够if (this.money>=drawMoney) { // 余额足够System.out.println(userName+"取款余额"+this.money+"余额足够,开始取款");
// 取钱Thread.sleep(1000); // 取钱需要1sSystem.out.println(userName+"成功取出"+drawMoney+"元");
// 更新余额this.money-=drawMoney;
System.out.println(userName+"取钱后,账户余额剩余"+this.money+"元");
        } else { // 余额不足System.out.println(userName+"取款余额不足!");
        }
    }
publicStringgetCardId() {
returncardId;
    }
publicvoidsetCardId(StringcardId) {
this.cardId=cardId;
    }
publicdoublegetMoney() {
returnmoney;
    }
publicvoidsetMoney(doublemoney) {
this.money=money;
    }
}

取钱线程类

publicclassDrawThreadextendsThread {
// 接收处理的账户对象privateAccountacc;
publicDrawThread() {
    }
publicDrawThread(Accountacc, Stringname) {
super(name);
this.acc=acc;
    }
@Overridepublicvoidrun() {
// 调用取钱方法try {
acc.drawMoney(100000);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
    }
}

测试类

publicclassThreadDemo {
publicstaticvoidmain(String[] args) {
// 1.定义线程类,创建一个共享的账户对象Accountacc=newAccount("icbc-001", 100000);
// 2.创建2个线程对象,代表小明和小红同时登陆账户取钱newDrawThread(acc, "小明").start();
newDrawThread(acc, "小红").start();
    }
}

程序运行结果如下:

小红取款余额100000.0余额足够,开始取款

小明取款余额100000.0余额足够,开始取款

小明成功取出100000.0元

小明取钱后,账户余额剩余0.0元

小红成功取出100000.0元

小红取钱后,账户余额剩余-100000.0元

分析:小红取款线程访问账户资源,判断余额,余额足够,开始取钱(线程休眠代表取钱过程),在此期间,小明取款线程访问账户资源,判断余额,余额足够,开始取钱,后两者均取钱成功,更新账户余额。

小红线程取钱过程中,在更新账户余额之前,小明线程执行判断余额任务,在小红线程取款完毕后,小明线程已经执行完判断余额任务,不再重复判断,同样开始取款,出现线程安全问题。

2.线程同步

2.1线程同步思想概述

取钱案例出现问题的原因?

多个线程同时执行,发现账户都是够钱的。

如何才能保证线程安全呢?

让多个线程实现先后依次访问共享资源,这样就解决了安全问题

线程同步的核心思想

加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

2.2方式一:同步代码块

同步代码块

作用:把出现线程安全问题的核心代码给上锁。

原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

同步代码块代码格式:

synchronized(同步锁对象) {

操作共享资源的代码(核心代码)

}

锁对象要求

理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。

例如,对于某一确定字符串,在堆内存中具有唯一地址,同步锁对象若是某一字符串,则针对所有的线程来讲,锁对象均是唯一的,则表明所有的线程对象均需要遵守唯一进入,任务执行结束后解锁的原则。同步锁对象若是某一对象,因不同对象的地址不同,因此当且仅当对于不同的线程来说,同步锁对象是同一对象时,才会上锁,若对于不同的线程对象,同步锁对象是不同的(堆内存中地址不同),那么这些线程对象不受同步锁的影响

因此锁对象用任意唯一的对象并不好,会影响其他无关线程的执行。

锁对象的规范要求

规范上:建议使用共享资源作为锁对象

对于实例方法建议使用this作为锁对象

对于静态方法建议使用字节码(类名.class)对象作为锁对象(与用字符串效果一致,类名.class更规范)

示例代码如下:

更改第3节Account账户类中的drawMoney方法代码。

publicvoiddrawMoney(doubledrawMoney) throwsInterruptedException {
// 获取取钱人姓名StringuserName=Thread.currentThread().getName();
// 同步代码块synchronized (this) {
// 判断账户余额是否足够if (this.money>=drawMoney) { // 余额足够System.out.println(userName+"取款余额"+this.money+"余额足够,开始取款");
// 取钱Thread.sleep(1000); // 取钱需要1sSystem.out.println(userName+"成功取出"+drawMoney+"元");
// 更新余额this.money-=drawMoney;
System.out.println(userName+"取钱后,账户余额剩余"+this.money+"元");
            } else { // 余额不足System.out.println(userName+"取款余额不足!");
            }
        }
    }

程序运行结果如下:

小明取款余额100000.0余额足够,开始取款

小明成功取出100000.0元

小明取钱后,账户余额剩余0.0元

小红取款余额不足!

2.3方式二:同步方法

同步方法

作用:把出现线程安全问题的核心方法给上锁。

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

同步方法格式:

修饰符 synchronized 返回值类型 方法名称(形参列表) {

操作共享资源的代码

}

同步方法底层原理

同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。

如果方法是实例方法,同步方法默认用this作为的锁对象,但是代码要求高度面向对象。

如果方法是静态方法,同步方法默认用类名.class作为的锁对象

示例代码如下:

更改第3节Account账户类中的drawMoney方法代码。

publicsynchronizedvoiddrawMoney(doubledrawMoney) throwsInterruptedException { // synchronized代表同步方法// 获取取钱人姓名StringuserName=Thread.currentThread().getName();
// 判断账户余额是否足够if (this.money>=drawMoney) { // 余额足够System.out.println(userName+"取款余额"+this.money+"余额足够,开始取款");
// 取钱Thread.sleep(1000); // 取钱需要1sSystem.out.println(userName+"成功取出"+drawMoney+"元");
// 更新余额this.money-=drawMoney;
System.out.println(userName+"取钱后,账户余额剩余"+this.money+"元");
        } else { // 余额不足System.out.println(userName+"取款余额不足!");
        }
    }

2.4方式三:Lock锁

Lock锁

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象

ReentrantLock构造器

方法名

说明

public ReentrantLock()

获得Lock锁的实现类对象

 

Lock类常用API

方法名

说明

void lock()

获得锁

void unlock()

释放锁

 

Lock使用格式:

public void m() {

lock.lock();

try {

// ...method body

} finally {

lock.unlock();

}

}

示例代码如下:

更改第3节Account账户类的代码。

publicclassAccount {
privateStringcardId;
doublemoney; // 账户余额// final修饰,表示锁对象唯一且不可替换privatefinalLocklock=newReentrantLock();
publicAccount() {
    }
publicAccount(StringcardId, doublemoney) {
this.cardId=cardId;
this.money=money;
    }
publicvoiddrawMoney(doubledrawMoney) throwsInterruptedException { // synchronized代表同步方法// 获取取钱人姓名StringuserName=Thread.currentThread().getName();
lock.lock(); // 上锁try {
// 判断账户余额是否足够if (this.money>=drawMoney) { // 余额足够System.out.println(userName+"取款余额"+this.money+"余额足够,开始取款");
// 取钱Thread.sleep(1000); // 取钱需要1sSystem.out.println(userName+"成功取出"+drawMoney+"元");
// 更新余额this.money-=drawMoney;
System.out.println(userName+"取钱后,账户余额剩余"+this.money+"元");
            } else { // 余额不足System.out.println(userName+"取款余额不足!");
            }
        } finally { // 将解锁方法添加到finally语句中,保证即使try语句出现异常,也可以将上锁的资源解锁,等待下一线程访问lock.unlock(); // 解锁        }
    }
publicStringgetCardId() {
returncardId;
    }
publicvoidsetCardId(StringcardId) {
this.cardId=cardId;
    }
publicdoublegetMoney() {
returnmoney;
    }
publicvoidsetMoney(doublemoney) {
this.money=money;
    }
}
相关文章
|
3月前
|
存储 监控 安全
一天十道Java面试题----第三天(对线程安全的理解------>线程池中阻塞队列的作用)
这篇文章是Java面试第三天的笔记,讨论了线程安全、Thread与Runnable的区别、守护线程、ThreadLocal原理及内存泄漏问题、并发并行串行的概念、并发三大特性、线程池的使用原因和解释、线程池处理流程,以及线程池中阻塞队列的作用和设计考虑。
|
1月前
|
安全 Java 开发者
在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制
【10月更文挑战第3天】在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制,如`synchronized`关键字、`Lock`接口及其实现类(如`ReentrantLock`),还有原子变量(如`AtomicInteger`)。这些工具可以帮助开发者避免数据不一致、死锁和活锁等问题。通过合理选择和使用这些机制,可以有效管理并发,确保程序稳定运行。例如,`synchronized`可确保同一时间只有一个线程访问共享资源;`Lock`提供更灵活的锁定方式;原子变量则利用硬件指令实现无锁操作。
20 2
|
3月前
|
监控 安全 Java
Java多线程调试技巧:如何定位和解决线程安全问题
Java多线程调试技巧:如何定位和解决线程安全问题
137 2
|
3月前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
|
3月前
|
Java
多线程线程同步
多线程的锁有几种方式
|
2月前
|
安全 Java
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
LinkedBlockingQueue 是线程安全的,为什么会有两个线程都take()到同一个对象了?
46 0
|
3月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
77 6
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
86 5
|
3月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
84 3
|
3月前
|
存储 安全 Java
【多线程面试题十七】、如果不使用synchronized和Lock,如何保证线程安全?
这篇文章探讨了在不使用`synchronized`和`Lock`的情况下保证线程安全的方法,包括使用`volatile`关键字、原子变量、线程本地存储(`ThreadLocal`)以及设计不可变对象。