还在为Synchronized烦恼吗?那么请移步Java并发编程数据安全之Synchronized篇

本文摘要目录:
1 并发编程带来的数据安全
2 怎么解决数据安全
3 Java 层面Synchronized数据安全处理
4 常见面试题

1 并发编程带来的数据安全:
当变量在生命周期内可以被多个线程可见且可以被修改,就会产生数据安全的隐患
如以下例子:

public class MyThreadSafe1 implements Runnable{
	private static int i = 0 ;
	@Override
	public void run() {
		i ++;
	}
	public static void main(String[] args) throws InterruptedException {
		MyThreadSafe1 thread1 = null;
		for (int i = 0; i < 1000; i++) {
			thread1 = new MyThreadSafe1();
			new Thread(thread1).start();
		}
		Thread.sleep(3000);
		System.out.println(i);
	}
}

在这里插入图片描述
主线程的输出 为 小于等于 1000 的值
解释: 由于线程在对 i++ 操作时候,底层并不是原子性的操作,需要先从主内存中获取到 i的值,然后进行 ++ 操作后,存回到主内存中,而在存回主内存前,其它进行线程就可能取到还没有存回到主内存i 的最新值。

2 怎么解决数据安全:
问题的本质在于多线程去访问了共享的数据,所以如果我们将数据声明为非线程共享的 或者将 共享数据的访问变成线程串行访问,则可以来解决线程的安全问题。

3 Java 层面Synchronized数据安全处理:
synchronized 通过加锁使得线程变为串行访问
3.1 synchronized 关键字
Synchronized jdk1.6 之前 重量级锁 之后对锁进行优化引入其它锁的概念(偏向锁,轻量级锁)
重量级锁:使得线程进入阻塞状态,需要通过操作系统层面唤醒线程回到可以运行状态,涉及cpu 的切换非常耗性能,但是释放内存;
偏向锁,轻量级锁:不会使得线程进入阻塞状态,轻量级锁内部采用自旋锁的方式来等待获取锁(以消耗cpu 来换取 线程的非阻塞)
Synchronized 的使用粒度:
我们知道程序执行类静态方法可以使用类或者类实例进行调用,非静态方法由类的实列进行调用,方法中的代码块也与调取方法的类或者实列相关;
当我们给对象加锁(可以理解为给这个对象的内存上锁,注意 只是这块内存,其他同类对象都会有各自的内存锁),这时候在其他线程中执行该对象的这个同步方法(注意:是该对象)就会产生互斥;
当我们给类加锁 这时候,只要是这个类产生的对象,在调用这个静态方法时都会产生互斥;
(1)Synchronized 声明在实列方法上,锁对象的实例,进入方法前需要需要获得当前实列的锁

// 对象锁
  public synchronized void inc0 (){
	  num++;
  }

(2)Synchronized 声明在类方法上,锁类对象,进入方法前需要获得类锁

 //类锁
 public static synchronized void inc1 (){
	  num++;
 }

(3)Synchronized 声明在代码块中,锁类/对象,进入同步代码块前需要获得当前实列的锁/类锁

synchronized (MyThread1.class) {// 类锁
		num++;
	}

