Java并发编程之锁机制

2022-11-11  本文已影响0人  宏势

一、JAVA锁实现

锁是用来控制多个线程访问共享资源的方式,JDK提供三种方式的锁实现:(1)Synchronized 关键字(2)Lock(3)原子操作类(无锁)

1.Synchronized

synchronized是基于JVM内置锁实现,基于进入与退出Monitor对象实现方法同步和代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现

代码块同步是使用monitorenter 和monitorexit指令实现的。

private static Object lock = new Object();
public void test(){ //
    synchronized(lock){//编译后,插入monitorenter指令到同步代码块开始位置
            
    }//编译后,插入monitorexit指令到同步代码块结束位置
}

任何对象都有一个monitor与之关联,获取对象的锁即是获取对象所对应的monitor的所有权,synchronized用的锁是存在JAVA对象头里的

JAVA对象头包括 MarkWord,类型指针,数据长度三部分;MarkWord存储对象的锁,hashcode等信息

JDK1.6 对synchronized 进行优化,引入了偏向锁和轻量级锁,减少获取锁和释放锁带来的性能消耗。

锁级别从低到高依次是:无锁状态- 偏向锁-轻量级锁-重量级锁

例子:

public class SynchronizedTest {

    private static Object lock = new Object();
    public synchronized static void staticMethod(){  //实际上是对该类对象加锁,俗称“类锁”
            ...
    }
    public synchronized void method(){ //实际上是对调用方法的对象加锁,俗称“对象锁”
            ...
    }
    public void method1(){ 
        synchronized (SynchronizedTest.class){ //对SynchronizedTest.class对象加锁
                ...
        }
    }
    public void method2(){
        synchronized (lock){ //对lock对象加锁
                ...
        }
    }
    public static void main(String[] args) {
    }
}

同一个对象在两个线程中分别访问该对象的两个同步方法 会互斥

不同对象在两个线程中调用同一个同步方法 不会互斥

用类直接在两个线程中调用两个不同的同步静态方法 会互斥

一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法 不会互斥

2. Lock

JDK1.5 引入Lock接口(以及相关实现类)(java.util.concurrent.locks包中)实现锁功能,需要显示的获取和释放锁。Lock拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种同步特性。

class X {     
  private final ReentrantLock lock = new ReentrantLock();     
  // ...        
  public void m() {       
    lock.lock();  // block until condition holds   不要写到try里,防止获取锁(自定义锁的实现)发生异常,导致锁无故释放    
    try {         
      // ... method body       
    } finally {   //确保最终能释放锁      
      lock.unlock();      
    }     
  }   
}

读写锁:

public class ReadWriteTest{
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Object data = null;
    public void read(){
        readWriteLock.readLock().lock();
        try{
            System.out.println(Thread.currentThread().getName() + " ready to read data" );
            Thread.sleep(new Random().nextInt(1000));
            System.out.println(Thread.currentThread().getName() + ":" + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
    public void write(Object data){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " ready to write" + data);
            Thread.sleep(new Random().nextInt(2000));
            this.data = data;
            System.out.println(Thread.currentThread().getName() + ":" + this.data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }
    public static void main(String[] args) {
        ReadWriteTest readWriteTest = new ReadWriteTest();
        for(int i = 0; i < 5; i++){
            new Thread(() -> {
                int j= 10;
                while (j > 0) {
                    readWriteTest.read();j--;
                }
            }, "Reader"+i).start();
        }
        for(int i = 0; i < 2; i++){
            new Thread(() -> {
                int j= 10;
                while (j > 0) {
                    readWriteTest.write(new Random().nextInt(10000));
                    j--;
                }
            }, "Writer"+i).start();
        }
    }
}

实现原理

利用队列同步器AbstractQueuedSynchronizer(AQS)实现,AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。

lock.png AQS.png lock-share.png lock-share-release.png

Node 属于AQS的内部类,成员:除了前置和后置节点还有节点对应的线程(thread),节点的状态(waitStatus),nextWaiter(主要用于条件变量)

自定义同步类

自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在底层实现好了。自定义同步器实现时主要实现以下几种方法:

同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。当然,接口的实现要直接依赖sync,它们在语义上也存在某种对应关系!而sync只用实现资源state的获取-释放方式tryAcquire-tryRelelase,至于线程的排队、等待、唤醒等,上层的AQS都已经实现好了,我们不用关心。内置同步类ReentrantLock/ReentrantReadWriteLock/CountDownLatch/Semaphore/ 都是基于AQS实现的。

public class TwinsLock implements Lock {

    private final Sync sync = new Sync(2);

    static class Sync extends AbstractQueuedSynchronizer {
        public Sync(int state){setState(state);}

        protected final int tryAcquireShared(int unused) {
            for (;;) {
                int c = getState();
                int newState = c - unused;
                if(newState < 0 || compareAndSetState(c, newState)){
                    return newState;
                }
            }
        }
        protected final boolean tryReleaseShared(int unused) {
            for (;;) {
                int c = getState();
                if (compareAndSetState(c, c + unused))
                    return true;
            }
        }
    }
    @Override
    public void lock() {
        sync.acquireShared(1);
    }
    @Override
    public void unlock() {
        sync.releaseShared(1);
    }
    //其它接口省略
    public static void main(String[] args) {
        TwinsLock twinsLock = new TwinsLock();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                twinsLock.lock();
                try{
                    String name = Thread.currentThread().getName();
                    System.out.println(name+" start ...");
                    Thread.sleep(3000);
                    System.out.println(name+" end ...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    twinsLock.unlock();
                }
            }, "Thread-"+i).start();
        }
    }
}

3.原子操作类

JDK 1.5 新增原子操作类(java.util.concurrent.atomic包中),是CAS非阻塞算法的实现方式,相对于synchronized/lock 这种阻塞算法,它性能更好。

原子操作类主要解决变量并发访问的同步问题。Atomic包里的类基本都是使用Unsafe实现。

(1)原子更新基本类型

public class BasicType extends Thread{

