juc并发编程
线程上下文
概念
我们都知道,jvm由堆、栈、方法区组成,其中栈内存就是给线程用的,每个线程启动后,虚拟机就会为其分
配一块栈内存。而每个栈又由多个栈帧组成,对应着每次方法调用时所占用的内存。每个线程只能有一个
活动栈帧,对应着当前正在执行的那个方法。而线程上下文就是由于一些原因导致cpu不再执行当前线程,
转而执行另一个线程的代码。
产生的原因
- 线程的cpu时间片运行完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法
当出现了上下文切换,在java中,java会使用程序计数器来保存当前线程的状态,是是线程私有的。
上下文切换频繁发生会影响性能
join方法
首先先看一个图
如果想让main线程等待t执行完再执行,就需要用到join方法。
join方法的作用就是等待线程的执行,要等待谁执行,就用谁.join()!
join的再理解
join限时同步
interrupt
打断阻塞
打断正常(不会直接打断程序的运行,只会把打断标识置为true)
守护线程
概念
默认情况下,java进程需要等待所有的线程运行完才结束,如下图:
当有守护线程,即使是守护线程的代码没有执行完,也会强制结束(垃圾回收就是守护线程的体现)
线程安全-局部变量
局部变量存放于在jvm栈中,在多线程情况下,会为每一个线程开辟一块栈内存,因此不存在线程安全的问题。
但是局部变量引用的对象未必,如果对象没有逃离方法的作用范围,它是线程安全的,如果逃离了,则需要考虑线程安全。
线程安全问题-暴露引用
线程安全-组合调用
虽然有一些线程安全的集合,提供了一些线程安全的方法,但是又的组合调用会出现线程不安全的问题,如图:
此时key的值成为了1,发生了这样的问题,其实主要是第一个线程在执行get方法完后,释放锁,还没 开始执行put方法的时候,第二个线程拿到了cpu分到的时间片,开始执行get方法,在释放锁后,可能 cpu分配的时间片用完了,第一个线程拿到执行权,进行put操作,在执行完put操作后,第二个线程拿到 执行权,执行put方法将第一个线程的值覆盖了。
Monitor
Java 对象头(以 32 位虚拟机为例)
普通对象
|--------------------------------------------------------------|
Object Header (64 bits)
|------------------------------------|-------------------------|
Mark Word (32 bits) Klass Word (32 bits)
|------------------------------------|-------------------------|
数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits)
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) |array length(32bits)
|--------------------------------|-----------------------|------------------------|
其中 Mark Word 结构为
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked
|-------------------------------------------------------|--------------------|
| |11 | Marked for GC
|-------------------------------------------------------|--------------------|
64 位虚拟机 Mark Word
Monitor的工作原理
synchronized-Monitor(监视器)
在jdk6以前,在多线程的情况下,我们在使用synchronized的时候,对象头里面的的MarkWord会存储指向
Monitor的指针,从而获取Monitor,线程1成为monitor的主人,此时线程2来了,尝试获取monitor锁,但是
发现minitor已经有主人了,于是在EntityList(阻塞队列)中等待,等待线程1执行完。
1synchronized-Lightweight Locked(轻量级锁)
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
- 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
- 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
- 如果 cas 失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
8.2锁膨胀
概念
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量
级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
如以下代码:
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- `即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED
- 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
8.3 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出
了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋重试成功的情况:在自旋的某一次,当持有锁的线程解锁了,这个时候,就可以得到锁并成功上锁
- 自旋重试失败的情况:在自旋的过程中,持有锁的线程一直不释放锁,于是竞争的线程就会进入到
EntiryList中阻塞
注意:
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会
- 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能
偏向锁
- 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
- Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
例如:
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
}
}
如图解释偏向锁:
偏向锁的状态
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
-XX:BiasedLockingStartupDelay=0
来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
-在上面测试代码运行时在添加 VM 参数-XX:-UseBiasedLocking
禁用偏向锁
偏向锁的撤销
调用对象的hascode
:调用对象的hascode后,因为此对象的对象头的markword此时存储的是线程的ThreadId,所以调用hacode会导致偏向锁被撤销。- 调用 wait/notify
wait/notify
-
Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
-
BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
-
BLOCKED 线程会在 Owner 线程释放锁时唤醒
-
WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
-
obj.wait()
: 让进入 object 监视器的线程到 waitSet 等待 -
obj.notify()
: 在 object 上正在 waitSet 等待的线程中挑一个唤醒 -
obj.notifyAll()
: 让 object 上正在 waitSet 等待的线程全部唤醒
必须获得锁对象,成为monitor的owner 才可以调用以上三个方法。`虚假唤醒
有两个正在waitSet中等待的线程,我们想要唤醒一个线程,为了防止随机性,使用notifyAll()唤醒,但是产生了一个弊端,就是第二个不想唤醒的线程也被唤醒被执行了。解决方法就是换成while循环,循环判断条件
10.设计模式-保护性暂停模式
1. 定义
即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点 -
有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
-
如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
-
JDK 中,join 的实现、Future 的实现,采用的就是此模式因为要等待另一方的结果,因此归类到同步模式
线程的活跃性-死锁
ReentranLock锁
锁的超时
JMM(Java Memory Model)
JMM 即 Java Memory Model,它定义了主存(共享信息存储的位置)、工作内存(私有信息存储的位置)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、
CPU 指令优化等。
JMM 体现在以下几个方面
原子性 - 保证指令不会受到线程上下文切换的影响
可见性 - 保证指令不会受 cpu 缓存的影响
有序性 - 保证指令不会受 cpu 指令并行优化的影响
可见性
1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
2.因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
3.1秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
voliate关键字
禁止指令重排
保证可见性
不保证原子性
无锁实现保护共享资源
package com.wl.Juc.Mehod;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class RunTest11 {
public static void main(String[] args) {
// 加锁的方式实现
Account accountUnsafe = new AccountUnsafe(10000);
Account.demo(accountUnsafe);
// 不加锁的方式实现(使用cas)
Account accountCas = new AccountCas(10000);
Account.demo(accountCas);
}
}
// 无锁实现保护共享资源
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(Integer balance) {
this.balance =new AtomicInteger(balance);
}
@Override
public void withdraw(Integer money) {
while (true) {
int prev = balance.get();
int next = prev - money;
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
@Override
public Integer getBalance() {
return balance.get();
}
}
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public void withdraw(Integer money) {
synchronized (this) {
this.balance -= money;
}
}
@Override
public Integer getBalance() {
synchronized (this) {
return balance;
}
}
}
// 账户接口
interface Account {
void withdraw(Integer money);
Integer getBalance();
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end - start) / 1000_000 + " ms");
}
}
CAS
在保护共享资源,我们使用最多的方式是加锁。但是加锁有的时候会使程序的运行效率比较低,这个时候我们可以使用CAS来实现对共享资源的保护。
其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
CAS和Synchronized
- CAS是基于乐观锁的实现,它认为别人不会修改数据,它会在每次修改数据前判断是否有人修改了当前数据,如果修改了,会下次再尝试。它的执行效率比synchronized高的原因是,他不会陷入阻塞,会一直持续执行。
- synchronized是基于悲观锁的实现,它认为别人会修改数据,于是在每次操作数据的时候上锁,直到自己操作完成释放锁,才会让别人修改数据。这样会发生上下文切换,进入阻塞
原子整数(是java提供的线程安全的操作)
AtomicInteger ac = new AtomicInteger(100);
System.out.println(ac.incrementAndGet()); // ++i
System.out.println(ac.getAndIncrement()); // i++
System.out.println(ac.addAndGet(-10)); //负数代表减,正数代表+
System.out.println(ac.get()); //获取最新值
System.out.println(ac.updateAndGet(s->s*10));//可以自己选择运算
原子引用
package com.wl.Juc.Mehod;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
public class RunTest12 {
public static void main(String[] args) {
DecimalAccountCas decimalAccountCas = new DecimalAccountCas(new BigDecimal("10000"));
DecimalAccount.demo(decimalAccountCas);
}
}
interface DecimalAccount {
// 获取余额
BigDecimal getBalance();
// 取款
void withdraw(BigDecimal amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}
class DecimalAccountCas implements DecimalAccount {
AtomicReference<BigDecimal> ref;
public DecimalAccountCas(BigDecimal ref) {
this.ref = new AtomicReference<BigDecimal>(ref);
}
@Override
public BigDecimal getBalance() {
return ref.get();
}
@Override
public void withdraw(BigDecimal amount) {
while (true) {
// 获取最新值
BigDecimal pre = ref.get();
// 获取修改后的值
BigDecimal next = pre.subtract(amount);
if (ref.compareAndSet(pre, next)) {
break;
}
}
}
}
原子引用ABA问题
在执行CAS操作的时候,只能比较并更新值,无法判断在此期间,别人是否修改过共享变量值。于是就存在线程1修改共享变量的值A->B,线程2修改共享变量的值B->A。
package com.wl.Juc.Mehod;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class RunTest13 {
static AtomicReference<String> arf=new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
RunTest13.other();
Thread.sleep(1000);
arf.compareAndSet(arf.get(),"C");
log.debug("change A->C");
}
public static void other() throws InterruptedException {
new Thread(
()->{
log.debug("change A->B", arf.compareAndSet(arf.get(),"B"));
},"t1"
).start();
Thread.sleep(500);
new Thread(
()->{
log.debug("change B->A", arf.compareAndSet(arf.get(),"A"));
},"t2"
).start();
}
}
解决ABA问题-AtomicStampedReference
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程希望:
- 只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再一个版本号
- 解决ABA问题主要是加版本号,每次修改让版本号加1
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t2").start();
}
原子数组
AtomicIntegerArray | 原子整型数组 |
---|---|
AtomicLongArray | 原子长整型数组 |
AtomicReferenceArray | 原子引用数组 |
#####案例如下 |
package com.wl.Juc.Mehod;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class RunTest15 {
public static void main(String[] args) {
// 原始数组
demo(
()->new int[10],
(arr)->arr.length,
(arr,index)->arr[index]++,
(arr)-> System.out.println(Arrays.toString(arr))
);
// 使用原子数租
demo(
()->new AtomicIntegerArray(10),
(arr)->arr.length(),
(arr,index)->arr.getAndIncrement(index),
(arr)-> System.out.println(arr)
);
}
/**
参数1,提供数组、可以是线程不安全数组或线程安全数组
参数2,获取数组长度的方法
参数3,自增方法,回传 array, index
参数4,打印数组的方法
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer ) {
List< Thread> ts = new ArrayList<>();
T array = arraySupplier.get();
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j%length);
}
}));
}
ts.forEach(t -> t.start()); // 启动所有线程
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}); // 等所有线程结束
printConsumer.accept(array);
}
}
原子更新器
AtomicReferenceFieldUpdater | 原子引用类型字段更新 |
---|---|
AtomicIntegerFieldUpdater | 原子整数类型字段更新 |
AtomicLongFieldUpdater | 原子长整型字段更新 |
#####代码如下 |
package com.wl.Juc.Mehod;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class RunTest16 {
public static void main(String[] args) {
Student stu = new Student();
// 字段更新器
AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
System.out.println(updater.compareAndSet(stu,null,"李四"));
System.out.println(stu);
}
}
class Student{
volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
UnSafe类
UnSafe类是不可以直接获取的,只能通过反射来进行操作。
import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class RunTest17 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// Unsafe是可以直接操作内存的一个类,不是不安全的类。
Field theUnsafe= Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
// 求出People中 id和name的偏移值
long idOffset = unsafe.objectFieldOffset(People.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(People.class.getDeclaredField("name"));
// 修改People对象中的值
People people = new People();
unsafe.compareAndSwapInt(people,idOffset,0,10);
unsafe.compareAndSwapObject(people,nameOffset,null,"张三");
// 输出people对象
System.out.println(people);
}
}
@Data
class People{
volatile int id;
volatile String name;
}
自己模仿写出AtomicInteger
package com.wl.Juc.Mehod;
import sun.misc.Unsafe;
public class RunTest18 {
public static void main(String[] args) {
Account.demo(new MyAtomicInteger(10000));
}
}
class MyAtomicInteger implements Account{
private volatile int value;
// Unsafe类
private static final Unsafe unsafe;
// 偏移值
private static long offsetValue;
public MyAtomicInteger(int value) {
this.value = value;
}
static {
// 初始化UnSafe类
unsafe = UnSafeUtils.getUnsafe();
try {
// 获取value的偏移值
offsetValue=unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
public int getValue() {
return value;
}
public void decrement(int amount){
while (true){
int pre=this.value;
int next=pre-amount;
if (unsafe.compareAndSwapInt(this,offsetValue,pre,next)){
break;
}
}
}
@Override
public void withdraw(Integer money) {
this.decrement(money);
}
@Override
public Integer getBalance() {
return getValue();
}
}
工具类
package com.wl.Juc.Mehod;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnSafeUtils {
private static Unsafe unsafe;
private UnSafeUtils(){}
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe= (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public static Unsafe getUnsafe(){
return unsafe;
}
}
Account接口
// 账户接口
interface Account {
void withdraw(Integer money);
Integer getBalance();
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end - start) / 1000_000 + " ms");
}
}
不可变对象
我们在操作共享资源的时候,可以使用加锁或者CAS无锁保证线程安全,还有一种就是使用不可变的对象保证线程安全。
以下是不可变时间日期保证线程安全:
package com.wl.Juc.Mehod;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
/**
* 不可变类的使用
*/
public class RunTest20 {
public static void main(String[] args) {
// 使用不可变类保证线程安全
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(
() -> {
try {
TemporalAccessor parse = dtf.parse("2021-01-09");
System.out.println(parse);
} catch (Exception e) {
e.printStackTrace();
}
}
).start();
}
}
/**
* 加锁实现
*/
private static void Lock () {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(
() -> {
try {
synchronized (sdf) {
Date parse = sdf.parse("2021-01-09");
System.out.println(parse);
}
} catch (ParseException e) {
e.printStackTrace();
}
}
).start();
}
}
}
线程池
Executor线程连接池
4. 线程池的7大参数
public ThreadPoolExecutor(
int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时了没有人会调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂,创建线程的,一般不用动
RejectedExecutionHandler handler) // 拒绝策略
5.四大策略
AbortPolicy() 抛出异常 不处理
CallerRunsPolicy 不抛出异常 哪里来的去哪里
DiscardOldestPolicy 队列满了,尝试和最早的竞争,如果竞争成功则执行,否则被抛弃,不会抛出异常
DiscardPolicy 队列满了,丢掉任务,不会抛出异常
常见的线程池
FixedThreadPool(固定线程池)
- 这种线程池的特点就是一个线程池中的线程数量是固定的。如果此线程池数量过少,当线程池任务过多时,会导致大量线程等待,如果过少,会浪费资源。
- 使用的是 LinkedBlockingQueue(双向链表),阻塞队列是无界的。
SingleThreadPool(单线程池)
- 这种线程池的特点就是一个线程池里只有一个线程。当线程任务过多,会造成大量线程等待。
- 使用的是 LinkedBlockingQueue(双向链表),阻塞队列是无界的。
CachedThreadPool(弹性缓存线程池)
-这种线程池的特点是线程池在创建的时候是一个线程都没有的,当来了线程任务的时候,会自动新建线程,如果线程池有空余线程,则不会新建,这种线程池一般可以容纳几万个线程,线程空余60s会被回收,但是这样的写法也会带来一个缺点,一旦线程无限增长,到导致内存OOM。
- 使用的是 SynchronousQueue(同步队列),它没有容量,没有线程来取是放不进去的
Fork/Join
- Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型
运算 - 所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
- Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运
算效率 - Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
13.AQS原理
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
ReentrantLock底层也用到了AQS,在ReentrantLock中有一个Sync(同步器),他继承了AQS。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)