JAVA基础第一弹-Synchronized和Lock

103 阅读16分钟

大家好,今天和大家一起分享一下java中的同步器synchronized和锁机制Lock

 

锁简介

从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock

锁的公平性

synchronized是非公平锁。

ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:

ReentrantLock lock = new ReentrantLock(true);ReentrantLock和ReentrantReadWriteLock都有关于公平锁的方法:

1、isFair()//判断锁是否是公平锁

2、isLocked()//判断锁是否被任何线程获取了

3、isHeldByCurrentThread()//判断锁是否被当前线程获取了

4、hasQueuedThreads()//判断是否有线程在等待该锁

公平锁FairSync

公平锁的实现机理在于每次有线程来抢占锁的时候,都会检查一遍有没有等待队列,

if (!hasQueuedPredecessors() &&

    compareAndSetState(0, acquires)) {

    setExclusiveOwnerThread(current);

    return true;

}

其中hasQueuedPredecessors是用于检查是否有等待队列的:

public final boolean hasQueuedPredecessors() {

        Node t = tail; // Read fields in reverse initialization order

        Node h = head;

        Node s;

        return h != t &&

            ((s = h.next) == null || s.thread != Thread.currentThread());

}

非公平锁NonfairSync

非公平锁在获取锁时随机抢占,

if (c == 0) {

    if (compareAndSetState(0, acquires)) {

        setExclusiveOwnerThread(current);

        return true;

    }

}

总结:公平锁在请求获取锁时,若有队列存在,直接加入队列。非公平锁请求获取锁时,直接尝试获取锁,获取不成功才会加入队列。

 

Synchronized

Synchronized三种用法

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。从语法上讲,Synchronized总共有三种用法:

1、修饰普通方法  作用相当于当前实例加锁,进入同步代码前要获得当前实例的锁

2、修饰静态方法  作用相当于当前类对象加锁,进入同步代码前要获得当前实例的锁

3、修饰代码块  指定加锁对象,对给定对象加锁,进入同步代码块要获取给定对象的锁


 

下面是使用 synchronized 关键字的三种不同方式的示例。为了便于理解,假设我们有一个简单的银行账户类 BankAccount,其中包含存款和取款的方法。

  1. 修饰普通方法

当 synchronized 用于普通方法时,它会锁定调用该方法的对象实例。这意味着在同一时刻只有一个线程可以执行该对象的同步方法。

 

public class BankAccount {

    private double balance;

 

    public BankAccount(double balance) {

        this.balance = balance;

    }

 

    // 同步普通方法

    public synchronized void deposit(double amount) {

        if (amount > 0) {

            balance += amount;

            System.out.println("存入 " + amount + ", 新余额: " + balance);

        }

    }

 

    // 同步普通方法

    public synchronized void withdraw(double amount) {

        if (amount > 0 && balance >= amount) {

            balance -= amount;

            System.out.println("取出 " + amount + ", 新余额: " + balance);

        }

    }

}

  1. 修饰静态方法

当 synchronized 用于静态方法时,它会锁定该类的 Class 对象。这意味着在任何时刻,整个类的所有对象只能有一个线程执行这些静态同步方法。

 

public class BankAccount {

    private static double totalBalance = 0; // 假设这是所有账户的总余额

 

    // 同步静态方法

    public static synchronized void addTotalBalance(double amount) {

        totalBalance += amount;

        System.out.println("增加总余额 " + amount + ", 新总余额: " + totalBalance);

    }

 

    // 同步静态方法

    public static synchronized void subtractTotalBalance(double amount) {

        if (totalBalance >= amount) {

            totalBalance -= amount;

            System.out.println("减少总余额 " + amount + ", 新总余额: " + totalBalance);

        }

    }

}

  1. 修饰代码块

当 synchronized 用于代码块时,它可以锁定任意对象。这样可以更细粒度地控制同步,提高程序的并发性能。

 

public class BankAccount {

    private double balance;

