2.脏读与sync关键字细节问题

脏读

对于对象的同步和异步方法,我们在设计自己程序员的时候,一定要考虑问题的整体性,不然就会出现数据不一致的错误,很经典的错误就是脏读。也就是数据的一致性问题。存在多个线程工作(主线程,子线程),当一个线程(子线程)在设置值的时候,比如这个过程需要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();
	}
	

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值