乐观锁
每次读取数据的时候都认为数据没有被修改过,读取数据的时候不加锁 , 但是在更新的时候会去对比一下原来的值,看有没有被别人更改过。适用于读多写少的场景
juc中atomic 使用的就是乐观锁,即CAS
悲观锁
每次读取数据的时候都认为数据已经被修改过,读取数据的时候也会加锁。别人想要拿到数据就要等待锁。适合写操作比较多的场景
synchronized 实现也是悲观锁
独占锁
独占锁指锁一次只能被一个线程占有
ReentrantLock 就是独占锁
共享锁
锁可以被多个线程持有
ReadWriteLock 中 Read 共享, write独占
如果当前线程持有obj对象的锁,而内部代码块中还需要获取obj锁,直接放行的方式就是可重入锁。
synchronized 和 ReentrantLock都是可重入锁
实现方式为为锁对象设置一个计数器和占有他的线程,每次获取锁的时候计数器加一,释放锁的时候计数器减一,当计数器为0的时候释放锁。
公平锁
所有尝试获取锁的线程都会加入锁的等待队列,每次唤醒队列中的第一个线程。
常见于AQS
非公平锁
抢占锁时候判断锁是否被占有,没被占有直接抢占锁,如果被占有就加入等待队列
ReentrantLock使用非公平锁
非公平锁的性能比公平锁好,因为线程有机会不阻塞直接获得锁,公平锁需要唤醒阻塞队列中的线程,所以公平锁的CPU开销会比较大。
偏向锁
仅有一个线程在使用锁,没有竞争线程,就是偏向锁,一旦有其他线程产生竞争,锁升级为轻量级锁
轻量级锁
当前有两个线程,一个持有锁,另一个会自旋等待锁;当再有一个线程(3个以上)同时竞争锁的时候,锁升级为重量级锁
重量级锁
其他线程试图获取锁的时候都会进入阻塞队列,只有当前线程释放锁的时候才会唤醒线程。
获取不到锁就一直循环试图获取锁。
synchronized、ReentrantLock属于互斥锁;(都是独占锁)
ReadWriteLock 属于读写锁(读为共享锁,写为独占锁)
Java 中线程的实现方式主要有四种,利用Spring 注解@Asyn 也可以实现,这里不做详细讨论。它们分别是:
class TestThread extends Thread {
@Override
public void run() {
// do something ....
}
}
class TestThread implements Runnable {
@Override
public void run() {
// do something ....
}
}
// 创建Callable 对象
Callable<String> stringCallable = () -> {
System.out.println("do something");
Thread.sleep(2000);
return "ok";
};
// 根据Callable 对象创建FutureTask 对象
FutureTask<String> stringFutureTask = new FutureTask<>(stringCallable);
// 创建线程并启动
new Thread(stringFutureTask).start();
// 阻塞当前线程直到FutureTask返回处理结果
String result = stringFutureTask.get();
在开发过程中不建议使用直接使用继承Thread类或者直接实现Runnable 的方式来管理线程,当每接收一个请求创建一个线程,线程执行完毕再销毁的这种模式会引发如下事实:
使用线程池后,优点如下:
但是线程池使用不当也会有一些风险,比如:
定义:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
其中各个参数的含义如下:
Alibaba 开发手册明确规定不允许使用java 自带的线程池,现在对四种线程池做详细总结:
- Executors. newFixedThreadPool
- Executors. newCachedThreadPool
- Executors. newSingleThreadExecutor
- Executors. newScheduledThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
关注点:核心线程数为0,最大线程数为最大值,阻塞队列选用SynchronousQueue。
缺点:来了任务就执行,而且可容纳的最大线程数很大,过期时间很长,很容易OOM。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
关注点:核心线程数为n,最大线程数为n,阻塞队列选用LinkedBlockingQueue。
缺点:同时执行n个线程,但是队列采用LinkedBlockingQueue,大小默认为Integer最大值,所以和CachedPool一样容易爆内存。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor( 1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
关注点:核心线程数为1,最大线程数为1,阻塞队列选用LinkedBlockingQueue。
缺点:一个一个任务顺序执行,阻塞队列还是无限的(Integer最大值约等于无限),容易OOM。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize,
Integer.MAX_VALUE,
0,
NANOSECONDS,
new DelayedWorkQueue());
}
关注点:核心线程数为corePoolSize,最大线程数为最大值,阻塞队列选用DelayedWorkQueue。
缺点:允许最大线程数为Integer最大值,大量任务到来的时候创建这么多线程完全是不可取的。可能导致OOM。
想要监控线程池状态有两种方案可以实现
继承线程池并重写下列方法:
类 | 方法名 | 解释 |
---|---|---|
Object | wait | 等待Object 锁 |
Object | notify | 通知所有等待该锁的线程中的其中一个 |
Object | notifyAll | 通知所有等待该锁的线程 |
Thread | join | A线程中调用B线程的Join 方法后,A会立即阻塞,等待线程B执行完成后再唤醒进入就绪状态。 |
Thread | yield | 线程执行yield 方法后会放弃时间片,进入就绪队列,随时准备再次执行。但是不会释放锁。 |
Thread | interrupt | 中断线程,仅仅是给线程状态设置为中断,并不打断线程的运行 |
Thread | isInterrupted | 判断线程状态是否为中断状态 |
Thread | setDaemon | 设置当前线程为后台线程 |
Thread | sleep | 阻塞当前线程一段时间 |
Thread | suspend | 挂起线程,不释放锁,容易死锁 |
Thread | stop | 终止线程,释放锁。 |
在Java 中实现多线程通信有多种方式(这里先使用两种,如果有其他想法可以评论下)
- 使用synchronized 配合wait 和 notifyAll 使用
- 使用ReentrantLock 配合 Condition 使用(JUC 包)
- CountDownLatch 实现线程通信
这里通过一个示例来展示每种方式的用法, 新建两个线程交替打印1-100之间的数字,打印完以后主线程输出打印完毕。
使用Synchronized 来配合wait 和 notifyAll 来使用,其中
- synchronized 来获得一个对象的锁
- 使用notifyAll 通知其他等待此锁的对象可以竞争该锁。
- 使用wait 表示等待该对象释放锁。
题解代码:
class TestThread implements Runnable {
private static Object lock = new Object();
// JUC 包中的类,初始设置一个值,使用countDownLatch.await()的时候会阻塞当前线程,直到初始值减小到0为止
public static CountDownLatch countDownLatch = new CountDownLatch(2);
private static volatile int increNum = 1;
private static volatile int currentThreadNum = 0;
private static final int MAX_NUM = 100;
private volatile int threadNum;
public TestThread(int threadNum) {
this.threadNum = threadNum;
}
@Override
public void run() {
try {
while (increNum < MAX_NUM) {
synchronized (lock) { // 获得锁
while (this.threadNum == currentThreadNum) {
lock.wait(); // 交替打印实现,释放锁
}
System.out.println(String.format("Thread %d : %d" , this.threadNum , increNum));
++ increNum;
currentThreadNum = this.threadNum;
lock.notifyAll(); // 通知所有线程可以抢锁了
}
}
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
new Thread(new TestThread(1)).start();
new Thread(new TestThread(2)).start();
TestThread.countDownLatch.await();
System.out.println("print over");
}
}
使用ReentrantLock 配合 Condition 实现线程通信,相关方法:
- ReentrantLock 可以看成一把锁,谁获得锁谁可以访问资源。
- Condition 也是一把锁,可以看成和ReentrantLock 相关联的锁,获得ReentrantLock 锁的线程可以操作Condition 这把锁,用来实现与其他线程的交互功能。
初始化
ReentrantLock lock = new ReentrantLock(); Condition lockCondition = lock.newCondition();
方法:
lock.lock(); // 尝试获得锁,获取不到阻塞当前线程,直到获得锁 lock.tryLock(); // 尝试获得锁一次,返回获取成功或者失败 lock.unlock(); // 释放锁,要在finally 中使用 lockCondition.signalAll(); // 通知所有等待此condition的线程 lockCondition.signal(); // 通知其他等待此condition 的一个线程,发出信号 lockCondition.await(); // 等待其他线程对此Condition 做signal 操作,否则阻塞当前线程,可以设置最长等待时间。
public class Main {
public static void main(String[] args) throws InterruptedException {
new Thread(new TestThread(1)).start();
new Thread(new TestThread(2)).start();
while (true) {
try {
TestThread.lock.lock();
TestThread.lockCondition.await();
System.out.println("print over");
break;
} finally {
TestThread.lock.unlock();
}
}
}
}
class TestThread implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static Condition lockCondition = lock.newCondition();
private static volatile int increNum = 1;
private static volatile int currentThreadNum = 0;
private static final int MAX_NUM = 100;
private volatile int threadNum;
public TestThread(int threadNum) {
this.threadNum = threadNum;
}
@Override
public void run() {
while (increNum < MAX_NUM) {
try {
lock.lock();
while (this.threadNum != currentThreadNum) {
System.out.println(String.format("Thread %d : %d", this.threadNum, increNum));
++increNum;
currentThreadNum = this.threadNum;
}
if (increNum >= MAX_NUM) {
lockCondition.signalAll();
break;
}
} finally {
lock.unlock();
}
}
}
}
AQS是ReentrantLock的核心组件,内部设置state变量,持有锁线程,等待队列等信息。每次加锁给state+1(前提判断持有线程为当前线程),如果不是当前线程抢占锁则加入等待队列。state为0的时候再从队列中取出线程执行。
原理简介
CAS 包含如下三个重要属性
- 内存地址V
- 旧的预期值 A
- 新的预期值 B
CAS 每次进行更新操作时,先从比较内存地址V中的数据和A是否相等,如果相等则更新内存地址V的值为B,否则什么都不做。
流程如下:
- 从V中获取值A
- 根据A计算目标值B
- 原子操作-----判断V中的值是否为A,如果为A则将V中的值改为B
存在的问题
- 自旋时间长,开销比较大
- 只能保证一个变量的原子操作
- 会出现ABA问题
ABA问题以及解决方案
ABA问题:
CAS的过程:
- 从V中获取值A
- 根据A计算目标值B
- 原子操作-----判断V中的值是否为A,如果为A则将V中的值改为B
因为只有第三步是原子操作,所以线程X获取值A,线程Y在此期间将值改为B,然后线程Z将值改为A,最后X线程执行第三步的时候对比值A还是原来的哪个值,所以会产生问题。
解决方案
- 加版本号
- 使用JUC中的 AtomicStampedReference
- AtomicReference == AtomicInteger
- AtomicReference 会给对象加一个时间戳
- synchronized 发生异常时,会主动释放锁;
- ReentrantLock 发生异常不会释放锁,容易造成死锁。
- Lock是可中断锁
notify 只会唤醒一个线程,而notifyAll 会唤醒等待此Object 锁的所有线程。
保证可见性
直接从内存读取数据,而不是从寄存器中读取数据。
禁止指令重排序
指令重排是jvm 优化代码的一种方式,但是再有volatile的地方会禁止指令重排,比如:
int a = 1; int b = 1; a++; b++; volatile int c = a + b; a++; b++;
这里前4行可能指令重排,比如先做b++在做a++;
后两行数据也可能颠倒顺序,但不影响程序执行。
如果大家还有什么想补充的请再评论区告诉我
资源共享,共同进步!