书接上回, 上篇博客中总结了synchronized的原理和CAS的实现原子类, 我们将要继续学习CAS实现自旋锁, CAS中的ABA问题, Callable创建线程等等..
CAS实现自旋锁
首先我们来看一段伪代码:
public class SpinLock {
// owner表示是哪个线程持有这把锁, 设为null表示当前线程没有加锁
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
// Thread.currentThread(): 获取当前线程的引用
// 如果该先线程处于加锁状态, 就会返回false, 就会进入循环等待...
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
CAS的ABA问题
通过比较寄存器和内存的值,通过这里的是否相等,来判定内存的值是否发生了改变.
如果内存的值变了, 就存在其它线程修改.
如果内存的值没变, 就没有别的线程修改, 后面进行修改就是安全的.
那么问题来了: 如果内存的值没变, 就一定没有别的线程修改吗?
这就是ABA问题… A->B->A
即使以上情况发生概率很小, 但它还是会发生的, 就需要我们去处理!
这个时候我们就引入了版本号, 通过版本号的值是否被修改, 来判断数据有没有修改.
Callable接口
Callable是一个interface, 相当于把线程封装成"返回值".
我们可以通过Callable创建线程:
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 = 0; i < 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
// 如果call方法没执行完, 会进入阻塞等待.
Integer ret = futureTask.get();
System.out.println(ret);
}
Runnable 能表示一个任务, 通过run方法, 返回void.
Callable 也能表示一个任务, 通过call方法, 返回一个具体的泛型参数.
如果在设计多线程的地方, 我们更看重过程, 推荐使用Runable,如果更看重结果, 更推荐使用Callable.
要注意的是, Callable不能直接作为Thread的构造方法参数,要引入FutureTask.
用FutureTask对象作为Thread的构造方法参数.
ReentrantLock
ReentrantLock也是与synchronized类似的, 都是一个加锁的组件.
但它要手动设置lock():加锁和unlock(): 解锁.
ReentrantLock特点:
- 提供tryLock方法进行加锁:
对于lock方法, 加锁失败就进入阻塞等待.
对于tryLock方法, 加锁失败就返回false或者在规定的等待时间中返回, 给我们提供了更多的操作空间. - ReentrantLock有两种加锁模式, 可以工作在公平锁状态下, 也可以工作在非公平锁状态下.
- ReentrantLock也具有等待通知功能, 搭配Condition类来使用.要比synchronized的wait,notify方法功能更强.
实际开发中, 多线程开发, 还是首选synchronized.
信号量Semaphone
信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.
我们申请一个资源, 计数器-1, 称为p操作.
我们释放一个资源, 计数器+1, 称为v操作.
我们利用代码来熟悉pv操作过程:
public static void main(String[] args) throws InterruptedException {
// 申请4个可用资源
Semaphore semaphore = new Semaphore(4);
// 申请一个资源
semaphore.acquire();
System.out.println("p操作");
// 申请一个资源
semaphore.acquire();
System.out.println("p操作");
// 申请一个资源
semaphore.acquire();
System.out.println("p操作");
// 申请一个资源
semaphore.acquire();
System.out.println("p操作");
// 释放一个资源
semaphore.release();
System.out.println("v操作");
// 申请一个资源
semaphore.acquire();
System.out.println("p操作");
}
CountDownLatch组件
作用: 把主线程拆成多个线程进行工作, 用来提高执行效率,比如说IDM下载器, 就可以分配多个线程来下载.
public static void main(String[] args) throws InterruptedException {
// 分为10个线程
CountDownLatch count = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
System.out.println("线程" + i + "开始工作");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程" + i + "结束工作");
// 象征任务结束
count.countDown();
});
thread.start();
}
// 所有线程都已经执行完 await->allwait
count.await();
System.out.println("所有线程都执行完了");
}
for循环中的i其实是访问不到的, 此处设计变量捕获, 它会给我们报final修饰或者像final的对象.
public static void main(String[] args) throws InterruptedException {
// 分为10个线程
CountDownLatch count = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
int n = i;
Thread thread = new Thread(() -> {
System.out.println("线程" + n + "开始工作");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程" + n + "结束工作");
// 象征任务结束
count.countDown();
});
thread.start();
}
// 所有线程都已经执行完 await->allwait
count.await();
System.out.println("所有线程都执行完了");
}
此时只要新创建一个变量, 就可以了.我们新创建的变量的n, 实际上就是没有改的, 也就是像final的变量.
小结
多线程方面的知识已经总结完了, 我将会复习文件操作的知识, 有收获的小伙伴多多支持.