    private final Object lock = new Object(); // 明确指定锁对象

 

    public BankAccount(double balance) {

        this.balance = balance;

    }

 

    // 同步代码块

    public void deposit(double amount) {

        synchronized (lock) {

            if (amount > 0) {

                balance += amount;

                System.out.println("存入 " + amount + ", 新余额: " + balance);

            }

        }

    }

 

    // 同步代码块

    public void withdraw(double amount) {

        synchronized (lock) {

            if (amount > 0 && balance >= amount) {

                balance -= amount;

                System.out.println("取出 " + amount + ", 新余额: " + balance);

            }

        }

    }

}

每种方式都有其适用场景,选择合适的方式可以使多线程编程更加安全高效。

Synchronized 原理

synchronized 是一种内置的锁机制,用于实现多线程环境下的同步控制,以防止多个线程同时访问同一资源而引发的数据不一致问题。synchronized 可以通过两种主要方式应用:作为方法修饰符或作为代码块的一部分。

1. 内部锁(Intrinsic Locks)

Synchronized 使用的是内部锁,也称为监视器锁(Monitor Lock)。每个对象都有一个与之关联的锁。当一个线程想要进入由 synchronized 保护的代码段时,它必须先获取该代码段相关对象的锁。一旦线程获得了锁,其他试图访问同一代码段的线程就必须等待,直到第一个线程释放锁。

2. 锁的获取与释放

  • 获取锁:当线程尝试进入 synchronized 方法或代码块时,它会尝试获取与该方法或代码块相关的对象的锁。如果锁已被其他线程持有,则当前线程会被阻塞,直到锁可用为止。
  • 释放锁:当线程退出 synchronized 方法或代码块时,或者在该方法或代码块内抛出异常时,锁会被自动释放。

3. 内存模型

除了提供互斥访问外,synchronized 还确保了内存可见性。即,当一个线程释放锁时,它所做的所有更改都会刷新到主内存中;当另一个线程获取同一个锁时,它会看到之前线程所做的所有更新。这有助于避免由于缓存导致的不一致问题。

Synchronized 是 Java 中实现线程同步的重要工具,它通过内部锁机制提供了线程安全的访问控制。通过合理使用 synchronized 修饰符和代码块,可以有效地解决多线程环境下的数据竞争问题,确保程序的正确性和一致性。然而,过度使用 synchronized 也可能导致性能瓶颈,因此在实际开发中需要权衡同步的粒度和范围。

 

synchronized的可重入性

 

Synchronized 在 Java 中不仅提供了基本的锁机制,还支持可重入性(Reentrancy)。可重入性是指一个线程在已经持有一个对象的锁的情况下,可以再次获取该对象的锁,而不会因为重复获取锁而导致死锁。这种特性使得 synchronized 更加灵活和强大。

可重入性的原理

  1. 锁计数器:每个对象锁都有一个计数器,初始值为 0。当一个线程第一次获取锁时,计数器加 1。每当该线程再次进入一个需要相同锁的同步代码块时,计数器继续加 1。每次线程退出一个同步代码块时,计数器减 1。当计数器减为 0 时,锁被完全释放。
  2. 线程标识:每个对象锁还记录了当前持有锁的线程标识。当一个线程尝试获取一个已经被自己持有的锁时,系统会检查锁的持有者是否是当前线程。如果是,则允许线程继续执行,计数器加 1。

举个例子说明普通方法的可重入性:

假设我们有一个类 ReentrantExample,其中包含一个递归调用的 synchronized 方法。

 

public class ReentrantExample {

    public synchronized void outerMethod() {

        System.out.println("outerMethod 被调用");

        innerMethod();

    }

 

    public synchronized void innerMethod() {

        System.out.println("innerMethod 被调用");

    }

 

    public static void main(String[] args) {

        ReentrantExample example = new ReentrantExample();

        example.outerMethod();

    }

}

