浅谈synchronized

一、 并发编程中的三个问题

(注:本博文中多线程编程使用了Lambda表达式来编写,如果读者对于Lambda表达式不了解,可以看博主的这篇博文Lambda表达式

1.可见性

可见性(Visibility):是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。

案例演示:一个线程根据boolean类型的标记flag, while循环,另一个线程改变这个flag变量的值,另一个线程并不会停止循环。

public class SeeOkDemo {

	public static void main(String[] args) throws InterruptedException {
		// 共享资源类
		DataSource dataSource = new DataSource();
		// 第一个线程
		new Thread(() -> {
			// 在第二个线程修改资源类的内容后,第一个线程不能感知到
			while (dataSource.flag) {
				// 只要flag是true,那么这个线程就一直在这里转
			}
			System.out.println("2s后~~");
		}).start();

		// 主线程休眠2秒
		Thread.sleep(2000);

		// 第二个线程,修改资源类的内容
		new Thread(() -> {
			dataSource.changeFlagToFalse();
		}).start();

	}
}

//资源类
class DataSource {
	boolean flag = true;

	// 对flag属性进行修改
	public void changeFlagToFalse() {
		this.flag = false;
		System.out.println(Thread.currentThread().getName() + "\t 我把falg修改为flase~~");
	}
}

运行结果:程序一直在线程1的while循环中,可见在多线程并发编程时,会出现可见性问题,当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。
在这里插入图片描述

2.原子性

原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。

案例演示:5个线程各执行1000次 i++;

public class AutomicDemo {
	static int number = 0;

	public static void main(String[] args) throws InterruptedException {
		// 共享资源类
		DataSource2 source = new DataSource2();
		//开启5个线程同时执行++的操作
		for (int i = 1; i <= 5; i++) {
			new Thread(() -> {
				source.numberAdd1000();
			}, String.valueOf(i)).start();
		}

		// 当除了主线程后台的GC线程之外,还有其他子线程,那么主线程yield
		while (Thread.activeCount() > 2) {
			Thread.yield();
		}
		// 其他子线程执行完毕输出number
		System.out.println(source.number);
	}
}

//资源类
class DataSource2 {
	int number = 0;

	public void numberAdd1000() {
		for (int i = 0; i < 1000; i++) {
			this.number++;
		}
	}
}

运行结果:3611小于预期的5000,在多线程并发的情况下确实存在原子性的问题
在这里插入图片描述
反汇编:使用javap反汇编class文件,得到下面的字节码指令(截取部分)

9: getstatic #12 // Field number:I
12: iconst_1
13: iadd
14: putstatic #12 // Field number:I

分析:由此可见number++是由多条语句组成,以上多条指令在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出现问题。比如一个线程在执行13: iadd时,另一个线程又执行9: getstatic。会导致两次number++,实际上只加了1。

小结: 并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。

3.有序性

有序性(Ordering):是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

public static void main(String[] args) {
	int a = 10;
	int b = 20;
}

即在实际运行中,程序可能是先执行 int b =20;这个语句,而后再执行 int a = 10的语句,前提是两个语句之间没有依赖关系。

小结: 程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。

二、Java内存模型

  1. Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
  2. Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。
  • 主内存

    主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。

  • 工作内存

    每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。
    在这里插入图片描述
    Java内存模型的作用:Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。

小结: Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。

三、主内存与工作内存之间的交互

在这里插入图片描述
Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
对应如下的流程图:
在这里插入图片描述
注意:

  1. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值
  2. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中

小结
主内存与工作内存之间的数据交互过程

lock -> read -> load -> use -> assign -> store -> write -> unlock

四、synchronized保证三大特性

1.synchronized与原子性

使用synchronized保证原子性

package synchronizedProject;

public class AutomicDemo {
	static int number = 0;

	public static void main(String[] args) throws InterruptedException {
		// 共享资源类
		DataSource2 source = new DataSource2();
		// 开启5个线程进行++操作
		for (int i = 1; i <= 5; i++) {
			new Thread(() -> {
				source.numberAdd1000();
			}, String.valueOf(i)).start();
		}

		// 当除了主线程后台的GC线程之外,还有其他子线程,那么主线程yield
		while (Thread.activeCount() > 2) {
			Thread.yield();
		}
		// 其他子线程执行完毕输出number
		System.out.println(source.number);
	}
}

//资源类
class DataSource2 {
	int number = 0;

	public void numberAdd1000() {
		for (int i = 0; i < 1000; i++) {
			// 添加synchronized代码块
			synchronized (DataSource2.class) {
				this.number++;
			}
		}
	}
}

运行结果: 可见添加了synchronized代码块后解决了多线程并发的可见性问题
在这里插入图片描述
原理: 对number++;增加同步代码块后,保证同一时间只有一个线程操作number++;。就不会出现安全问题。

小结: synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

2.synchronized与可见性

使用synchronized保证可见性

package synchronizedProject;

public class SeeOkDemo {

	public static void main(String[] args) throws InterruptedException {
		// 共享资源类
		DataSource dataSource = new DataSource();
		// 第一个线程
		new Thread(() -> {
			// 在第二个线程修改资源类的内容后,第一个线程不能感知到
			while (dataSource.flag) {
				// 在while循环里添加synchronized代码块
				synchronized (dataSource) {

				}
			}
			System.out.println("2s后~~");
		}, "线程1").start();

		// 主线程休眠2秒
		Thread.sleep(2000);

		// 第二个线程,修改资源类的内容
		new Thread(() -> {
			dataSource.changeFlagToFalse();
		}, "线程2").start();

	}
}

//资源类
class DataSource {
	boolean flag = true;

	// 对flag属性进行修改
	public void changeFlagToFalse() {
		this.flag = false;
		System.out.println(Thread.currentThread().getName() + "\t 我把falg修改为flase~~");
	}
}

运行结果: 2s后~ 打印出来了,说明跳出了while循环,可见synchronized解决了可见性的问题
在这里插入图片描述
原理: synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值
结合下图理解:
在这里插入图片描述

3.synchronized与有序性

synchronized保证有序性的原理:synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。

小结:synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性

四、synchronized的特性

1.可重入特性

一个线程可以多次执行synchronized,重复获取同一把锁。

package synchronizedProject;

/**
 * synchronized是可重入锁
 * 
 * @author 阿楠
 *
 */
public class ReentrantDemo {
	public static void main(String[] args) {
		// 共享资源类对象
		Resource resource = new Resource();
		// 线程1
		new Thread(() -> {
			resource.test();
		}, "线程1").start();

	}

	// test2()方法,内部带有synchronized代码块
	public static void test2() {
		synchronized (Resource.class) {
			System.out.println(Thread.currentThread().getName() + "进入同步代码块2");
		}
	}
}

//资源类
class Resource {
	public void test() {
		// test()方法内部有synchronized代码块
		synchronized (this) {
			System.out.println(Thread.currentThread().getName() + "进入同步代码块1");
			// 执行test2()方法,里面也有同步代码块
			ReentrantDemo.test2();
		}
	}

}

运行结果:线程1依次进入了带有synchronized代码块的逻辑方法中。
在这里插入图片描述

可重入的原理: synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.

可重入的好处:

  1. 可以避免死锁
  2. 可以让我们更好的来封装代码

小结: synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块时,计数器的数量会-1,知道计数器的数量为0,就释放这个锁。

2.不可中断特性

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

  • synchronized属于不可被中断
  • Lock的lock方法是不可中断的
  • Lock的tryLock方法是可中断的

五、synchronized原理

我们要看synchronized的原理,但是synchronized是一个关键字,看不到源码。我们可以将class文件进行反汇编。
在这里插入图片描述
monitorenter: synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待

monitorexit

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
  3. monitorexit释放锁。monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

同步方法: 同步方法在反汇编后,会增加ACC_SYNCHRONIZED 修饰。会隐式调用monitorenter和monitorexit。

小结
通过javap反汇编我们看到synchronized使用编程了monitorentor和monitorexit两个指令.每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数,当执行到monitorexit时,recursions会-1,当计数器减到0时这个线程就会释放锁

面试题:synchronized与Lock的区别

  1. synchronized是关键字,而Lock是一个接口。
  2. synchronized会自动释放锁,而Lock必须手动释放锁。
  3. synchronized是不可中断的,Lock可以中断也可以不中断。
  4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  5. synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  6. Lock可以使用读锁提高多线程读效率。
  7. synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值