提示:本文基于上一篇文章继续阐述线程安全问题
全文内容为:
1.线程安全问题产生的原因
2.同步锁的使用和原理
全文手打,都是精华,有案例有内容,代码可复制运行思考,创作不易,请耐心看
本人能力有限,如有遗漏或错误,敬请指正,互相交流,谢谢
其他文章
1.Java多线程基本概念和常用API(面试高频)
2.创建多线程的方式(1)
3.Java并发线程池使用和原理(通俗易懂版)
4.基于SpringBoot+Async注解整合多线程
文章目录
一、线程安全问题是什么
用Java来说,当多个线程操作同一个变量的时候,就会产生线程安全问题。
举个经典栗子:窗口卖票问题,现在假设有三个窗口ABC在卖票,票数总共100张,那么卖票的规则是只要票数>0并且人员进到了窗口里面就可以买票
现在假设100张票不同的窗口总共已经卖出去了99张,现在剩下最后一张,那么如果有三个人刚好都分别进到了ABC三个窗口里面,如果A窗口卖掉了最后一张,但是此时票数为0的数据由于数据延时还没传到卖票系统中,
所以在BC窗口符合上述卖票的条件==①票数>0 ②人员进了窗口(进入了run方法)
所以最后B和C窗口都卖出了最后一张票,这就导致数据混乱
最终得出:什么叫线程安全问题?
通俗点理解就是:线程一在执行任务的时候,还没有执行完,线程二也进来执行线程一还没执行完的任务,这就乱套了,这种情况就是线程不安全。
上述代码示例:
public class MyRunnable implements Runnable {
//总票数
private int ticket;
@Override
public void run() {
while(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖出最后"+ticket+"票");
//这里模拟网络延迟,数据没有上传到车站系统中
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//延迟过后才更新数据到后台
ticket--;
}
}
}
public static void main(String[] args) {
//station:创建车站,假设票数只卖到最后一张
MyRunnable station=new MyRunnable(1);
//创建三个窗口,因为传入的runnable对象是同一个,所以ticket是共享的
Thread t1=new Thread(station,"窗口A");
Thread t2=new Thread(station,"窗口B");
Thread t3=new Thread(station,"窗口C");
t1.start();
t2.start();
t3.start();
}
二、产生线程安全问题的条件
- 多线程并发 (上述多个窗口)
- 有共享数据 (最后一张票)
- 共享数据有修改的行为(卖出去后,票数要减1)
三、解决线程安全问题:线程同步机制
线程同步机制就是“线程排队执行”,线程不能并发。
代价:牺牲一部分效率,但是数据安全第一的方法
轮流进去,锁住,就算被堵住了,也要等执行完才能下一个
类似于只有一个厕所,三个人上厕所,轮流上,等上一个人出来才能进去
3.1 synchronized同步代码块(代码上)
先不管这个是什么,先记住:只要使用这个关键字,锁必须是同一把(不懂下面有解释)
给同步代码块包裹在内的代码加一把锁,同一时间只有占有这把锁的线程能执行代码
其他线程只能等该线程执行完后,再抢占获取锁,抢占后就能执行代码。这样保证了“同步机制”
按照上面上厕所的例子来说,给这个厕所门上锁,那么外面的人没有钥匙怎么进去呢?
代码举例:改造上述的代码
public class MyRunnable implements Runnable {
//锁的对象
private Object object;
//总票数
private int ticket;
@Override
public void run() {
//加锁
synchronized (object) {
//添加一个标志位,当卖出票就设置为true
boolean flag=false;
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖出最后" + ticket + "票");
//这里模拟网络延迟,数据没有上传到车站系统中
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//延迟过后才更新数据到后台
ticket--;
//卖出票设置为true
flag=true;
}
//当没有卖出票走这个判断
if(!flag){
System.out.println("票卖完了,"+Thread.currentThread().getName() + "无法卖出票");
}
}
}
public static void main(String[] args) {
//station:创建车站,假设票数只卖到最后一张。new Object()创建一个对象传入进去,所以三个线程共用object这个对象,也就是共用同一把锁
MyRunnable station=new MyRunnable(new Object(),1);
//创建三个窗口,因为传入的runnable对象是同一个,所以ticket是共享的
Thread t1=new Thread(station,"窗口A");
Thread t2=new Thread(station,"窗口B");
Thread t3=new Thread(station,"窗口C");
t1.start();
t2.start();
t3.start();
}
可以看出保证线程安全了
3.2 同一把锁的理解
//obj就是锁,要保证不同线程里面的obj,都是同一个,也就是A线程.obj==B线程.obj
synchronized(obj){
}
我们为了保证线程安全,就是要让每个线程不在同一时间执行一样的代码,轮流执行
那么一个门上如果有两个钥匙能开门,那同一时间是否会有两个线程能进去?
synchronized同步锁具有互斥性,这相当于线程由并行执行变成串行执行(轮流),保证了线程的安全性,但损失了性能。
3.3 synchronized同步代码块(方法上)
放在方法上,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B或C或D正在用这个方法(或者该类的其他同步方法,这句话不懂向下看就明白了)
有的话要等正在使用synchronized方法的线程B或C或D运行完这个方法后,再运行此线程A
没有的话,锁定调用者,然后直接运行
synchronized关键字放在实例方法上,锁的是“this”(谁调用方法,锁的对象就是谁)
好比上面这个代码,由于线程的run()方法是由station这个对象执行的(忘记就看上面)
所以锁的对象就是station,由于三个Thread都是由station创建来的,所以是同一把锁
public class MyRunnable implements Runnable {
private Object object;
//总票数
private int ticket;
@Override //可以注意到同步代码块放在方法上了
public synchronized void run() {
boolean flag = false;
while (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖出最后" + ticket + "票");
//这里模拟网络延迟,数据没有上传到车站系统中
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//延迟过后才更新数据到后台
ticket--;
flag = true;
}
if (!flag) {
System.out.println("票卖完了," + Thread.currentThread().getName() + "无法卖出票");
}
}
}
适用情况:当整个方法需要同步的时候,并且共享对象是this
ps:补充说明黄色部分
public class A {
//放在方法上,锁的是this
public synchronized void method1(){
}
//放在方法上,锁的是this,和上面是同一把锁,所以如果有线程调用了method2()
//其他线程也访问不了method1(),因为此时“this”这个锁被占有了
public synchronized void method2(){
}
}
3.4 synchronized同步代码块(静态方法上)
放在静态方法中(共享对象是“类”)
适用情况:类锁只有一把,创建100个对象但是类锁还是只有一个
通常用于保护静态变量安全
public class A {
//锁的对象是:A.class,所以不管创建多少个A的实例,A.class只有一个
public synchronized static void AMethod(){}
}
public class B{
public void BMethod(){
//类锁的第二种写法,直接指明是某个类
synchronized (B.class){
}
}
}
3.5 synchronized的原理
加锁实际上就是在锁对象的对象头中写入当前线程id
每个线程要想调用这个同步方法,都会先去锁对象的对象头看看当前线程id是不是自己的。
举例:假设锁对象的对象头是 key,开始key=null。 那么现在线程A获取了锁先进去方法,此时key=A,那么线程B想调用方法的时候,会查看key,发现key=A,不等于B,所以不会执行方法
注意:synchronized是可重入锁。也就是说当线程访问对象的同步方法时,在调用其他同步方法时无需再去获取其访问权。因为我们实际上锁的是对象,对象头里面记录的都是当前线程的ID。
四、死锁问题:都不放弃锁
线程A执行方法1获取了锁obj1,那么其他线程是无法访问方法1的,在方法1最后一行代码是获取方法2的锁,只有执行完这行代码,方法一才能退出
好巧不巧,在线程A获取锁obj1的同时,线程B执行了方法2获取了锁obj2,但是方法2的最后一行代码是获取方法1的锁,只有执行完这行代码,方法二才能退出
所以A和B互相占了对方退出方法时必须要占有的锁,僵持过后就成了死锁
public class MyThread extends Thread{
Object ob1;
Object ob2;
public MyThread(Object ob1,Object ob2){
this.ob1=ob1;
this.ob2=ob2;
}
public void run(){
//拿了obj1这个锁
synchronized (ob1){
try {
//睡眠这个时间,线程m2执行run()方法,获取了obj2这个锁
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//由于线程m2占有obj2锁,所以线程m1拿不到,线程m2拿不到obj1锁也出不去,最后两个线程都出不去
synchronized (ob2){
System.out.println("---");
}
}
}
}
public class MyThread1 extends Thread{
Object ob1;
Object ob2;
public MyThread1(Object ob1,Object ob2){
this.ob1=ob1;
this.ob2=ob2;
}
public void run(){
synchronized (ob2){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ob1){
System.out.println("---");
}
}
}
}
public class Test{
public static void main(String[] args) throws InterruptedException {
Object ob1=new Object();
Object ob2=new Object();
MyThread m1=new MyThread(ob1,ob2);
MyThread1 m2=new MyThread1(ob1,ob2);
m1.start();
m2.start();
}
}
五、总结
1. java中的同步方法会增加你程序的性能的消耗,所以只有在正真需要的时候才使用同步。使用同步代码块是需要保护资源才使用
2.静态方法加锁,和xx.class 锁效果一样,都是类锁
3.“this锁”需要强调的是,关于同一个类的方法上的锁,来自于调用该方法的对象,如果调用该方法的对象是相同的,那么锁必然相同,否则就不相同。比如 new A().x() 和 new A().x(),对象不同,锁不同
感谢看到最后,码字不易,纯手打,有帮助点个⭐是更新的动力!