在这个例子中,outerMethod 和 innerMethod 都是 synchronized 方法。当 outerMethod 被调用时,当前线程获取了 ReentrantExample 实例的锁。然后,outerMethod 调用了 innerMethod。由于 innerMethod 也是 synchronized 方法,它需要获取同一个实例的锁。但由于当前线程已经持有该锁,因此可以直接进入 innerMethod,而不会因为锁的冲突而被阻塞。

代码块的可重入性举例说明:

假设我们有一个类 ReentrantBlockExample,其中包含一个 synchronized 代码块。

 

public class ReentrantBlockExample {

    private final Object lock = new Object();

 

    public void outerMethod() {

        synchronized (lock) {

            System.out.println("outerMethod 被调用");

            innerMethod();

        }

    }

 

    public void innerMethod() {

        synchronized (lock) {

            System.out.println("innerMethod 被调用");

        }

    }

 

    public static void main(String[] args) {

        ReentrantBlockExample example = new ReentrantBlockExample();

        example.outerMethod();

    }

}

在这个例子中,outerMethod 和 innerMethod 都包含一个 synchronized 代码块,锁定的是同一个 lock 对象。当 outerMethod 被调用时,当前线程获取了 lock 对象的锁。然后,outerMethod 调用了 innerMethod。由于 innerMethod 也需要获取 lock 对象的锁,但由于当前线程已经持有该锁,因此可以直接进入 innerMethod,而不会因为锁的冲突而被阻塞。

可重入性的优势:

  1. 避免死锁:可重入性使得一个线程可以在已经持有一个锁的情况下再次获取该锁,而不会因为重复获取锁而导致死锁。
  2. 简化编程:可重入性使得编写多层嵌套的同步代码变得更加容易,而不需要额外的锁管理逻辑。

虽然 synchronized 的可重入性带来了便利,但也需要注意以下几点:

  1. 锁的粒度:尽量使用细粒度的锁,避免不必要的性能开销。
  2. 死锁风险:尽管可重入性减少了死锁的风险,但在复杂的多线程环境中,仍需谨慎设计锁的获取和释放顺序,以避免死锁。

LOCK

简介

1、 JDK1.5之后,java.util.concurrent.locks包下提供了另外一种方式实现同步:Lock

2、 synchronized是内置锁,不需要显式的获取和释放,Lock是一个接口,是显式锁

3、 synchronized和ReentrantLock都是可重入锁

Lock优势

Lock与synchronize比较的优势:

1、 无限期等待:synchronize线程释放锁的条件:执行完成、或异常退出,若线程执行时间很长,例如io,其他线程只能无限期阻塞,Lock可以给锁加个等待超时时间或者中断等待

2、 读写区分: synchronize如果多个线程并发读操作,并不互相影响,但是仍然会互相阻塞。无法进行读锁和写锁的分别对待

3、 synchronize无法知道线程是否成功获取到锁

Lock劣势

Lock与synchronize比较的劣势:

1、 synchronized会自动释放锁,Lock需要用户去手动释放锁,否则会引起死锁

locks包下的接口

java.util.concurrent.locks下的接口:

 

1.1   lock()

获取锁,如果锁已被其他线程获取,则等待

如果采用Lock,在发生异常时,不会自动释放锁,必须主动去释放,否则会形成死锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,例如:

Lock lock = ...;

lock.lock();

try{

    //处理任务

}catch(Exception ex){

    

}finally{

    lock.unlock();   //释放锁

     }

加锁的具体步骤:

  1. 读取表示锁状态的变量

  2. 如果表示状态的变量的值为0,那么当前线程尝试将变量值设置为1(通过CAS操作完成)

2.1 若成功,表示获取了锁,

2.1.1 如果该线程(或者说节点)已位于在队列中,则将其出列(并将下一个节点则变成了队列的头节点)

2.1.2 如果该线程未入列,则不用对队列进行维护

2.1.3 然后当前线程从lock方法中返回,对共享资源进行访问。            

