Java并发编程之锁
一、 Lock 锁
java.util.concurrent.locks.Lock
为什么有了synchronized,还需要Lock呢?
- 使用方式更灵活
- 性能开销小
1.1 ReentrantLock
简单示例:
public class TestLock {
private Lock lock=new ReentrantLock();
private int value;
public void add(){
try {
lock.lock();
value++;
} finally {
lock.unlock();
}
}
}
ReentrantLock:可重入锁
new ReentrantLock(true):可重入锁+公平锁
可重入锁:某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。 ReentrantLock和synchronized都是可重入锁。
公平锁:先排队的先获得锁。
可重入锁的示例:
public class TestLock {
private Lock lock=new ReentrantLock();
private int value;
public void add(){
try {
lock.lock();
value++;
//已经获取了锁,再进入也要获取锁的print方法,不会产生死锁
print();
} finally {
lock.unlock();
}
}
public void print(){
try {
lock.lock();
System.out.println("打印内容");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new TestLock().add();
}
}
1.2 ReentrantReadWriteLock 读写锁
读写锁示例:
public class TestLock {
private int sum;
private ReadWriteLock lock=new ReentrantReadWriteLock(true);
public int incrAndGet(){
try {
lock.writeLock().lock();
sum++;
return sum;
} finally {
lock.writeLock().unlock();
}
}
public int getSum(){
try {
lock.readLock().lock();
return sum;
} finally {
lock.readLock().unlock();
}
}
}
读读不互斥,读写互斥,写写互斥
适用于读多写少的场景。
读为什么要加锁呢?
答:避免读的时候去写。
1.3 Condition的用法
使用Condition实现了生产-消费的案例
public class TestLock {
private int sum;
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty=lock.newCondition();
public void consumer() throws InterruptedException {
try {
lock.lock();
while (sum<=0){
notEmpty.await();
}
sum--;
System.out.println("消费者:"+sum);
notFull.signal();
} finally {
lock.unlock();
}
}
public void producer() throws InterruptedException {
try {
lock.lock();
while (sum>=20){
notFull.await();
}
sum++;
System.out.println("生产者:"+sum);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
TestLock testLock=new TestLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
while (true){
testLock.consumer();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
while (true){
testLock.producer();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
1.4 LockSupport 锁当前线程
使用示例:
public class TestThread {
public static void main(String[] args) throws InterruptedException {
ChildThread childThread = new ChildThread(Thread.currentThread());
childThread.start();
LockSupport.park();
System.out.println("主线程结束");
}
private static class ChildThread extends Thread{
private Thread thread;
public ChildThread(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("模拟一些初始化操作");
LockSupport.unpark(thread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
用锁的最佳实践
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不再调用其他对象的方法时加锁
二、并发原子类
并发原子类所在的包:java.util.concurrent.atomic
使用示例:
public class TestLock {
private AtomicInteger atomicInteger=new AtomicInteger();
public int add(){
return atomicInteger.incrementAndGet();
}
public int get(){
return atomicInteger.get();
}
}
整个AtomicInteger的实现是无锁的。
实现原理:
- volatile保证读写操作都可见
- 使用CAS指令,通过自旋重试保证写入
- CAS是基于Unsafe的API Compare-And-Swap
- CPU 硬件指令支持 :CAS指令
LongAdder 对AtomicLong的改进:
分段思想。
三、并发工具类
我们已经有了锁、有了并发原子类,为什么我们还需要并发工具类呢?
思考以下场景:有一个方法,用10个线程来分别都执行,但是同时只能有4个在执行。用锁的方式就不好实现。所以JDK给我们提供了以下的并发工具类。
3.1 Semaphore 信号量
应用场景:同一时间控制并发线程数。
public class TestLock {
//声明4个permits(许可证)的信号量
Semaphore semaphore = new Semaphore(4);
public static void main(String[] args) {
TestLock testLock = new TestLock();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
testLock.test();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
public void test() throws InterruptedException {
//拿到一个permits(许可证),也可以带参,一个线程拿两个,如:semaphore.acquire(2);
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "在工作");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放一个permits
semaphore.release();
}
}
}
3.2 CountDownLatch 闭锁
应用场景:Master线程等待所有Worker线程把任务执行完。
示例:公司有5个人,每个人都完成了手头的工作,老板才下班。
public class TestLock2 {
CountDownLatch countDownLatch=new CountDownLatch(5);
public static void main(String[] args) throws InterruptedException {
TestLock2 testLock = new TestLock2();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
testLock.test();
}
}).start();
}
testLock.countDownLatch.await();
System.out.println("大家都工作完了,老板(监工)可以下班了");
}
public void test() {
System.out.println(Thread.currentThread().getName() + "在工作");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
System.out.println(Thread.currentThread().getName() + "工作完了");
}
}
}
相当于CountDownLatch有一个计数器,调用了await方法后等待在那里了,每调用countDown方法就减一,只有当计数器减为0了,await才被唤醒。
3.3 CyclicBarrier 栅栏
场景:任务执行到一定阶段,等其他任务对齐
在作用上很类似CountDownLatch,只是使用的方法不一样。
示例:
public class TestLock2 {
CyclicBarrier cyclicBarrier=new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
TestLock2 testLock = new TestLock2();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
testLock.test();
}
}).start();
}
}
public void test() {
System.out.println(Thread.currentThread().getName() + "在工作");
try {
int i = new Random().nextInt(10);
TimeUnit.SECONDS.sleep(i);
System.out.println(Thread.currentThread().getName() + "工作完了");
cyclicBarrier.await();
System.out.println("大家都工作完了,老板(监工)可以下班了");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
CyclicBarrier没有countDown方法,而是都阻塞到await那里,等阻塞的个数达到,就可以放行。
3.4 AQS 同步队列器
AQS是AbstractQueuedSynchronizer的缩写,它是构建锁或其他组件的基础。如:CountDownLatch、Semaphore、ReentrantLock。
里面有两种资源共享方式:独占|共享。 独占的实现如ReentrantLock,共享的实现如 Semaphore 。子类负责实现公平锁或者非公平锁。
以ReentrantLock为例,里面有一个state变量,如果state为0,则表示未锁定,调用tryAcquire方法独占锁,并将state+1,此后其他线程来就不能占用了,放入队列里去等着。如果还是当前线程调用,那么他是可以再获得锁的,只是state还要累加。(可重入的概念)但是要注意,获得多少次就要释放多少次才行。