线程安全
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
取钱模型展示
需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。
如果两人同时来取钱,而且2人都要取钱10万元,可能出现什么问题。
1、需要提供一个账户类,创建一个账户对象代表2人的共享账户
2、需要定义一个线程类,线程类可以处理账户对象
3、创建2个线程对象,传入同一个账户对象。
4、启动2个线程,去同一个账户对象中取钱10万。
账户类
public class Account {
private String cardId;
private double money;
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public void drawMoney(double money) {
//获取到现在谁来取钱
String name = Thread.currentThread().getName();
//判断余额是否足够
if (this.money >= money){
//取钱
System.out.println(name + "来取钱成功" +money);
//更新金额
this.money -= money;
System.out.println(name + "取钱后剩余:" +this.money);
}else{
//余额不足
System.out.println(name + "来取钱,余额不足");
}
}
}
线程类
线程类需要接收账户,因为每创建一个线程对象就要把账户对象交给线程对象,
意味着线程类需要接收账户,使用有参构造器传递账户类
public class DrawThread extends Thread{
private Account acc;
public DrawThread(Account acc,String name){
super(name);
this.acc = acc;
}
@Override
public void run() {
//小明小红取钱
acc.drawMoney(1000);
}
}
主方法
public static void main(String[] args) {
//1、定义一个线程类,创建一个共享的账户对象
Account acc = new Account("IBC-1001",1000);
//2、创建两个线程对象,代表小明和小红同时进来了
new DrawThread(acc,"小明").start();
new DrawThread(acc,"小红").start();
}
运行结果
模拟售票案例
创建3个线程,同时开启,对共享的票进行出售
public class Demo01Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);//即为共享资源
Thread t2 = new Thread(run);//在三个对象中传递一个run就是100张分来被卖
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
实现卖票案例
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while (true) {
//先判断票是否存在
if (ticket > 0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,开始买票ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
模拟售票线程安全问题产生的原理
线程安全问题是不能产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了CPU的执行权,让其她线程只能等待,等待当前线程卖完票,其它线程在进行卖票
保证:使用一个线程在卖票
线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
加锁:把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
为了保证每个线程都能正常执行原子操作Java引入了线程同步机制。
那有三种方式完成同步操作:
1.同步代码块。
2.同步方法。
3.锁机制。
同步代码块
把出现线程安全问题的核心代码给上锁,每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
理论上锁对象只要对于当前同时执行的线程来说是用同一个对象即可。
卖票案例出现了线程安全问题
卖出了不存在的票和重复的票
解决线程安全问题的一种方案:使用同步代码块
格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
1、通过代码块中的锁对象,可以使用任意的对象
2、但是必须保证多个线程使用的锁对象是同一个
3、锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行
锁对象的规范要求:
规范上:建议使用共享资源作为锁对象
对于实例方法建议使用this作为锁对象
对于静态方法建议使用字节码(类名.calss)对象作为锁对象。
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private int ticket = 100;
//创建一个锁对象
Object obj = new Object();//必须创建在run方法外边,不然就会有三个锁
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while (true) {
//创建同步代码块
synchronized (obj){
//先判断票是否存在
if (ticket > 0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,开始买票ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
}
public class Demo01Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);//即为共享资源
Thread t2 = new Thread(run);//在三个对象中传递一个run就是100张分来被卖
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
同步技术的原理
同步方法
卖票案例出现了线程安全问题
卖出了不存在的票和重复的票
解决线程安全问题的第二种方案:使用同步方法
使用步骤:
1、把访问了共享数据的代码抽取出来,放到一个方法中
2、在方法上添加synchronized修饰符
格式:定义方法的格式
修饰符 synchronized 返回之类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
System.out.println("this:" + this);//this:cn.itcast.day14.demo07.RunnableImpl@34a245ab
//使用死循环,让卖票操作重复执行
while (true) {
payTicket();
}
}
//定义一个同步方法
//同步方法也会把方法内部的代码锁住
//只让一个线程执行
//同步方法的锁对象是谁?
//就是实现类对象 new RunnableImpl()
//也就是this
public synchronized void payTicket(){
//先判断票是否存在
if (ticket > 0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,开始买票ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--;
}
}
}
public class Demo01Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
System.out.println("run:" + run);//run:cn.itcast.day14.demo07.RunnableImpl@34a245ab
//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);//即为共享资源
Thread t2 = new Thread(run);//在三个对象中传递一个run就是100张分来被卖
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
静态的同步方法
锁对象是谁?不是this
this是创建对象之后产生的,静态方法优先于对象
静态方法的锁对象是本类的class属性–>class文件对象(反射)
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private static int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
System.out.println("this:" + this);//this:cn.itcast.day14.demo07.RunnableImpl@34a245ab
//使用死循环,让卖票操作重复执行
while (true) {
payTicketStatic();
}
}
//静态的同步方法
//锁对象是谁?不是this
//this是创建对象之后产生的,静态方法优先于对象
//静态方法的锁对象是本类的class属性-->class文件对象(反射)
//
//
public static /*synchronized*/ void payTicketStatic() {
synchronized (RunnableImpl.class) {
//先判断票是否存在
if (ticket > 0) {
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,开始买票ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
Lock锁机制
1、为了更清晰的表达如何解锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便
2、Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
3、Lock是接口不能直接实例化,这里采用的实现类ReentrantLock锁对象。
创建Lock锁的时候,可以使用final修饰,锁对象是唯一和不可替换的
卖票案例出现了线程安全问题
卖出了不存在的票和重复的票
解决线程安全问题的第三种方案:使用Lock锁机制
Java.util.concurrent.Locks.lock接口
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作
Lock接口中的方法:
void unlock()获取锁
void unlock()释放锁
java.util.concurrent.lock.ReentrantLock implements Lock 接口
使用步骤:
1、在成员位置创建一个ReentrantLock对象
2、在可能会出现安全问题的代码前调用Lock接口中的lock获取锁
3、在可能会出现安全问题的代码后调用Lock接口中的unlock获取锁
public class RunnableImpl implements Runnable{
//定义一个多个线程共享的票源
private int ticket = 100;
//1、在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();
//设置线程任务:卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while (true) {
//2、在可能会出现安全问题的代码前调用Lock接口中的lock获取锁
l.lock();
//先判断票是否存在
if (ticket > 0){
//提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(100);
//票存在,开始买票ticket--
System.out.println(Thread.currentThread().getName() + "-->正在卖第" + ticket + "张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {//无论是否程序出现异常,锁都会释放
//3、在可能会出现安全问题的代码后调用Lock接口中的unlock获取锁
l.unlock();
}
}
}
}
}