2.2 若失败,则当前线程将自身放入等待(锁的)队列中并阻塞自身

  1. 如果表示状态的变量的值为1,那么将当前线程放入等待队列中,然后将自身阻塞

1.2   tryLock()

尝试获取锁,如果获取成功,返回true,如果获取失败(锁已被其他线程获取),返回false,无论结果如何都会立即返回,不会一直等待。

1.3   tryLock(long time, TimeUnit unit)

和tryLock()类似,在获取不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,返回false。如果获取到锁,返回true。例如:

Lock lock = ...;

if(lock.tryLock()) {

     try{

         //处理任务

     }catch(Exception ex){

        

     }finally{

         lock.unlock();   //释放锁

     }

}else {

    //如果不能获取锁,则直接做其他事情

}

1.4   lockInterruptibly()

当线程通过这个方法去获取锁时,等待获取锁过程中,这个线程能够响应中断,即中断线程的等待状态(只有等待锁的过程中才可以中断,当线程获取锁后,是无法被interrupt()方法中断的)。即当线程A通过lock.lockInterruptibly()想获取某个锁时,对线程A调用threadA.interrupt()方法能够中断线程A的等待过程。由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。例如:

public void method() throws InterruptedException {

    lock.lockInterruptibly();

    try { 

     //.....

    }

    finally {

        lock.unlock();

    } 

}

源码分析lockInterruptibly

lock()底层使用AQS的acquireQueued方法。

lock.lockInterruptibly() 底层使用AQS的doAcquireInterruptibly

方法。

acquireQueued和doAcquireInterruptibly的唯一不同是,当出现中断后,doAcquireInterruptibly会抛出异常:

if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())

        throw new InterruptedException();

而acquireQueued只是修改变量的值:

if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())

                    interrupted = true;

1.5   unLock()

释放锁

释放锁的过程:

解锁主要体现在AbstractQueuedSynchronizer.release和Sync.tryRelease

