JUC中的各种锁(ReentrantLock等)

ReentrantLock(可重入锁)

概念

​ ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁

简介

ReentrantLock常常对比着synchronized来分析

ReentrantLock常常对比着synchronized来分析,我们先对比着来看然后再一点一点分析。

(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。(ReentrantLock等lock方法一定要在finally进行unlock

(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。

(4 )ReentrantLock和synchronized最主要的就区别是ReentrantLock还可以实现公平锁机制。也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁(不公平锁是只要锁被释放,其他线程一起争抢锁)

公平锁和非公平锁

从ReentrantLock的构造中可以看到,ReentrantLock提供两种锁:公平锁和非公平锁,其内部实现了两种同步器NonfairSyncFairSync派生自AQS,主要才采用了模板方法模式,主要重写了AQS的tryAcquire、lock方法

ReentrantLock的实现原理

ReentrantLock在内部使用了内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操作带来效率问题。所以说本质上常见的同步锁是使用CAS+volatile来实现的

代码


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

public class T02_ReentrantLock2 {
    //非公平锁
	Lock lock = new ReentrantLock();
	void m1() {
		try {
			lock.lock(); //synchronized(this)
			for (int i = 0; i < 10; i++) {
				TimeUnit.SECONDS.sleep(1);
				System.out.println(i);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	void m2() {
		try {
			lock.lock();
			System.out.println("m2 ...");
		} finally {
			lock.unlock();
		}
	}

	public static void main(String[] args) {
		T02_ReentrantLock2 rl = new T02_ReentrantLock2();
		new Thread(rl::m1).start();
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		new Thread(rl::m2).start();
	}
}
输出结果
==========================================
0
1
2
3
4
5
6
7
8
9
m2 ...     
==========================================  
  

使用场景

  • 发现该操作已经在执行中则不再执行
  • 如果该操作已经在执行,则等待一个个执行(同步执行,与synchronized类似)
  • 如果该操作已经在执行,则尝试等待一段时间,超时则放弃执行
  • 如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作

CountDownLatch

概念

  • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
  • CountDownLatch是一个同步容器,但是有人叫它发令枪,也有人叫它门闩。初始化设定线程的个数,调用countDownLatch.await()阻塞所有线程,直到countDownLatch.countDown()为0,那么将继续执行剩余的操作。

主要方法

  • countDownLatch类中只提供了一个构造器:

     public CountDownLatch(int count) {}
    
  • 类中有三个方法是最重要的:

    //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
    public void await() throws InterruptedException { };   
    //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
    public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
    //将count值减1
    public void countDown() { };  
    

代码示例

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchTest {
    public static void main(String[] args) {
        //new CountDownLatch(2);这里设置成2是因为下面只创建了两个新线程,如果大于2,则会到countDownLatch.countDown()方法中不肯能为0.导致主线程一直阻塞
       final CountDownLatch countDownLatch = new CountDownLatch(2);
        System.out.println("主线程开始执行");
        //第一个子线程开始执行
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println("子线程"+Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                // countDownLatch.countDown(); 这一句话要写在finally中,不然的话出现异常会导致无法减一,然后出现死锁的
                    countDownLatch.countDown();
                }
            }
        });
        executorService.shutdown();
        //第二个子线程开始执行
        ExecutorService executorService2 = Executors.newSingleThreadExecutor();
        executorService2.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println("子线程"+Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown();
                }

            }
        });
        executorService2.shutdown();
        System.out.println("等待两个线程执行完毕");
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        }
        System.out.println("两个子线程执行完毕,继续执行主线程");
        System.out.println("主线程执行完毕");
    }

}


========================================
主线程开始执行
等待两个线程执行完毕
子线程pool-2-thread-1
子线程pool-1-thread-1
两个子线程执行完毕,继续执行主线程
主线程执行完毕
========================================
使用CountDownLatch使主线程阻塞,直到线程1,2执行完主线程才会继续执行

使用场景

  • 游戏多线程加载图片,音乐等相关资源,需要全部加载完必须的资源才能正常的进入游戏

CyclicBarrier

概念

从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。它的作用就是会让所有线程都等待完成后才会继续下一步行动

例子:当你和朋友一起吃饭,有人先到,有人后到,正常情况下就会等到一起才会点菜吃饭

主要方法

  • CyclicBarrier提供了一个构造器
//表示需要等到几个线程一起才能执行下一个线程
public CyclicBarrier(int parties)
 //parties 是参与线程的个数, Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
public CyclicBarrier(int parties, Runnable barrierAction这个线程做什么处理)
  • 类中有两个重要方法
//线程调用 await() 表示自己已经到达栅栏
public int await() throws InterruptedException, BrokenBarrierException
//BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

