文章目录
Java并发
见过并发编程的代码,但是对并发编程稀里糊涂的,一篇文章带你理清并发的脉络。
一、概述
为什么要用并发?
1、CPU运行速度高,I/O、网络传输等速度慢,并发可以充分压榨CPU性能。
2、一个服务要处理多个请求端的响应。
并发为什么会带来一致性问题?
一项技术的出现,是为了解决一个大问题,但是往往会带来一些新的小问题。使用并发导致的就是数据一致性问题。这我们都知道,但是到底为什么会导致数据不一致呢?
是基于jvm的内存模型,变量是保存在主内存中的(可以大概理解为jvm的堆空间),线程在使用变量时,会将变量从主内存拷贝到工作内存中(可以大概理解为jvm的栈空间),多个java线程对同一个变量进行拷贝-更改-写回主内存,就会导致一致性问题。
基于以上的点,学习并发我们需要解决的就是三大问题:
1、怎么进行并发编程。
2、怎样保证线程安全。
3、线程交互。
二、并发编程
传统的并发编程(用于理解底层实现,基本不用)
最原始的方式,就是创建任务(实现Runnable接口/实现Callable接口),创建线程(实例化Thread)运行。
- 实现Runnable接口,创建线程Thread运行
// 定义任务-Runnable
class Task1 implements Runnable {
@Override
public void run() {
// 具体业务
System.out.println();
}
}
public static void main(String[] args) {
// 创建线程-Runnable
Thread t1 = new Thread(new Task1());
t1.start();
}
- 实现Runnable接口,不过是在创建线程时,使用Lambda表达式简化任务创建
public static void main(String[] args) {
// 创建线程-Runnable2
Thread t2 = new Thread(() -> {
// 具体业务
System.out.println();
});
}
- 实现Callable接口
其中FutureTask继承了Runnable,可以传递给Thread用于创建线程。
// 定义任务2
class Task2 implements Callable {
@Override
public Object call() throws Exception {
return null;
}
}
public static void main(String[] args) {
// 创建线程-Callable
FutureTask<String> ft = new FutureTask<>(new Task2());
Thread t3 = new Thread(ft);
t3.start();
}
- 继承Thread类(java只能继承一个父类,因此此方法更不推荐)
class MyThread extends Thread {
@Override
public void run() {
// 具体业务
System.out.println();
}
}
使用Executor工具类
以上传统方法,线程Thread是需要自己创建并启动的,需要自己管理线程的生命周期。因此除非简单的功能,一般不使用。
java中提供了Executor下的一系列工具类,用于管理Thread线程,常用的就ExecutorService、ScheduledExecutorService,可以将任务提交给工具类,使用工具类来管理线程的启停等。下面介绍这两个工具类怎样使用:
使用ExecutorService(最常用)
// 定义任务
class Task1 implements Runnable {
@Override
public void run() {
// 具体业务
System.out.println();
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
// 提交已创建的任务
executor.submit(new Task1());
// 使用Lambda表达式提交任务
executor.submit(() -> {
// 具体业务
});
// 使用Lambda表达式提交带有返回值的任务
Future<String> future = executor.submit(() -> {
// 具体业务
return "返回结果";
});
}
使用ScheduledExecutorService
ScheduledExecutorService是带有定时功能的线程处理器。
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
// 每1秒执行一次任务
executorService.schedule(() -> {
// ...
return "Hello world";
}, 1, TimeUnit.SECONDS);
// 每1秒执行一次任务,并带有返回值
Future<String> future = executorService.schedule(() -> {
// ...
return "Hello world";
}, 1, TimeUnit.SECONDS);
}
三、怎样保证数据安全
volatile关键字
volatile的实现原理
基于概述中jvm内存模型,线程是将变量从主内存拷贝到工作内存,操作后再赋值到主内存,volatile保持一致原理是,对变量做以下两条限制:
1、要求线程修改变量后,必须立即写回主内存。(非volatile变量可能会等其他操作后,再写回主内存)
2、要求线程在使用变量时,必须使用最新的值。(在使用时,强制刷新一次)
以上两条就能保证volatile变量在一个线程中的修改,立即在另个线程可见。
volatile的使用场景
基于以上条件,volatiltile并不能完全保证一致性,例如多个线程同时从主内存获取到变量,然后更新到主内存。因此只有在以下场景,才应该使用volatile:
1、运算结果不依赖当前变量的值,或者只有单一线程会修改。(使用volatile变量作为锁标志)
2、不与其他变量参与不变约束。
其他的一些书里讲到的什么64位double、long类型的特殊情况,基本不会出现或者新版jvm已优化,日常开发完全不用考虑。
synchronized关键字
synchronized的实现原理
使用字节码指令,在synchronized同步的代码块前后锁定对象。
synchronized的使用场景
目前版本jdk,synchronized的性能与Lock相差无几,所以选择时无需权衡性能因素。synchronized胜在简洁,并且是手动释放锁,Lock需要主动编码释放锁。因此能使用synchronized,就使用synchronized。
Lock
Lock的功能更强大也更灵活,如果关键字满足不了,就需要上Lock了。
Lock是一个接口,与Executor一样,它的多个实现类,提供了不同功能的锁。锁的基本操作如下,获取锁lock(获取不到时线程会被阻塞),释放锁unlock。还提供了其他的常用api,例如tryLock(),lock的非阻塞版本,尝试立即获取锁,成功就返回true,tryLock(long timeout, TimeUnit timeUnit) 等待制定事件,然后放弃获取Lock
Lock lock = ...;
lock.lock();
try {
// access to the shared resource
} finally {
lock.unlock();
}
Lock的实现类有 ReentrantLock可重入锁,ReentrantReadWriteLock可重入读写锁,StampedLock java8引入的支持乐观锁。
四、线程之间的协作
学会以上部分,就能大多数RPC调用中涉及的异步及安全问题了,因为RPC调用主要是多线程分别处理不同任务,任务之间的交互极少。
但是对于个别业务场景,依赖于多任务调度,就需要线程之间的协作了,例如线程A需要等待线程B执行完才能处理。
也就是wait和notify操作。
public class Data {
private String packet;
// true 时,生产者发送数据,消费者等待
// false 时,消费者消费数据,生产者等待
private boolean transfer = true;
public synchronized String receive() {
while (transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
transfer = true;
String returnPacket = packet;
notifyAll();
return returnPacket;
}
public synchronized void send(String packet) {
while (!transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
transfer = false;
this.packet = packet;
notifyAll();
}
}
public static void main(String[] args) {
// 定义需要生产消费的包裹
String packets[] = {
"First packet",
"Second packet",
"Third packet",
"Fourth packet",
"End"
};
// 创建线程,发送包裹
Thread sendThread = new Thread(() -> {
for (String packet : packets) {
data.send(packet);
// Thread.sleep() 模拟耗时长的业务操作
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
// 创建线程,接受包裹
Thread receiveThread = new Thread(() -> {
for(String receivedMessage = load.receive();
!"End".equals(receivedMessage);
receivedMessage = load.receive()) {
//Thread.sleep() 模拟耗时长的业务操作
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
// 启动线程
sendThread.start();
receivedMessage.start();
}