JAVA并发编程之锁应用

Java并发包是Java中提供的一个用于支持多线程编程的工具包。Java并发包提供了多种机制来控制线程的执行,保证线程的安全性和可靠性。下面我们将介绍Java并发包的使用方法,并给出示例。

synchronized

public class SynchronizedDemo {
​
  private int v;
  private static int a;
  private final Object lock = new Object();
​
  // 修饰非静态方法 对象锁
  public synchronized void add(int value) {
    v += value; // 临界区
  }
​
  public void sub(int value) {
    // 修饰局部代码块 对象锁
    synchronized (lock) {
      v -= value; // 临界区
    }
  }
​
  // 修饰静态方法 类锁
  public static synchronized void multi(int value) {
    a *= value; // 临界区
  }
​
  public static void div(int value) {
    // 修饰局部代码块 类锁
    synchronized (SynchronizedDemo.class) {
      a /= value; // 临界区
    }
  }
} 
复制代码

java编译器会在synchronized修饰的方法或代码块前后自动Lock,unlock。

synchronized修饰代码块,锁定是个obj对象,或者是一个类,sychronized(this.class)
synchronized修饰静态方法,锁定是当前类的class对象
synchronized修饰非静态方法,锁定的是当前实例对象this。

实现原理:

synchronized关键字底层使用的锁叫做Monitor锁。但是,我们无法直接创建和使用Monitor锁。Monitor锁是寄生存在的,每个对象都会拥有一个Monitor锁。如果我们想要使用一个新的Monitor锁,我们只需要使用一个新的对象,并在synchronized关键字后,附带声明要使用哪个对象的Monitor锁即可。

当使用sychronized修饰方法的时候,编译器只不过是在函数的flags中添加了ACC_SYNCHRONIZED标记而已,其他部分跟没有添加synchronized的函数的字节码相同。

当使用synchronized修饰局部代码块的时候,字节码通过monitorenter和monitorexit来标记synchronized的作用范围。但有两点需要再解释一下

synchronized关键字底层使用的锁叫做Monitor锁。但是,我们无法直接创建和使用Monitor锁。Monitor锁是寄生存在的,每个对象都会拥有一个Monitor锁,在字节码中,通过monitorenter前面的几行字节码来指定。
以下字节码中有两个monitorexit,添加第二个monitorexit的目的是为了在代码抛出异常时仍然能解锁。
monitor锁实现原理
synchronized在底层使用不同的锁来实现,重量级锁,轻量级锁,偏向锁等。

实际上,synchronized使用的重量级锁,就是前面提到的对象上的Monitor锁。JVM有不同的实现版本,因此,Monitor锁也有不同的实现方式。在Hotspot JVM实现中,Monitor锁对应的实现类为ObjectMonitor类。因为Hotspot JVM是用C++实现的,所以,ObjectMonitor也是用C++代码定义的。

Synchronized的缺点
无法判断获取锁的状态。
虽然会自动释放锁,但如果如果锁的那个方法执行时间较长就会一直占用着不去释放,不能让使用同一把锁的方法继续执行,影响程序的运行。不能设置超时。
当多个线程尝试获取锁时,未获取到锁的线程会不断的尝试获取锁,而不会发生中断,这样会造成性能消耗。
不能够实现公平锁

Lock和Condition

Java并发包中的 Lock 和 Condition 接口提供了一种更为灵活的同步机制。与 synchronized 不同的是,它们可以支持更为细粒度的锁控制,并且可以避免死锁问题。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockConditionExample {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    private void method1() throws InterruptedException {
        lock.lock();
        try {
            System.out.println("method1 is running");
            condition.await();
            System.out.println("method1 is finished");
        } finally {
            lock.unlock();
        }
    }

    private void method2() {
        lock.lock();
        try {
            System.out.println("method2 is running");
            condition.signal();
            System.out.println("method2 is finished");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LockConditionExample example = new LockConditionExample();
        Thread thread1 = new Thread(() -> {
            try {
                example.method1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(example::method2);
        thread1.start();
        Thread.sleep(1000);
        thread2.start();
    }
}

Lock 和 Condition 接口用于控制 method1 和 method2 方法的执行顺序。其中 method1 方法会先获取锁并进入等待状态,而 method2 方法会在一段时间后唤醒 method1 方法并释放锁。这样就可以保证 method1 方法先执行。

Semaphore

Semaphore 是一个计数信号量,用于控制同时访问某个资源的线程数。可以将 Semaphore 看作是一种计数器,每当有线程访问该资源时,计数器的值减一;当计数器的值为零时,其他线程需要等待,直到有线程释放该资源。

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

public class Counter {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

public class IncrementThread extends Thread {
    private Counter counter;

    public IncrementThread(Counter counter) {
        this.counter = counter;
    }

    public void run() {
        for (int i = 0; i < 1000000; i++) {
            counter.increment();
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new IncrementThread(counter);
        Thread t2 = new IncrementThread(counter);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Count: " + counter.getCount());
    }
}

区别:

Synchronized 被称为隐式锁,也叫 JVM 锁,因为它锁的持有和释放都是隐式的,无须开发者干预。Java 1.5 引入新的锁机制,其中锁的实现基于 Lock 接口:

public interface Lock {
    // 加锁
    void lock();
    // 解锁
    void unlock();
    // 可中断获取锁,获取锁时可响应中断操作
    void lockInterruptibly() throws InterruptedException;
    // 尝试非阻塞获取锁,能够获得返回true,否则返回false
    boolean tryLock();
    // 根据时间尝试获取锁,能够获得返回true,否则返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 获取等待通知组件,该组件与当前锁绑定
    Condition newCondition();
}
 

ReentrantLock 锁基于 AQS 队列同步器实现,全称 AbstractQueuedSynchronizer,其中它的抽象类如下:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
    // 0表示锁未被占用,1表示已占用
    private volatile int state;
    // 指向同步队列队头
    private transient volatile Node head;
    // 指向同步队列队尾
    private transient volatile Node tail;
    // 其余属性省略
}

公平锁和非公平锁的差别在于:

公平锁:先请求锁的线程会优先获取到锁,原理是所有需要获得锁的线程都会进入到队列中,队列的特点是先进先出,先进入的请求线程会在头部,后进入的请求线程都会在队列的尾部。

非公平锁:则不会按照线程请求获得锁的先后顺序,会立马进行一次获取锁的请求操作。

synchronized:属于独占锁、悲观锁、可重入锁、非公平锁

ReentrantLock:继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

Lock:Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁

猜你喜欢

转载自blog.csdn.net/hongyucai/article/details/130968750