代码示例

public class T07_TestCyclicBarrier {
    public static void main(String[] args) {
//        CyclicBarrier barrier = new CyclicBarrier(20);
        CyclicBarrier barrier = new CyclicBarrier(20, () -> System.out.println("满20"));
        for(int i=0; i<100; i++) {

                new Thread(()->{
                    try {
                        barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }).start();
            
        }
    }
}

=====================================
满20
满20
满20
满20
满20

使用场景

  • 可以用于多线程计算数据,最后合并计算结果的场景。

phaser

概念

​ phaser与CountDownLatch非常相似,允许我们协调线程的执行。与CountDownLatch相比,它具有一些额外的功能。Phaser是在线程动态数需要继续执行之前等待的屏障。在CountDownLatch中,该数字无法动态配置,需要在创建实例时提供

主要方法

  • 构造方法
//无参构造默认阶段数为0,创建一个没有初始注册方,没有父级且初始阶段号为0的新相位器。使用此相位器的任何线程都需要首先为其注册  
public Phaser() {
        this(null, 0);
    }
//创建指定数量为parties的相位器
    public Phaser(int parties) {
        this(null, parties);
    }
  • 核心方法
 //使线程阻塞,等待指定线程数目之后再继续运行
public int arriveAndAwaitAdvance() {}
//注销到达的相关线程
public int arriveAndDeregister() {}
//注册一个新线程
    public int register() {
        return doRegister(1);
    }

代码示例

import java.util.Random;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
public class T09_TestPhaser2 {
    static Random r = new Random();
    static MarriagePhaser phaser = new MarriagePhaser();
 
    public static void main(String[] args) {
        phaser.bulkRegister(7);
        for(int i=0; i<5; i++) {
            new Thread(new Person("p" + i)).start();
        }
        new Thread(new Person("新郎")).start();
        new Thread(new Person("新娘")).start();
    }
    static class MarriagePhaser extends Phaser {
        @Override
        //该方法会在phaser.arriveAndAwaitAdvance();进行相关判断,phase阶段一定是从0开始进行系列操作,只有当前Phaser中有指定数量的线程时才会进行运行
        protected boolean onAdvance(int phase, int registeredParties) {
            switch (phase) {
                case 0:
                    System.out.println("所有人到齐了!" + registeredParties);
                    System.out.println();
                    return false;
                case 1:
                    System.out.println("所有人吃完了!" + registeredParties);
                    System.out.println();
                    return false;
                case 2:
                    System.out.println("所有人离开了!" + registeredParties);
                    System.out.println();
                    return false;
                case 3:
                    System.out.println("婚礼结束!新郎新娘抱抱!" + registeredParties);
                    return true;
                default:
                    return true;
            }
        }
    }
  static void milliSleep(int milli) {
        try {
            TimeUnit.MILLISECONDS.sleep(milli);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    static class Person implements Runnable {
        String name;

        public Person(String name) {
            this.name = name;
        }

        public void arrive() {

            milliSleep(r.nextInt(1000));
            System.out.printf("%s 到达现场!\n", name);
            phaser.arriveAndAwaitAdvance();
        }

        public void eat() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 吃完!\n", name);
            phaser.arriveAndAwaitAdvance();
        }

        public void leave() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 离开!\n", name);


            phaser.arriveAndAwaitAdvance();
        }
        private void hug() {
            if(name.equals("新郎") || name.equals("新娘")) {
                milliSleep(r.nextInt(1000));
                System.out.printf("%s 洞房!\n", name);
                phaser.arriveAndAwaitAdvance();
            } else {
                phaser.arriveAndDeregister();
          /*      phaser.register();
                System.out.println(phaser.getPhase());*/
            }
        }
        @Override
        public void run() {
            arrive();
            eat();
            leave();
            hug();

        }
    }
}

======================================
p3 到达现场!
新郎 到达现场!
p2 到达现场!
p1 到达现场!
p0 到达现场!
p4 到达现场!
新娘 到达现场!
所有人到齐了!7

新娘 吃完!
p0 吃完!
p1 吃完!
p4 吃完!
新郎 吃完!
p2 吃完!
p3 吃完!
所有人吃完了!7

p0 离开!
p3 离开!
新郎 离开!
新娘 离开!
p2 离开!
p4 离开!
p1 离开!
所有人离开了!7

新娘 洞房!
新郎 洞房!
婚礼结束!新郎新娘抱抱!2

使用场景