    public static final AtomicInteger aInt = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            aInt.incrementAndGet(); //并发操作是原子性,无须加锁
        }
        System.out.println(Thread.currentThread().getName()+":"+ aInt);
    }
    public static void main(String[] args) throws InterruptedException{
        Thread a = new BasicType();
        a.start();
        Thread b = new BasicType();
        b.start();
        a.join();
        b.join();
        System.out.println("main exit");
    }
}

(2)原子更新数组

int array[] = {1,2,3};
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(array);
int result = atomicIntegerArray.addAndGet(1, 100);
System.out.println(atomicIntegerArray.get(1));//输出结果102
System.out.println(array[1]);//输出结果2

AtomicIntegerArray(array) 会将当前数组复制一份,所以对AtomicIntegerArray 数组元素修改,不会影响传入的数组

(3)原子更新引用

AtomicReference:原子更新引用类型。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicMarkableReference:原子更新带有标记位的引用类型

class Person {
    private String name;
        ...
}
public static void main(String[]args){
    Person old =  new Person("A");
  AtomicReference<Person> reference = new AtomicReference(old);
  reference.getAndSet(new Person("B")); //更新应用指向,不会改变旧引用值
  System.out.println(reference.get().getName());//输出B
  System.out.println(old.getName());//输出 A
}
class Person {
    volatile public String name; //更新属性必须是volatile
    public Person(String name){this.name = name;}
}
public static void main(String[]args){
    //所在类需有访问name属性的权限
    AtomicReferenceFieldUpdater referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Person.class,String.class,"name");
    Person initOld = new Person("C");
    referenceFieldUpdater.set(initOld,"D");
    System.out.println(referenceFieldUpdater.get(initOld)); //输出D
    System.out.println(initOld.getName());//输出D
}

String val = "hello";
AtomicMarkableReference<String> atomicMarkableReference = new AtomicMarkableReference(val, false);
atomicMarkableReference.compareAndSet(val,"hello world",false, true);
atomicMarkableReference.compareAndSet("hello world","hello",true, true);

(4)原子更新属性类

class Person {
    volatile public int age; //更新属性必须是volatile
    public Person(int age){this.age = age;}
}
public static void main(String[]args){
        AtomicIntegerFieldUpdater  atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");
    Person initOld = new Person(10);
    atomicIntegerFieldUpdater.getAndSet(initOld,50);
    System.out.println(atomicIntegerFieldUpdater.get(initOld));//输出50
    System.out.println(initOld.getAge());//输出50
}
AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference(val,1);
int stamp =  atomicStampedReference.getStamp();
atomicStampedReference.compareAndSet(val, "hello world", stamp, stamp+1);
stamp = atomicStampedReference.getStamp();
atomicStampedReference.compareAndSet("hello world", "hello", stamp, stamp+1);

AtomicMarkableReference 与 AtomicStampedReference 一样也可以解决 ABA的问题,两者唯一的区别是,AtomicStampedReference 是通过 int 类型的版本号,而 AtomicMarkableReference 是通过 boolean 型的标识来判断数据是否有更改过。可以理解成AtomicMarkableReference是AtomicStampedReference的简化版

二、JAVA锁的种类

上一篇下一篇

猜你喜欢

热点阅读