线程安全以及线程同步synchronized

项目放在服务器中,而服务器已经将线程的定义、线程对象的创建,线程的启动等都已经实现了,这些代码我们不需要编写。

最重要的是:编写的程序需要放到一个多线程的环境中,这些数据在多线程并发的环境下是否是安全的。

1、什么时候数据在多线程并发的环境下会存在安全问题?

要满足三个条件:

(1)多线程并发
(2)有共享数据
(3)共享数据有修改行为

满足上边条件会存在线程安全问题。
 

2、如何解决

排队执行解决线程安全问题。这种机制被称为: "线程同步机制"(就是线程不能并发了,要排队)


3、两个术语

(1)异步编程模型

线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫:异步编程模型。(就是多线程并发,效率较高)

(2)同步编程模型

线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,两个线程发生等待关系,这就是同步编程模型。

4、线程同步机制

如果不使用线程同步有可能带来的问题:

一个银行取款的问题,假设有2个人a和b同时取一张银行卡中的1万块钱,两个操作是同步的,此时会有一定概率出现a和b同时取款,查询余额时都是1万元,a取完5000,此时需要把剩余金额更新到数据库,假设网络不太好,延时了,数据库金额还是1万元,这时b也取了5000,之后,会造成余额还有5000,因为a取完钱之后,更新剩余金额的操作延时了!

模拟下:

多个线程共享同一个对象,多个线程同时执行时,我用sleep模拟出现上边的问题

// ATM机
public class Account {
    // 余额1万元
    private Integer draw = 10000;
    // 更新余额的方法
    public void update(Integer endBalance) {
        this.draw = endBalance;
    }
    // 取款+更新余额 功能
    public void withDraw(Integer money) {
        // 取款 - t1和t2线程会同时取款
        Integer endBalance = this.balance() - money;
        // 更新余额
        // 有可能更新余额有延时或者网络异常,下边使用sleep模拟延时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.update(endBalance);
    }
    // 查看余额功能
    public Integer balance() {
        return this.draw;
    }
}
public class Lv20210810 {
    public static void main(String[] args) {
        // 创建多个线程(相当于a和b同时在取款)
        MyTread t1 = new MyTread();
        MyTread t2 = new MyTread();
        // 创建Account对象
        Account account = new Account();
        t1.setAccount(account);
        t2.setAccount(account);
        // 线程设置名称
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}
class MyTread extends Thread {
    // 多个线程共享同一个对象
    public Account account;
    // 设置Account对象
    public void setAccount(Account ac) {
        this.account = ac;
    }
    // 取款操作
    @Override
    public void run() {
        Integer money = 5000;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 取款 - 假设每个线程都取5000元(多线程并发执行这个方法)
        this.account.withDraw(money);
        // 查询余额
        Integer seeBalance = this.account.balance();
        // 打印当前线程及余额
        System.out.println(Thread.currentThread().getName()+":线程取款5000,余额为"+seeBalance);
    }
}

 看到结果,银行要亏死!
 

5、解决上边问题,可以使用同步线程 - synchronized

排它锁

只能排队执行,不能并发执行(可以理解为阻塞状态)

(1)synchronized 语法

synchronized() {
        // 线程同步代码
}

小括号传的是多线程共享的数据对象,假设t1、t2、t3、t4有四个线程,如果希望t1和t2需要排队执行,那么在小括号里就填写t1和t2线程的共享对象即可。

// ATM机
public class Account {
    private Integer draw = 10000;
    public void update(Integer endBalance) {
        this.draw = endBalance;
    }

    // 取款+更新余额 功能
    public void withDraw(Integer money) {
        // this 就是的共享对象,小括号不一定是this,只要是共享对象就行
        synchronized (this) {
            // 取款 - t1和t2线程会同时取款
            Integer endBalance = this.balance() - money;
            // 更新余额
            this.update(endBalance);
        }
    }
    // 查看余额功能
    public Integer balance() {
        return this.draw;
    }
}

(2)java语言中,任何一个对象都一把锁(就是一个标记而已,只是把它叫做锁),一百个对象100把锁,上边代码执行原理:

1)假设t1和t2线程并发,开始执行上边代码的时候,肯定是一个先一个后;
2)假设t1先执行了,遇到了synchronized关键字,这个时候会自动找"后边共享对象" 的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中,一直都是占有这把锁的,直到同步代码块结束,这把锁才会释放。
3)假设t1已经占有这把锁,此时t2也遇到了synchronized关键字,也会去占有后边的共享对象的这把锁,解锁这把锁被t1占有了,t2只能在同步代码块外边等待t1的结束,t1结束了,会归还这把锁,t2才能占有这把锁,进入同步代码块。