  • 在需要对数据进行拦截的时候

ReadWriteLock

概念

ReadWriteLock是JDK5中提供的读写分离锁。读写分离锁可以有效地帮助减少锁竞争,以提高系统性能

读写锁允许多个线程同时读,使得B1,B2,B3之间真正并行。但是,考虑都数据完整性,写写操作和读写操作间依然时需要相互等待和持有锁的

主要方法

//读锁
Lock readLock();
//写锁
Lock writeLock();

代码示例

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class T10_TestReadWriteLock {
    static Lock lock = new ReentrantLock();
    private static int value;
    static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static Lock readLock = readWriteLock.readLock();
    static Lock writeLock = readWriteLock.writeLock();

    public static void read(Lock lock) {
        try {
            lock.lock();
            Thread.sleep(1000);
            System.out.println("read over!");
            //模拟读取操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void write(Lock lock, int v) {
        try {
            lock.lock();
            Thread.sleep(1000);
            value = v;
            System.out.println("write over!");
            //模拟写操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        Runnable readR = ()-> read(readLock);
        Runnable writeR = ()->write(writeLock, new Random().nextInt());
        for(int i=0; i<5; i++) new Thread(readR).start();
        for(int i=0; i<2; i++) new Thread(writeR).start();
    }
}
==========
read over!
read over!
read over!
read over!
read over!
write over!
write over!

Semaphore

概念

semaphore 是一个计数信号量,必须由获取它的线程释放。

常用于限制可以访问某些资源的线程数量,例如通过 Semaphore 限流。

Semaphore 只有3个操作:

  1. 初始化
  2. 增加
  3. 减少

代码示例

import java.util.concurrent.Semaphore;
public class T11_TestSemaphore {
    public static void main(String[] args) {
        //Semaphore s = new Semaphore(2);
        Semaphore s = new Semaphore(2, true);//设置是否是公平锁,默认是非公平锁
        //允许一个线程同时执行
        //Semaphore s = new Semaphore(1);
        new Thread(()->{
            try {
                //进行请求
                s.acquire();

                System.out.println("T1 running...");
                Thread.sleep(200);
                System.out.println("T1 running...");

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                s.release();
            }
        }).start();

        new Thread(()->{
            try {
                s.acquire();

                System.out.println("T2 running...");
                Thread.sleep(200);
                System.out.println("T2 running...");

                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
==========================
T1 running...
T2 running...
T1 running...
T2 running...

Exchanger

概念

一个用于两个工作线程之间交换数据的封装工具类,简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据

主要方法

Exchanger():无参构造方法。
V exchange(V v):等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。
V exchange(V v, long timeout, TimeUnit unit):等待另一个线程到达此交换点(除非当前线程被中断或超出了指定的等待时间),然后将给定的对象传送给该线程,并接收该线程的对象。

代码示例

import java.util.concurrent.Exchanger;

public class T12_TestExchanger {
    static Exchanger<String> exchanger = new Exchanger<>();
    /*
    * exchanger只能是两个线程之间,多个线程需要进行线程同步
    * */
    public static void main(String[] args) {
        new Thread(()->{
            String s = "T1";
            try {
                //在此会阻塞等待T2线程进行数据交换
                s = exchanger.exchange(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + s);
        }, "t1").start();

        new Thread(()->{
            String s = "T2";
            try {
                s = exchanger.exchange(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + s);
        }, "t2").start();
    }
}
=============================
t2 T1
t1 T2

LockSupport

概念

LockSupport是一个线程工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,也可以在任意位置唤醒。它的内部其实两类主要的方法:park(停车阻塞线程)和unpark(启动唤醒线程)。

主要方法

public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);

代码示例

public class T13_TestLockSupport {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if(i == 5) {
//                    当前线程停止,进入到阻塞状态
                    LockSupport.park();
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();
        //LockSupport.park()之后,可以使用unpark方法使进程继续运行
        //不进行 LockSupport.unpark(t)则会导致线程一直都会阻塞
       LockSupport.unpark(t);     
    }
}

park和unpark的优点

一个线程它有可能在别的线程unPark之前,或者之后,或者同时调用了park,那么因为park的特性,它可以不用担心自己的park的时序问题,否则,如果park必须要在unpark之前,那么给编程带来很大的麻烦!!

考虑一下,两个线程同步,要如何处理?

在Java5里是用wait/notify/notifyAll来同步的。wait/notify机制有个很蛋疼的地方是,比如线程B要用notify通知线程A,那么线程B要确保线程A已经在wait调用上等待了,否则线程A可能永远都在等待。另外,是调用notify,还是notifyAll?

notify只会唤醒一个线程,如果错误地有两个线程在同一个对象上wait等待,那么又悲剧了。为了安全起见,貌似只能调用notifyAll了。

park/unpark模型真正解耦了线程之间的同步,线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态。

posted @ 2021-06-08 15:00  知白守黑,和光同尘  阅读(216)  评论(0编辑  收藏  举报