目录
线程安全(重点)
在以后的开发中,我们的项目都是运行在服务器当中,服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。最重要的是,编写的代码需要放到一个多线程的环境下运行,更需要关注的是这些数据在多线程并发的环境下是否是安全的。
多线程并发
同步与异步
现在有10000条数据,例如批量复制接口,复制一条数据,但是这条数据关联这标签、邮件、图片等等关联表数据,我代码里面对这些数据进行操作处理,到最后批量插入数据库里面,但是执行这个接口花费了很长时间,尤其在用户体验上非常不好,(如果在有网关的情况下,网关设置的接口超时时间可能会一直进行超时)如何优化这个接口呢?
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型
其实这里批量插入的操作就是同步编程模型,上一条数据没有执行完,上一条数据就无法进行执行,等待上一条执行完毕,效率较低。线程排队执行。
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高。)
可以通过异步编程模型进行优化,在插入数据的方法开多个线程进行异步处理,相当于多个人处理这些数据插入,谁也不管谁,效率较高。
什么是多线程并发?
争对上述的对10000条数据进行插入处理操作,如果我们的服务器端的机器是4核的CPU(4核CPU最多可运行4个线程),现在我们开启4个线程去处理这些数据的插入,这些线程相互独立,并行发起操作,这个就叫做多线程并发。
线程安全问题的满足条件
满足三个条件:
- 条件1:多线程并发。
- 条件2:有共享数据。
- 条件3:共享数据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题。
Java中的三大变量的安全问题
- 实例变量:堆 在类体中定义的变量。
- 局部变量:栈 在类的方法中定义的变量
- 静态变量:方法区 在类中以 static 关键字声明,但必须在方法之外
以上三大变量中
- 局部变量永远都不存在安全问题,因为局部变量不共享,只在方法里面生效。
- 常量也不会有线程安全问题
-
实例变量在堆中,静态变量在方法区中,堆和方法区只有一个,堆和方法区都是多线程共享的,所以可能存在线程安全问题
线程安全
不使用线程同步机制,多线程对同一个账户进行取款,出现了线程安全问题
线程不安全示例:
- 先定义一个账户进行取款操作,写一个取款操作地方法
package 多线程.线程安全;
/*
不使用线程同步机制,多线程对同一个账户进行取款,出现了线程安全问题
*/
public class Account {
private String acton;//账户
private double balance;//余额
public Account() {
}
public Account(String acton, double balance) {
this.acton = acton;
this.balance = balance;
}
public String getActon() {
return acton;
}
public double getBalance() {
return balance;
}
public void setActon(String acton) {
this.acton = acton;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void withdraw(double money){
//取款之前的余额
double before = this.balance;
//取款之后的余额
double after = before - money;
//模拟一下网络延迟,100%初出问题
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setBalance(after);
}
}
- 写一个类AccountThread继承一个线程类重写线程地run方法,通过构造方法来传递账户对象执行他地取款操作,线程执行。
package 多线程.线程安全;
public class AccountThread extends Thread {
//两个线程必须共享一个账户对象
private Account account;
//通过构造方法来传递账户对象
public AccountThread(Account account){
this.account = account;
}
@Override
public void run() {
//run方法的执行表示取款
double money = 5000;
//多线程并发执行这个方法(t1和t2两个栈)
account.withdraw(money);
System.out.println(Thread.currentThread().getName() + "余额:" + account.getBalance());
}
}
- 测试类测试,在测试类创建两个线程t1与t2进行执行,同一个账户地取款操作来模拟多线程并发。
package 多线程.线程安全;
public class Test01 {
public static void main(String[] args) {
//创建账户对象(只有一个)
Account account = new Account("001",10000);
//创建了两个线程,共享一个账户
AccountThread accountThread1 = new AccountThread(account);
AccountThread accountThread2 = new AccountThread(account);
//设置name
accountThread1.setName("t1");
accountThread2.setName("t2");
//启动取款
accountThread1.start();
accountThread2.start();
}
}
这里由于线程睡眠,模拟线程并发,001账户原来有10000元,线程t1先取款5000元,但是突然网络超时了(线程睡眠),但是t1线程钱还没取完,这时候t2线程抢占cpu开始取款,但是t1线程还没有取款,t2还是从10000元开始取,这就导致t1与t2的余额有误。实际账户的里面没有钱了,但是对应的账户的显示还有余额。
解决线程安全的方法: synchronized-线程同步
synchronized-线程同步 (线程同步锁锁的是对象而不是代码)
《锁的是对象,是通过对象头markword上面的两位来控制是不是加了锁,加了什么类型的锁》
如何解决线程安全问题呢?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?
线程排队执行。(不能并发)。用排队执行解决线程安全问题。这种机制被称为:线程同步机制。
专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。
线程同步就是线程排队了,线程排队了就会 牺牲一部分效率 ,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
注意:日常开发中要有争对性的进行异步处理,像统一插入数据库是可以支持线程异步的,但是当多线程异步的同时伴随这数据的修改,这样就会导致其他线程执行的数据可能会被另一个线程进行修改从而导致并发修改异常和线程安全问题,这样就可以使用线程同步机制。
线程同步机制的语法是:
synchronized(){
// 线程同步代码块。
}
synchronized后面小括号() 中传的这个“数据”是相当关键的。这个数据必须是 多线程共享
的数据。才能达到多线程排队。
() 中写什么?
那要看你想要哪些线程同步?
上述中出现的线程不安全问题有t1,t2,有2个线程。
- 你只需要在()中传入t1 t2共享的对象,这里的共享对象是:账户对象。
- 两个线程拥有相同的账户对象是共享的,那么this就是账户对象!!!
- ()不一定是this,这里只要是多线程共享的那个对象就行。
如果希望指定的线程同步,你只希望t1 t2 t3排队,t4 t5不需要排队,怎么办?
要在()中传入t1 t2 t3共享的对象,这个对象对于t4 t5不是共享的。
线程不安全解决方案示例:
package 多线程.线程安全;
/*
不使用线程同步机制,多线程对同一个账户进行取款,出现了线程安全问题
*/
public class Account {
private String acton;//账户
private double balance;//余额
public Account() {
}
public Account(String acton, double balance) {
this.acton = acton;
this.balance = balance;
}
public String getActon() {
return acton;
}
public double getBalance() {
return balance;
}
public void setActon(String acton) {
this.acton = acton;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
public void withdraw(double money){
//账户对象是共享的,this就是账户对象
synchronized (this){
//取款之前的余额
double before = this.balance;
//取款之后的余额
double after = before - money;
//模拟一下网络延迟,100%初出问题
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setBalance(after);
}
// //取款之前的余额
// double before = this.balance;
// //取款之后的余额
// double after = before - money;
// //模拟一下网络延迟,100%初出问题
// try {
// Thread.sleep(1000*5);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// //更新余额
// this.setBalance(after);
}
}
快看,现在的数据准确了啊,因为加了 synchronized原因,现在t1线程在执行过程中睡眠了5s,现在t1这个线程进入到阻塞状态,阻塞状态会放弃之前占有的CPU。而t2这个现在并不会抢占CPU,而是等待t1执行完毕的时候,才进行执行,这中线程同步机制解决的线程安全的问题。
线程安全的执行原理(锁)
其实在java语言中,任何一个对象都有一把“锁”,其实这把锁就是标记(只是把它叫做锁)
- 假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先有一个后
- 假设t1先执行了,遇到 synchronized,这时候自动找后面共享对象的对象锁
- 找到之后,并占有这把锁,然后执行同步代码块( synchronized(){ “同步代码快”} )中的程序,在执行过程中一直都是占有这把锁的,直到同步代码块代码结束,这把锁才会释放。
- 假设t1已经占有这把锁,此时t2也遇到了 synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有
- 于是,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束,t1会归还这把锁,此时t2等到这把锁,t2占有这把锁之后,进入同步代码块执行程序
这样就达到了线程排队执行
这里需要注意的是:
共享的对象一定是要选好了。这个共享的对象一定是你需要排队执行的这些线程对象共享的。
synchronized (this){
//取款之前的余额
double before = this.balance;
//取款之后的余额
double after = before - money;
//模拟一下网络延迟
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新余额
this.setBalance(after);
}
如何扩大安全同步的范围
扩大线程的同步范围,由原来的在账户类中的线程同步扩大到线程类中调用run中把withdraw方法进行同步方法执行账户取款的范围。扩大了同步范围,效率更低
package 多线程.线程安全;
public class AccountThread extends Thread {
//两个线程必须共享一个账户对象
private Account account;
//通过构造方法来传递账户对象
public AccountThread(Account account){
this.account = account;
}
@Override
public void run() {
//run方法的执行表示取款
double money = 5000;
//多线程并发执行这个方法(t1和t2两个栈)
//扩大线程同步范围
synchronized (account){
account.withdraw(money);
}
System.out.println(Thread.currentThread().getName() + "余额:" + account.getBalance());
}
}
在实例方法上使用synchronized
在实例方法中加入synchronized
synchronized 出现在实例方法上。一定锁的是this,所以这种方式不灵活;
缺点:
- synchronized出现在实例方法上,表示整个方法都需要同步,可能会无故扩大同步的范围,可能会导致程序的效率降低,所以这种方式不常用
好处:
- 代码写的比较少,节俭了。
package ThreadSafe3;
public class Account {
private String acton;//账户
private double balance;//余额
Object object = new Object();//实例变量(Account是多线程共享的,Account中的实例变量object也是共享的)
public Account() {
}
public Account(String acton, double balance) {
this.acton = acton;
this.balance = balance;
}
public String getActon() {
return acton;
}
public double getBalance() {
return balance;
}
public void setActon(String acton) {
this.acton = acton;
}
public void setBalance(double balance) {
this.balance = balance;
}
//取款方法
//在实例方法中加入synchronized
public synchronized void withdraw(double money){
double before = this.balance;
double after = before - money;
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
- 如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式
- 如果使用局部变量的话,建议使用StringBuilder,因为局部变量不存在线程安全问题,选择StringBuffer效率便较低。
- ArrayList是非线程安全的,Vector是线程安全的
- HshMap、HashSet是非线程安全的 ,HashTable是线程安全的。(后面会讲到为什么是线程安全的)
开发中如何解决线程安全问题
是一上来就选择线程同步吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。
- 第一种方案:尽量使用局部变量代替"实例变量和静态变量"。
- 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)
- 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制 。