3.2 synchronized实现原理:
synchronized 是对 对象进行的加锁,而对象在hotspot 中内存中的分配可以分 为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
在这里插入图片描述
其中对象头Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等(以32位jvm 为例)。
在这里插入图片描述
当某个对象被synchronized 关键字标记为同步锁时,那么围绕锁的一系列操作都和对象头有关,线程在获取锁的时候,实际上是获得一个对象的 ,monitor,多个线程去访问同步代码的时候,相当于去争取对象监视器,抢到后修改对象上的锁标识;类锁也是通过对象锁的方式实的,Classloader加载一个类并把类型信息保存到方法区后,会创建一个Class对象,存放在堆区,此时当类锁锁住一个对象的时候,实际上锁住的是那个类的Class对象。
3.3 synchronized 锁升级过程
synchronized 在 JDK1.6 之前是直接阻塞线程,jdk1.6之后为了减少获得锁和释放锁带来的性能开销,做了一些优化引入了偏向锁、轻量级锁的概念;synchronized 在jdk1.6之后回有四种状态:无锁,偏向锁,轻量级锁,重量级锁,锁的状态根据线程的竞争的激烈程度从低到高不断升级;
在这里插入图片描述
当不同的线程执行同步代码,其中一个线程先获得锁,将锁标记为偏向锁并且执行同步代码,另外一个线程进入后通过cas来获得锁,如果获取失败则撤销偏向锁,暂停原持有偏向锁的线程,随后在一次检查原持有的偏向锁的线程是否释放锁,没有释放锁,则升级锁位轻量级锁,在一定时间和一定次数后,case操作仍然没有获取到锁,则将锁升级为重量级锁,并阻塞线程(让出cpu),当获取锁的线程执行完同步代码块释放锁后,唤醒被阻塞的线程。

3.4 显示控制线程的阻塞和唤醒
在Object 对象中,提供了wait/notify/nofifyall,可以用于控制线程的状态;
wait:持有对象锁的线程,准备释放对象的锁,释放cpu 并使当前线程进入阻塞状态;
notify:持有对象锁的线程A ,准备释放对象的锁,并通知jvm 唤醒某个竞争线程X,线程A 执行完同步代码后,释放锁,线程X 直接获取锁并执行同步代码,其它竞争线程继续等待,即时线程X 执行完毕释放锁,其它竞争线程依然 处于等待,直至有 新的notify,notifyAll 被调用;
notifyAll:notify唤醒竞争同一个对象的某个线程,notifyAll 唤醒竞争同一个对象的所有 线程,来让这些线程自己竞争去获取锁;
wait/notify 示意
在这里插入图片描述
4 常见面试题
4.1.为什么wait()必须在同步(Synchronized)方法/代码块中调用?
wait 方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列,
而释放锁的前提是必须要先获得锁,先获得锁才能释放锁 并且这些操作都和监视器是相关的,所以 wait 必须要获得一个监视器锁。

4.2.为什么notify(),notifyAll()必须在同步(Synchronized)方法/代码块中调用?
notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)。
参考网址:https://blog.csdn.net/qq_42145871/article/details/81950949

4.3 请你简述一下synchronized与java.util.concurrent.locks.Lock的相同之处和不同之处?
考察点:锁机制
参考回答:
主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

4.4 JAVA中如何确保N个线程可以访问N个资源,但同时又不导致死锁?
考察点:死锁
参考回答:
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
预防死锁,预先破坏产生死锁的四个条件。互斥不可能破坏,所以有如下三种方法:
(1)破坏请求和保持条件,进程必须等所有要请求的资源都空闲时才能申请资源,这种方法会使资源浪费严重(有些资源可能仅在运行初期或结束时才使用,甚至根本不使用). 允许进程获取初期所需资源后,便开始运行,运行过程中再逐步释放自己占有的资源,比如有一个进程的任务是把数据复制到磁盘中再打印,前期只需获得磁盘资源而不需要获得打印机资源,待复制完毕后再释放掉磁盘资源。这种方法比第一种方法好,会使资源利用率上升。
(2)破坏不可抢占条件,这种方法代价大,实现复杂。
(3)破坏循坏等待条件,对各进程请求资源的顺序做一个规定,避免相互等待。这种方法对资源的利用率比前两种都高,但是前期要为设备指定序号,新设备加入会有一个问题,其次对用户编程也有限制。

4.5 请问什么是死锁(deadlock)?
考察点:线程死锁
参考回答:
两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。
例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。

4.6 请说明一下synchronized的可重入怎么实现?
考察点:锁
参考回答:
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

4.7 请讲一下非公平锁和公平锁在reetrantlock里的实现过程是怎样的?
考察点:锁
参考回答:
如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,FIFO。对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁还需要判断当前节点是否有前驱节点,如果有,则表示有线程比当前线程更早请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值