JUC并发常用工具学习
今天主要来和大家分享一下JUC相关的一些简单知识,线程池文章就不介绍了,前面的文章有介绍,本文主要介绍Lock和认识synchronized和并发的一些工具类的使用。
Lock
传统的锁有synchronized关键字,我们可以直接在方法和代码块中使用它。
在Java中有ReentrantLock、ReentrantReadWriteLock
常用的ReentrantLock,默认采用的是非公平锁,也有公平锁的实现方式,简单点说,
-
公平锁需要根据申请锁的顺序来获取锁,按照先来先服务的原则。
-
非公平锁可以不按照申请锁的顺序,后申请的线程也可以按先申请锁的线程先获取锁。
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
下面我们也可以通过一个卖票的小例子来看ReentrantLock的使用,其中最主要的是lock和unlock方法,这方法需要成对出现,否则可能会出现锁未释放的情况。
class Ticket1 {
private int number = 50;
Lock lock = new ReentrantLock();
/**
* 卖票的方式
*/
public synchronized void sale() {
// 加锁
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余" + number);
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
}
简单对比Lock和synchronized:
-
synchronized和Lock的类型不同,一个是关键字,一个是类。
-
Lock可以判断锁的状态,而synchronized是不可以的。
-
synchronized的锁释放是自动释放的,而Lock是需要手动释放锁。
-
synchronized线程会一直等待,而Lock是不一定等待下去。
-
synchronized是可重入锁,是非公平锁,Lock可以重入锁,可以设置是否公平。
-
Lock适合锁大量代码,synchronized适合锁少量的代码。
生产者的消费者问题
synchronized版
这个问题一直都是比较经典的问题,我们可以使用synchronized关键字实现配置wait方法,notifyAll方法和notify方法实现。
注意wait方法只能出现在同步方法中。
下面的例子需要防止虚假唤醒的问题,即需要采用循环的方式而不能采用if的方式,在JDK1.8文章的也有说明
class Data {
private int number = 0;
/**
* 资源类
*/
public synchronized void increment() {
// if (number != 0) {
while (number != 0) {
// 等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知其他线程
this.notifyAll();
}
public synchronized void decrement() {
// if (number == 0) {
while (number == 0) {
// 等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 通知
this.notifyAll();
}
}
Lock版
可能有人也会说,有synchronize不九可以处理了么,在Java中还有一个配合也是可以实现这个功能的,也就是Lock配置Condition的await方法和signal方法,这个比synchronizd强在哪里呢?这个可以实现精准唤醒,而通过synchronized方法里面的notify不能实现精准唤醒。
下面这个例子就可以实现按顺序实现唤醒。
class Data2 {
private Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
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() + "=>" + number);
// 唤醒指定的人
number = 2;
condition2.signal();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (number != 2) {
// 等待
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 唤醒指定的人
number = 3;
condition3.signal();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (number != 3) {
// 等待
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "=>" + number);
// 唤醒指定的人
number = 1;
condition1.signal();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
}
理解锁
我们如何判断锁的是谁?锁的是调用该方法的对象还是Class模板呢?这样可以帮助我们深入理解锁。
这里还是采用狂神视频的提到的8锁问题,应该看完就可以理解synchronized锁的判断。
场景一
-
是输出发短信还是打电话?发短信
-
发短信延迟4秒,是发短信还是打电话?发短信
synchronized锁的是方法的调用者,两个方法用的是同一把锁,谁先拿到先执行。
public class Test1 {
@SneakyThrows
public static void main(String[] args){
Phone phone = new Phone();
new Thread(() -> {
phone.sendMsg();
}, "A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
phone.call();
}, "B").start();
}
}
class Phone {
public synchronized void sendMsg() {
System.out.println("sendMsg");
}
public synchronized void call() {
System.out.println("call");
}
}
场景二
-
下面先输出哪个结果?发短信还是hello? hello
hello方法是没有锁的,不是同步方法,不受锁影响
public class Test2 {
public static void main(String[] args){
Phone1 phone = new Phone1();
new Thread(() -> {
phone.sendMsg();
}, "A").start();
new Thread(() -> {
phone.call();
}, "B").start();
new Thread(() -> {
phone.hello();
}, "C").start();
}
}
class Phone1 {
public synchronized void sendMsg() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendMsg");
}
public synchronized void call() {
System.out.println("call");
}
public void hello() {
System.out.println("hello");
}
}
场景三
- 下面重新声明两个对象,一个调用同步发短信方法,一个调用call方法,先执行哪个方法? call
因为两个同步方法,锁的是方法调用者的对象,这里有两把锁
public class Test2 {
public static void main(String[] args){
Phone1 phone1 = new Phone1();
Phone1 phone2 = new Phone1();
new Thread(() -> {
phone1.sendMsg();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.call();
}, "B").start();
}
}
class Phone1 {
public synchronized void sendMsg() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendMsg");
}
public synchronized void call() {
System.out.println("call");
}
public void hello() {
System.out.println("hello");
}
}
场景四
- 增加两个静态同步方法,只有一个对象,先打印,发短信?打电话?发短信
类一加载就有了,这里锁的Class对象,Class是唯一的
public class Test3 {
public static void main(String[] args){
Phone3 phone3 = new Phone3();
new Thread(() -> {
phone3.sendMsg();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone3.call();
}, "B").start();
}
}
class Phone3 {
public static synchronized void sendMsg() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendMsg");
}
public static synchronized void call() {
System.out.println("call");
}
public void hello() {
System.out.println("hello");
}
}
-
两个对象,是发短信还是打电话?发短信
Class模板只有一个
public class Test3 {
public static void main(String[] args){
Phone3 phone3 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(() -> {
phone3.sendMsg();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone2.call();
}, "B").start();
}
}
- 一个对象,一个静态同步方法,一个普通同步方法,先发短信,还是先打电话?先打电话
sendMsg锁的是Class模板,call锁的是调用的对象
- 如果是两个对象,结果也是一样,结果还是打电话
public class Test4 {
public static void main(String[] args){
Phone4 phone4 = new Phone4();
new Thread(() -> {
phone4.sendMsg();
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone4.call();
}, "B").start();
}
}
class Phone4 {
public static synchronized void sendMsg() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendMsg");
}
public synchronized void call() {
System.out.println("call");
}
}
常见的并发辅助类
CountDownLatch
CountDownLatch里面定义了一个减法计算器,和一个阻塞队列,常用的方法有countDown和await方法,countDown相当于-1,await方法会等待计算器归零,然后再被唤醒向下执行。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数6
CountDownLatch countDownLatch = new CountDownLatch(5);
// -1
countDownLatch.countDown();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "go out!");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
// 等待计数器归零,然后再向下执行
countDownLatch.await();
System.out.println("close door!");
}
}
CyclicBarrier
需要等待线程全部达到共同屏障点的同步辅助,其实就是一个加法计算器,主要是可以实现让一组线程达到同一屏障点,然后再进行后续操作。
public class CyclicBarrierDemo {
public static void main(String[] args){
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () ->{
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
int temp = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore
一个计数信号量,其中常用的两个方法acquire用于获取许可证,release释放许可证。
可以用于限流等场景,比如下面的停车场例子,当初始化为一个时,也可以用于做互斥锁。
public class SemaphoreTest {
public static void main(String[] args){
// 停车位
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
// 等到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
读写锁
读锁可以允许多个线程同时读,而写锁只允许一个线程写。
下面的例子模拟了一个缓存,这里也采用volatie关键字,用于保证map的数据的可见性。
public class ReadWriteLockDemo {
public static void main(String[] args){
MyCache myCache = new MyCache();
// 多线程写
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.put(temp + "", temp + "");
}, String.valueOf(i)).start();
}
// 多线程读
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.get(temp + "");
}, String.valueOf(i)).start();
}
}
}
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 一个线程写
*/
public void put(String key, Object value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入");
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入成功!");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
/**
* 多个线程读
*/
public void get(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取");
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取成功:" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
总结
本文章分享的主要是一些并发工具类的使用和介绍,希望大家喜欢!
参考资料
狂神的JUC视频(B站上有,讲的还可以! )