项目放在服务器中,而服务器已经将线程的定义、线程对象的创建,线程的启动等都已经实现了,这些代码我们不需要编写。
最重要的是:编写的程序需要放到一个多线程的环境中,这些数据在多线程并发的环境下是否是安全的。
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)在类上添加也同样的道理,类锁只有一个,所以需要等待。