【2019春招准备:14.synchronized、lock、wait、volatile】

本文深入探讨Java并发编程的关键概念,如线程锁、自旋锁、锁消除、锁粗化,以及悲观锁和乐观锁的原理。详细对比了synchronized、volatile、Lock和ReentrantLock的区别,解释了wait、sleep、notify和notifyAll的功能及使用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【内容】
pthread_mutext_lock
reentrantLock
synchronized
数据库锁
【参考】
java中的锁-朱小厮(墙裂推荐)

Java中的公平锁和非公平锁实现详解- 平菇虾饺
【补充】

  • 自旋锁
  • 锁消除
  • 锁粗化
  • 悲观锁、乐观锁

=======================================================
【内容】

1. pthread_mutext_lock(llinux)

thread_mutex_lock的作用实际就是上锁,这个函数和pthread_mutex_unlock配套使用。
两句函数中间的代码就是被上锁的代码,被上锁的代码只能有一个线程使用,别的线程执行到这里会发生阻塞,只有unlock之后,别的线程才能使用lock之后进入代码。

2. reentrantLock使用和实现

  • 常见形式
    在这里插入图片描述
  • ReentrantLock将具体的实现委托给内部类,而不是自己继承AbstractQueuedSynchronizer
    内部实现的sync:NonFairSync FairSync

【公平性分析】
内部都维护一个双向链表,链表的节点是每一个请求当前锁的线程(如果是公平锁,每次都是队首取值;如果是非公平锁,每一个想获取锁的进程都有几率直接获取到,空出来的时候直接抢占,不用去唤醒等待队列)
链表节点Node和状态State都是volatile关键字

  • 其可重入性是基于Thread.currentThread()实现的: 如果当前线程已经获得了执行序列中的锁, 那执行序列之后的所有方法都可以获得这个锁。

可重入分析
重入:也叫做递归锁,外层函数获取锁之后,如果内层的递归函数任然有获取锁的代码,但是不影响。
ReentrantLock 和synchronized 都是可重入锁。
可重入锁最大的作用是避免死锁。

3. synchronized 、volatile、Lock、ReentrantLock区别

https://blog.csdn.net/ztchun/article/details/60778950
https://www.cnblogs.com/baizhanshi/p/6419268.html

synchronizedvolatileLock
确保的性质共享资源的同步性(原子性、包括可见性)变量在多线程之间的可见性(不包括原子性)
修饰方法、代码块、对象(变量)对象(变量)
本质关键字(内置语言实现)关键字(内置语言实现)接口

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

volatile

  • 首先我们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器或者是CPU缓存上进行,最后才写入内存。而在这个过程中,变量的新值对其他线程是不可见的。

synchronized

  • Synchronized(对象锁) ;Static Synchronized(类锁)锁在class或者classloader对象上(类锁)
  • 线程1调用对象A的方法syn_Fun1(),需要首先获取这个对象的锁。如果A已经被其它线程锁定了(不一定是调用syn_Fun1(),可以是该对象的其他syn方法;但如果不是syn方法不会锁定对象)

Lock(接口))

public interface Lock {
  void lock();
  void lockInterruptibly() throws InterruptedException;
  boolean tryLock();
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  void unlock();
   Condition newCondition();

synchronized由于是java内部实现,比较死板,容易浪费资源(一旦锁定其它线程必须等待),因此有lock的灵活改进
使用lock可以知道是否获取了锁
但是有长必有短,使用lock必须手动释放unlock
http://www.dewen.net.cn/q/9077

  • lock(), 拿不到lock就不罢休,不然线程就一直block。 比较无赖的做法。
    tryLock(),马上返回,拿到lock就返回true,不然返回false。 比较潇洒的做法。
    带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。比较聪明的做法。
  • 先说说线程的打扰机制,每个线程都有一个 打扰 标志。这里分两种情况,
    A 线程在sleep或wait,join, 此时如果别的进程调用此进程的 interrupt()方法,此线程会被唤醒并被要求处理InterruptedException;(thread在做IO操作时也可能有类似行为,见java thread api)
    B 此线程在运行中, 则不会收到提醒。但是 此线程的 “打扰标志”会被设置, 可以通过isInterrupted()查看并 作出处理。
    **lockInterruptibly()**和上面的第一种情况是一样的(没有获取到锁,而是在等待锁状态), 线程在请求lock并被阻塞时,如果被interrupt,则“此线程会被唤醒并被要求处理InterruptedException”。
// Lock
Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock();   //释放锁
}

//tryLock
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

ReentrantLock
“可重入锁”,是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
synchronized和RentrantLock都是可重入锁,不会在同步方法内获取不到被锁定的对象而不能进入他的另外一个同步方法

package reentrantLock;

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;

public class Test {

	private ArrayList<Integer> arrayList = new ArrayList<>();

	public void insert(Thread thread) {
		ReentrantLock lock = new ReentrantLock();
		lock.lock();
		try {
			System.out.println(thread.getName() + " 获得了锁");
			for (int i = 0; i < 5; ++i) {
				arrayList.add(i);
			}
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		} finally {
			System.out.println(thread.getName() + " 释放了锁");
			lock.unlock();
		}
	}

	public static void main(String[] args) {

		final Test test = new Test();
		new Thread() {
			public void run() {
				test.insert(Thread.currentThread());
			}
		}.start();

		new Thread() {
			public void run() {
				test.insert(Thread.currentThread());
			}
		}.start();
	}
}

Thread-0 获得了锁
Thread-1 获得了锁
Thread-0 释放了锁
Thread-1 释放了锁

4. wait sleep notify notifyall

wait():释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu,执行代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会

notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。

notifyAll()则是唤醒所有等待的线程。

【补充】

  • 自旋锁

java的线程都是映射到操作系统的原生线程上,无论阻塞还是唤醒都需要从用户态切换到内核态。但是转换的过程往往需要很多时间,避免在转换过程中,其他线程获取到cpu资源,本虚拟线程执行一个忙循环(自旋)
作用:避免了线程切换的开销
参数默认是自适应的

  • 锁消除

如果判定一个被lock的方法或者synchronized的方法,jvm判定这段代码中,堆上的所有数据不会逃逸,可以当做栈数据处理,认为是线程私有的,无需进行同步操作。

public String concatString(String s1, String s2, String s3)
    {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }
public synchronized StringBuffer append(StringBuffer sb) {
        super.append(sb);
        return this;
    }
  • 锁粗化

jvm判定相邻的代码重复加锁解锁操作,实际应该是一步到位,直接优化成整体的加锁解锁

悲观锁
每次在拿数据的时候都会上锁
常见在关系型数据库:行锁,表锁等,读锁,写锁;synchronized也是悲观锁

存在问题:
在多线程下枷锁释放锁比较多,因此上下文切换和调度的时候性能低下
一个线程的锁可能导致多个线程挂起(没有获取到对象的线程们)
优先级高的线程等待优先级低的线程,引起优先级倒置,影响系统功能的实现

乐观锁(CAS) 主要讲这个
只在更新的时候会判断一下在此期间别人有没有去更新这个数据
适用于多读的应用类型,这样可以提高吞吐量
java.util.concurrent包下面的原子变量类就是使用了乐观锁(非阻塞,)

CAS ( compare and swap )
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
V:内存位置
A:更新之前,预期这个位置应该的值是多少
B:待更新写入的值
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作
【存在的问题】:ABA(可以通过检查引用是否相等解决);循环时间开销大,只能保证一个共享变量的原子操作

乐观锁的使用场景,例子:elasticSearch
es大部分场景下都是一个读多写少的系统
https://m635674608.iteye.com/blog/2361304

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值