目录
并发/并行
并行
同一时间 多个处理机分别处理不同的事情
并发
是在某一个时间段内,一个处理机交替做多个事情
因为摩尔定律失效(通过提高CPU主频来提高性能),必须要采用多核CPU让程序并发或并行执行,让CPU一直活动,压榨硬件资源,提高性能
性能(切换开销等);访问共享变量线程安全问题;
并发编程就是要让在多核,多线程下,也只能一次只有一个线程对共享资源访问
线程/进程/管程
- 程序:一段静态的代码,存在于硬盘当
- 进程:进程是程序运行和操作系统资源分配的最小单位,进程在运行中拥有独立的内存单元
- 线程:是操作系统进行任务调度的最小单元;是一个进程内部的最小执行单元,一个进程中可以由多个线程,这些线程共享进程的内存资源,每个线程有独立的栈来存储数据,同一进程中的多个线程可以并发执行
- 管程:就是Monitor监视器,Monitor就是一种同步机制,它用来保证同一时间只有一个线程可以访问被保护到的数据和代码,JVM中是基于进入和退出(Monitor管程对象)来实现的;执行线程要先成功持有管程,当执行完就要释放管程,同一时间只能有一个线程持有一个管程
- 协程:轻量级的线程,是由程序直接管理,对内核不可见。协程的切换不需要进入内核态,减少了线程切换的资源消耗。
- 纤程:也是轻量级线程,为了企业程序的更好移植到Windows系统,而在操做系统中增加的一个概念
Java内存模型(JMM)
运行出现的问题
cpu、内存、硬盘间IO速度差异
为了平衡,做如下优化:
- CPU 增加了缓存,以均衡内存与 CPU 的速度差异
- 编译程序优化指令执行顺序,使得缓存能够得到更加合理地利用
- 操作系统以线程分时复用 CPU,均衡 I/O 设备与 CPU 的速度差异
JMM
Java内存模型线程如何对内存进行操作。系统设定了主内存
和工作内存
这两个抽象的区域。主内存中存储Java中的变量,对于所有线程都是共享的;每条线程都会有自己的工作内存,工作内存可以是(缓存、写缓冲区、寄存器),保存的是主存中某些变量的拷贝。
java多线程在工作时,先将主内存中的数据读到线程工作内存中,然后在工作内存中对数据进行操作,操作完成后再将数据写回到主内存;线程之间无法相互直接访问,变量传递均需要通过主存完成
并发编程核心问题
- 线程本地缓存:B线程看不到A线程中操作过的共享数据,带来可见性问题
- 编译优化重排指令带来有序性问题
- 线程切换带来原子性问题
可见性
一个线程对共享变量修改,另一个线程能够立即看到
有序性
有序性指的是程序按照代码的先后顺序执行
Java 内存模型中,编译器为了优化性能,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
原子性
线程切换会带来原子性问题
count = count +1; 是两个操作,先在操作数栈中计算,再赋值
在执行到 count =count +1;时,会将栈帧中局部变量存count = ; 然后在操作数栈中执行count+1操作,如果此时没有赋值给count但切换到B线程,B线程中即使可见A中的count,但是其值仍为0;
volatile
被volatile关键字修饰的共享变量
- 一个线程操作后,可以保证在另一个线程中立即可见
- 禁止进行指令重排
- 不能保证原子性
原理
有序性
在读写volatile修饰的变量前后加上特定的内存屏障
来禁止指令重排。
可见性
通过Lock前缀
+缓存一致性协议
实现的。对volatile修饰的变量进行操作时,JVM会发送一个 Lock 前缀指令给 CPU,CPU在执行完写操作后会立即更新到内存,同时因为缓存一致性协议,其他各CPU都会对总线嗅探
,查看自己缓存(工作内存)中的数据是否被修改,如果修改了就把缓存数据删掉,再从内存中读一遍。
MESI 缓存一致性协议
在多核 CPU 中,内存中的数据会在多个核心中存在数据副本,某一个核心发生修改操作,就产生了数据不一致的问题,而一致性协议正是用于保证多个 CPU缓存共享数据的一致性
保证原子性
加锁
被 synchronized 修饰某段代码后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一定能保证原子操作,synchronized 也能够保证可见性和有序性
JUC–原子变量
原子类的原子性是通过 volatile + CAS 实现原子操作的
AtomicInteger 类中的 value 是有 volatile 关键字修饰的,这就保证了 value的内存可见性,这为后续的 CAS 实现提供了基础
低并发情况下:使用 AtomicInteger
CAS
Compare-And-Swap 比较交换,该算法是硬件对于并发操作的支持,适用于低并发情况下
CAS 是乐观锁的一种实现方式,他采用的是自旋锁的思想,是一种轻量级的锁机制
CAS 包含了三个操作数:
- 内存值 V
- 预估值 A (比较时,从内存中再次读到的值)
- 更新值 B (更新后的值)
一个线程内在操作完成后想改变主内存变量之前先再次从主内存中读回这个变量成为预期值A,验证内存值与预期值是否相等,相等的话再写回,不相等就什么不做
优点:这种做法的效率高于加锁,当判断不成功不能更新值时,因为没有加锁,不会阻塞,继续获得cpu执行权,继续判断执行
缺点:该方式不断地循环判断,就是不断的自旋,会导致cpu消耗,并发量大时cpu就会跑满,可能会产生ABA问题
原理
例如AtomicInteger的i++操作 getAndIncrement(),查看底层源码
compareAndSet()底层也是compareAndSwapInt()
unsafe类下的方法gatAndAndInt()
unsafe类是cas的核心类,是jvm的运行jar包里的sun.misc包下,里边都是本地方法,直接调用操作系统底层资源执行任务
this指当前对象,valueOffset就是变量在内存中偏移地址,1是加的数
这有一条do while循环语句,while里比较真实值和期望值,相等在加上取反,跳出循环,成功写入;不相等取反条件为true,继续while循环,也就是自旋。
compareAndSwapInt()是本地方法,在unsafe类下的方法,是一条操作系统原语,原语的执行必须是连续的,在执行过程中不能被打断,不会造成数据不一致问题。汇编语言带有Atomic前缀,就可以保证比较交换是原子性的。
ABA问题
即某个线程将内存值由 A 改为B再 改为A。当另外一个线程使用预期值去判断时,预期值与内存值相同,误以为该变量没有被修改过而可能导致的问题,有些业务需求就在意这个问题。
通过使用类似添加版本号的方式解决,比较值和版本号
AtomicStampedReference< User>类,加上版本号,修改时改变版本号,对比时也要比较版本号
JUC常用类
就是java.util.concurrent并发包、java.util.concurrent.atomic原子性包、java.util.concurrent.locks锁下的类
AtomicInteger
int 类型包装类,保证原子性
public class AtomicIntegerDemo {
static int num = 0;
static AtomicInteger num1 = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
num++;
num1.getAndIncrement();
}
}, String.valueOf(i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("num:" + num); //16816
System.out.println("num1:" + num1);//20000
}
}
AtomicReference<>
让自己的类保证原子性
public static void main(String[] args) {
User user1 = new User("张三", 15);
User user2 = new User("李四", 20);
AtomicReference<User> userAtomicReference = new AtomicReference<>();
userAtomicReference.set(user1);
userAtomicReference.compareAndSet(user1, user2);
System.out.println(userAtomicReference);// User{name='李四', age=20}
userAtomicReference.compareAndSet(user1, null);
System.out.println(userAtomicReference);// User{name='李四', age=20}
}
ConcurrentHashMap
HashMap 是线程不安全的,不能并发操作,会出现并发修改异常ConcurrentModificationExceotion (遍历集合,并删除集合中的数据出现过)
Hashtable 是线程 put 方法被synchronized修饰,相当于把整个hash表锁住了,锁的力度较大,效率低,用于低并发可以
ConcurrentHashMap 介于 HashMap 与 Hashtable 之间,并没有将整个hash表锁住。jdk8 之前使用segment的分段锁机制(给每个位置创建一个锁标志对象)就会浪费内存空间,其中segment继承自ReentrantLock。jdk8之后使用 CAS思想+synchronized实现,插入元素时,检测hsah表对应位置是否是第一个节点,如果是采用CAS机制(循环检查)向第一个位置插入节点,如果此位置已经有值,那么就以第一个Node对象为标志进行加锁,使用的是synchronized实现,读操作不加锁,使用Volatile修饰Node的val和next,数组也被Volatuile修饰,保证扩容的时候可见,不会读到脏数据。ConcurrentHashMap 替代了 Hashtable 的独占锁,进而提高性能
get()不加锁
因为Node的元素和next都是被volatile修饰的,多线程下,一个线程对节点元素的修改和添加元素对其他线程都是可见的
键值不能为null
ConcurrentHashMap的键值都不能为null:为了消除歧义,无法分辨key是没找到还是key本来为null
是绝对线程安全的吗?
ConcurrentHashMap是线程安全的,那是在他们的内部操作,其外部操作还是需要自己来保证其同步的,特别是静态的ConcurrentHashMap,其有更新和查询的过程,要保证其线程安全,需要syn一个不可变的参数才能保证其原子性
CopyOnWriteArrayList
ArrayList是线程不安全的,add()方法没有被锁修饰,不能并发操作,会出现java.util.ConcurrentModificationException异常: 并发修改异常
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
arrayList.add(UUID.randomUUID().toString().substring(0, 6));
System.out.println(arrayList);
}, String.valueOf(i)).start();
}//java.util.ConcurrentModificationException
}
解决方案:
- Vector是线程安全的,但是对读写操作都加了锁,效率较低
- Collections.synchronizedList(new ArrayList<>());
- CopyOnWriteArrayList
CopyOnWriteArrayList对读写分离,读 不影响数据,不加锁,写 加锁,写操作不影响读操作,只有两个线程同时写时才互斥;添加时先将原数组复制出一个副本,将数据添加到副本,就不影响读操作,在将副本替换原数组;
CopyOnWriteArraySet
HashSet不安全,也会出现java.util.ConcurrentModificationException异常
HashSet的底层是HashMap,HashSet的add()方法,用键存储加的值,值是一个叫present常量
解决:
- Collections.synchronizedSet(new ArrayList<>());
- CopyOnWriteArraySet
不允许重复数据的单列集合线程安全
底层用CopyOnWriteArrayList,在add时判断是否存在
CountDownLatch
是一个辅助类
使一个线程,等待其他线程执行完后再执行
相当于一个线程计数器,是一个递减的计数器
先指定一个数量,当有一个线程执行完计数为0时执行
countDownLatch.countDown():计数器 -1
countDownLatch.await():关闭计数器
public static void main(String[] args) throws InterruptedException {
CountDownLatch count = new CountDownLatch(5);
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t上完自习离开教室");
count.countDown();
}, String.valueOf(i)).start();
}
count.await();
System.out.println(Thread.currentThread().getName()+"\t学生走完了,关门");
}
CyclicBarrier
一个同步辅助类
让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门
是一个加法计数器,当线程数量达到一定值时,就会执行
cyclicBarrier.await():计数+1
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
System.out.println("人到齐了,开会");
});
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t来了");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
Semaphore
可伸缩的,acquire()添加,release()释放
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3, false);//模拟3个车位,非公平锁
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "\t抢车位");
try {TimeUnit.SECONDS.sleep(3);}catch (InterruptedException e){e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t离开");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
Future
主线程让一个子线程去操作任务,子线程操作的时候主线程就会去做别的事情,是异步进行的,过一会才去获取子线程的任务结果或修改任务状态
Future接口定义了一些操作异步任务执行的一些方法:异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务是否执行完成
FutureTask
FutureTask就是实现类,可以开启一个分支任务,为主线程异步处理一些耗时费力的复杂业务(老师上课让学生买水)
满足三个条件
1、多线程;2、有返回值;3、异步任务
FutureTask(Callable < V > callable)
使用
缺点
一但get()方法求结果,如果没有计算完成会导致主线程阻塞。如果线程不愿意等待很长时间,可以设置时间过期不候,但会抛出异常,或者只能轮询监听,这样对结果的获取不是很友好
CompletableFuture
对FutureTask做了改进和升级
- 完成任务后有回调通知,减少阻塞或轮询监听
- 多个任务前后依赖可以组合处理(异步任务处理计算相互独立,但后一个任务需要依赖前一个任务),也就是异步任务可以顺序处理
- 异步任务出错时可以回调方法
Java中的锁
乐观锁/悲观锁
乐观锁:认为自己在使用数据的时候不会有别的线程来修改数据,不会加锁,而是用版本号或者CAS的方式判断我写入的过程中这个数据有没有被修改,如果没有被修改,就写入成功,如果被修改,就放弃或者重试;适合读多写少的场景
悲观锁:认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据前加锁,确保数据不会被其他线程修改,保证安全;适合写操作多的场景;像sync和lock都是悲观锁
可重入锁
当一个同步方法中,调用了另一个和他使用同一把锁的方法时,在外层方法中,即使没有释放锁的情况下,也可以进入另一个方法
Synchronized 而言,也是一个可重入锁,所以上述情况不会导致死锁
读写锁(ReadWriteLock)
真正的锁
- 多个读者可以同时进行读
- 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
- 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
分段锁
一种加锁的思想,在每个分段上都会单独加锁,把锁进一步细粒度化,以提高并发效率
自旋锁
采用自旋(循环重试)的方式进行尝试获取执行权,不会让线程进入阻塞状态
适用于锁的时间较短情况
共享锁/独占锁
共享锁:可以被多个线程共享的锁;(ReadWriteLock读是共享锁)
独占锁:一次只能有一个线程获取锁;(ReentrantLock,Synchronized,ReadwriteLock写锁)都是独占锁
ReadwriteLock实现方式:使用一个同步队列,例如读线程获取资源,将标准的state设置为已被使用,然后将写线程加入到一个队列中等待
独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或 者共享
AQS
维护一个队列,让等待的队列排队
公平锁/非公平锁
公平锁:维护一个线程等待的队列,依次去执行线程(ReentrantLocak默认是非公平锁,可以在创建时通过构造方法指定其公平)
非公平锁:没有队列,一旦释放锁,线程就去抢占,哪个线程抢到了就先执行;整体效率高(synchroized)
Java对象头
在虚拟机中,对象在堆内存的存储布局分为三个部分
synchronized 使用的锁对象是存储在 Java 对象头里,它是轻量级锁和偏向锁的关键
MarkWord占8个字节,类型指针8个字节。 如果仅new Object()会共占用16个字节
Synchronized
Synchronized是关键字,可以修饰代码块和方法;是隐式的加锁和释放锁;不会出现死锁;可重入锁;非公平锁;加锁操作不可被取消;加了锁也不可被中断;只能随机唤醒一个或者所有;
实现加锁和释放锁是指令级别的,是通过monitorenter和monitorexit这两个字节码指令,在虚拟机执行到monitorenter时,会首先尝试获取对象的锁,如果没有锁,就让线程拥有这个对象的锁,计数+1;如果线程拥有的就是这个对象的锁,就直接让计数+1;如果执行monitorexit指令,就让计数器-1;计数器减到0,锁就被释放。因为它是用计数器加一减一记录锁状态,所以就可以实现可重入性。
Synchronized通过在对象头中设置标记,达到了获取锁和释放锁的目的。
为什么一个enter对应两个exit?
因为sync是可重入锁,要保证他正常退出和异常退出
使用
- 修饰代码块:对括号里配置的对象(同步监视器)加锁;synchronized在继承Thread情况下修饰代码块时 , 因为创建了两个Thread对象 , 为了保证只有一个进入synchronized修饰的代码块内 , 所以static Object obj = new Object(); obj表示锁对象;;但如果在继承Runnable接口的情况下 , 只new了一个对象 , 所以直接用synchronized (this) {…}修饰(表示锁对象)
- 修饰方法:使用当前对象加锁。如果是继承Thread类,修饰的方法必须是静态的,成为类方法才能保证同步。继承Runnable不用
Synchronized锁升级
Java对线程的阻塞或唤醒都要依靠操作系统接入,需要在内核态
和用户态
相互切换,会消耗大量的系统资源。JDK5前,Synchronized只有重量级锁,因为监视器monitor要依赖操作系统的系统互斥量
,就要转入内核态去完成,这样非常消耗资源。
JDK6之后,为了减少锁释放带来的性能消耗,引入了偏向锁和轻量级锁
- 无锁状态
- 偏向锁状态 :一直只有一个线程在获取锁,可以更方便的获取到锁
- 轻量级锁状态 :当是偏向锁时,再有一个线程来访问,锁状态升级为轻量级锁,线程等待时不会进入阻塞状态,采取自旋方式重新获得锁,效率提高
- 重量级锁状态:当锁的状态为轻量级锁时,如果有的线程自旋次数过多,或者大量线程访问,那么锁升级为重量级锁,此时未获得锁的线程不再自旋,进入重量级锁;重量级锁会让其他申请的线程进入阻塞,性能降低
JIT编译器对锁的优化
锁消除
:如果每个线程在资源类中都new 一个对象并那次当锁,这样就是没有意义的,JIT编译器就会无视他,实际只会用到一把锁
锁粗化
:如果本来在一段代码块加锁就可以,但是我把这段代码分成多段分别加锁,不断加锁释放锁,这也是没有意义的且消耗资源,编译器就会对此优化
用户态和内核态
早期操作系统不区分,应用程序就能随意访问系统资源,这样非常危险系统容易崩。按照CPU指令的重要程度进行划分:Ring0(内核态)~Ring3(用户态),内核态可以访问任意空间,用户态只能访问用户空间,用户态程序不能随意操作内核地址,对操作系统进行了保护。
对系统资源进行操作需要进入内核态,比如创建线程,对线程的阻塞唤醒操作
ReentrantLock
是类的层面实现控制的,主要利用 CAS思想+AQS 队列来实现。是可重复锁;默认是非公平锁,可以设置为公平锁;需要手动加锁释放锁,必须在finally代码块中unlock();会出现死锁;有trylock()所以可以实现条件加锁;有lockInterruptibly()所以锁可被中断;有condition,可以使用signal()分组唤醒、精确唤醒;
lock():当一个线程执行lock()方法,会使用CAS的方式去改变state的值:如果改变失败,如果是非公平锁实现就让他 自旋,如果是公平锁实现,会将当前线程信息包装成Node插入到队列尾部,同时阻塞当前线程。
AQS
AbstractQueuedSynchronizer
:抽象队列式同步器
AQS是一个抽象类,定义了一套多线程访问共享资源的同步器框架,像是一个队列管理员,当多线程操作时,对这些线程进行排队管理。
它的设计基于模板方法模式,如果需要实现自定义的同步器只需重写其中方法就行
内部有一个volatile修饰的同步状态state
和一个FIFO双向队列
,state标志是否持有锁,volatile保证可见性和有序性,并提供了compareAndSetState()
保证修改的原子性。线程如果获取同步state失败,会将当前线程和等待信息等构建成一个Node,将Node使用CAS的方式插入到FIFO队列尾部,同时阻塞当前线程,当线程将同步状态state释放时,会把队列的首节点唤醒,使其获取同步状态state,并把这个节点从队列中删除。
CountDownLatch、CyclicBarrier、Semaphore、ReentrantLock、ReentrantReadWriteLock 都是依赖AQS
阻塞队列
Collection下接口 Queue 下接口 BlockingQueue
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列(Integer.MAX_VALUE–>可以当无界)
- SynchronousQueue:不存储元素的阻塞队列,单个元素的队列,(0库存)
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
ArrayBlockingQueue
SynchronousQueue
0库存,存入一个元素,等到这个被拿出,其他的才能存入
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t put 1");
blockingQueue.put(1);
System.out.println(Thread.currentThread().getName() + "\t put 2");
blockingQueue.put(2);
System.out.println(Thread.currentThread().getName() + "\t put 3");
blockingQueue.put(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, ("T1")).start();
new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "\t get" + blockingQueue.take());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "\t get" + blockingQueue.take());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "\t get" + blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, ("T2")).start();
}
生产者消费者问题
方案一
synchronized、wait()、notify() JavaSE第十章.线程
方案二
/**
* 资源类
* 资源类带着能操作的方法
* 设定,最多库存三个;
*/
class ShareDate {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
//1、判断
while (number >= 3) {
//有库存:等待
condition.await();
}
//2、生产
number++;
System.out.println(Thread.currentThread().getName() + "生产\t 库存:" + number);
//3、通知唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
//1、判断
while (number == 0) {
//无库存:等待
condition.await();
}
//2、消费
number--;
System.out.println(Thread.currentThread().getName() + "消费\t 库存:" + number);
//3、通知唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 场景模拟
*/
public class Demo1 {
public static void main(String[] args) {
//生产资源
ShareDate shareDate = new ShareDate();
//模拟生产者
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
shareDate.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T1").start();
}
//模拟消费者
for (int i = 0; i < 4; i++) {
new Thread(() -> {
try {
shareDate.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T2").start();
}
}
}
为什么用while而不是if?
在超过两个线程的环境下,使用if会出现超买现象
为什么用Condition?
替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效
使用这种方法的优点?
精确唤醒
方案三
交替打印
/**
* 交替打印
* A印5次 然后B印10次 然后C印15次
* 依次顺序,交替轮流打印三轮
*
* @author Deevan
*/
class ShareResource {
private int number = 1; //定位哪个打印机
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
//不是自己印,没轮到自己:等着
while (number != 1) {
condition1.await();
}
//到自己了:打印
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
//印完了,指定下一个打印机,并唤醒
number = 2;
condition2.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (number != 2) {
condition2.await();
}
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
number = 3;
condition3.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (number != 3) {
condition3.await();
}
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
number = 1;
condition1.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class Demo0 {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
/*
三个线程交替执行打印三轮
*/
new Thread(() -> {
for (int i = 1; i <= 3; i++) {
shareResource.print5();
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i <= 3; i++) {
shareResource.print10();
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i <= 3; i++) {
shareResource.print15();
}
}, "C").start();
}
}
线程池
为什么使用线程池?
线程的创建和销毁需要调用操作系统内核的API,需要进入内核态,让操作系统为其分配一些资源,频繁地创建销毁线程就会增大开销,所以创建线程池缓解压力;
将线程和任务解耦,线程只需要不断检查是否有任务,如果有就调用该任务的run()。而不是将任务绑定线程让线程start()
创建
jdk5之后,提供ThreadPoolExecutor类来实现线程池的创建,是建议被使用的,里面有7个参数来设置对线程池的特征的定义
public class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":task "+taskNum+"执行完毕");
}
}
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 200,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2),Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 1; i <= 9; i++) {
MyTask myTask = new MyTask(i);
executor.execute(myTask);//添加任务到线程池
}
executor.shutdown();
}
}
-------------------------------------------------------------------
pool-1-thread-5:task 7执行完毕
pool-1-thread-4:task 6执行完毕
pool-1-thread-3:task 5执行完毕
pool-1-thread-1:task 1执行完毕
main:task 8执行完毕
pool-1-thread-2:task 2执行完毕
pool-1-thread-4:task 9执行完毕
pool-1-thread-5:task 3执行完毕
pool-1-thread-3:task 4执行完毕
构造器中各个参数
corePoolSize:核心线程池大小;
在创建时核心线程池数量默认为0,有任务来了后,才会创建线程去执行;
除非调用prestartAllCoreThreads()或者 prestartCoreThread();进行预创建;不会销毁
maximumPoolSize:线程池最大总数,表示线程池中最大能装多少个线程
keepAliveTime:非核心线程池中的线程在没有任务的情况下最久保留时间
unit:为 keepAliveTime 的时间单位 TimeUnit.SECONDS;
workQueue:一个等待队列,可以自己来指定等待队列的实现类
threadFactory:线程工厂,主要来创建线程
handler:拒绝策略,表示当拒绝处理任务时的策略
线程池的执行
- 检测核心线程池是否已满
- 未满:在核心线程池创建一个线程处理
- 已满:将任务添加到一个等待队列中
- 核心线程池和等待队列都满,创建非核心线程池来处理任务
- 非核心线程池也满,使用相应的拒绝策略
线程池中的队列
线程池有以下工作队列(按 FIFO排序任务):
ArrayBlockingQueue:有界队列,是一个用数组实现的有界阻塞队列
LinkedBlockingQueue:可设置容量队列,基于链表结构的阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,吞吐量通常要高于 ArrayBlockingQuene
线程池的拒绝策略
- AbortPolicy :抛出异常,阻止系统正常工作
- DiscardOleddestPolicy :丢弃最老的一个请求
- DiscardPolicy :丢弃无法处理的任务,不予任何处理
- CallerRunsPolicy :让提交任务的线程去执行 例如main线程
execute 与 submit 的区别
都是向线程池提交任务
- execute:没有返回值
- submit:有返回值,会返回一个future类型的对象,通过这个对象可以判断任务是否执行成功
如何合理配置线程池参数
主要配置总线程池大小
- cpu密集型:该任务需要大量的运算,而没有阻塞:(cpu核数+1)
- IO密集型:需要大量IO,需要阻塞(cpu核数*2);阻塞占比越高,线程个数越多
关闭线程池
- shutdownNow:立即终止线程任务
- shutdown:不会接收新的任务,在执行完后关闭
创建线程的四种方式
- 继承Thread类,重写run(),start() 启动
- 实现Runnable接口,重写run(),丢给Thread类实例,启动其start();①解决不能多继承问题;②多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
- 实现Callable接口,重写call(),丢进FutureTask对象中,把FutureTask对象再丢给thread对象,futureTask.get();可以接收返回值、异常
- 使用线程池
ThreadLocal
线程变量
为每一个线程都有自己专属的变量副本,使得多个线程间的变量互不影响
就是让人手一份,每个线程只能操作自己的,解决线程安全问题
比如我们玩联机游戏,每个玩家对应一个线程,而每个玩家的信息(血量、护盾、枪支、子弹数)都保存在自己的ThreadLocal内,保存这些不用写回主内存的信息
public class ThreadLocalDemo {
//创建一个ThreadLocal对象,用来为每个线程会复制保存一份变量,实现线程封闭
private static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
localNum.set(1);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
localNum.set(localNum.get() + 10);
System.out.println(Thread.currentThread().getName() + ":" + localNum.get());//11
localNum.remove();//当线程中不再使用线程变量时,及时将变量清除
}
}.start();
new Thread() {
@Override
public void run() {
localNum.set(3);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
localNum.set(localNum.get() + 20);
System.out.println(Thread.currentThread().getName() + ":" + localNum.get());//23
localNum.remove();//当线程中不再使用线程变量时,及时将变量清除
}
}.start();
System.out.println(Thread.currentThread().getName() + ":" + localNum.get());//0
}
}
原理分析
因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal内部维护了一个 Map(ThreadLocalMap);Map的键就是ThreadLocal,值就是自己存的值。
调用set方法时,会在底层获取到当前正在执行的线程对象,在当前线程中创建ThreadLocalMap对象,键为ThreadLocal 对象,值为自己set的值
调用get()方法时,先通过当前的线程去找对象的ThreadLocalMap
最终的变量是放在了当前线程的ThreadLocalMap 中 , 并不是存在ThreadLocal 上,ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值
内存泄漏问题
ThreadLocalMap 的键是ThreadLocal,是一个被弱引用对象管理的
如果使用强引用,就会导致Map中key指向的ThreadLocal对象和value指向的对象不能被回收。使用弱引用会大概率减少内存泄漏问题,弱引用就会在垃圾回收的时候将Entry的key指向null
举个例子:一个Thread代表一个人,这个人有自己的身份证卡片也就是ThreadLocal,所有人的信息要存在一个户籍室中也就是ThreadLocalMap中,如果这个线程结束了,也就是人没了,我们就要连带着将身份证删除,如果身份证是被户籍室强引用关联的,那么这个身份证就不能删除,为了保证Thread和ThreadLocal的一致存在性,所以就要用弱引用关联
解决办法:在ThreadLocal中的变量被使用完成后,使用remove() 进行消除
使用场景
- 再进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递
- 线程间数据隔离
- 进行事务操作,用于存储线程事务信息
- 数据库连接,Session会话管理
spring在使用JDBC连接的时候,采用线程绑定connection,实现事务的隔离,就是用ThreadLocal隔离的