public final boolean release(int arg) { 

    if (tryRelease(arg)) { 

        Node h = head; 

        if (h != null && h.waitStatus != 0) 

            unparkSuccessor(h); 

        return true; 

    } 

    return false; 

 

private void unparkSuccessor(Node node) { 

    /*

     * If status is negative (i.e., possibly needing signal) try

     * to clear in anticipation of signalling. It is OK if this

     * fails or if status is changed by waiting thread.

     */ 

    int ws = node.waitStatus; 

    if (ws < 0) 

        compareAndSetWaitStatus(node, ws, 0);  

 

    /*

     * Thread to unpark is held in successor, which is normally

     * just the next node.  But if cancelled or apparently null,

     * traverse backwards from tail to find the actual

     * non-cancelled successor.

     */ 

    Node s = node.next; 

    if (s == null || s.waitStatus > 0) { 

        s = null; 

        for (Node t = tail; t != null && t != node; t = t.prev) 

            if (t.waitStatus <= 0) 

                s = t; 

    } 

    if (s != null) 

        LockSupport.unpark(s.thread); 

}

注:LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。AQS就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。

1.6   newCondition()

条件变量

Condition

Condition接口定义的方法,await对应于Object.wait,signal对应于Object.notify,signalAll对应于Object.notifyAll

ReadWriteLock

public interface ReadWriteLock {

Lock readLock();

Lock writeLock();}

ReadWriteLock也是一个接口,在它里面只定义了两个方法:一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

3.1 readLock()

        获取读锁,例如:

        public class Test {

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    

    public static void main(String[] args)  {

        final Test test = new Test();

        

        new Thread(){

            public void run() {

                test.get(Thread.currentThread());

            };

        }.start();

        

        new Thread(){

            public void run() {

                test.get(Thread.currentThread());

            };

        }.start();

        

    } 

    

    public void get(Thread thread) {

        rwl.readLock().lock();

        try {

            long start = System.currentTimeMillis();

            

            while(System.currentTimeMillis() - start <= 1) {

                System.out.println(thread.getName()+"正在进行读操作");

            }

            System.out.println(thread.getName()+"读操作完毕");

        } finally {

            rwl.readLock().unlock();

        }

    }

}

如果有一个线程已经占用了读锁,此时其他线程如果要申请写锁,则申请写锁的线程需要一直等待该线程释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程需要一直等待释放写锁。

3.2 writeLock()

        获取写锁

 

locks包下的类

java.util.concurrent.locks下的类

ReentrantLock

ReentrantLock 是 Java 并发包 java.util.concurrent.locks 中提供的一个可重入的互斥锁。与 synchronized 关键字相比,ReentrantLock 提供了更多的灵活性和功能,例如公平锁、非公平锁、可中断锁等待等。这些高级功能使得 ReentrantLock 在某些场景下比 synchronized 更加适合。

主要特性

1.           可重入性:ReentrantLock 支持可重入性,即一个线程在已经持有一个锁的情况下可以再次获取该锁,而不会因为重复获取锁而导致死锁。

2.           公平锁与非公平锁:ReentrantLock 允许用户选择是否使用公平锁。公平锁按照请求锁的顺序来分配锁,而非公平锁则允许插队,可能会导致某些线程长时间无法获取锁。

3.           可中断锁等待:ReentrantLock 支持可中断的锁等待,即等待锁的线程可以被中断并抛出 InterruptedException。

4.           锁的绑定:ReentrantLock 可以绑定多个条件对象(Condition),使得线程可以在不同的条件下等待和唤醒。

基本用法

创建和使用 ReentrantLock

 

import java.util.concurrent.locks.ReentrantLock;

 

public class Counter {

    private int count = 0;

    private final ReentrantLock lock = new ReentrantLock();

 

    public void increment() {

        lock.lock(); // 获取锁

        try {

            count++;

        } finally {

            lock.unlock(); // 释放锁

        }

    }

 

    public int getCount() {

        lock.lock(); // 获取锁

        try {

            return count;

        } finally {

            lock.unlock(); // 释放锁

        }

    }

 

    public static void main(String[] args) {

        Counter counter = new Counter();

        // 模拟多线程环境

        Thread t1 = new Thread(() -> {

            for (int i = 0; i < 1000; i++) {

                counter.increment();

            }

        });

 

        Thread t2 = new Thread(() -> {

            for (int i = 0; i < 1000; i++) {

                counter.increment();

            }

        });

 

        t1.start();

        t2.start();

 

        try {

            t1.join();

            t2.join();

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

 

        System.out.println("最终计数值: " + counter.getCount());

    }

}

高级用法

公平锁与非公平锁

默认情况下,ReentrantLock 是非公平锁。可以通过构造函数指定是否使用公平锁。

private final ReentrantLock fairLock = new ReentrantLock(true); // 公平锁

private final ReentrantLock nonFairLock = new ReentrantLock(false); // 非公平锁

可中断锁等待

ReentrantLock 提供了 lockInterruptibly 方法,允许线程在等待锁时被中断。

import java.util.concurrent.locks.ReentrantLock;

 

public class InterruptibleLockExample {

    private final ReentrantLock lock = new ReentrantLock();

 

    public void doSomething() throws InterruptedException {

        lock.lockInterruptibly(); // 可中断的锁等待

        try {

            // 执行一些操作

        } finally {

            lock.unlock();

        }

    }

 

    public static void main(String[] args) {

        InterruptibleLockExample example = new InterruptibleLockExample();

        Thread t = new Thread(() -> {

            try {

                example.doSomething();

            } catch (InterruptedException e) {

                System.out.println("线程被中断");

            }

        });

 

        t.start();

        t.interrupt(); // 中断线程

    }

}

条件对象(Condition)

ReentrantLock 可以绑定多个条件对象,这些条件对象允许线程在不同的条件下等待和唤醒。

 

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.ReentrantLock;

 

public class ConditionExample {

    private final ReentrantLock lock = new ReentrantLock();

    private final Condition notFull = lock.newCondition();

    private final Condition notEmpty = lock.newCondition();

    private final int[] buffer = new int[10];

    private int count = 0;

 

    public void produce(int value) throws InterruptedException {

        lock.lock();

        try {

            while (count == buffer.length) {

                notFull.await(); // 缓冲区满,生产者等待

            }

            buffer[count++] = value;

            notEmpty.signalAll(); // 通知消费者

        } finally {

            lock.unlock();

        }

    }

 

    public int consume() throws InterruptedException {

        lock.lock();

        try {

            while (count == 0) {

                notEmpty.await(); // 缓冲区空,消费者等待

            }

            int value = buffer[--count];

            notFull.signalAll(); // 通知生产者

            return value;

        } finally {

            lock.unlock();

        }

    }

 

    public static void main(String[] args) {

        ConditionExample example = new ConditionExample();

 

        Thread producer = new Thread(() -> {

            for (int i = 0; i < 20; i++) {

                try {

                    example.produce(i);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        });

 

        Thread consumer = new Thread(() -> {

            for (int i = 0; i < 20; i++) {

                try {

                    System.out.println("消费: " + example.consume());

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        });

 

        producer.start();

        consumer.start();

    }

}

总结

ReentrantLock 是 Java 并发包中提供的一个功能强大的可重入锁。它不仅支持基本的锁机制,还提供了公平锁、非公平锁、可中断锁等待和条件对象等高级功能。通过合理使用 ReentrantLock,可以编写出更灵活、更高效的多线程代码。

 

ReentrantReadWriteLock

ReentrantReadWriteLock 是 Java 并发包 java.util.concurrent.locks 中提供的一个可重入的读写锁。它允许多个读线程同时访问资源,但写线程独占资源,确保数据的一致性和完整性。读锁和写锁都可以重入,支持公平和非公平模式。适用于读多写少的场景,提高并发性能。

锁降级

锁降级指的是先获取到写锁,然后获取到读锁,然后释放了写锁的过程。

if (exclusiveCount(c) != 0 &&

        getExclusiveOwnerThread() != current)

        return -1;

所以当前线程是可以在获取了写锁的情况下再去获取读锁的。

锁的可重入性

ReentrantLock和synchronized都是可重入锁

        当线程获取该监听对象的锁后,再次请求该监听对象其他同步方法,不需要释放锁即可直接获取锁。

不可重入锁例子:

public class UnreentrantLock {

 

    private AtomicReference owner = new AtomicReference();

 

    public void lock() {

        Thread current = Thread.currentThread();

        for (;;) {

            if (!owner.compareAndSet(null, current)) {

                return;

            }

        }

    }

 

    public void unlock() {

        Thread current = Thread.currentThread();

        owner.compareAndSet(current, null);

    }

}

改成可重入锁:

public class UnreentrantLock {

 

    private AtomicReference owner = new AtomicReference();

    private int state = 0;

 

    public void lock() {

        Thread current = Thread.currentThread();

        if (current == owner.get()) {

            state++;

            return;

        }

        for (;;) {

            if (!owner.compareAndSet(null, current)) {

                return;

            }

        }

    }

 

    public void unlock() {

        Thread current = Thread.currentThread();

        if (current == owner.get()) {

            if (state != 0) {

                state--;

            } else {

                owner.compareAndSet(current, null);

            }

        }

    }

}

ReentrantLock中可重入锁实现

非公平锁获取:

final boolean nonfairTryAcquire(int acquires) {

            final Thread current = Thread.currentThread();

            int c = getState();

            if (c == 0) {

                if (compareAndSetState(0, acquires)) {

                    setExclusiveOwnerThread(current);

                    return true;

                }

            }

            else if (current == getExclusiveOwnerThread()) {

                int nextc = c + acquires;

                if (nextc < 0) // overflow

                    throw new Error("Maximum lock count exceeded");

                setState(nextc);

                return true;

            }

            return false;

        }