线程基础 (三) synchronized锁 和 代码块锁

  • 线程安全:当多个线程访问某一个类(对象或方法时),这个类始终都能表现出正确的行为,这个类就是线程安全的
  • synchronized:可以在任意对象或方法上加锁,加锁的这段代码叫做互斥区或者临界区

例子:

public class MyThread extends Thread{
	private int count=0;
	@Override
	public void run() {
		count++;
		System.out.println(this.currentThread().getName()+"count:"+count);
	}
	
	public static void main(String[] args) {
		MyThread thread= new MyThread();
		Thread t1 = new Thread(thread, "线程1");
		Thread t2 = new Thread(thread, "线程2");
		Thread t3 = new Thread(thread, "线程3");
		Thread t4 = new Thread(thread, "线程4");
		Thread t5 = new Thread(thread, "线程5");
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
	}
}

运行结果:

首先代码没有得到我们预期的结果:12345。另外多次执行该代码时每次得到的结果不一致,所以该方法线程不安全

我们尝试改动上述代码

public synchronized void run() {
		count++;
		System.out.println(this.currentThread().getName()+"count:"+count);
	}

在run方法上加上synchronized关键字,得到我们预期运行结果,且每次运行结果一致,线程安全

  • 关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,
  •  所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock)
  •  在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)
  • 关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,这里如果是把一段代码或方法(函数)当作锁,其实获取的也是对象锁,只是监视器(对象)不同而已,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。但是这有个前提:既然锁叫做对象锁,那么势必和对象相关,所以多个线程访问的必须是同一个对象。

    如果多个线程访问的是多个对象,那么Java虚拟机就会创建多个锁

public class MultiThread {
	private static int num=0;
	
	public static synchronized void printNum(String tag){
		if(tag.equals("a")){
			num=100;
			System.out.println("a执行完毕");
		}else{
			num=200;
			System.out.println("b执行完毕");
		}
		System.out.println(tag+"--num--"+num);
	}
	
	public static void main(String[] args) {
		MultiThread m1 = new MultiThread();
		MultiThread m2 = new MultiThread();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				m1.printNum("a");
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				m2.printNum("b");
			}
		});
		t1.start();
		t2.start();
	}

同步异步

  • 同步:synchronized 同步的概念就是共享,如果没有共享的数据就没有必要同步
  • 异步:asynchronized 异步的概念就是独立,相互之间不受任何制约,就像AJAX在处理请求的同时,我们依然可以访问页面

例子:

/**
 * 对象锁的同步和异步问题 
 */
public class SychronizedThread  {
	//同步方法
	public synchronized void method1(){
		System.out.println(Thread.currentThread().getName());
		try {
			//休眠4秒
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	//非同步方法
	public synchronized void method2(){
		System.out.println(Thread.currentThread().getName());
	}
	
	public static void main(String[] args) {
		SychronizedThread s = new SychronizedThread();
		//线程1
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.method1();
			}
		},"线程1");
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.method2();
			}
		},"线程2");
		t1.start();
		t2.start();
	}
线程1
线程2

如果method2前面不加synchronized字段修饰,最终线程1,线程2会同时输出,因为两个方法互不影响,异步

如果method2前面加synchronized修饰,则两个方法之间同步,线程1打印后会停顿4秒再继续打印线程2

t1线程先持有object对象的Lock锁,t2线程可以以异步的方式调用对象中的非synchronized修饰的方法
t1线程先持有object对象的Lock锁,t2线程如果在这个时候调用对象中的同步(synchronized)方法则需等待,也就是同步

脏读

在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的。注意这里 局部变量是不存在脏读的情况

/**
 * 脏读
 */
public class DirtyRead {
	private String username="xuxu";
	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 synchronized void getValue(){
		System.out.println("getValue--username:"+this.username+"password"+this.password);
	}
	
