多线程
- CAS实现自旋锁
- CAS的ABA问题
- Callable接口
- ReentrantLock
- 信号量Semaphone
- CountDownLatch组件
- 小结
java">书接上回, 上篇博客中总结了synchronized的原理和CAS的实现原子类, 我们将要继续学习CAS实现自旋锁, CAS中的ABA问题, Callable创建线程等等..
CAS实现自旋锁
首先我们来看一段伪代码:
java">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
即使以上情况发生概率很小, 但它还是会发生的, 就需要我们去处理!
javascript">这个时候我们就引入了版本号, 通过版本号的值是否被修改, 来判断数据有没有修改.
Callable接口
Callable是一个interface, 相当于把线程封装成"返回值".
我们可以通过Callable创建线程:
java">public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic 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操作过程:
java">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下载器, 就可以分配多个线程来下载.
java">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->allwaitcount.await();System.out.println("所有线程都执行完了");}
for循环中的i其实是访问不到的, 此处设计变量捕获, 它会给我们报final修饰或者像final的对象.
java">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->allwaitcount.await();System.out.println("所有线程都执行完了");}
此时只要新创建一个变量, 就可以了.我们新创建的变量的n, 实际上就是没有改的, 也就是像final的变量.
小结
多线程方面的知识已经总结完了, 我将会复习文件操作的知识, 有收获的小伙伴多多支持.