java并发编程
我们在用java并发编程时会用到java.util.concurrent(简称JUC)包,该包下包含了并发编程的类。
什么是线程
线程(thread)是操作系统能够进行运算调度的最小单位。
并发编程顾名思义就是让多个线程并行执行(多核CPU下),提高系统的并发能力。
线程状态
==NEW== 初始状态
==RUNNABLE== 运行状态
==BLOCKED== 阻塞状态
==WAITING== 等待状态
==TIME_WAITING== 超时等待状态
==TERMINATED== 终止状态
lock和synchronized 的区别
1、synchronized 是java关键字,Lock是juc下的api
2、synchronized 为非公平锁,lock可以是公平锁、非公平锁
3、synchronized 会自动释放锁,无需我们在程序中手动释放;lock必须调用unlock进行释放。
4、使用synchronized 时,如果线程阻塞,另外一个线程会一直等待;但是lock可以使用trylock尝试获取,超过一定时间获取不到便放弃。
5、synchronized 可用于方法、代码块等适合代码量较小的场景,lock可以精确锁定,具体到某行代码。
线程的精确通知
线程直接通知的三大步骤:
==条件判断==
==执行动作==
==通知线程==
Object类有wait和notify/notifyAll通知,但是不能做到精确的通知。那么在juc下有没有可以精确通知的实现方式呢?答案:==有==
先看api中的Condition接口
来段示例,让三个线程交替执行创建、获取、提交的动作
package com.example.springboot.test.juc.notice;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* condition实现精准通知
* 通知三部曲:
* 1、条件判断
* 2、逻辑执行
* 3、通知
*/
public class Notice {
public static void main(String[] args) {
Flow flow = new Flow();
new Thread(()->{
for (int i = 0; i < 10; i++) {
flow.create();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
flow.get();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
flow.submit();
}
},"C").start();
}
}
class Flow{
String flag = "A";
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void create(){
lock.lock();
try {
while (!"A".equals(flag)) {
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "创建");
flag = "B";
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void get(){
lock.lock();
try {
while(!"B".equals(flag)){
condition2.await();
}
System.out.println(Thread.currentThread().getName() + "获取");
flag = "C";
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void submit(){
lock.lock();
try {
while (!"C".equals(flag)) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + "提交");
flag = "A";
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
8锁问题知识点
1、被 synchronized 修饰的方式,锁的对象是方法的调用者。
2、被 static 修饰,锁的对象就是 Class模板对象,这个则全局唯一!
//锁的是唯一的class模板
synchronized(Hello.class){
}
//锁的是new出来的当前示例对象
synchronized(this){
}
不安全的集合类
list
我们经常用到的ArrayList非线程安全的,在并发场景下进行add操作会出现并发修改异常(ConcurrentModificationException),怎么避免异常?
1、使用安全集合vector
List list = new Vector<>();
2、使用工具类中的转线程安全的方法
List list = Collections.synchronizedList(new ArrayList<>())
3、使用juc下的copyonwrite
List list = new CopyOnWriteArrayList<>()
这里的copyonwrite是一种思想,写数据的时候利用拷贝的副本来执行,然后移动指针指向新的数据
set
同样的经常使用到的HashSet也是非线程安全的,HashSet本质是HashMap,存放的为HashMap中的key,所以是不重复的。
那么怎么实现线程安全呢?
1、使用工具类的转线程安全的方法
Set set = Collections.synchronizedSet(new HashSet<>())
2、copyonwrite思想
Set set = new CopyOnWriteArraySet()
map
HashMap也是非线程安全的,如果使用线程安全的map可以使用concurrentHashMap,如下:
Map map = new ConcurrentHashMap<>();
探索HashMap
HashMap本质是数组 + 链表、红黑树的数据结构
初始化容量为2的4次方,即16,加载因子为0.75f。
put数据时,先根据key的hash值判断数据存放哪个数组,如果数组中有值,则插入数组的链表中,当链表长度大于等于8时,转化为红黑树。
读写锁
并发包下的读写锁ReadWriteLock,有两个方法readLock()和writeLock(),返回类型为Lock,实现类有ReentrantReadWriteLock。
使用方法如下,lock和unlock同样需要成对出现,防止死锁。
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
//do something
lock.readLock().unlock();
lock.writeLock().lock();
//do something
lock.writeLock().unlock();
阻塞队列
阻塞队列顾名思义,使用时会阻塞,典型的应用为生产者消费者模式,juc下接口类为BlockingQueue,一下为实现类
其中有一个特殊的队列SynchronousQueue,此队列只存一个元素,现存现取。
阻塞队列在什么情况下会阻塞呢?
1、当队列满了,继续往队列中添加时。
2、当队列为空时,要从队列取值时。
使用方法
方法 | 第一组会抛出异常 | 返回一个布尔值,不会抛出异常 | 延时等待 | 一直等待 |
---|---|---|---|---|
插入 | add() | offer(e) | offer(e,time) | put() |
取出 | remove() | poll() | poll(time) | take() |
检查 | element() | peek() | - | - |
函数式接口
juc下定义了四个函数式接口,分别为:
==Function== : 有一个输入参数有一个输出参数
==Consumer==:有一个输入参数,没有输出参数
==Supplier==:没有输入参数,只有输出参数
==Predicate==:有一个输入参数,判断是否正确!
异步回退
CompletableFuture类可以实现异步回退的功能,该类实现了Future接口。
实现方式如下:
package com.example.springboot.test.juc.async;
import java.util.concurrent.CompletableFuture;
public class ComplateFutureTest {
public static void main(String[] args) throws Exception {
CompletableFuture completableFuture1 = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName() + "异步执行完成");
});
System.out.println("无返回值的" + completableFuture1.get());
CompletableFuture completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + "异步执行返回结果");
// int i = 5/0;
return 1024;
});
System.out.println(completableFuture.whenComplete((t, r) -> {
System.out.println("t--->" + t + "\r\n" + "r--->" + r);
}).exceptionally((s) -> {
return 404;
}).get());
}
}
线程池
线程池的默认创建方式不支持使用,看代码可以发现他们最终都使用了ThreadPoolExecutor自定义创建
package com.example.springboot.test.juc;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolTest {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,//常驻核心线程数
6,//线程池能够容纳的同时执行的最大线程数
3L,//线程空闲时间,达到指定值时会销毁线程,直到剩下core个
TimeUnit.SECONDS,//时间单位,配合keepAliveTime使用
new LinkedBlockingQueue(3),//缓存队列,当线程大于max时会进入阻塞队列
Executors.defaultThreadFactory(),//线程工厂,用于生产一组相同任务的线程
new ThreadPoolExecutor.CallerRunsPolicy());//拒绝策略,当线程超过core+max+queue数量时会用到
for (int i = 0; i < 20; i++) {
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName() + "正在执行任务");
});
}
threadPoolExecutor.shutdown();
}
}
其中拒绝策略有四种,分别为:
•==AbortPolicy(默认)==:丢弃任务并抛出RejectedExecutionException异常。
• ==DiscardPolicy== :丢弃任务,但是不抛出异常,这是不推荐的做法。
• ==DiscardOldestPolicy== :抛弃队列中等待最久的任务,然后把当前任务加入队列中。
• ==CallerRunsPolicy== :调用任务的run()方法绕过线程池直接执行,谁调用的谁来处理。
juc的辅助类
juc下有三个辅助类,分别是CountDownLatch、CyclicBarrier、Semaphore。
下面分别介绍三个辅助类的作用。
CountDownLatch
CountDownLatch是一个减法计数器,有两个常用方法countDown()和await()。countDown()每调用一次计数器会减一,await方法会阻塞等待所有的线程执行完成,它不要求调用countDown线程等待计数到达零之前继续,它只是阻止任何线程通过await ,直到所有线程可以通过。
package com.example.springboot.test.juc.assist;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "执行完成");
countDownLatch.countDown();
}).start();
}
countDownLatch.await(5, TimeUnit.SECONDS);
System.out.println("全部执行完成");
}
}
CyclicBarrier
CyclicBarrier译为篱栅,围栏。它允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。有两个await()方法,如果当前线程不是最后一个线程,那么它被禁用以进行线程调度,并且处于休眠状态,直到所有的线程都调用或者调用reset()方法,用法如下:
package com.example.springboot.test.juc.assist;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, ()->{
System.out.println("队伍集合成功");
});
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(()->{
System.out.println(num + "对集合完毕");
try {
cyclicBarrier.await(2, TimeUnit.SECONDS);
} catch (Exception e){
}
}).start();
}
}
}
Semaphore
Semaphore译为信号量,信号量维护一组许可证,调用acquire()会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。用法如下:
package com.example.springboot.test.juc.assist;
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "来了");
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "走了");
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
JMM
JMM意思是Java memory model,Java内存模型,它是一种理论,和线程安全相关。
所有的线程是如何工作的:
八大操作:
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
-
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则: 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存 (可见)
不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
对一个变量进行unlock操作之前,必须把此变量同步回主内存
volatile
volatile的特性:
- 保证可见性,即不同线程对这个变量进行操作时的可见性,一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
可见性验证:
package com.example.springboot.test.juc.jmm;
import java.util.concurrent.TimeUnit;
public class VolateTest {
volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println("让线程开始运行");
while (flag){
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("此时flag为:" + flag);
}
}
以上代码在flag不加volatile时,程序会一直在while中循环,因为主线程修改了flag的值,对于线程A是不可见的,所以线程A会一直循环,加上volatile之后,主线程对flag变量的操作对于线程A就可见了,会停止循环。
不保证原子性验证:
package com.example.springboot.test.juc.jmm;
import java.util.concurrent.CountDownLatch;
public class VolatileTest2 {
volatile static int num = 0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int m = 0; m < 5; m++) {
new Thread(()->{
for (int i = 0; i < 20; i++) {
add();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("当前num的值为:" + num);
}
private /**synchronized*/ static void add(){
num++;
}
}
num加与不加volatile,最终num的值都不是期望的100,如果在add方法上
加上synchronized修饰,则一定是100。
那么有没有办法在不使用synchronized时也能保证其正确性?
juc提供了一系列原子类,比较常用的有AtomicInteger。
如下:
package com.example.springboot.test.juc.jmm;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileTest2 {
// volatile static int num = 0;
static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int m = 0; m < 5; m++) {
new Thread(()->{
for (int i = 0; i < 20; i++) {
add();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("当前num的值为:" + num);
}
private static void add(){
num.getAndIncrement();
}
}
此时add方法不使用synchronized修饰也能得到期望的100值。
禁止指令重排
这个无法用代码验证,属于理论知识。
指令重排是指代码在实际运行时不一定是按照你写的顺序执行的。
源代码->编译器(优化重排)->指令并行重排-> 内存系统的重排-> 最终执行的!
代码经过编译器,再经过重排,最后是最终的执行结果。单线程也不能避免指令重排。
如程序:
int a = 0;//第一步
int b = 1;//第二步
a = a + 1;//第三步
int c = a + b;//第四步
经过重排后,程序不一定按照1234步执行,可能是1234、1324、2134、2314、3124、3214。
volatile使用内存屏障(Memory Barrier)来禁止指令重排,内存屏障有两个作用:
- 保证特定的执行顺序!
- 保证某些变量的内存可见性 (votatile就是用它这个特性来实现的)
单例模式
饿汉式模式
package com.example.springboot.test.juc.single;
/**
* 饿汉式模式
*/
public class HungryMan {
//构造器私有
private HungryMan(){
}
private static final HungryMan hungryMan = new HungryMan();
public static HungryMan getSingleInstance(){
return hungryMan;
}
}
class HungryTest{
public static void main(String[] args) {
HungryMan hungryMan = HungryMan.getSingleInstance();
HungryMan hungryMan2 = HungryMan.getSingleInstance();
System.out.println(hungryMan.hashCode());
System.out.println(hungryMan2.hashCode());
}
}
懒汉式
package com.example.springboot.test.juc.single;
/**
* 懒汉式模式
*/
public class LazyMan {
private LazyMan(){}
private volatile static LazyMan lazyMan = null;
public static LazyMan getSingleInstance(){
if(lazyMan == null){
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
class LazyTest{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(LazyMan.getSingleInstance().hashCode());
}).start();
}
}
}
这种懒汉式会被反射式破坏,不安全
package com.example.springboot.test.juc.single;
import java.lang.reflect.Constructor;
/**
* 懒汉式模式
*/
public class LazyMan {
private LazyMan(){
System.out.println("构造器");
}
private volatile static LazyMan lazyMan = null;
public static LazyMan getSingleInstance(){
if(lazyMan == null){
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
class LazyTest{
public static void main(String[] args) throws Exception {
// for (int i = 0; i < 10; i++) {
// new Thread(()->{
// System.out.println(LazyMan.getSingleInstance().hashCode());
// }).start();
// }
Constructor constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan lazyMan = (LazyMan) constructor.newInstance();
LazyMan lazyMan2 = (LazyMan) constructor.newInstance();
LazyMan lazyMan1 = LazyMan.getSingleInstance();
System.out.println(lazyMan.hashCode());
System.out.println(lazyMan1.hashCode());
System.out.println(lazyMan2.hashCode());
}
}
使用枚举类:
package com.example.springboot.test.juc.single;
import java.lang.reflect.Constructor;
public enum EnumSingle {
SINGLEINSTANCE;
public EnumSingle getSingleInstance(){
return SINGLEINSTANCE;
}
}
class EnumTest {
public static void main(String[] args) throws Exception {
Constructor constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingle enumSingle = constructor.newInstance();
System.out.println(enumSingle.hashCode());
}
}
在jdk没被修改的情况下,枚举类是安全的单例模式。
CAS
package com.zeroun.test.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class CasTest {
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println(atomicInteger.compareAndSet(0, 5));
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicInteger.compareAndSet(6, 10));
}).start();
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "----" + atomicInteger.compareAndSet(5, 6));
}
}
CAS缺点:
缺点:
1、循环(自旋)开销很大!
2、内存操作,每次只能保证一个共享变量的原子性!
3、出现ABA 问题。
那么怎么解决ABA问题呢?加上版本号或者时间戳,原子引用。
package com.example.springboot.test.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtoReferTest {
public static void main(String[] args) {
AtomicStampedReference reference = new AtomicStampedReference(1,1);
new Thread(()->{
int stamp = reference.getStamp();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "当前stamp的值:" + stamp);
System.out.println(reference.compareAndSet(1, 5, reference.getStamp(), reference.getStamp() + 1));
System.out.println(reference.compareAndSet(5, 10, reference.getStamp(), reference.getStamp() + 1));
},"A").start();
new Thread(()->{
int stamp = reference.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "当前stamp的值:" + stamp);
// System.out.println(reference.compareAndSet(10, 15, reference.getStamp(), reference.getStamp() + 1));
System.out.println(reference.compareAndSet(10, 15, stamp, stamp + 1));
},"B").start();
}
}
线程A和B先获取到版本号,然后A执行了更新,此时B也按照此版本更新,虽然旧值和期望值正确,但是版本号不对,所以更新失败。
死锁
死锁发生在互抢资源时,当一个线程拿到锁A,去拿锁B,而另一个线程拿到锁B,去拿锁A时,就发生互相抱着锁去抢别人的锁,就发生了死锁。
package com.example.springboot.test.juc.deadLock;
import java.util.concurrent.TimeUnit;
public class DeadLockTest {
public static void main(String[] args) {
String lockA = "a";
String lockB = "b";
new Thread(new test(lockA,lockB)).start();
new Thread(new test(lockB,lockA)).start();
}
}
class test implements Runnable{
String lockA;
String lockB;
public test(String lockA,String lockB){
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "拿到了锁" + lockA + ",想拿到锁" + lockB);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "想拿到锁" + lockB);
}
}
}
}
当死锁发生时,使用jprofiler工具进行分析,idea安装插件,并配置启动,就能分析当前程序发生的死锁。