	public static void main(String[] args) {
		DirtyRead dr = new DirtyRead();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				dr.setValue("李四", "123456");
			}
		});
		t1.start();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		dr.getValue();
		
	}

如果getValue()方法前没有加synchronized关键字

则会产生脏读

业务整体需要使用完整的synchronized,保持业务的原子性。

锁重入

当一个线程获取一个对象的锁后,再次请求该对象,是可以再次获取该对象的锁

/**
 * 锁重入
 */
public class SyncDubbo {
	public synchronized void method1(){
		System.out.println("method1");
		method2();
	}
	public synchronized void method2(){
		System.out.println("method3");
		method3();
	}
	public synchronized void method3(){
		System.out.println("method3");
	}
	public static void main(String[] args) {
		SyncDubbo td = new SyncDubbo();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				td.method1();
			}
		});
		t1.start();
	}
}
public class SyncDubbo02 {
	
	static class Sup{
		public int count=10;
		public synchronized void opearationSup(){
			count--;
			System.out.println("sup:"+count);
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	static class Sub extends Sup{
		public synchronized void opearationSub(){
			while(count>0){
				count--;
				System.out.println("sub:"+count);
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				opearationSup();
			}
		}
	}
	
	public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				Sub s =new Sub();
				s.opearationSub();
			}
		});
		t1.start();
	}
}

出现异常,锁自动释放

要处理异常不然会对业务逻辑产生错误

要么记录异常,要么直接终止



使用synchronized代码块加锁

为什么要使用synchronized代码块加锁

直接在方法上面加锁在某些情况下还是有很大的弊端,因为当一个A线程执行一个同步方法的时候如果需要耗费很多时间,则另一个线程如果也要执行这个方法会等待很长的时间。可以使用代码块加锁优化时间

/**
 * 同步块
 */
public class SynchronizedBlock {
	public void method1(){
		//非同步代码 执行时间短
		for (int i = 0; i <= 50; i++) {
			System.out.println("耗时短的方法:----"+Thread.currentThread().getName()+"="+i);
		}
		//同步代码,需要执行时间长
		synchronized(this){
			for (int j = 0; j <= 10; j++) {
				System.out.println("耗时长的方法:++++"+Thread.currentThread().getName()+"="+j);
			}
		}
	}
	
	public static void main(String[] args) {
		SynchronizedBlock s = new SynchronizedBlock();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.method1();
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.method1();
			}
		});
		t1.start();
		t2.start();
	}
}

结果

第一部分

耗时短的方法:----Thread-0=49
耗时短的方法:----Thread-0=50
耗时长的方法:++++Thread-0=0
耗时长的方法:++++Thread-0=1
耗时短的方法:----Thread-1=28
耗时长的方法:++++Thread-0=2

当t0线程访问对象的synchronized代码块的时候,t1线程依然可以访问对象方法中其余非synchronized块的部分,第一部分的执行结果证明了这一点

耗时长的方法:++++Thread-0=6
耗时长的方法:++++Thread-0=7
耗时长的方法:++++Thread-0=8
耗时长的方法:++++Thread-0=9
耗时长的方法:++++Thread-0=10
耗时长的方法:++++Thread-1=0
耗时长的方法:++++Thread-1=1
耗时长的方法:++++Thread-1=2
耗时长的方法:++++Thread-1=3

当t0线程进入对象的synchronized代码块的时候,t1线程如果要访问这段synchronized块,那么访问将会被阻塞,第二部分的执行结果证明了这一点

所以,从执行效率的角度考虑,有时候我们未必要把整个方法都加上synchronized,而是可以采取synchronized块的方式,对会引起线程安全问题的那一部分代码进行synchronized就可以了


两个synchronized块之间具有互斥性

/*
 * synchronized块之间具有互斥性
 */
public class SynchronizedBlock2 {
	public void methodA(){
		synchronized(this){
			for (int i = 0; i <10; i++) {
				System.out.println(Thread.currentThread().getName()+"--"+i);
			}
		}
	}
	
