一、什么是JUC
指的是java.util下的三个并发编程工具包。
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
在Java中,并发编程离不开多线程开发,Java实现多线程的方式主要有以下4种:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 开启线程池
以下是几种方式的简单示例:
public class Demo1 {
public static void main(String[] args) {
// 继承Thread类
Thread1 thread1 = new Thread1();
thread1.start();
System.out.println(Thread.currentThread().getName() + " is running ");
// 实现Runnable接口
Thread2 thread2 = new Thread2();
Thread thread = new Thread(thread2);
thread.start();
// 实现Callable接口
ExecutorService executorService = Executors.newSingleThreadExecutor();
FutureTask<Integer> futureTask = new FutureTask<>(new Thread3());
executorService.execute(futureTask);
try {
Integer integer = futureTask.get();
System.out.println(Thread.currentThread().getName() + " is running " + " result is " + integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
static class Thread1 extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ");
}
}
static class Thread2 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ");
}
}
static class Thread3 implements Callable<Integer> {
@Override
public Integer call() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + " is running " + " result is " + 666);
return 666;
}
}
}
二、线程和进程
在讨论进程和线程之前,有必要先了解一下并发和并行的概念。
并发:同一时刻多个线程访问同一个资源。
例:春运抢票,电商秒杀活动
并行:多项工作互不影响,同时进行。
例:泡面,烧水和准备方便面及料包可以同时进行。
进程:指在操作系统中运行的一个应用程序实例,进程是操作系统资源分配的最小单位。
线程:程序执行的最小单位,可以认为是进程内的一个独立执行流。一个进程可以包含多个线程,至少包含一个线程。
Java语言本身是不能直接开启线程的,通过查看start()方法的源代码,可以看到Java是通过调用一个叫start0()的native方法来实现开启线程的操作的。以下是截取的Thread.java的一部分源码。
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
线程从开启到执行完成,会经历一些状态,这些状态一起组成了线程的生命周期。一个线程有以下几种可能的状态,以下代码也来自Java Thread.java源码:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called {@code Object.wait()}
* on an object is waiting for another thread to call
* {@code Object.notify()} or {@code Object.notifyAll()} on
* that object. A thread that has called {@code Thread.join()}
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
从注释中可以看出,一个线程进入WAITING状态,除了LockSupport.park的情形外,主要有wait和sleep两种方式,二者的区别是面试经常会考察的内容:
- wait()方法属于Object类的成员方法,任何对象的实例都能调用。sleep()是Thread类的静态方法。
- wait()方法调用时会释放锁,而sleep()不需要占用锁,因此也无需释放。
- wait()方法必须在同步代码块中使用,不需要捕获异常。sleep()方法可以在任何地方使用,必须要捕获异常。
三、Lock锁与Synchronized
先来看一个案例:假设3个售票员,同时卖30张票。分别用Lock和Synchronized实现,怎样才能保证线程安全。
用Synchronized实现:
public class Demo2 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}).start();
}
static class Ticket {
private int remaining = 30;
public synchronized void sale() {
if (remaining > 0) {
System.out.println(Thread.currentThread().getName() + " sale " + remaining--);
}
}
}
}
用Lock实现:
public class Demo3 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
ticket.sale();
}
}).start();
}
static class Ticket {
private int remaining = 30;
Lock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (remaining > 0) {
System.out.println(Thread.currentThread().getName() + " sale " + remaining--);
}
} finally {
lock.unlock();
}
}
}
}
通过对比以上两个例子,我们可以得出以下结论:
- synchronized是Java内置的关键字,Lock是Java的一个接口;
- synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁;
- synchronized会自动释放锁,Lock必须手动释放锁;
- synchronized和Lock都是可重入锁,synchronized是默认的非公平锁,Lock的锁是否公平锁可以设置;
四、线程间通信
最典型的线程间通信场景,莫过于生产者消费者模型。接下来分别以Object的wait/notify,Lock类中的await/signal来实现该案例。
wait/notify(这里有一个问题需要注意,当线程数量比较多的时候,notifyAll可能会导致虚假唤醒,因此这里的判断必须用while,而非if)
public class Demo4 {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
resource.produce();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
resource.consume();
}
}).start();
}
static class Resource {
private int num = 0;
public synchronized void produce() {
try {
if (num > 0) {//这里需要用while
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + " current num: " + num);
this.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void consume() {
try {
if (num == 0) {// 这里需要用while
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + " current num: " + num);
this.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
await/signal(这里需要注意,使用await()和signal()的方法中必须有对应的锁,而不能以synchronized修饰)
package com.lantian3.practise.bio.juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo5 {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
resource.produce();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
resource.consume();
}
}).start();
}
static class Resource {
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void produce() {
lock.lock();
try {
while (num > 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + " current num: " + num);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
while (num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + " current num: " + num);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
接下来看一个案例,使线程按约定顺序执行,这个案例中我们开启A,B,C三个线程,让它们依次执行。从代码中可以看出,主要是利用限制条件主动去唤醒对应的线程。
public class Demo6 {
public static void main(String[] args) {
Test test = new Test();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
test.printA();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
test.printB();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
test.printC();
}
}).start();
}
static class Test {
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
private int num = 1;
public void printA() {
lock.lock();
try {
while (num != 1) {
condition1.await();
}
System.out.println(Thread.currentThread().getName() + " AAAAAAAA ");
num = 2;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (num != 2) {
condition2.await();
}
System.out.println(Thread.currentThread().getName() + " BBBBBBBB ");
num = 3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (num != 3) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + " CCCCCCCC ");
num = 1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
五、八锁现象
其实就是关于Java锁的8个问题,通过对这8个问题的探索,我们大致可以得出Java中的锁,究竟是对什么上锁。
- 一个资源类,有两个非静态方法,都被synchronized修饰,开启两个线程,分别调用两个方法,哪个方法先执行?
public class Demo7 {
public static void main(String[] args) {
Phone phone = new Phone();
// 这个情况下,由于先开启发短信线程,因此大概率先输出发短信。
new Thread(() -> phone.sendSms()).start();
new Thread(() -> phone.call()).start();
}
static class Phone {
public synchronized void sendSms() {
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
}
- 如果,其中一个方法增加sleep方法,使其延迟执行,哪个方法先执行?
public class Demo7 {
public static void main(String[] args) {
Phone phone = new Phone();
// 发短信线程中增加延迟4秒,结果依然大概率先输出发短信。
new Thread(() -> phone.sendSms()).start();
new Thread(() -> phone.call()).start();
}
static class Phone {
public synchronized void sendSms() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
}
- 在以上基础上增加一个非静态的非同步方法,三个方法同时执行,输出顺序是怎样的?
public class Demo7 {
public static void main(String[] args) {
Phone phone = new Phone();
// 无论普通方法位置在哪里,都会先输出,然后其他线程才会依次输出。
new Thread(() -> phone.sendSms()).start();
new Thread(() -> phone.call()).start();
new Thread(() -> phone.printTest()).start();
}
static class Phone {
public synchronized void sendSms() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
public void printTest() {
System.out.println("普通方法");
}
}
}
- 如果在调用发短信和打电话方法时,分别创建两个Phone对象去调用,输出顺序如何?
public class Demo7 {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
// 此种情况下,先输出打电话,由此我们可以初步猜测,非静态方法的锁和对象有很大关系。
new Thread(() -> phone1.sendSms()).start();
new Thread(() -> phone2.call()).start();
}
static class Phone {
public synchronized void sendSms() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
}
- 如果把发短信和打电话的方法改为静态方法,输出顺序如何?
public class Demo7 {
public static void main(String[] args) {
new Thread(() -> Phone.sendSms()).start();
new Thread(() -> Phone.call()).start();
}
static class Phone {
public static synchronized void sendSms() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
}
- 如果分别通过两个对象去调用,输出顺序如何?
public class Demo7 {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone1.sendSms()).start();
new Thread(() -> phone2.call()).start();
}
static class Phone {
public static synchronized void sendSms() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
}
- 发短信和打电话分别来自静态同步方法和非静态同步方法,通过同一对象进行调用,输出顺序如何?
public class Demo7 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> phone.sendSms()).start();
new Thread(() -> phone.call()).start();
}
static class Phone {
public static synchronized void sendSms() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
}
- 发短信和打电话分别来自静态同步方法和非静态同步方法,通过两个对象进行调用,输出顺序如何?
public class Demo7 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone.sendSms()).start();
new Thread(() -> phone2.call()).start();
}
static class Phone {
public static synchronized void sendSms() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
}
通过以上案例的输出情况我们可以得出以下结论:
- 普通同步方法,锁的是调用方法的对象,谁先拿到锁谁先执行,同一个对象拿到的是同一把锁。
- 静态同步方法,锁的是类对象(Phone.class),谁先拿到锁谁先执行,由于类只加载一次,因此只有一把锁。
- 非静态非同步方法,由于不受锁控制,因此不受锁的影响。
- 补充一点,如果synchronized锁的某一代码块,锁的是括号里配置的对象。
六、集合类线程不安全
List:在此案例中,我们可以从结果中发现,list的size绝大多数情况下是小于1000的,说明ArrayList在多线程场景下是线程不安全的。
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
int finalI = i;
new Thread(() -> {
list.add(finalI);
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
如果想在多线程场景下使用list,并且想要保证线程安全,可以使用以下List:
- Vector–add,set,remove等方法,被synchronized修饰
- Collections.synchronizedList–返回的包装器类会对每个修改列表状态的方法添加synchronized关键字
- CopyOnWriteArrayList–add,set,remove等方法被锁(通常是ReentrantLock)保护;写时复制
public class Demo8 {
public static void main(String[] args) {
// List<Integer> list = new Vector<>();
// List<Integer> list = new CopyOnWriteArrayList<>();
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
// List<Integer> list = new ArrayList<>();
CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
int finalI = i;
new Thread(() -> {
list.add(finalI);
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
以上案例中,分别采用上述3种List,可以保证每次list的size都是1000,即线程安全。
Set:
HashSet本质上其实是HashMap,set的值本质上是HashMap的key,value为固定值new Object();,这个可以从源码中看出来。
与List类似,Set也有同样的线程安全问题,对应的解决方案如下:
- CopyOnWriteArraySet
- Collections.synchronizedSet
public class Demo9 {
public static void main(String[] args) {
// HashSet<Integer> set = new HashSet<>();
// Set<Integer> set = new CopyOnWriteArraySet();
Set<Integer> set = Collections.synchronizedSet(new HashSet<Integer>());
CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
int finalI = i;
new Thread(() -> {
set.add(finalI);
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(set.size());
}
}
Map:
同样的问题在Map中也存在。
解决方法也是两种:
- ConcurrentHashMap–JDK7之前采用分段锁,同一个segment更新时才会发生锁竞争,JDK8后CAS+volatile变量,不需要分段锁,进一步提高了并发效率。
- Collections.synchronizedMap
public class Demo10 {
public static void main(String[] args) {
HashMap<Integer, Integer> hashMap = new HashMap<>();
// Map<Integer, Integer> hashMap = new ConcurrentHashMap<>();
// Map<Integer, Integer> hashMap = Collections.synchronizedMap(new HashMap<>());
CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
int finalI = i;
new Thread(() -> {
hashMap.put(finalI, finalI);
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(hashMap.size());
}
}
七、常用辅助类
- CountDownLatch,上面的例子也用到过,计数器归零时程序继续向下执行。
public class Demo11 {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
System.out.println(finalI);
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("close door");
}
}
- CyclicBarrier,值得注意的是,这个类其实是可以循环使用的,这一点从名称中也能看出来,也就是说,每次有7个线程执行完,都会触发一次CyclicBarrier的回调方法。
public class Demo12 {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(7, () -> {
System.out.println("执行完毕");
});
for (int i = 0; i < 70; i++) {
int finalI = i;
new Thread(() -> {
System.out.println(finalI);
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
- Semaphore,可以与抢车位进行类比,10辆车抢3个车位,每次有车离开,后边的车才能停。
public class Demo13 {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 拿到信号了 ");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " 时间到,可以释放信号了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
}
八、读写锁
写锁是独占锁,读锁是共享锁,读写锁指的是一个资源可以被多个读线程访问,也可以被一个写线程访问,读写是互斥的,读读是共享的。以下示例通过读写锁安全地读写一个map数据。
public class Demo14 {
public static void main(String[] args) {
HashMap map = new HashMap();
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
try {
readWriteLock.writeLock().lock();
map.put(finalI, finalI);
System.out.println(Thread.currentThread().getName() + " 写入成功 ");
} finally {
readWriteLock.writeLock().unlock();
}
}).start();
}
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 读取成功 " + map.get(finalI / 2));
} finally {
readWriteLock.readLock().unlock();
}
}).start();
}
}
}
九、阻塞队列
比较典型的有ArrayBlockingQueue和SynchronousQueue
操作 | 抛异常 | 不抛异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(E e,long timeout,TimeUnit unit) |
移除 | remove() | poll() | take() | poll(long timeout, TimeUnit unit) |
获取首元素 | element() | peek() |
队列都有以上四组API。以下以ArrayBlockingQueue做一些案例演示。
public class Demo15 {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.add("a"));
System.out.println(queue.add("a"));
System.out.println(queue.add("a"));
// System.out.println(queue.add("a")); 抛异常,因为超出了队列的容量。
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
// System.out.println(queue.remove()); 抛异常,因为已经没有可删除的元素
}
}
public class Demo16 {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.offer("a"));
System.out.println(queue.offer("a"));
System.out.println(queue.offer("a"));
System.out.println(queue.offer("a")); // 不抛异常,返回false
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll()); // 不抛异常,返回null
}
}
public class Demo17 {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
queue.put("a");
queue.put("b");
queue.put("c");
// queue.put("d"); // 如果队列已满,这里继续添加会阻塞
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
// System.out.println(queue.take()); // 如果队列已空,这里继续取数据会阻塞
}
}
public class Demo18 {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
queue.offer("a");
queue.offer("b");
queue.offer("c");
queue.offer("d", 2, TimeUnit.SECONDS); // 等待2秒后,仍无法添加元素,就退出
System.out.println("********************************");
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll());
System.out.println(queue.poll(2, TimeUnit.SECONDS)); // 等待2秒后,仍无法获取元素,就退出
}
}
SynchronousQueue:该队列只存储一个元素,必须队列为空时才能添加元素。
该案例中,每次取出一个元素后,才会继续添加元素到队列。
public class Demo19 {
public static void main(String[] args) {
SynchronousQueue<String> queue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " put ---- 1 ");
queue.put("1");
System.out.println(Thread.currentThread().getName() + " put ---- 2 ");
queue.put("2");
System.out.println(Thread.currentThread().getName() + " put ---- 3 ");
queue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " take ---- 1 ");
queue.take();
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " take ---- 2 ");
queue.take();
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " take ---- 3 ");
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
十、线程池
重点可以归纳为:三大方式、七大参数、四种拒绝策略
线程的创建和销毁十分浪费资源,如果能有一个池子,保留一定数量的线程,每次提交任务都可以复用线程池中的线程,可以有效提高效率。另外,线程创建过多的时候,容易引起内存溢出或者CPU资源占满的情况。
在工作实践中,都不推荐使用Executors的newFixedThreadExecutor和newSingleThreadPool、newCachedThreadPool去创建线程池。因为前两者允许的请求队列长度为Integer.MAX_VALUE(大约21亿)极易堆积大量请求,导致内存溢出。后者允许创建的线程数量是Integer.MAX_VALUE,同样容易导致内存溢出。
下面分别在案例中看下这三个方法:
public class Demo20 {
public static void main(String[] args) {
// ExecutorService pool = Executors.newSingleThreadExecutor(); // 创建单个线程的线程池
// ExecutorService pool = Executors.newFixedThreadPool(5); // 创建固定线程的线程池
ExecutorService pool = Executors.newCachedThreadPool(); // 创建可伸缩线程池
try {
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " OK ");
});
}
} finally {
pool.shutdown();
}
}
}
从Java的源码中,我们可以看到,以上三个方法,实际上是通过ThreadPoolExecutor来实现的,让我们一起从源码中看一下它的七个参数。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂:创建线程,一般不用动
RejectedExecutionHandler handler)
7大参数的意义依次是:
- 核心线程数,是线程池的基本大小。线程池至少会维持这个数量的线程处于运行状态,即使这些线程空闲。
- 最大线程数,线程池允许同时存在的最大线程数。如果当前活动线程数小于核心线程数,并且有新的任务提交时,线程池会创建新的线程直至达到核心线程数。如果此时队列已满并且仍有任务提交,则继续创建线程直到达到最大线程数为止。
- 非核心线程闲置超时时长。在线程池中的线程数超过核心线程数时,当这些线程空闲时间超过了keepAliveTime,则会被终止,以减少资源消耗。单位由unit参数指定。
- unit:keepAliveTime参数的时间单位,如TimeUnit.SECONDS等
- workQueue:工作队列,用于保存等待执行的任务。常见类型有LinkedBlockingQueue、ArrayBlockingQueue和SynchronousQueue等。
- 线程工厂,用于创建新线程。默认情况下使用Executors.defaultThreadFactory(),也可以自定义。
- 拒绝策略,当线程池无法接受新任务(线程池已满,且队列已满),拒绝策略将决定如何处理新提交的任务。Java提供了一些预置的拒绝策略如AbortPolicy(跑出异常)、CallerRunsPolicy(调用者自己执行新提交的任务)、DiscardPolicy(直接丢弃新任务)、DiscardOldestPolicy(抛弃队列中最老的任务,尝试提交新任务)等。
以下案例执行过程大致如下:线程池可接受的任务是最大线程数+队列长度,即5+3=8。循环中创建了9个线程,并且拒绝策略采用丢弃队列中最老的任务。i=0和1时,可由核心线程执行,i=2/3/4时进入队列中排队,i=5/6/7时,线程池继续创建线程用于执行任务,i=8时触发拒绝策略,丢弃队列中最老的任务,即i=2。输出结果也可以证明这一点,输出结果中没有i=2的情况。
public class Demo21 {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
try {
for (int i = 0; i < 9; i++) {
int finalI = i;
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + finalI + " OK ");
});
}
} finally {
executor.shutdown();
}
}
}
关于如何确定开启线程数量:
- CPU核心数:
- 对于CPU密集型任务,理想情况下线程数应该接近于可用的处理器核心数。这是因为单个核心一次只能执行一个线程,过高的线程数会造成上下文切换开销,反而降低效率。可以通过
Runtime.getRuntime().availableProcessors()
获取当前系统的处理器核心数。
- 对于CPU密集型任务,理想情况下线程数应该接近于可用的处理器核心数。这是因为单个核心一次只能执行一个线程,过高的线程数会造成上下文切换开销,反而降低效率。可以通过
- IO密集型任务:
- 对于IO密集型任务,线程数可以根据IO等待时间来调整。由于IO操作期间CPU可能处于空闲状态,因此可以适当增加线程数,使得在等待IO时,其他线程可以继续执行,充分利用CPU资源。一般推荐的线程数可以是 CPU 核心数的1.5倍至2倍。
十一、函数式接口
可以理解为只有一个抽象方法的接口,最典型的就是Runnable,以下代码摘自Java源码:
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Java内置四大核心函数式接口
- Consumer
- Supplier
- Function<T, R>
- Predicate
Function接口简单示例:
public class Demo22 {
public static void main(String[] args) {
Function<String, String> function = (str) -> str + "---";
System.out.println(function.apply("999"));
}
}
Predicate接口简单示例:
public class Demo23 {
public static void main(String[] args) {
Predicate<String> predicate = str -> "".equals(str);
System.out.println(predicate.test(""));
System.out.println(predicate.test("999"));
}
}
Consumer接口简单示例:
public class Demo24 {
public static void main(String[] args) {
Consumer<String> consumer = str -> System.out.println("000" + str);
consumer.accept("888");
}
}
Supplier接口简单示例:
public class Demo25 {
public static void main(String[] args) {
Supplier<String> supplier = () -> "999999";
System.out.println(supplier.get());
}
}
十二、stream流式计算
其实就是把计算交给Stream去做,对集合做筛选、过滤、遍历等操作:
public class Demo26 {
public static void main(String[] args) {
Dog dog1 = new Dog(1, 1, 10.5F);
Dog dog2 = new Dog(2, 4, 15.5F);
Dog dog3 = new Dog(3, 3, 5.5F);
Dog dog4 = new Dog(4, 3, 6.5F);
Dog dog5 = new Dog(5, 2, 8.5F);
Dog dog6 = new Dog(6, 2, 9.5F);
Dog dog7 = new Dog(7, 1, 3.5F);
List<Dog> dogs = Arrays.asList(dog1, dog2, dog3, dog4, dog5, dog6, dog7);
dogs.stream()
.filter(dog -> dog.getId() % 2 == 0)
.filter(dog -> dog.getAge() > 2)
.map(dog -> Math.round(dog.getWeight()))
.sorted()
.limit(2)
.forEach(System.out::println);
}
}
class Dog {
private int id;
private int age;
private float weight;
public Dog(int id, int age, float weight) {
this.id = id;
this.age = age;
this.weight = weight;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public float getWeight() {
return weight;
}
public void setWeight(float weight) {
this.weight = weight;
}
}
十三、ForkJoin
简而言之就是把大量任务递归拆分成小任务分别执行,最后再收集汇总,计算最终结果。
接下来以一个加和计算来示例ForkJoin的操作:
首先创建一个ForkJoinDemo类,进行fork join计算
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if ((end - start) <= temp) {
long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
Long middle = (start + end) / 2;
ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(start, middle);
forkJoinDemo1.fork();
ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle + 1, end);
forkJoinDemo2.fork();
return forkJoinDemo1.join() + forkJoinDemo2.join();
}
}
}
然后创建测试类进行计算,并分别对比传统方式和stream parallel方式与fork join方式的效率。实际开发中,一定要具体问题具体分析,采用最优的方式。
public class Demo27 {
private static final long START = 0L;
private static final long END = 10_0000_0000L;
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
/**
* 传统方法
*/
public static void test1() {
long sum = 0L;
long start = System.currentTimeMillis();
for (long i = START; i <= END; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("traditional result: " + sum + " cost time: " + (end - start));
}
/**
* ForkJoin方式
*/
public static void test2() throws ExecutionException, InterruptedException {
long sum;
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo forkJoinDemo = new ForkJoinDemo(START, END);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("fork join result: " + sum + " cost time: " + (end - start));
}
/**
* 并行流Stream计算方式
*/
public static void test3() {
long sum;
long start = System.currentTimeMillis();
sum = LongStream.rangeClosed(START, END).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("stream result: " + sum + " cost time: " + (end - start));
}
}
十四、异步回调
没有返回值的异步回调:
public class Demo28 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " runAsync ");
});
System.out.println("-----------------------------");
System.out.println(completableFuture.get());
}
}
有返回值的异步回调:
public class Demo29 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " supplyAsync ");
int i = 10 / 0;
return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t=> " + t); // 正常返回信息
System.out.println("u=> " + u); // 错误信息
}).exceptionally((e -> {
System.out.println(e.getMessage());
return 234;
})).get());
}
}
十五、JMM Java内存模型
Java内存模型(Java Memory Model, JMM)中定义的8种操作是为了保证多线程环境下内存操作的原子性和可见性,进而确保并发程序的正确性。这些操作主要围绕主内存(Main Memory)和工作内存(Working Memory)之间的数据同步展开。以下是这8种操作的概述:
1. lock(锁定)
-
主要作用于主内存的变量,将一个变量标志为线程独占状态,通常发生在对共享变量进行写操作前。
-
在硬件层面,对应的是对内存区域加锁,使得在锁释放前,其他线程无法对该变量进行读写。
2. unlock(解锁)
-
解除对主内存中某个变量的锁定状态,使得其他线程可以再次对其进行锁定。
-
在释放锁之后,先前对变量的修改对于其他线程变得可见。
3. read(读取)
-
从主内存中读取一个变量的值到当前线程的工作内存中。
-
该操作确保了变量的值从共享内存传输到线程私有的本地内存。
4. load(加载)
-
将读取到的主内存中的变量值放入工作内存的变量副本中。
-
这是一个工作内存的操作,确保了线程能够基于最新的值进行计算。
5. use(使用)
-
在工作内存中使用读取到的变量副本进行计算。
-
虽然不是所有参考资料都明确列出“use”,但在规范中它代表了从工作内存中读取变量并使用的操作。
6. assign(赋值)
-
修改工作内存中变量副本的值。
-
这是对工作内存中变量的修改操作。
7. store(存储)
-
将工作内存中的变量值回写到主内存中。
-
当线程完成对变量的修改后,需要将修改的结果同步回主内存。
8. write(写入)
-
完成实际的主内存写操作,使得其他线程能够观察到该变量的最新值。
-
write操作是store操作的具体实现,确保了变量在主内存中的更新。
这些操作共同构成了Java内存模型中的内存间交互协议,它们之间遵循一定的规则和顺序,确保了多线程环境下的内存一致性。同时,Java编译器和JVM会在适当的时候插入必要的内存屏障指令,以保证这些操作的正确排序和内存可见性。值得注意的是,这些概念相对底层,开发者通常不需要直接处理这些细节,而是通过使用Java标准库提供的并发工具(如synchronized、volatile关键字、Lock接口等)来间接实现内存模型的要求。
Java内存模型中volatile是一个非常重要的关键字。
- 保证内存可见性;
public class Demo30 {
private static volatile int num = 0; // 当不使用volatile修饰时,线程无法知晓num值的变化,线程进入死循环
// 当使用volatile修饰时,线程对num的变化可见,跳出循环,输出“end”
public static void main(String[] args) {
new Thread(() -> {
while(num == 0) {
}
System.out.println("end");
}).start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
-
防止指令重排序;
这种情况实际上是非常难以复现的,最典型的就是在DCL(Double Check Lock)单例模式中,如果不使用volatile修饰变量,可能导致获取到的单例是中间状态,而导致程序出错。后边的内容中会讲到。 -
无法保证操作的原子性。
public class Demo31 {
private static volatile int num = 0;
// 执行结果绝大多数num小于2万,表明volatile无法保证操作的原子性。
// 可以在该方法增加synchronized解决该问题
// 或者把num变量改为AtomicInteger
public static void add() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 1000; i1++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield(); // 如果除main和gc线程,还有其他线程在执行,就让其他线程优先执行。
}
System.out.println(Thread.currentThread().getName() + " --- " + num);
}
}
十六、单例模式
- 饿汉式–最简单有效,唯一的缺陷在于当对象占用空间较大时,可能浪费内存空间。
public class Demo33 {
private static final Demo33 DEMO33 = new Demo33(); // 在类加载时就创建好对象。
// 私有化构造器,防止从外部创建新对象。
private Demo33() {
}
// 通过静态方法获取对象
public static Demo33 getInstance() {
return DEMO33;
}
}
- 懒汉式
public class Demo34 {
private volatile static Demo34 demo34;
private static boolean flag = false;
private Demo34() {
synchronized (Demo34.class) {
if (!flag) {
flag = true;
} else {
throw new RuntimeException("Don't use reflection to get instance.");
}
}
}
public static Demo34 getInstance() {
if (null == demo34) {
synchronized (Demo34.class) {
if (null == demo34) {
demo34 = new Demo34();
}
}
}
return demo34;
}
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// for (int i = 0; i < 10; i++) {
// new Thread(() -> {
// System.out.println(Demo34.getInstance());
// }).start();
// }
System.out.println("***********************************");
Demo34 instance = Demo34.getInstance();
System.out.println(instance);
Field flag = Demo34.class.getDeclaredField("flag");
flag.setAccessible(true);
Constructor<Demo34> declaredConstructor = Demo34.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
flag.set(demo34, false);
Demo34 demo34 = declaredConstructor.newInstance();
System.out.println(demo34);
flag.set(demo34, false);
Demo34 newInstance = declaredConstructor.newInstance();
System.out.println(newInstance);
System.out.println(demo34 == newInstance);
}
}
- 静态内部类
public class Demo35 {
private Demo35() {
}
public static class InnerClass {
private static final Demo35 DEMO_35 = new Demo35();
}
public static Demo35 getInstance() {
return InnerClass.DEMO_35;
}
}
- 枚举类–最大的优势就是可以防止通过反射创建新对象。
public enum Demo36 {
INSTANCE;
public Demo36 getInstance() {
return INSTANCE;
}
}
十七、深入理解CAS
Java中的CAS(Compare and Swap)本质上是一种原子操作指令,它可以用来实现无锁的数据同步机制。在硬件层面,许多现代处理器提供了CAS指令,它可以在不锁定总线或者其他全局资源的情况下,比较并更新内存中的数据。如果内存位置的值与期望值相符,则更新该位置的值;若不符,则不做任何修改。
在Java中,CAS操作通过JNI(Java Native Interface)调用底层操作系统或者硬件提供的原子指令实现。具体在Java API层面,java.util.concurrent.atomic
包下的原子类(如AtomicInteger、AtomicLong等)广泛使用了CAS来保证多线程环境下的原子性操作。
例如,AtomicInteger
类中的compareAndSet
方法就实现了CAS操作,它的逻辑大致如下:
public final boolean compareAndSet(int expect, int update) {
// native修饰的方法表明它是通过JNI调用C/C++编写的本地方法实现的
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这里unsafe
是sun.misc.Unsafe
类的对象,它提供了直接操作内存的底层能力,包括直接调用CAS指令。compareAndSwapInt
方法就是利用CPU级别的CAS指令完成的原子操作。
总结来说,Java中的CAS是一种基于硬件支持的并发原语,它允许在不使用互斥锁的前提下,以原子方式读取、比较并有条件地更新内存中的某个数值,从而极大地提升了并发环境下程序的性能,减少了锁竞争带来的开销。然而,尽管CAS在很多场景下非常高效,但也存在ABA问题以及长时间循环重试可能导致性能下降等问题,在实际应用中需要结合具体业务场景合理选择并发控制策略。
public class Demo37 {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2024);
System.out.println(atomicInteger.compareAndSet(2024, 2025));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2024, 2025));
System.out.println(atomicInteger.get());
}
}
// 输出如下
// true
// 2025
// false
// 2025
ABA问题大致如下例所示:
public class Demo38 {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2024);
System.out.println(atomicInteger.compareAndSet(2024, 2025));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2025, 2024));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2024, 8888));
System.out.println(atomicInteger.get());
}
}
十八、原子引用
通过增加stamp记录修改历史,即便出现ABA问题,也可以通过stamp的值判断。
public class Demo39 {
public static void main(String[] args) {
AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1, 1);
new Thread(() -> {
int stamp = reference.getStamp();
System.out.println(Thread.currentThread().getName() + " 1 -> " + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + reference.compareAndSet(
1,
2,
reference.getStamp(),
reference.getStamp() + 1));
System.out.println(Thread.currentThread().getName() + " 2 -> " + reference.getStamp());
System.out.println(Thread.currentThread().getName() + reference.compareAndSet(
2,
1,
reference.getStamp(),
reference.getStamp() + 1));
System.out.println(Thread.currentThread().getName() + " 3 -> " + reference.getStamp());
}).start();
new Thread(() -> {
int stamp = reference.getStamp();
System.out.println(Thread.currentThread().getName() + " 1 -> " + stamp);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + reference.compareAndSet(
1,
6,
reference.getStamp(),
reference.getStamp() + 1));
System.out.println(Thread.currentThread().getName() + " 2 -> " + reference.getStamp());
}).start();
try {
TimeUnit.SECONDS.sleep(8);
System.out.println("---------------------------");
System.out.println(reference.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
十九、Java中的锁
- 公平锁和非公平锁
Java中最典型的两种锁,就是synchronized和ReentrantLock。
synchronized关键字实现的锁是隐式的,且默认是非公平锁。也就是说,当多个线程等待同一个锁时,被唤醒的线程不一定是等待时间最长的线程,操作系统可以自由选择哪个线程获取锁。
ReentrantLock类提供了公平锁和非公平锁两种选择。如果不指定构造函数参数,默认创建的是非公平锁。但是可以通过构造函数创建公平锁,例如new ReentrantLock(true),这里的参数true表示创建公平锁。
以上二者默认情况下表现为悲观锁,即获取数据前会先加锁。java.util.concurrent.atomic包下的原子类,使用CAS实现乐观锁机制。
以下摘自ReentrantLock源码中的两个构造函数:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync : new NonfairSync();
}
- 自旋锁
代码分析:
SpinLock
类中定义了一个AtomicReference<Thread>
类型的原子引用变量atomicReference
,用于存储当前持有锁的线程。- 在
myLock()
方法中,首先获取当前线程,然后进入一个while
循环。循环体为空,仅仅判断atomicReference
是否可以被设置为当前线程。如果atomicReference
当前为null
,则通过compareAndSet(null, thread)
方法将其设置为当前线程。compareAndSet()
是一个原子操作,它会比较当前值是否为null
,如果是则将值设置为thread
,否则不做任何改变并返回false
。 - 当第一个线程(例如 “T1”)调用
myLock()
时,它会进入while
循环并尝试设置atomicReference
为自身。由于此时atomicReference
初始值为null
,所以第一次尝试就能成功,跳出循环,线程获得锁。 - 若第二个线程(例如 “T2”)在第一个线程还未释放锁时调用
myLock()
,它也会进入while
循环。但由于此时atomicReference
已经被设置为 “T1”,compareAndSet(null, thread)
将一直返回false
,于是 “T2” 线程会持续自旋,不断地重复检查atomicReference
是否仍被 “T1” 线程持有。 - 当 “T1” 线程执行完毕调用
myUnLock()
方法时,它会将atomicReference
设置回null
。这时,如果 “T2” 线程还在自旋,下一次循环将会发现atomicReference
变为null
,然后成功设置为 “T2”,从而 “T2” 线程得以获取锁。
输出结果为:
T1 ==> myLock
T2 ==> myLock
T1 ==> myUnLock
T2 ==> myUnLock
public class Demo40 {
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
new Thread(() -> {
spinLock.myLock();
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.myUnLock();
}
}, "T1").start();
new Thread(() -> {
spinLock.myLock();
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.myUnLock();
}
}, "T2").start();
}
static class SpinLock {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " ==> myLock ");
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnLock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " ==> myUnLock ");
atomicReference.compareAndSet(thread, null);
}
}
}
-
死锁
在Java中,死锁(Deadlock)通常出现在多个线程互相等待对方释放资源,从而导致所有线程都无法继续执行的情况。死锁的发生需要四个必要条件(亦称死锁四要素):- 互斥条件(Mutual Exclusion):至少有一个资源在任何时刻只能被一个线程占有。例如,一个锁只能被一个线程持有。
- 持有并等待条件(Hold and Wait):已经持有至少一个资源的线程正在等待获取其他资源,而它又不愿意释放已经持有的资源。
- 不可剥夺条件(No Preemption):资源一旦被线程占有,除非该线程主动释放,否则其他线程无法强行剥夺。
- 循环等待条件(Circular Wait):存在一个线程等待队列,形成一个线程—资源的循环等待链。例如,线程A持有资源1并等待资源2,线程B持有资源2并等待资源1。
一个典型的Java死锁示例是两个线程分别持有了一个资源,并尝试获取对方的资源,但都不愿意释放已经持有的资源,这就形成了死锁:
实际开发中可以用以下方法避免或预防死锁出现:
- 预防死锁:
- 资源有序分配法:为系统中的所有资源赋予一个全局唯一的编号,并要求所有线程按编号顺序申请资源。这样可以打破循环等待条件,例如在上述死锁示例中,可以规定线程先申请编号较小的资源。
- 一次性申请所有资源:让线程一次性申请它所需要的全部资源,避免持有并等待条件的产生。
- 避免死锁:
- 设置超时:在申请资源时设置一个合理的超时时间,当超过这个时间还没有获取到资源时,线程主动放弃已获得的部分资源,然后重新尝试获取资源,打破死锁。
- 资源预先分配:尽可能地预先为线程分配所需的全部资源,或者提前规划好资源分配方案,确保不会形成死锁。
文章的最后,如果大家想看视频版本的讲解,可以关注以下B站账号。
一、JUC开篇介绍