同步操作将从 edgevagrant/JAVA-000 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
2.(必做)思考有多少种方式,在 main 函数启动一个新线程,运行一个方法,拿到这个方法的返回值后,退出主线程?写出你的方法,越多越好,提交到 Github。
一共 10 种,大致如下:
代码也放到了当前文件的 code 文件夹工程中
NoLockMethod.java
package homework;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* 无锁方式
* 主要是通过不断循环查询返回值是否为空,来判断值是否已经计算完成
*/
public class NoLockMethod {
private volatile Integer value = null;
public void sum(int num) {
value = fibo(num);
}
private int fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
public int getValue() {
while (value == null) {
}
return value;
}
public static void main(String[] args) throws InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
final NoLockMethod method = new NoLockMethod();
Thread thread = new Thread(() -> {
method.sum(45);
});
thread.start();
int result = method.getValue(); //这是得到的返回值
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
// 然后退出main线程
}
}
ThreadJoinMethod.java
package homework;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* Thread join 方式
*/
public class ThreadJoinMethod {
public static void main(String[] args) throws InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
AtomicInteger value = new AtomicInteger();
Thread thread = new Thread(()-> {
value.set(sum());
});
thread.start();
thread.join();
int result = value.get(); //这是得到的返回值
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
// 然后退出main线程
}
private static int sum() {
return fibo(45);
}
private static int fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
}
SynchronizedMethod.java
package homework;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* 通过的管程等待-通知机制,来获取值
* synchronized方式
*/
public class SynchronizedMethod {
private volatile Integer value = null;
synchronized public void sum(int num) {
value = fibo(num);
notifyAll();
}
private int fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
synchronized public int getValue() throws InterruptedException {
while (value == null) {
wait();
}
return value;
}
public static void main(String[] args) throws InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
final SynchronizedMethod method = new SynchronizedMethod();
Thread thread = new Thread(() -> {
method.sum(45);
});
thread.start();
int result = method.getValue(); //这是得到的返回值
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
// 然后退出main线程
}
}
SemaphoreMethod.java
package homework;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* Semaphore方式
*/
public class SemaphoreMethod {
private volatile Integer value = null;
final Semaphore semaphore = new Semaphore(1);
public void sum(int num) throws InterruptedException {
semaphore.acquire();
value = fibo(num);
semaphore.release();
}
private int fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
public int getValue() throws InterruptedException {
int result;
semaphore.acquire();
result = this.value;
semaphore.release();
return result;
}
public static void main(String[] args) throws InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
final SemaphoreMethod method = new SemaphoreMethod();
Thread thread = new Thread(() -> {
try {
method.sum(45);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
int result = method.getValue(); //这是得到的返回值
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
// 然后退出main线程
}
}
LockConditionMethod.java
package homework;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* 管程Lock Condition方式
*/
public class LockConditionMethod {
private volatile Integer value = null;
private Lock lock = new ReentrantLock();
private Condition calComplete = lock.newCondition();
public void sum(int num) {
lock.lock();
try {
value = fibo(num);
calComplete.signal();
} finally {
lock.unlock();
}
}
private int fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
public int getValue() throws InterruptedException {
lock.lock();
try {
while (value == null) {
calComplete.await();
}
} finally {
lock.unlock();
}
return value;
}
public static void main(String[] args) throws InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
final LockConditionMethod method = new LockConditionMethod();
Thread thread = new Thread(() -> {
method.sum(45);
});
thread.start();
int result = method.getValue(); //这是得到的返回值
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
// 然后退出main线程
}
}
CyclicBarrierMethod.java
package homework;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* CyclicBarrierMethod 方式
*/
public class CyclicBarrierMethod {
private volatile Integer value = null;
CyclicBarrier barrier;
public void setBarrier(CyclicBarrier barrier) {
this.barrier = barrier;
}
public void sum(int num) throws BrokenBarrierException, InterruptedException {
value = fibo(num);
barrier.await();
}
private int fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
public int getValue() throws InterruptedException {
return value;
}
public static void main(String[] args) throws InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
final CyclicBarrierMethod method = new CyclicBarrierMethod();
CyclicBarrier barrier = new CyclicBarrier(1, ()-> {
int result = 0; //这是得到的返回值
try {
result = method.getValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
});
method.setBarrier(barrier);
Thread thread = new Thread(() -> {
try {
method.sum(45);
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
// 然后退出main线程
}
}
CountDownLatchMethod.java
package homework;
import java.util.concurrent.CountDownLatch;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* CountDownLatchMethod 方式
*/
public class CountDownLatchMethod {
private volatile Integer value = null;
private CountDownLatch latch;
public void sum(int num) {
value = fibo(num);
latch.countDown();
}
private int fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
public int getValue() throws InterruptedException {
latch.await();
return value;
}
/**
* latch没有重置功能,用这个函数来传入新的
* @param latch
*/
public void setLatch(CountDownLatch latch) {
this.latch = latch;
}
public static void main(String[] args) throws InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
CountDownLatch latch = new CountDownLatch(1);
final CountDownLatchMethod method = new CountDownLatchMethod();
method.setLatch(latch);
Thread thread = new Thread(() -> {
method.sum(45);
});
thread.start();
int result = method.getValue(); //这是得到的返回值
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
// 然后退出main线程
}
}
FutureMethod.java
package homework;
import java.util.concurrent.*;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* Future
*/
public class FutureMethod implements Callable<Long> {
private long sum(int num) {
return fibo(num);
}
private long fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
@Override
public Long call() throws Exception {
return sum(45);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Long> future = executor.submit(new FutureMethod());
long result = future.get(); //这是得到的返回值
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
// 然后退出main线程
executor.shutdown();
}
}
FutureTaskMethod.java
package homework;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* FutureTask
*/
public class FutureTaskMethod {
static class Get implements Callable<Integer> {
FutureTask<Integer> sum;
public Get(FutureTask<Integer> sum) {
this.sum = sum;
}
@Override
public Integer call() throws Exception {
return sum.get();
}
}
static class Sum implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return fibo(45);
}
private int fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
FutureTask<Integer> sum = new FutureTask<>(new Sum());
FutureTask<Integer> get = new FutureTask<>(new Get(sum));
Thread sumT = new Thread(sum);
sumT.start();
Thread getT = new Thread(get);
getT.start();
int result = get.get(); //这是得到的返回值
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
// 然后退出main线程
}
private static int sum() {
return fibo(36);
}
private static int fibo(int a) {
if ( a < 2)
return 1;
return fibo(a-1) + fibo(a-2);
}
}
CompletableFutureMethod.java
package homework;
import java.util.concurrent.CompletableFuture;
/**
* 本周作业:(必做)思考有多少种方式,在main函数启动一个新线程或线程池,
* 异步运行一个方法,拿到这个方法的返回值后,退出主线程?
* 写出你的方法,越多越好,提交到github。
*
* CompletableFuture 方式
*/
public class CompletableFutureMethod {
public static void main(String[] args) throws InterruptedException {
long start=System.currentTimeMillis();
// 在这里创建一个线程或线程池,
// 异步执行 下面方法
int result = CompletableFuture.supplyAsync(()-> sum()).join();
// 确保 拿到result 并输出
System.out.println("异步计算结果为:"+result);
System.out.println("使用时间:"+ (System.currentTimeMillis()-start) + " ms");
// 然后退出main线程
}
private static int sum() {
return fibo(45);
}
private static int fibo(int a) {
if ( a < 2) {
return 1;
}
return fibo(a-1) + fibo(a-2);
}
}
4.(必做)把多线程和并发相关知识带你梳理一遍,画一个脑图,截图上传到 Github 上。 可选工具:xmind,百度脑图,wps,MindManage 或其他。
这个知识梳理基本就是下面的: Java 并发概览 脑图也放到里面了
参考训练营老师的内容和下面三个,用自己的逻辑主线重新整理一遍知识点
并发相关知识如下:
下面两个必要条件:当前变量有读有写;当前变量被多个线程访问
下面的三个方面
在多核心 CPU 的环境下,每颗 CPU 有自己缓存,线程操作的是不同的 CPU 缓存,导致了数据不一致,也就是线程 A 和 B 对变量的操作相互对于两者都是不可见的。
在下面的示例代码中,我们使用两个线程对变量各自进行 10000 的累加,理应得到 20000 的数值,但很多情况下,都是小于这个数的,就是由于可见性的问题导致的。
package temp;
public class VisiblenessTest {
private long count = 0;
private void add() {
for (int i = 0; i < 10000; i++) {
count += 1;
}
}
public long getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
final VisiblenessTest visiblenessTest = new VisiblenessTest();
Thread thread1 = new Thread(()->{
visiblenessTest.add();
});
Thread thread2 = new Thread(()->{
visiblenessTest.add();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(visiblenessTest.getCount());
}
}
一个线程对共享变量的修改,另外一个线程能立即看到,称为可见性
我们都知道 CPU 的时间切片,每个线程的执行时间都是不确定的,而 CPU 的指令和我们的程序指令之间还是有些差别的,如同下面这个简单的加一语句:
number += 1
在直觉中我们觉得它是一步就能完成的,但在 CPU 中需要多条指令去完成,最少三条,大致如下:
一个+1 操作分为三个操作,加上 CPU 的切换,可能带来我们不想要的结果,如下面的例子:我们两个线程都执行+1 操作,希望得到的是 2,却得到了 1
一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性
在代码中我们写入的语句可能如下:
int a = 7;
int b = 7;
但编译器在优化后可能变成了:
int b = 7;
int a = 7;
顺序的不确定性可能会带个我们不可预知的错误,比如经典的双重检查创建单例对象:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
加入线程 AB 同时执行,A 先得到了锁,B 则进入阻塞。A 创建成功以后,B 唤醒,检查 instance 不为 null,直接返回,不再创建对象。看起来一切完美,但还是会出现问题,而问题出在操作 new 上,直觉上 new 的操作为:
但编译器实际优化后是这样:
当线程 A 在第二步的时候发生了线程切换,并切到了 B 上,B 判断 instance 已经初始化了,直接返回,但如果其他线程访问 instance 变量的话,因为对象并没有初始化,就会出现空指针异常。如下图所示:
可见性的问题是各个线程数据写入了各自的缓存中,直观的解决办法是禁止使用缓存,全部写到内存中
有序性的问题是编译优化的指令重排序,直观的解决办法是禁止编译器优化
但缓存和编译优化的目的都是为了提升程序的性能的,粗暴的全部禁用掉,那性能可能就堪忧了
那合理的方案就是合理禁用缓存和编译优化了,也可以说是局部禁用缓存和编译优化
在 Java 中提供了程序员解决这两方面的问题的方法,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则
原子性的问题是在当前线程还没有完全执行完当前变量的一套操作的时候,发生了线程切换,而且其他的线程对当前变量也有操作,导致了不可预知的错误。
解决的办法就是在当前变量发送操作的时刻,只能有一个线程能进行操作,发生了线程切换那就等待到下一个时间切片,在这期间,不允许其他线程进行操作。王宝令老师专栏的描述是下面这样的,意思应该差不多:
“同一时刻只有一个线程执行”这个条件非常重要,我们称之为互斥。如果我们能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。
Java 里面锁相关的大致是 synchronized 之类的了,这里不进行详细说明了,简单提一下。
锁的基本使用步骤如下:有点像蹲坑,一个坑只能一个人用,后面的人需要等前面的人用完了才能用......
大致模型如下:
在用锁的时候需要注意锁与保护资源之间的关系
可以是一把锁保护多个资源,也就是 1:N,如下图,没有并发问题
但不能是多把锁保护一个或者多个资源,也就是相当于一个锁发了多个钥匙,可以多个人打开锁进去了,如下图,没有保护作用,有并发问题
用锁的时候注意对受保护对象进行精细化关联,使用细粒度锁,这样能提高程序性能
此外需要注意一个锁保护多个资源时,资源释放相互有关联,有关联的话就需要用粒度比较大的锁。这里不再详细的赘述,可以参考下面两篇文章:
虽然使用锁的好处有很多,但万事万物都是两面性的,锁的使用不当,容易发生下面这些问题:死锁、活锁、饥饿、性能问题
死锁的比较专业的定义如下:
一组互相竞争资源的线程因相互等待,导致“永久”阻塞的现象
下面的死锁的示例代码,锁 AB 是两个线程所需要的,但刚开始彼此各获得了 A 和 B,线程 1 等待 B,线程 2 等待 A,但没有完成操作两个线程就不会释放锁,他们之间就会这样一直等待下去。
package com.company;
public class DeadLockSample extends Thread {
private String first;
private String second;
public DeadLockSample(String name, String first, String second) {
super(name);
this.first = first;
this.second = second;
}
@Override
public void run() {
synchronized (first) {
System.out.println(this.getName() + " get lock: " + first);
try {
Thread.sleep(1000);
synchronized (second) {
System.out.println(this.getName() + " get lock: " + second);
}
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
String lockA = "LockA";
String lockB = "LockB";
DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);
DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);
t1.start();
t2.start();
t1.join();
t2.join();
}
}
那应该如何预防死锁?死锁必须具备下面四个条件才会发生:
第一个互斥没有办法,毕竟加锁互斥是基础,但对于其他的还是有办法的:
在上面死锁的不可占用解决方案中:
存在一种情况:两个线程几乎同时获得锁和释放锁,并一直循环,虽然没有阻塞,但程序还是执行不下去,这种情况就叫活锁。
就如同两个人在路口相遇,两个人同时想要对方,于是一起向右,一起向左,向右、向左......
解决的办法就是随机的等待一个时间再去获取锁
如同我先不同,看你往左了,我就往右就行了
所谓“饥饿”指的是线程因无法访问所需资源而无法执行下去的情况。
在 CPU 繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生饥饿;持有锁的线程,如果执行的世界过长,也可能导致饥饿
饥饿有三种解决方案:
加锁就意味着只能一个线程进行访问,而这部分代码一个一个的线程访问就相当于串行化了。所有如果锁的影响区域过大,那就不能发挥成多线程的优势了,甚至可以因为多线程的上下文切换而导致多线程程序性能还不如单线程程序
避免性能问题,有下面两个方案:
一、不战而屈人之兵,方是上上策,所以最好的方案就是使用无锁的算法和数据结构,相关技术如下:
可重入锁:顾名思义,指线程可以重复获取同一把锁
如下面的示例代码:在 addOne 函数中获得了锁,进入 get 函数后,如果不是可重入锁,那就会发送阻塞;如果是可重入锁,则获取成功,继续执行
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public int get() {
// 获取锁
rtl.lock(); ②
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get(); ①
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
读写锁遵守下面三条原则:
synchronnized 的代码块使用、函数使用等使用比较简单,与其配合的还有 wait 和 notify,可以实现一个等待-通知机制。
如下面的示例程序,apply 函数一次性申请所有的锁资源,但申请不到的时候进入等待状态。当锁释放以后,再次唤醒运行
package temp;
import java.util.ArrayList;
import java.util.List;
public class NotifySynchronized {
private List<Integer> als = new ArrayList<>();
synchronized void apply(Integer lock1, Integer lock2) {
while (als.contains(lock1) || als.contains(lock2)) {
try {
System.out.println("无法一次性申请所有资源,进入等待");
wait();
} catch (Exception e) {
}
}
System.out.println("资源申请成功");
als.add(lock1);
als.add(lock2);
}
synchronized void free(Integer lock1, Integer lock2) {
als.remove(lock1);
als.remove(lock2);
System.out.println("资源释放成功");
notifyAll();
}
public static void main(String[] args) throws InterruptedException {
final Integer lock1 = 1;
final Integer lock2 = 2;
final NotifySynchronized example = new NotifySynchronized();
example.apply(lock1, lock2);
Thread thread = new Thread(()->{
example.apply(lock1, lock2);
});
thread.start();
Thread.sleep(10000);
example.free(lock1, lock2);
thread.join();
}
}
这里涉及到的一些知识点稍微提及下:
尽量使用 notifyAll(),notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。
wait() 方法和 sleep() 方法都能让当前线程挂起一段时间,那它们的区别是什么?
Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分,相关原理请查看链接:08 | 管程:并发编程的万能钥匙
在前面的 synchronized 中,它虽然有等待-通知机制,但还是不够灵活。Lock 和 Condition 相当于它的灵活变种,尤其在解决死锁问题上,能很好的破坏不可强占条件。
Lock 和 Condition 相比较 synchronized 多了下面三个:
响应的 API 如下:
// 支持中断的API
void lockInterruptibly() throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
Lock 主要是用于互斥,简单使用的示例代码如下:
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
Condition 用于同步,使用示例如下:
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
//入队后,通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
//出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
信号量的一个方便使用场景是:限流器,能运行特定多个线程访问保护资源
信号量模型可以简单概括为:一个计数器、一个等待队列、三个方法,计数器和等待队列是私有的,通过调用信号量模型提供的三个方法来访问他们。
这三个方法分别是:init、down、up
模型大致如下:
简单使用的示例如下:
class ObjPool<T, R> {
final List<T> pool;
// 用信号量实现限流器
final Semaphore sem;
// 构造函数
ObjPool(int size, T t){
pool = new Vector<T>(){};
for(int i=0; i<size; i++){
pool.add(t);
}
sem = new Semaphore(size);
}
// 利用对象池的对象,调用func
R exec(Function<T,R> func) {
T t = null;
sem.acquire();
try {
t = pool.remove(0);
return func.apply(t);
} finally {
pool.add(t);
sem.release();
}
}
}
// 创建对象池
ObjPool<Long, String> pool =
new ObjPool<Long, String>(10, 2);
// 通过对象池获取t,之后执行
pool.exec(t -> {
System.out.println(t);
return t.toString();
});
使用于读多写少的场景,使用示例如下,实现一个缓存工具类:
class Cache<K,V> {
final Map<K, V> m =
new HashMap<>();
final ReadWriteLock rwl =
new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
final Lock w = rwl.writeLock();
// 读缓存
V get(K key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
// 写缓存
V put(K key, V value) {
w.lock();
try { return m.put(key, v); }
finally { w.unlock(); }
}
}
比 ReadWriteLock 更激进的锁,在特定条件下相对也更快,性能更好
StampedLock 的乐观读,允许一个线程获取写锁,也就是说不是所有的写操作都被阻塞
StampedLock 使用是有特定场景的,读多写少,对一致性延迟有容忍。需要注意它不是可重入锁。使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()
示例如下:
class Point {
private int x, y;
final StampedLock sl =
new StampedLock();
//计算到原点的距离
int distanceFromOrigin() {
// 乐观读
long stamp =
sl.tryOptimisticRead();
// 读入局部变量,
// 读的过程数据可能被修改
int curX = x, curY = y;
//判断执行读操作期间,
//是否存在写操作,如果存在,
//则sl.validate返回false
if (!sl.validate(stamp)){
// 升级为悲观读锁
stamp = sl.readLock();
try {
curX = x;
curY = y;
} finally {
//释放悲观读锁
sl.unlockRead(stamp);
}
}
return Math.sqrt(
curX * curX + curY * curY);
}
}
有时候我们线程不是各自运行的,他们之间有一定的约束和步骤,比如线程 3 需要线程 1 和 2 完成后才执行之类的,有点类似于拓扑序列,涉及到这部分,可以使用 CountDownLatch 和 CyclicBarrier,这里就再详细写了,自行查看链接学习吧
复杂的拓扑序列需要使用 FutureTask 之类的
大致就是 Java 并发包中的那些 Atomic 之类的,使用的 CAS 原理
CAS:《Java 并发编程实战》 第十二章 原子变量与非阻塞同步机制
用锁的最佳实践你已经知道,用锁虽然能解决很多并发问题,但是风险也是挺高的。可能会导致死锁,也可能影响性能。这方面有是否有相关的最佳实践呢?有,还很多。但是我觉得最值得推荐的是并发大师 Doug Lea《Java 并发编程:设计原则与模式》一书中,推荐的三个用锁的最佳实践,它们分别是:
永远只在更新对象的成员变量时加锁
永远只在访问可变的成员变量时加锁
永远不在调用其他对象的方法时加锁
这三条规则,前两条估计你一定会认同,最后一条你可能会觉得过于严苛。但是我还是倾向于你去遵守,因为调用其他对象的方法,实在是太不安全了,也许“其他”方法里面有线程 sleep() 的调用,也可能会有奇慢无比的 I/O 操作,这些都会严重影响性能。更可怕的是,“其他”类的方法可能也会加锁,然后双重加锁就可能导致死锁。并发问题,本来就难以诊断,所以你一定要让你的代码尽量安全,尽量简单,哪怕有一点可能会出问题,都要努力避免。
除了并发大师 Doug Lea 推荐的三个最佳实践外,你也可以参考一些诸如:减少锁的持有时间、减小锁的粒度等业界广为人知的规则,其实本质上它们都是相通的,不过是在该加锁的地方加锁而已。你可以自己体会,自己总结,最终总结出自己的一套最佳实践来。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。