并发和多线程
一、简述
线程和进程:
线程:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。线程共享进程所拥有的全部资源。
进程:进程是程序在执行过程中分配和管理资源的基本单位,每个进程都拥有独立的地址空间和系统资源
并发与并行:
并发:在同一时间间隔内发生两个或多个事件。在Java中,并发通常是通过多线程来实现的
并行:在同一时刻内发生两个或多个事件。并行需要多个CPU核心或处理器来真正同时执行多个任务。
二、多线程实现方式
2.1 实现Runnable接口
public class ThreadImpl implements Runnable{
//实现run方法
@Override
public void run() {
System.out.println("线程1");
}
public static void main(String[] args) {
//创建ThreadImpl的实例
Runnable runnable = new ThreadImpl();
//将实例交给线程对象(thread)处理,并开启线程
new Thread(runnable).start();
//也可以直接调用匿名内部类实现
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("线程1");
}
}; //等价于 Runnable runnable2 = () -> {System.out.println("线程1")};
new Thread(runnable1).start();
//还可以使用lambda表达式
new Thread(()-> {
System.out.println("线程1");
}).start();
}
}
2.2 继承Thread类
public class ThreadImpl extends Thread{
//重写run方法
@Override
public void run() {
System.out.println("线程1");
}
public static void main(String[] args) {
//创建ThreadImpl的实例
ThreadImpl thread = new ThreadImpl();
//开启线程
thread.start();
}
}
2.3 实现Callable接口
Callable 接口与 Runnable 接口类似,但 Callable 接口可以返回结果,并且可以抛出异常
public class ThreadImpl implements Callable {
//实现call方法
@Override
public Object call() throws Exception {
int sum = 0;
for (int j = 0; j < 10 ; j++) {
sum += 1;
}
return "计算的结果为" + sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建实例
ThreadImpl threadCallable = new ThreadImpl();
//封装
//FutureTask实现了RunnableFuture接口,而RunnableFuture接口同时继承了Runnable和Future接口
FutureTask<String> futureTask = new FutureTask<>(threadCallable);
//启动线程
new Thread(futureTask).start();
//获取线程执行结果,这个方法会阻塞当前线程,直到结果可用或者抛出异常。
String string = futureTask.get();
System.out.println(string);
}
}
三、线程安全问题
3.1 使用synchronized加锁
它能够确保在同一时间只有一个线程可以执行某个方法或代码块,从而避免多线程同时访问共享资源时可能产生的数据不一致或竞争条件问题
public class ThreadSafeImpl {
private int count = 0;
// 使用synchronized将访问共享资源的核心代码块加锁
// 推荐使用this作为实例方法的锁对象;如果使用的是静态方法,推荐使用类名.class作为锁对象
public void threadSafe() {
synchronized (this) {
count++;
}
}
// 使用synchronized修饰实例方法,确保线程安全
public synchronized void increment() {
count++;
}
}
3.2 使用 ReentrantLock锁
提供了比synchronized更丰富的功能。支持可中断的锁获取尝试、可定时的锁获取尝试、以及公平锁
public class ThreadSafeImpl {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void threadSafe2(){
// 加锁
lock.lock();
try{
count++;
}catch (Exception e){
e.printStackTrace();
}finally {
// 解锁
lock.unlock();
}
}
}
3.3 使用并发合集ConcurrentHashMap
ConcurrentHashMap是线程安全的,允许多个线程同时进行读写操作而不会导致数据不一致。
支持高效的并发更新操作,如put、get和remove等。
在jdk1.8之前,使用分段锁机制实现线程安全,将哈希表分成多个段(segment),每个段维护一个独立的哈希表和锁
在jdk1.8后,采用数组、单链表、红黑树的存储结构,当链表长度超过一定阈值时(默认为8),链表会转换为红黑树以提高查找效率,查询的时间复杂度从 O(n) 降低到 O(logN);
使用CAS操作和synchronized关键字来实现更细粒度的锁控制。CAS是原子操作,可以在不使用锁的情况下更新共享变量
ConcurrentHashMap的扩容触发条件:
数组中元素个数大于数组长度✖负载因子(默认0.75)
需要在短时间内插入大量元素时,使用putAll方法可能会触发扩容
链表长度过长,链表长度大于8(默认阈值)时
ConcurrentHashMap扩容流程简述:
1、计算扩容标识戳
ConcurrentHashMap允许协助扩容,这个是用来协调多个线程的扩容操作
2、初始化新数组,通常为原数组的两倍
3、设置transferIndex变量
transferIndex变量用于表示已经分配给扩容线程的table数组索引位置。在扩容开始前,通常被设置为原数组的长度
4、计算扩容区间
每个线程会根据transferIndex和步长(stride,通常最小值为16)来计算自己需要迁移的数组区间
5、设置ForwardingNode节点
迁移完毕后,会将旧数组中的对应位置设置为ForwardingNode节点,以告知访问此桶的其他线程该节点已经迁移完毕。
6、检查并结束扩容
最后一个结束扩容的线程会检查整个数组的数据是否都已经迁移到新数组中,并更新相关状态变量以结束扩容操作
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
Runnable runnable = () -> {
for (int i = 0; i < 10; i++) {
concurrentHashMap.put("key" + i + " value", i);
}
};
//创建线程
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
//等待线程执行完
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
concurrentHashMap.forEach((key, value) -> {
System.out.println(key + value);
});