synchronized和ReentrantLock

前置知识

共享可变

线程问题原因

  • 有限资源的竞争

如何解决

  • 局部化
  • final化
  • 同步化
  • 原子变量化
可见性
  • volitile
  • 一个线程修改的结果,另外一个线程立马就知道了
原子性
  • 在编程世界里,某个操作如果不可分割我们就称之为该操作具有原子性
  • 例如i++ ,它不是原子操作 ,存在线程安全问题,这个时候我们需要使用同步机制来保证其原子性,以确保线程安全。
有序性
  • 指令优化:重排序
  • 如何解决
    • volatile , final ,synchronized ,显式锁
线程安全
  • 《Java并发编程实战》当多个线程访问某类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的

线程同步
  • 保证数据的正确完整性

    多线程编程里面,一些较为敏感的数据是不允许被多个线程同时访问的,使用线程同步技术,确保数据在任何时刻最多只有一个线程访问,保证数据的完整性。

  • 线程同步的4种机制

    • 临界区(Critical Section)

      通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。

    • 互斥量(Mutex)

      采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享

    • 信号量(Semaphore)

      它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目

    • 事件(Event)

      通过事件通知的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作

JAVA中锁的4种状态
  • 无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态 它会随着竞争情况逐渐升级。

  • 锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

  • 这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

  • 锁自旋

    • 在遇到锁的竞争或者等待时,线程可以不着急进入阻塞状态,而是等一等,看看锁是不是马上就释放了,这就是锁自旋。锁自旋在一定程度上可以对线程进行优化处理 (旋转等待哈)
  • 偏向锁

    • 偏向锁主要为了解决在没有竞争情况下锁的性能问题。(总是偏向老熟人,除非有人强烈想认识我)

      在大多数情况有些锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当某个线程获得锁的情况,该线程是可以多次锁住该对象,但是每次执行这样的操作都会因为CAS(CPU的Compare-And-Swap指令)操作而造成一些开销消耗性能,为了减少这种开销,这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。当有其他线程在尝试着竞争偏向锁时,持有偏向锁的线程就会释放锁。

  • 锁膨胀

    多个或多次调用粒度太小的锁,进行加锁解锁的消耗,反而还不如一次大粒度的锁调用来得高效

  • 轻量级锁

    “对于绝大部分的锁,在整个同步周期内都是不存在竞争的” , 如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

synchronized

synchronized主要包括两种方法:synchronized 方法、synchronized 块。

来个问题
  • 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?

    分几种情况:

    • 其他方法前是否加了synchronized关键字,如果没加,则能。
    • 如果这个方法内部调用了wait,则可以进入其他synchronized方法。
    • 如果其他个方法都加了synchronized关键字,并且内部没有调用wait,则不能。
    • 如果其他方法是static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是this。
    • 总的来说: 其它线程能不能访问其它方法,1.就看其它方法有没有上锁 2.用的是不是同一把锁 3.当前线程有没有wait
尽量选择synchronized块
  • 它使临界区变的尽可能小了

  • 使用如下

    synchronized(object) {  
        //共享数据保护起来  
    }
    
    synchronized (this) {
        //共享数据保护起来 
    }
    
这个锁到底是什么
  • 对于同步方法,锁是当前实例对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。
  • 对于静态同步方法,锁是当前对象的Class对象。
原理

字节码锁 ,monitor enter ,monitor exit

sync queue

锁升级 , 对象头 = mark word +

mark word

LOCK 更强大、灵活的锁机制

public class ThreadTest {
    Lock lock = new Lock();
    public void test(){
        lock.lock();
        //被保护的数据
        lock.unlock();
    }
}
可重入性
  • 意味着自己可以再次获得自己的内部锁,而不需要阻塞
public class Father {
    public synchronized void method(){
        //do something
    }
}
public class Child extends Father{
    public synchronized void method(){
        //do something 
        super.method();
    }
}

如果锁是不可重入的,上面的代码就会死锁,因为调用child的method(),首先会获取父类Father的内置锁然后获取Child的内置锁,当调用父类的方法时,需要再次后去父类的内置锁,如果不可重入,可能会陷入死锁。

  • 可重入性的实现

    是通过每个锁关联一个请求计数器和一个占有它的线程,当计数为0时,认为该锁是没有被占有的,那么任何线程都可以获得该锁的占有权。当某一个线程请求成功后,JVM会记录该锁的持有线程 并且将计数设置为1,如果这时其他线程请求该锁时则必须等待。当该线程再次请求请求获得锁时,计数会+1;当占有线程退出同步代码块时,计数就会-1,直到为0时,释放该锁。这时其他线程才有机会获得该锁的占有权。

主要接口或者子类

java.util.concurrent.locks提供了非常灵活锁机制,为锁定和等待条件提供一个框架的接口和类,它不同于内置同步和监视器,该框架允许更灵活地使用锁定和条件

  • ReentrantLock:一个可重入的互斥锁,是一种递归无阻塞的同步机制,为lock接口的主要实现。

  • ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。

  • ReentrantReadWriteLock 实现了ReadWriteLock 接口 ,ReadLock WriteLock 是其静态内部类

与synchronized的区别

ReentrantLock与synchronized的区别

  • ReentrantLock 可重入的互斥锁 ,是一种递归无阻塞的同步机制

  • ReentrantLock具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。

  • ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合

  • ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。

  • ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。

  • ReentrantLock支持中断处理,且性能较synchronized会好些。

  • ReentrantLock提供公平锁机制,构造方法接收一个可选的公平参数。这些锁将访问权授予等待时间最长的线程。否则该锁将无法保证线程获取锁的访问顺序。但是公平锁与非公平锁相比,公平锁吞吐量更低。

