lock

简介

Lock接口有以下方法

  • void lock(); 获得
  • void lockInterruptibly(); 获取锁定除非当前线程是interrupted
  • Condition newCondition();返回一个新Condition绑定到该实例
  • bollean tryLock()只有在调用时才可以获得锁.
  • boolean try lock (long time ,TimeUnit unit);如果在给定的等待时间内是空闲的,并且当前线程尚未得到interrupted则获取该锁
  • void unlock();释放锁

lock有以下的实现类

ReentrantLock :可重入锁

ReentranReadWriteLock.ReadLock:可重入锁的读锁

ReentrantReadWriteLock.WriteLock:可重入锁的写锁

公平锁

优点:所有的线程都能得到资源,不会饿死在队列中.

缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大

非公平锁

优点:可以减少cpu唤醒线程的开销,整体的吞吐效率会高,cpu也不必唤醒所有线程,会年少线程的数量

缺点:导致队列中的线程一直获取不到锁或者长时间获取不到锁,导致饿死

实例

代码

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

class Main implements Runnable {
    static int flag=0;
    Lock lock=new ReentrantLock();
    @Override
    public void run() {
        lock.lock();
        try {
            for (int i=0;i<10000;i++){
                flag++;
            }
        }finally {
            lock.unlock();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        Main main=new Main();
        Thread thread=new Thread(main,"a");
        Thread thread2=new Thread(main,"b");
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();
        System.out.println(flag);
    }
}

synchronized与Lock的区别

1.synchronized是关键字,而lock是一个接口

2.synchronized会自动释放锁,而lock必须手动释放锁

3.synchronized是不可中断的,Lock可以中断也可以不中断

4.通过Lock可以知道线程有没有拿到锁,而synchronized

4.synchronized能锁住方法和代码块,而Lock只能锁住代码块

5.lock可以使用读锁提高多线程效率.

6.synchronized是非公平锁,ReentrantLock可以控制是否是公平锁

生产者消费者问题

synchronized版本

public class Main{
    //线程交替执行
    //俩个线程操作同一个变量
    public static void main(String[] args) {
        Data data=new Data();
        new Thread(()->{
            for(int i=0;i<10;i++){
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"a").start();

        new Thread(()->{
            for (int i=0;i<10;i++){
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"b").start();
    }
}
//等待,业务,通知
class Data{
    public int number=0;
    public synchronized void increment() throws InterruptedException {
        //用if判断的时候,会出现虚假唤醒问题,所以等待最好是在while循环中
        while (number!=0){
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程去唤醒
        this.notify();
    }
    public synchronized void decrement() throws InterruptedException {
        while (number==0){
            this.wait();
            //等待
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程唤醒
        this.notify();
    }
}

lock版本

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

public class Main{
    //线程交替执行
    //俩个线程操作同一个变量
    public static void main(String[] args) {
        Data2 data=new Data2();
        new Thread(()->{
            for(int i=0;i<10;i++){
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"a").start();

        new Thread(()->{
            for (int i=0;i<10;i++){
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"b").start();
    }
}
//等待,业务,通知
class Data2{
    public int number=0;
    Lock lock =new ReentrantLock();
    Condition condition=lock.newCondition();
    public  void increment() throws InterruptedException {
        try {
            lock.lock();
            //业务代码
            while(number!=0){
                //等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
    public  void decrement() throws InterruptedException {
        try {
            lock.lock();
            //业务代码
            while(number==0){
                //等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }
}

condition实现精准通知唤醒

通过signal精确指定执行流程

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

class Main{
    public static void main(String[] args) {
        Data data=new Data();

        new Thread(()->{
            for (int i=0;i<10;i++){
                data.printA();
            }
        },"a").start();
        new Thread(()->{
            for (int i=0;i<10;i++){
                data.printB();
            }
        },"b").start();
        new Thread(()->{
            for (int i=0;i<10;i++){
                data.printC();
            }
        },"c").start();

    }
}
class Data{
    private Lock lock=new ReentrantLock();
    private Condition condition1=lock.newCondition();
    private Condition condition2=lock.newCondition();
    private Condition condition3=lock.newCondition();
    private int number =1;
    public void printA(){
        lock.lock();
        try {
            //判断->执行->通知
            while(number!=1){
                //等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
            //唤醒指定的人
            number=2;
            condition2.signal();//唤醒指定的线程
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void printB(){
        lock.lock();
        try {
            while (number!=2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>BBBBBBB");
            number=3;
            condition3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            while (number!=3){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+"=>BBBBBBB");
            number=1;
            condition1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

集合类安全问题

传统的类,列入arraylist,set,hashmap等

ReentranLock

相当于与synchronized它具备以下的特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 基本语法

    import java.util.concurrent.locks.ReentrantLock;
    
    class Main{
        static ReentrantLock lock=new ReentrantLock();
    
        public static void main(String[] args) {
            lock.lock();
            try {
    
            }finally {
                lock.unlock();//释放锁
            }
        }
    }

与synchronized一样,都支持可重入

特性

可重入

可重入是指同一个线程如果首次得到了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁住

可打断

如果不喜欢线程一直等,希望等待可以被终止,就可以用lockInterruptibly()实现

import java.util.concurrent.locks.ReentrantLock;

class Main{
    static ReentrantLock lock=new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            try{
                System.out.println("尝试获得锁");
                //如果没有竞争那么此方法就会获取lock对象锁
                //如果有竞争就进入阻塞队列,可以被其他线程用interruput
                lock.lockInterruptibly();
            }catch (InterruptedException e){
                e.printStackTrace();
                System.out.println("没有获得锁,正在返回");
                return ;
            }
            try {
                System.out.println("获取到锁");
            }finally {
                lock.unlock();
            }
        },"t1");
        lock.lock();
        t1.start();

        Thread.sleep(1);
        System.out.println("打断");
        //这样就可以对原来的线程进行打断,如果用的是lock就没有办法进行打断
        t1.interrupt();
    }
}

以上代码的执行结果是

尝试获得锁
打断
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at Main.lambda$main$0(Main.java:12)
    at java.lang.Thread.run(Thread.java:745)
没有获得锁,正在返回

锁超时

可打断是一种主动的避免死锁的方法,而锁超时是一种被动的防止死锁的方法

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

class Main{
    static ReentrantLock lock=new ReentrantLock();


    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("尝试获得锁");
            //trylock()获取锁,如果过去到了返回true反之返回false
            try {
                //可以传入时间参数来指定等待的时间,前面单数是时间,后面的参数是单位
                if (!lock.tryLock(1, TimeUnit.SECONDS)){
                    System.out.println("获取不到锁");
                    return;
                }
            } catch (InterruptedException e) {
                //这里抛出的异常是因为,trylock的等待也可以打断,因此如果设置了打断时间,就会抛出这个异常
                //处理的方式也是获取不到所并进行返回
                e.printStackTrace();
                System.out.println("获取不到锁");
                return;
            }
            try {
                System.out.println("获取到了锁");
            }finally {
                lock.unlock();
            }
        },"t1");
        lock.lock();
        t1.start();
    }
}

synchronized和reentrantlock一样默认创建的是非公平锁,但是reentrantlock可以在构造方法中传入true指定为公平锁

公平锁

优点:所有的线程都能得到资源,不会饿死在队列中.

缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大

非公平锁

优点:可以减少cpu唤醒线程的开销,整体的吞吐效率会高,cpu也不必唤醒所有线程,会年少线程的数量

缺点:导致队列中的线程一直获取不到锁或者长时间获取不到锁,导致饿死

公平一般没有必要!会降低并发度

原理

先从构造器开始看默认为非公平锁的实现

public ReentrantLock() {
    sync = new NonfairSync();
}

先来看看枷锁非公平锁

final void lock() {
    if (compareAndSetState(0, 1))//尝试用compareAndSetState试图把expect的状态改成1
        //如果修改成功了,就会把Owner线程改成当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

以下是成功时候的情况

当竞争出现的时候会调用acquire方法

public final void acquire(int arg) {
    //如果是真则执行逻辑与的下半部分
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//创建一个节点对象,并加入到等待队列里
        selfInterrupt();
}

当竞争出现的时候

会执行以下操作

  • cas尝试将state由0改为1,结果失败
  • 进入tryacquire逻辑,这是state已经是1,结果仍失败
  • 接下来进入addWaiter逻辑,构造node队列

    • 途中黄色的三角表示该node的waitStatus状态,其中0为默认正常状态
    • node创建是懒惰的
    • 其中第一个node称为哑元或哨兵,用来占位,并不关联线程

之后会进入acquireQueued逻辑

  • acquireQueued会在一个死循环中不断尝试获得锁,失败后进入park阻塞
  • 如果自己是紧邻这head(排列第二位),那么在此tryAcquire尝试获取锁,当这是state仍未1,失败
  • 进入shouldParkAfterFailedAcquire逻辑,将前去的node,即head的waitStatus改为-1,这次返回false

  • shouldParkAfterFailedAcquire执行完毕回到acquireQueued,再次tryAcquire尝试获取锁,当然这时state仍为1,失败
  • 当再次进入shouldParkAferFailedAcquire 时,这时因为其前驱node 的waitStatus已经是-1,这次返回true
  • 进入partAndCheckInterrupt,Thread-1 park(灰色表示)

如果有多个线程竞争失败,变成这个样子

  • 当前队列不为null,并且head的waitStatus=-1,进入unparkSuccessor流程
  • 找到队列中离head最近的一-个Node (没取消的),unpark 恢复其运行,本例中即为
    Thread-1
  • 回到Thread-1的acquireQueued流程

如果加锁竞争成功(没有竞争)会设置

  • exclusiveOwnerThread 为Thread-1, state= 1
  • head指向刚刚Thread-1所在的Node,该Node清空Thread
  • 原本的head因为从链表断开,而可被垃圾回收
    如果这时候有其它线程来竞争(非公平的体现),例如这时有Thread-4来了

总结

这里就是通过CAS(乐观锁)去修改state的值。lock的基本操作还是通过乐观锁来实现的

没有获取到锁会干以下的事情

  • tryAcquire:会尝试再次通过CAS获取一次锁。
  • addWaiter:将当前线程加入上面锁的双向链表(等待队列)中
  • acquireQueued:通过自旋,判断当前队列节点是否可以获取锁。

ReentrantReadWriteLock

特性

ReentrantReadWriteLock(读写锁)

当数据库操作读远高于写时,这时候可以用读写锁,让读与读可以并发,提高性能

提供一个数据容器类内部分别用读锁保护数据的read()方法,写锁保护数据的write()方法

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Main{
    public static void main(String[] args) {
        //以下实验是俩个读锁同时进行读取操作
        DataContainer dataContainer=new DataContainer();
        new Thread(()->{
            dataContainer.read();
        },"t1").start();

        new Thread(()->{
            dataContainer.read();
        },"t2").start();

        //读和写同时进行,会先执行完读操作,再执行写操作
        new Thread(()->{
            dataContainer.write();
        },"t3").start();
    }
}

 class DataContainer{
    private Object data;
    private  ReentrantReadWriteLock rw=new ReentrantReadWriteLock();//读写锁对象
    private ReentrantReadWriteLock.ReadLock r=rw.readLock();
    private ReentrantReadWriteLock.WriteLock w=rw.writeLock();
    public Object read(){
        System.out.println("获取读锁");
        r.lock();
        try {
            System.out.println("读取");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return data;
        }finally {
            System.out.println("释放读锁");
            r.unlock();
        }
    }
    public void write(){
        System.out.println("获取写锁");
        w.lock();
        try {
            System.out.println("写锁");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            System.out.println("释放写锁");
            w.unlock();
        }
    }
}

根据以上得出,读与读可以并发,写写与读写不能进行并发

注意

  • 读锁不支持条件变量(写锁支持)
  • 重入时不支持升级:如果有读锁的情况下获取写锁,会导致读取写锁永久等待
  • 重入时降级支持:即持有写锁的情况下去获取毒读锁

集合类安全问题

在arraylist中如果使用多线程去操作arraylist这个类会出现并发修改异常

以下方法可以有效运行

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

class Main{
    public static void main(String[] args) {
        //这样就变得线程安全了
        List<Object> list = Collections.synchronizedList(new ArrayList<>());
//        List<String>list=new CopyOnWriteArrayList<>(); //也可以使用一个线程安全的类
        for(int i=1;i<=10;i++){
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

CopyOnWriteArrayList

copyonwrite写入时复制,cow,计算机程序设计的一种优化策略;

多个线程调用的时候,list读取的时候,固定的写入(覆盖)

在读写的时候避免覆盖造成数据问题

读写分离

copyWriteArrayList与vector对比

copyWriteArrayList中add使用的是lock锁,vector使用的是synchronized

Last modification:May 9, 2024
如果觉得我的文章对你有用,请随意赞赏