脏读
对于对象的同步和异步方法,我们在设计自己程序员的时候,一定要考虑问题的整体性,不然就会出现数据不一致的错误,很经典的错误就是脏读。也就是数据的一致性问题。存在多个线程工作(主线程,子线程),当一个线程(子线程)在设置值的时候,比如这个过程需要2秒,在1s时其他线程进来获取值了。此时就产生了脏读,读取的数据是不正确的。
错误现象:因为一个用了同步,一个没用同步,这实际上是一套关联的业务(整体性),别人还没改完,就不能读。现象是先出来get,一秒之后出set。
解决的方法也比较简单,两个方法同时加上同步关键字。这样就不会出现脏读了,常规操作就是get/set方法需要加上关键字可以防止脏读这也是一个思路,原理就是实现读写互斥。
package com.bjsxt.base.sync004;
/**
* 业务整体需要使用完整的synchronized,保持业务的原子性。
* @author alienware
*
*/
public class DirtyRead {
private String username = "bjsxt";
private String password = "123";
public synchronized void setValue(String username, String password){
this.username = username;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
System.out.println("setValue最终结果:username = " + username + " , password = " + password);
}
public void getValue(){
System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
}
public static void main(String[] args) throws Exception{
final DirtyRead dr = new DirtyRead();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
dr.setValue("z3", "456");
}
});
t1.start();
Thread.sleep(1000);
dr.getValue();
}
}
扩展数据库一致性的例子(oracle)
数据库的中很多数据1000W。9点发了一条查询语句,需要到9.10分才能查到那条数据(结果值为100)。但是在9.05分执行了一条DML语句(update),把值改为了200.请问select语句返回的结果是?
返回的结果一定是100。oracle数据库有一致性的概念(一致性读),9点的查询语句,永远看到的是9点那一瞬间的结果。(避免了脏读)oracle中有undo的概念,好比日志,当客户端执行dml(数据修改语句)语句时,会把老值放入undo,然后修改,如果失败,则从undo中回滚。9点的那天查询语句,发现值对应的undo有变化(现在值和undo不一样)了,就会去undo里面找老值。如果能找到,就返回,找不到就抛异常。宁肯抛异常,也不会把新值200给客户端看、这个抛出的异常叫做snapshot too old的经典异常(快照太旧)。这个问题体现了oracle一致性读的特性。
锁重入的问题
关键字sync有锁重入的功能,也就是在使用sync时,当一个线程得到了一个对象的锁之后,再次请求此对象时可以再次得到该对象的锁。
/**
* synchronized的重入
*
*/
public class SyncDubbo1 {
public synchronized void method1(){
System.out.println("method1..");
method2();
}
public synchronized void method2(){
System.out.println("method2..");
method3();
}
public synchronized void method3(){
System.out.println("method3..");
}
public static void main(String[] args) {
final SyncDubbo1 sd = new SyncDubbo1();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
sd.method1();
}
});
t1.start();
}
}
除了上面这个案例还可以使用父子嵌套重入锁,父类有个sync方法,子类有个sync方法,子类中的sync方法调用了父类的sync方法,这种也叫作重入锁,利用sync关键字,也是线程安全的。
如果有一个没用用关键字修饰,就是线程不安全的。
package com.bjsxt.base.sync005;
/**
* synchronized的重入
* @author alienware
*
*/
public class SyncDubbo2 {
static class Main {
public int i = 10;
public synchronized void operationSup(){
try {
i--;
System.out.println("Main print i = " + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Sub extends Main {
public synchronized void operationSub(){
try {
while(i > 0) {
i--;
System.out.println("Sub print i = " + i);
Thread.sleep(100);
this.operationSup();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Sub sub = new Sub();
sub.operationSub();
}
});
t1.start();
}
}
异常释放锁的机制
被sync修饰的方法遇到异常,会释放锁,此时其他线程会进入。这种情况如果多个任务不相关还好。
但是一旦有业务关系,就需要妥善处理(记录日志是必须的)。不处理的话可以加continue让他继续。如果出错了,后面一定不能执行,就抛出一个运行时异常,让他停止。
package com.bjsxt.base.sync005;
/**
* synchronized异常
* @author alienware
*
*/
public class SyncException {
private int i = 0;
public synchronized void operation(){
while(true){
try {
i++;
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " , i = " + i);
if(i == 20){
//Integer.parseInt("a");
throw new RuntimeException();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final SyncException se = new SyncException();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
se.operation();
}
},"t1");
t1.start();
}
}
sycn减小锁的粒度
使用sync声明的方法在某些情况下是有弊端的,比如A线程调用同步的方法执行一个很长时间的任务,那么B线程就必须等待比较长的时间才能执行,这样的情况可以使用sync代码块去优化代码执行的时间,也就是通常所说的减小锁的粒度。
package com.bjsxt.base.sync006;
/**
* 使用synchronized代码块减小锁的粒度,提高性能
*
*/
public class Optimize {
public void doLongTimeTask(){
try {
System.out.println("当前线程开始:" + Thread.currentThread().getName() +
", 正在执行一个较长时间的业务操作,其内容不需要同步");
Thread.sleep(2000);
synchronized(this){
System.out.println("当前线程:" + Thread.currentThread().getName() +
", 执行同步代码块,对其同步变量进行操作");
Thread.sleep(1000);
}
System.out.println("当前线程结束:" + Thread.currentThread().getName() +
", 执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final Optimize otz = new Optimize();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
otz.doLongTimeTask();
}
},"t2");
t1.start();
t2.start();
}
}
对象锁
上述是一种使用当前对象加锁的方式,事实上sync可以使用任意的object对象来加锁。
/**
* 使用synchronized代码块加锁,比较灵活
* @author alienware
*
*/
public class ObjectLock {
public void method1(){
synchronized (this) { //对象锁
try {
System.out.println("do method1..");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2(){ //类锁
synchronized (ObjectLock.class) {
try {
System.out.println("do method2..");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private Object lock = new Object();
public void method3(){ //任何对象锁
synchronized (lock) {
try {
System.out.println("do method3..");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ObjectLock objLock = new ObjectLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method1();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method2();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
objLock.method3();
}
});
t1.start();
t2.start();
t3.start();
}
}
类锁和对象锁不在此过多赘述,任意对象锁,要实现加锁,必须是对同一个对象进行操作。虽然是三种类型的锁的演示,看不出来锁的优先级,看不出来有任何先后顺序,就是简单的演示。
String锁的问题
需要注意的是避免使用string类型加锁。string常量在常量池中只有一个引用(相当于一把锁被一直循环持有),是可以实现加锁的效果的。但是如果new 一个string,就是一个对象,就可以都进来(因为是两个对象锁,不互相影响)。
/**
* synchronized代码块对字符串的锁,注意String常量池的缓存功能
* @author alienware
*
*/
public class StringLock {
public void method() {
//new String("字符串常量")
synchronized ("字符串常量") {
try {
while(true){
System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
Thread.sleep(1000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final StringLock stringLock = new StringLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
stringLock.method();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
stringLock.method();
}
},"t2");
t1.start();
t2.start();
}
}
锁改变的问题
要注意锁改变的问题,如果用的字符串变量做的锁,千万不要修改这个字符串。如果修改了就起不到加锁的作用了。
package com.bjsxt.base.sync006;
/**
* 锁对象的改变问题
* @author alienware
*
*/
public class ChangeLock {
private String lock = "lock";
private void method(){
synchronized (lock) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + "开始");
lock = "change lock";
Thread.sleep(2000);
System.out.println("当前线程 : " + Thread.currentThread().getName() + "结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final ChangeLock changeLock = new ChangeLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
changeLock.method();
}
},"t2");
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
虽然对于string变量改变之后锁就不生效了,但是对于object的set方法是没有影响的,因为在内存中是一块固定的空间。如对user对象加锁。
死锁
简单说明一下死锁问题:两个锁。两个线程,一个线程持有一把锁,然后去竞争对方的锁。形成了死锁。
package com.bjsxt.base.sync006;
/**
* 死锁问题,在设计程序时就应该避免双方相互持有对方的锁的情况
* @author alienware
*
*/
public class DeadLock implements Runnable{
private String tag;
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public void setTag(String tag){
this.tag = tag;
}
@Override
public void run() {
if(tag.equals("a")){
synchronized (lock1) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock1执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock2执行");
}
}
}
if(tag.equals("b")){
synchronized (lock2) {
try {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock2执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("当前线程 : " + Thread.currentThread().getName() + " 进入lock1执行");
}
}
}
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock();
d1.setTag("a");
DeadLock d2 = new DeadLock();
d2.setTag("b");
Thread t1 = new Thread(d1, "t1");
Thread t2 = new Thread(d2, "t2");
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}