/**
     * 默认构造方法,非公平锁
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * true公平锁,false非公平锁
     * @param fair
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
ReentrantLock Demo
// 任务内容
public class PrintQueue {
	private final Lock printLock = new ReentrantLock();
	public void printJob(Object document){
		try {
			// 获取锁
			printLock.lock();
			System.out.println(Thread.currentThread().getName() 
                               + ": 准备打印一份文件……");
			Long duration = (long) (Math.random() * 10000);
			System.out.println(Thread.currentThread().getName() 
                               + ": 正在打印,预计耗时 " + (duration / 1000) + " s");
			Thread.sleep(duration);
			System.out.println(Thread.currentThread().getName() 
                               + ": 文档打印完成!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			// 释放锁
			printLock.unlock();
		}
	}
}
// 打印任务
public class Job implements Runnable{
    
    private PrintQueue printQueue;
    
    public Job(PrintQueue printQueue){
        this.printQueue = printQueue;
    }
    
    @Override
    public void run() {
        printQueue.printJob(new Object());
    }
}

// TEST
public class Test {
	public static void main(String[] args) {
		PrintQueue printQueue = new PrintQueue();
		for (int i = 0; i < 5; i++) {
			new Thread(new Job(printQueue), "线程 " + i).start();
		}
	}
}

// 运行结果 
> lock.reentrantLock.Test
线程 1: 准备打印一份文件……
线程 1: 正在打印,预计耗时 4 s
线程 1: 文档打印完成!
线程 0: 准备打印一份文件……
线程 0: 正在打印,预计耗时 8 s
线程 0: 文档打印完成!
线程 3: 准备打印一份文件……
线程 3: 正在打印,预计耗时 5 s
线程 3: 文档打印完成!
线程 2: 准备打印一份文件……
线程 2: 正在打印,预计耗时 6 s
线程 2: 文档打印完成!
线程 4: 准备打印一份文件……
线程 4: 正在打印,预计耗时 8 s
线程 4: 文档打印完成!

Process finished with exit code 0

ReentrantLock结构图
graph BT; ReentrantLock(ReentrantLock<br/>sync: Sync)-.实现.->Lock(interface<br/>*********</br>Lock) ReentrantLock--组合-->Sync Sync--继承-->AQS FairSync--继承-->Sync NoFairSync--继承-->Sync

ReentrantLock实现Lock接口

Sync与ReentrantLock是组合关系,且FairSync(公平锁)、NonfairySync(非公平锁)是Sync的子类。

Sync继承AQS(AbstractQueuedSynchronizer)

  • AQS(AbstractQueuedSynchronizer)为java中管理锁的抽象类。该类为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。该类提供了一个非常重要的机制,在JDK API中是这样描述的:为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。 这么长的话用一句话概括就是:维护锁的当前状态和线程等待列表。

  • CLH:AQS中“等待锁”的线程队列。我们知道在多线程环境中我们为了保护资源的安全性常使用锁将其保护起来,同一时刻只能有一个线程能够访问,其余线程则需要等待,CLH就是管理这些等待锁的队列。

  • CAS(compare and swap):比较并交换函数,它是原子操作函数,也就是说所有通过CAS操作的数据都是以原子方式进行的。

公平和非公平锁
  • 非公平锁

    final void lock() {  
        // 通过cas尝试设置锁状态,若成功直接将锁的拥有者设置为当前线程 - 简单粗暴
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //否则调用acquire()尝试获取锁;
            acquire(1);
      }
    
  • 公平锁

    通过hasQueuedPredecessors()来判断该线程是否位于CLH队列中头部,是则获取锁;而非公平锁则不管你在哪个位置都直接获取锁。

    protected final boolean tryAcquire(int acquires) {
            //当前线程
            final Thread current = Thread.currentThread();
            //获取锁状态state
            int c = getState();
            /*
             * 当c==0表示锁没有被任何线程占用,在该代码块中主要做如下几个动作:
             * 则判断“当前线程”是不是CLH队列中的第一个线程线程(hasQueuedPredecessors),
             * 若是的话,则获取该锁,设置锁的状态(compareAndSetState),
             * 并切设置锁的拥有者为“当前线程”(setExclusiveOwnerThread)。
             */
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            /*
             * 如果c != 0,表示该锁已经被线程占有,则判断该锁是否是当前线程占有,若是设置state,否则直接返回false
             */
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    
  • 不同点

    获取锁机制不同,公平锁在获取锁时采用的是公平策略(CLH队列),而非公平锁则采用非公平策略它无视等待队列,直接尝试获取。

  • unlock

    • unlock最好放在finally中!
     public void unlock() {
        sync.release(1);
     }
     
     public final boolean release(int arg) {
         if (tryRelease(arg)) {
             Node h = head;
             if (h != null && h.waitStatus != 0)
                 unparkSuccessor(h);
             return true;
         }
         return false;
     }
    

    release(1),尝试在当前锁的锁定计数(state)值上减1。成功返回true,否则返回false。当然在release()方法中不仅仅只是将state - 1这么简单,- 1之后还需要进行一番处理,如果-1之后的新state = 0 ,则表示当前锁已经被线程释放了,同时会唤醒线程等待队列中的下一个线程,当然该锁不一定就一定会把所有权交给下一个线程,能不能成功就看它运气了

参考文档

chenssy并发编程实战系列

posted @ 2021-06-27 18:10  沉梦匠心  阅读(166)  评论(0)    收藏  举报