一、前述
在前面文章的学习中,我们一直都在围绕 synchronized 来讲述,在方法没有同步处理的时候,多个线程访问一个实例变量的时候,这个值已经被其他的线程更改过了,所以会出现数据脏读(dirtyRead),这个数据脏读也出现了好多次,那么本文就来看看到底是什么东西
二、数据脏读
2.1 数据脏读
线程数据脏读跟数据库的事务脏读又有一点不一样,关于事务脏读可以了解下另一篇文章:mysql 事务特性以及隔离级别
这里示例一个脏读:
User.java:
public class User {
public String username = "usernameA";
public String password = "passwordA";
synchronized public void setValue(String username, String password) {
try {
this.username = username;
Thread.sleep(5000);
this.password = password;
System.out.println("Thread Name = " + Thread.currentThread().getName() + ", Date = " + new Date() + ", setValue method , username=" + username + ", password=" + password);
} catch (Exception e) {
e.printStackTrace();
}
}
public void getValue() {
System.out.println("Thread Name = " + Thread.currentThread().getName() + ", Date = " + new Date() + ", getValue method , username = " + username + ", password = " + password);
}
}
UserThread.java:
public class UserThread extends Thread{
private User user;
public UserThread(User user){
super();
this.user = user;
}
@Override
public void run(){
user.setValue("usernameB","passwordB");
}
}
UserService:
public class UserService {
public static void main(String[] args) throws Exception{
System.out.println("Thread Start.... Thread Name = " + Thread.currentThread().getName() + ", Date = " + new Date());
User user = new User();
UserThread userThread = new UserThread(user);
userThread.start();
Thread.sleep(1000);
user.getValue();
}
}
Result:
说明:
(1)T1 时刻,开始主线程进入
(2)T2 时刻,创建的新的线程 Thread-0 启动,进入内部逻辑,将 username 赋值为 usernameB,然后开始睡眠 5s,同时 main 线程进入,开始睡眠 1s
(3)T3 时刻 = T2 时刻,两个线程同时睡眠
(4)T4 时刻,Thread-0 仍然在睡,main 唤醒往下执行代码,user 调用 getValue 输出值: `username = usernameB, password = passwordA`
(5)T5 时刻,也就是在 T4 4s 后,继续执行下面代码,将 password 赋值为 passwordB ,而后输出:`username=usernameB, password=passwordB`
由上,可以看到,main 线程读取到了 Thread-0 线程执行代码到一半的数据,造成了数据脏读
三、解决办法
解决上述脏读的办法简单有两个:
(1)Thread-0 睡眠的时候,不让 Main 线程调用这个对象的方法
(2)Thread-0 睡眠的时候,让 Main 也继续睡眠,睡眠时间直到 Thread-0 执行完毕
3.1 针对第一个办法:
在 User 类里,将 getValue 方法也加上 synchronized 同步锁,对对象锁住
User.java:
public class User {
public String username = "usernameA";
public String password = "passwordA";
synchronized public void setValue(String username, String password) {
try {
this.username = username;
Thread.sleep(5000);
this.password = password;
System.out.println("Thread Name = " + Thread.currentThread().getName() + ", Date = " + new Date() + ", setValue method , username=" + username + ", password=" + password);
} catch (Exception e) {
e.printStackTrace();
}
}
synchronized public void getValue() {
System.out.println("Thread Name = " + Thread.currentThread().getName() + ", Date = " + new Date() + ", getValue method , username = " + username + ", password = " + password);
}
}
Result:
可以看到 Thread-0 线程获取到对象锁以后, main 无法执行 getValue 只有等 Thread-0 睡眠五秒以后,唤醒重新执行下面的代码, main 线程才执行 getValue 方法,这样就不会造成数据脏读
3.2 针对第二种方法
将 main 线程的睡眠时间增加到 7s ,也就是至少要大于 Thread-0 的睡眠时间
UserService.java
public class UserService {
public static void main(String[] args) throws Exception{
System.out.println("Thread Start.... Thread Name = " + Thread.currentThread().getName() + ", Date = " + new Date());
User user = new User();
UserThread userThread = new UserThread(user);
userThread.start();
Thread.sleep(1000);
user.getValue();
}
}
Result
如图所示,增加 main 线程的睡眠时间以后,当 Thread-0 睡眠五秒以后唤醒, main 线程仍然在睡眠,因此 Thread-0 继续往下执行代赋值,第七秒时, main 线程唤醒执行 getValue 方法,输出 username = usernameB, password = passwordB
,没有出现数据脏读,是理想的结果