之前Java基础学习了使用synchronized来做锁来实现线程安全问题,今天学习使用JUC中的Lock,他比synchronized更加灵活。
锁在释放之后允许被其他线程抢占或者被一个线程重复抢占。
main方法中:
static int i = 0;
public static void main(String[] args) throws InterruptedException {
//声明ReentrantLock对象
Lock lock = new ReentrantLock();
//启动线程
new Thread(new Add(lock),"one").start();
new Thread(new Add(lock),"two").start();
// main所在的类是一个线程类 - 主线程
// 启动2个Add线程,在Add线程启动和准备过程中, 主线程会抢占资源继续执行的
// 所以要让主线程阻塞
Thread.sleep(3000);
System.out.println(i);
}
线程类:
class Add implements Runnable {
private Lock lock;
public Add(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
// 加锁
lock.lock();
for (int i = 0; i < 100000; i++) {
LockDemo.i++;
}
// 解锁
lock.unlock();
}
从代码中可以看出one线程和two线程都会去抢cpu中的资源,但是我们在线程中使用Lock,并且在线程启动的时候把Lock传递了进去,虽然是两个线程,但是Lock是同一个Lock,所以两个线程无论谁进去,都会锁住代码,直到解锁,才开始新一轮的抢占CPU。
分为读锁和写锁 。在使用的时候需要先创建ReentrantReadWriteLock,通过这个对象获取读锁或者写锁,之后再加锁解锁或者解锁 。(readwritelock是一个接口,ReentrantReadWriteLock实现了这个接口。)
main方法:
static int i = 0;
public static void main(String[] args) throws InterruptedException {
// 加写锁 - 获取写锁
Lock lock = new ReentrantReadWriteLock().writeLock();
new Thread(new Add(lock),"one").start();
new Thread(new Add(lock),"two").start();
// main所在的类是一个线程类 - 主线程
// 启动2个Add线程,在Add线程启动和准备过程中,
// 主线程会抢占资源继续执行的
// 让主线程阻塞
Thread.sleep(3000);
System.out.println(i);
}
线程类:
class Add implements Runnable {
private Lock lock;
public Add(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
// 加锁
lock.lock();
for (int i = 0; i < 100000; i++) {
LockDemo.i++;
}
// 解锁
lock.unlock();
}
可以看出和前面使用reentraLock的区别就是在获取锁对象的时候不一样。
线程在抢占CPU的时候,线程的执行次数不一定都是平均的,所以这就是非公平原则,Lock默认为非公平,而synchronized也是非公平的。反而之,所谓的公平就是每个线程的执行次数差不多,那么如何实现公平原则?
使用一个队列,让线程不再直接抢占资源,而是去抢占入队顺序。
1.CountDownLatch(闭锁): 对线程进行计数,在计数归零之前线程会陷入阻塞;直到计数归零为止,才会放开阻塞。一组线程结束之后开启另一组线程。
案例:5个学生,2个老师,7个人都要到达考场,才开始考试,使用countdownLatch需要传入一个参数,表示计数个数。在计数器没有到0的时候,.await方法前面的线程都需要阻塞。
main:
public static void main(String[] args) throws InterruptedException {
CountDownLatch cdl = new CountDownLatch(7);
new Thread(new Teacher(cdl)).start();
new Thread(new Teacher(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
// 正常逻辑:上面七个线程执行完成之后,才能开始执行下面的代码
// 在上面七个线程结束之前,当前线程需要阻塞
// 在计数归零之前,需要阻塞
cdl.await();
System.out.println("考试开始~~~");
}
考生线程逻辑:
class Student implements Runnable {
private CountDownLatch cdl;
public Student(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
try {
// 模拟:考生到考场花费的时间
Thread.sleep((long) (Math.random() * 10000));
System.out.println("考生到达考场~~~");
//计数器减一
cdl.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
老师线程逻辑:
class Teacher implements Runnable {
private CountDownLatch cdl;
public Teacher(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
try {
// 模拟:考官到考场花费的时间
Thread.sleep((long) (Math.random() * 10000));
System.out.println("考官到达考场~~~");
// 一个线程已经结束,需要减少一个计数
cdl.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.CyclicBarrier(栅栏): 对线程进行计数,在计数归零之前线程会陷入阻塞;直到计数归零为止,才会放开阻塞。这一组线程到达同一个点之后再分别继续执行。
案例:比赛跑步的时候,需要所有人到了起跑线上开始跑步,每个线程代表一个跑者,每个线程到起跑线就阻塞,直到所有线程都到了起跑线,计数器为0,再放开阻塞。
main:
public class CyclicBarrierDemo {
public static void main(String[] args) {
//计数器为6
CyclicBarrier cb = new CyclicBarrier(6);
new Thread(new Runner(cb), "1号").start();
new Thread(new Runner(cb), "2号").start();
new Thread(new Runner(cb), "3号").start();
new Thread(new Runner(cb), "4号").start();
new Thread(new Runner(cb), "5号").start();
new Thread(new Runner(cb), "6号").start();
}
}
线程类:
class Runner implements Runnable {
private CyclicBarrier cb;
public Runner(CyclicBarrier cb) {
this.cb = cb;
}
@Override
public void run() {
try {
// 模拟:运动员走到起跑线的时间
Thread.sleep((long) (Math.random() * 10000));
String name = Thread.currentThread().getName();
System.out.println(name + "运动员到了起跑线~~~");
// 正常逻辑:先到的运动员应该阻塞,
// 直到所有的运动员都到了起跑线才能往外跑
// 阻塞,计数
// 这个方法在阻塞的同时会减少一个计数
// 当计数归零的时候,会放开阻塞
cb.await();
System.out.println(name + "运动员跑了出去~~~");
} catch (Exception e) {
e.printStackTrace();
}
}
}
交换机。用于交换两个线程之间的信息。注意: 只能交换两个线程之间的信息。
案例:生产者和消费者,生产者生产“商品”放入交换机,并且把消费者放入交换机中的“钱”拿出来。
main:
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> ex = new Exchanger<>();
new Thread(new Producer(ex)).start();
new Thread(new Consumer(ex)).start();
}
}
生产者:
class Producer implements Runnable {
private Exchanger<String> ex;
public Producer(Exchanger<String> ex) {
this.ex = ex;
}
@Override
public void run() {
try {
String info = "商品";
// 生产者应该商品交换给消费者,应该收到消费者换过来的钱
String msg = ex.exchange(info);
System.out.println("生产者收到消费者换来的:" + msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费者:
class Consumer implements Runnable {
private Exchanger<String> ex;
public Consumer(Exchanger<String> ex) {
this.ex = ex;
}
@Override
public void run() {
try {
String info = "钱";
// 消费者应该将钱交还给生产者,应该受到生产者换过来的商品
String msg = ex.exchange(info);
System.out.println("消费者收到生产者换来的:" + msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
信号量它更像一个权限,线程只有拿到信号量,才可以有执行下去的权限,如果没有信号量就阻塞,直到前面的线程释放出信号量。信号量的作用是用于限流 。
案例:7个客人(线程)用餐,5张桌子作为信号量,客人进来用餐,取走一个信号量,吃完释放信号量,让后面阻塞的线程可以继续。
main:
public class SemaphoreDemo {
public static void main(String[] args) {
//声明信号量,5个信号
Semaphore s = new Semaphore(5);
for (int i = 0; i < 7; i++) {
//启动7个线程,表示7个客人
new Thread(new Eater(s)).start();
}
}
}
线程类:
class Eater implements Runnable {
private Semaphore s;
public Eater(Semaphore s) {
this.s = s;
}
@Override
public void run() {
try {
// 桌子的数量是有限的
// 如果桌子被全部占用,后来的客人就需要等待
// 桌子相当于信号,只要有信号,那么就可以使用
s.acquire();
System.out.println("来了一波客人,占用了一张桌子~~~");
// 模拟:客人吃饭花费的时间
Thread.sleep((long) (Math.random() * 10000));
System.out.println("客人买单离开,空出一张桌子~~~");
// 释放1个信号,被阻塞的线程就可以获取信号执行代码
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}