	public void methodB(){
		synchronized(this){
			for (int i = 0; i <10; i++) {
				System.out.println(Thread.currentThread().getName()+"++"+i);
			}
		}
	}
	
	public static void main(String[] args) {
		SynchronizedBlock2 s = new SynchronizedBlock2();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodA();
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodB();
			}
		});
		t1.start();
		t2.start();
	}
}
Thread-0--6
Thread-0--7
Thread-0--8
Thread-0--9
Thread-1++0
Thread-1++1
Thread-1++2
Thread-1++3
Thread-1++4
Thread-1++5

对于MethodB()方法synchronized块的访问必须等到对于MethodA()方法synchronized块的访问结束之后。那其实这个例子,我们也可以得出一个结论:synchronized块获得的是一个对象锁,换句话说,synchronized块锁定的是整个对象

同样synchronized代码块锁  和 同步方法也是互斥的

总结一下前面的内容:

1、synchronized同步方法

(1)对其他synchronized同步方法或synchronized(this)同步代码块呈阻塞状态

(2)同一时间只有一个线程可以执行synchronized同步方法中的代码

2、synchronized同步代码块

(1)对其他synchronized同步方法或synchronized(this)同步代码块呈阻塞状态

(2)同一时间只有一个线程可以执行synchronized(this)同步代码块中的代码

 

使用任意对象作为锁

前面都使用synchronized(this)的格式来同步代码块,其实Java还支持对"任意对象"作为对象监视器来实现同步的功能。这个"任意对象"大多数是实例变量方法的参数

修改上面的代码

public class SynchronizedBlock2 {
	private String ss = new String();
	
	public void methodA(){
		synchronized(ss){
			for (int i = 0; i <10; i++) {
				System.out.println(Thread.currentThread().getName()+"--"+i);
			}
		}
	}
	
	public void methodB(){
		synchronized(ss){
			for (int i = 0; i <10; i++) {
				System.out.println(Thread.currentThread().getName()+"++"+i);
			}
		}
	}
	
	public static void main(String[] args) {
		SynchronizedBlock2 s = new SynchronizedBlock2();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodA();
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodB();
			}
		});
		t1.start();
		t2.start();
	}
}

将synchronized(this) 换成 ss 同样只有一个方法结束时另一个才能执行

Thread-1++5
Thread-1++6
Thread-1++7
Thread-1++8
Thread-1++9
Thread-0--0
Thread-0--1
Thread-0--2
Thread-0--3

这个例子证明了:多个线程持有"对象监视器"为同一个对象的前提下,同一时间只能有一个线程可以执行synchronized(非this对象x)代码块中的代码。

锁非this对象具有一定的优点:如果在一个类中有很多synchronized方法,这时虽然能实现同步,但会受到阻塞,从而影响效率。但如果同步代码块锁的是非this对象,则synchronized(非this对象x)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,大大提高了运行效率。

锁的是当前这个线程,针对锁的对象的这段代码或方法,一次只能一个线程运行,其它线程运行到此的话会暂停,如果是执行其它非锁的则是异步的,注意这里不要被多线程搞迷糊了。单个线程执行的时候都是同步的,当这个线程被阻塞后,之后的代码(锁内的和锁外的)无论什么都不会执行,只有当唤醒或者恢复正常时才会继续往下走,走完锁内的代码就会放锁,然后继续走剩余的代码

 

注意一下"private String anyString = new String();"这句话,现在它是一个全局对象,因此监视的是同一个对象。如果移到try里面,那么对象的监视器就不是同一个了,调用的时候自然是异步调用,可以自己试一下。

最后提一点,synchronized(非this对象x),这个对象如果是实例变量的话,指的是对象的引用,只要对象的引用不变,即使改变了对象的属性,运行结果依然是同步的

细化synchronized(非this对象x)的三个结论