可以理解为洗手间只有一个蹲坑,2个人不能同时上厕所,一个人进去了,把门锁上了,下个人只能等到这个人结束之后才能进去。

这样就达到了线程排队执行。

这里注意的是:共享对象一定要选好了,这个共享对象一定是要需要排队执行的那些线程的共享对象!!!

6、*** 哪些对象有线程安全问题  ***

**********************************重要**********************************

java中三大变量

(1)实例变量:存在堆中,而多线程中方法区和堆是共享的!

(2)静态变量:存在方法区中。

(3)局部变量:存在栈中。

以上变量只有局部变量是线程安全的,因为它存在于栈中,多线程时,一个线程一个栈。

7、线程同步一般是来保护实例变量或者静态变量

局部变量和常量都不会有线程安全问题。

8、

如果synchronized出现在实例方法上,那么锁的是当前的this,也就是当前的对象,整个实例方法都需要同步,可能会无故扩大同步的范围,效率较低。

 看下StringBuffer源码:

实例方法上都有 synchronized 关键字。

 如果当前方法中有局部变量,那么建议使用StringBuilder,因为局部变量不存在线程安全的问题,没必要使用StringBuffer!

ArrayList是非线程安全的,Vector是线程安全的;HashMap、HashSet是非线程安全的,HashTable是线程安全的。

HashTable源码


9、总结

synchronized 三种写法:

(1)同步代码块,灵活

synchronized(线程共享对象) {
        // 同步代码块

(2)在实例方法上使用synchronized

表示共享的对象是this,并且同步的是整个实例方法。

(3)在静态变量上使用 synchronized

表示找类锁,类锁永远只有一把,就算创建100个对象,类锁也只有一把。

面试题

(1)当执行doAny方法时,是否要等到doSome方法执行完(要等5秒钟才能够执行doAny方法)?

先来屡下代码,很简单,刚开始先new了一个共享对象Obj,创建2个线程,然后给线程设置名称t1和t2,开始执行,执行顺序,在t1和t2中间加了sleep保证t1先执行,t2后执行。
在MyThread类的run方法中判断当前线程名是否是t1,如果是会调用Obj类中的doSome方法执行,如果不是,才会调用Obj类中的doAny方法。
在Obj类中的doSome实例方法上加上synchronized 关键字,doAny上没加。

理解synchronized 关键字的,都知道synchronized 在实例方法上,它锁的是this,也就是当前的对象,当执行doSome方法时,它会拿到当前对象锁,然后执行代码,一定会同步执行;但是执行doAny方法,不需要当前对象锁啊,因为方法上没有synchronized 关键字,所以,doAny方法的执行是不需要等待的!!!

public class Lv20210810 {
    public static void main(String[] args) {
        Obj obj = new Obj();
        MyTread t1 = new MyTread(obj);
        MyTread t2 = new MyTread(obj);
        t1.setName("t1");
        t2.setName("t2");
        // 下边为了保证t1先执行,加了一个sleep
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}
class MyTread extends Thread {
    public Obj obj;
    public MyTread(Obj obj) {
        this.obj = obj;
    }
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("t1")) {
            obj.doSome();
        }else {
            obj.doAny();
        }
    }
}
// 共享对象Obj
class Obj {
    // dosome方法加了synchronized
    public synchronized void doSome() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("doSome执行了:"+i);
        }
    }
    // doAny方法没加synchronized
    public void doAny() {
        System.out.println("doAny方法执行了");
    }
}

(2)

把doAny方法上加上了 synchronized 关键字,此时,执行doAny方法时,是需要等待doSome方法执行完后,才能执行的!因为 在执行doAny方法时,它要找线程共享对象的锁,但是,找不到,因为之前doSome方法没执行完,对象锁没有释放。

class Obj {
    // dosome方法加了synchronized
    public synchronized void doSome() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("doSome执行了:"+i);
        }
    }
    // doAny方法没加synchronized
    public synchronized void doAny() {
        System.out.println("doAny方法执行了");
    }
}

(3)在类上添加也同样的道理,类锁只有一个,所以需要等待。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值