文章目录
- Callable接口的用法
- Callable与FutureTask类
- 加锁的工作过程
- 什么是偏向锁呢?
- 举个例子
- 轻量级锁
- 重量级锁
- ReentrantLock
- ReentrantLock 的用法:
Callable接口的用法
Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.
我们之前写的代码重线程内部的lambda表达式中现实的run方法其内部的返回值是void因此当我们想要返回在方法内部实现返回一个数字的时候都很难做到。而Callbale就解决了这个方法
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer>callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum=0;
for(int i=1;i<=100;i++){
sum+=i;
}
return sum;
}
};
FutureTask<Integer>futureTask=new FutureTask<>(callable);
Thread thread=new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
Callable与FutureTask类
请看上面的callbale接口的运用。我们可以看到这个接口中我们实现了一个call方法他的返回值是一个Integer,这里call方法是我们的核心方法我们看一下源代码
我们发现这个源代码中其实也就只用一个call方法这个方法的返回类型是一个模板也就是说他的返回类型我们可以进行指定,因此我们这里实例处的类型是什么这里的返回值就是什么。
那么我们如何创建线程吗?是直接用实例出的对象作为new Thread()括号中的参数吗?
很明显不是如此我们需要一个中间类那就是FutureTask那么我们来看一下这里面的源代码熟悉一下继承关系
首先我们可以看到这个类继承了一个RunnableFuture这个类,我们不知道这个类是什么但是我们可以继续看源代码。
我们发现这个类继承Runnable然后我们再去想一下Thread的构造方法,是可以接受Runnable类的因此我们知道此时也可以接受这个FutureTask类而这个FutureTask类里面的构造函数有一个方法
我们可以知道FutureTask类可以接受callable类并且其内部就有一个Callable类。
加锁的工作过程
加锁的工作过程就是下面的这个过程
在刚开始的时候是无锁的一个状态。当第一个尝试加锁的进程出现的时候会进入偏向锁的状态。那么什么是偏向锁呢?
什么是偏向锁呢?
偏向锁你可以理解为钓鱼,因为偏向锁其实并不是真的加锁,而是加了一个标记记录这个锁属于哪个线程,但是此时并没有加锁,那么当出现另一个线程也去申请这个锁的时候,那么第一个线程才会对其加锁。偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销. 但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.
举个例子
相当于有一个帅哥叫做小帅,
他勾搭上了一个美女叫做小美
但是他很享受这个暧昧的过程因此不愿意和小美捅破那层窗户纸
此时当又有一个帅哥出现和他一起竞争小美
这时候小帅就慌了他会火速与小美表白这时候由于小美和小帅呆的时间长因此小美会优先同意小帅的表白。
这就是一个偏向锁。
这时候就会进入下一个状态那就是轻量级锁
轻量级锁
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).
此处的轻量级锁就是通过 CAS 来实现.
通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
如果更新成功, 则认为加锁成功
如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
当锁竞争激烈起来之后将会进入重量级锁状态
重量级锁
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁此处的重量级锁就是指用到内核提供的 mutex .
执行加锁操作, 先进入内核态.
在内核态判定当前锁是否已经被占用
如果该锁没有占用, 则加锁成功, 并切换回用户态.
如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒
这个线程, 尝试重新获取锁.
ReentrantLock
这个的意思很明显就是可重入锁,可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
ReentrantLock 也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”
ReentrantLock 的用法:
lock(): 加锁, 如果获取不到锁就死等.
trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
unlock(): 解锁
从这里我们可以看出来ReentrantLock 其实是一个类,他不是一个关键字是java封装的一个类那么这里面主要的方法就是上面的这三个我们来看一下如何使用
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
public class test_ReentrantLock {
public static void main(String[] args) {
ReentrantLock reentrantLock=new ReentrantLock();
int n=5;
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
reentrantLock.lock();
int sum=0;
while(sum<n){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
sum++;
System.out.println("我是线程1");
}
reentrantLock.unlock();
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
reentrantLock.lock();
int sum=0;
while(sum<n){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
sum++;
System.out.println("我是线程2");
}
reentrantLock.unlock();
}
});
thread2.start();
thread.start();
}
}
我们发现这时候打印的结果不会是乱序的说明此时加锁的目的是达到了的,可是这里如果只有这种应用是不是太低级了感觉要不要它无所谓啊。但其实这里面它最不同的就是trylock方法这个方法是很大的不同的。那么我们把代码改一下
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
public class test_ReentrantLock {
public static void main(String[] args) {
ReentrantLock reentrantLock=new ReentrantLock();
int n=5;
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
reentrantLock.tryLock();//第一处改动
int sum=0;
while(sum<n){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
sum++;
System.out.println("我是线程1");
}
reentrantLock.unlock();
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
reentrantLock.lock();
int sum=0;
while(sum<n){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
sum++;
System.out.println("我是线程2");
}
reentrantLock.unlock();
}
});
thread2.start();
try {//第二处改动
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread.start();
}
}
变成了上面的代码之后我们来观察一下结果。
我们发现此时的代码变成了乱序的了就像没有加锁一样而事实上确实也没有加锁,因为tryLock其实就是尝试加锁我们的t2线程是先启动的因此是先获取锁的这时候t1线程想要获取锁就获取不到,那么这时候t1线程就说那获取不到我就不获取了直接往下执行了,这就是和synchronized很大的不同之处。
抛出异常的原因是因为我们的t1线程没有获取倒锁但是代码里却有释放锁的代码因此抛出了异常。