synchronized(非this对象x)格式的写法是将x对象本身作为对象监视器,有三个结论得出:

1、当多个线程同时执行synchronized(x){}同步代码块时呈同步效果

2、当其他线程执行x对象中的synchronized同步方法时呈同步效果

3、当其他线程执行x对象方法中的synchronized(this)代码块时也呈同步效果

第一点很明显,第二点和第三点意思类似,无非一个是同步方法,一个是同步代码块罢了,举个例子验证一下第二点:

public class MyObject {
	public synchronized void methodA(){
		for (int i = 0; i <10; i++) {
			System.out.println(Thread.currentThread().getName()+"++"+i);
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

创建一个MyObject类 其中实现同步方法methodA

将MyObject实例通过构造函数传入SynchronizedBlock3类 作为同步代码块锁对象

public class SynchronizedBlock3 {
	
	private MyObject mo;
	
	public SynchronizedBlock3(MyObject mo){
		this.mo=mo;
	}
	
	public void methodB(){
		synchronized(mo){
			for (int i = 0; i <10; i++) {
				System.out.println(Thread.currentThread().getName()+"--"+i);
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	public static void main(String[] args) {
		MyObject mo = new MyObject();
		SynchronizedBlock3 s = new SynchronizedBlock3(mo);
		
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				mo.methodA();
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodB();
			}
		});
		t1.start();
		t2.start();
	}
}
Thread-0++6
Thread-0++7
Thread-0++8
Thread-0++9
Thread-1--0
Thread-1--1
Thread-1--2
Thread-1--3
Thread-1--4

methodB在等到methodA执行完毕以后才执行,是同步操作 成功验证第二点


锁定类静态方法和锁定类.Class和数据String的常量池特性

同步静态方法

/**
 * 静态方法锁(类锁)
 */
public class SynchronizedStatic {
	
	public synchronized static void methodA(){
		System.out.println("A开始执行");
		System.out.println("A结束执行");
	}
	public synchronized static void methodB(){
		System.out.println("B开始执行");
		System.out.println("B结束执行");
	}
	public synchronized void methodC(){
		System.out.println("C开始执行");
		System.out.println("C结束执行");
	}
	
	public static void main(String[] args) {
		
		SynchronizedStatic s = new SynchronizedStatic();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodA();
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodB();
			}
		});
		Thread t3 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodC();
			}
		});
		t1.start();
		t2.start();
		t3.start();
	}
}
A开始执行
C开始执行
C结束执行
A结束执行
B开始执行
B结束执行

上面的代码中,A,B方法属于静态方法锁,对方法C的调用和方法A方法B是异步的,说明了静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁

同步类.Class

同样上面的例子修改为

/**
 * Class锁(类锁)
 */
public class SynchronizedClass {
	
	public void methodA(){
		synchronized(SynchronizedClass.class){
			System.out.println("A开始执行");
			System.out.println("A结束执行");
		}
	}
	public synchronized static void methodB(){
		System.out.println("B开始执行");
		System.out.println("B结束执行");
	}
	public synchronized static void methodC(){
		System.out.println("C开始执行");
		System.out.println("C结束执行");
	}
	
	public static void main(String[] args) {
		
		SynchronizedClass s = new SynchronizedClass();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodA();
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodB();
			}
		});
		Thread t3 = new Thread(new Runnable() {
			@Override
			public void run() {
				s.methodC();
			}
		});
		t1.start();
		t2.start();
		t3.start();
	}
}
B开始执行
B结束执行
A开始执行
A结束执行
C开始执行
C结束执行

多次运行 3个方法同步执行证明  静态方法  和静态类.class 锁 都是一样获取的类锁

数据String的常量池特性(需要避免

在使用同步的时候,如果要锁定String , 两个线程运行的时候,很可能有一个无限等待无法获取到锁

可以采用Object o = new Object()的方式创建一个锁

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值