JUC并发编程总结
前言
学习自b站狂神说
提示:以下是本篇文章正文内容,下面案例可供参考
一、进程和线程
1. 进程和线程
-
进程:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个基本单位。
-
线程:
-
- 线程是进程的一个实体,是进程中执行运算的最小单位。
-
- java默认有两个线程:main 和 GC(垃圾回收)
java真的可以开启线程吗? 不可以。
java调用本地方法开启线程,底层是C++。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
//本地方法,底层C++,java无法直接操作硬件(java是运行在虚拟机上的)
private native void start0();
2. 并发编程:
-
并发:(多个线程操作同一个资源)
CPU一核,模拟出来多条线程。 -
并行:(多个人一起行走)
CPU多核,多个线程可以同时执行。
并发编程的本质:充分利用CPU的资源(所有公司都看重)
二、Synchronized和Lock
1. Synchronized
以卖票为例,多个线程买票,需要排队获取锁,保证线程安全。代码如下:
public class SaleTestDemo {
public static void main(String[] args) {
//并发,多个线程操作同一个资源类
Ticket ticket = new Ticket();
//Runnable 函数式接口 jdk1.8 Lambda 表达式
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"C").start();
}
}
//资源类 OOP
class Ticket {
//属性 方法
private int number = 30;
//卖票的方式
//synchronized 本质: 队列 锁
public synchronized void sale(){
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "票,剩余:" + number);
}
}
}
2. Lock
代码如下:
class Ticket2 {
//属性 方法
private int number = 30;
Lock lock = new ReentrantLock(true); //默认:false 非公平锁
//卖票的方式
public void sale(){
lock.lock(); //加锁
lock.tryLock(); //尝试获取锁
try {
//业务代码
}
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock(); //解锁
}
}
}
new ReentrantLock(true); //默认:false 非公平锁
// 可重入锁(普通锁)
new ReentrantLock
// 读锁
ReentrantReadWriteLock.ReadLock
// 写锁
ReentrantReadWriteLock.WriteLock
3. Sychronized和Lock的区别
三、生产者消费者问题
线程通信问题:生产者消费者问题。生产线程操作完毕后通知消费线程去消费,不满足生产条件时 生产线程处于等待状态。消费线程消费完成后,通知生产线程。
虚假唤醒:线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待
class Data{
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
if (number != 0) {
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if (number != 1) {
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
this.notifyAll();
}
}
一个生产者,一个消费者情况下。A + 1 ,B - 1 。使用if 和synchronized 没有问题。
但是,两个生产者,两个消费者时,就会出现大于1,或者小于0的情况,还可能造成死锁。
1. 防止虚假唤醒
解决方法如下:
class Data2{
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//+1
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
condition.await(); //等待
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll(); //唤醒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
condition.await(); //等待
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll(); //唤醒
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Condition condition = lock.newCondition();
condition.await(); //等待
condition.signalAll(); //唤醒
使用while 和 lock 锁,在while条件满足时,等待唤醒,执行完后在用signalAll() 唤醒其他线程。
2. Condition 实现精准唤醒
如果要专门唤醒某个线程,则需要线程对应的Condition 对象调用 signal() 方法。
代码如下:
private Condition condition1 = lock.newCondition(); // A
private Condition condition2 = lock.newCondition(); // B
private Condition condition3 = lock.newCondition(); // C
private Condition condition4 = lock.newCondition(); // D
A线程业务执行完成后,调用condition2.signal(); B线程被唤醒。
四、八锁现象
1. 三个方法的执行顺序:
public class TestDemo02 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone = new Phone2();
new Thread(()->{ phone.sendEmail(); },"A").start();
//sleep 1s
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone.call();},"B").start();
new Thread(()->{ phone.hello();},"C").start();
}
}
class Phone2{
public synchronized void sendEmail() {
try { //延迟4S
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public synchronized void call() {
System.out.println("打电话");
}
public void hello() { //不加锁
System.out.println("hello");
}
}
hello
发邮件
打电话
- 原因: synchronized 锁的是对象,hello()方法未加锁,所以不需要等待。
2. static synchronized 和 synchronized 的区别
class Phone3{
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public synchronized void call() {
System.out.println("打电话");
}
}
打电话
发邮件
- 原因: static synchronized 锁的是类(或者说类模板),而synchronized 锁的是对象。
3. 不同对象调用static synchronized 方法。
public class TestDemo03 {
public static void main(String[] args) throws InterruptedException {
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(()->{ phone1.sendEmail(); },"A").start();
//sleep 1s
TimeUnit.SECONDS.sleep(1);
new Thread(()->{ phone2.call();},"B").start();
}
}
class Phone3{
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发邮件");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
发邮件
打电话
- 原因: static synchronized锁的是类模板,不管多少个对象调用,都是同一把锁。
五、集合类不安全
1.List不安全
//并发下,ArrayList不安全,写入操作时,会发生并发修改异常
//java.util.ConcurrentModificationException
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
-
解决方案:
* 1. List<String> list = new Vector<>(); * 2. List<String> list = Collections.synchronizedList(new ArrayList<>()); * 3. JUC List<String> list = new CopyOnWriteArrayList<>();
Vector的原理是synchronized锁。
Collections.synchronizedList(new ArrayList<>()):把ArrayList变安全。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
CopyOnWrite: 写入时复制: 在写入前先复制一份,将元素插入到复制的list中,然后再将插入后的list写入到原来的list中。 在写入时避免覆盖,造成数据问题。
2. Set 不安全
HashSet 底层 :Hashmap (key是无法重复的)
- Collections.synchronizedSet();
- 使用CopyOnWriteArraySet。
3. Map不安全(重点)
- Collections.synchronizedMap();
- Map<String,String> map = new ConcurrentHashMap<>();
ConcurrentHashMap的原理:
1.7版本: 使用锁分段技术。
数据结构:ReentrantLock + Segment数组 + HashEntry 数组, 每个HashEntry的元素又是一个链表。
- 将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据。
- Segment分段锁,Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度为Segment的个数,可以通过构造函数指定,数组扩容不会影响其他的Segment。
1.8版本 CAS操作 + synchronized锁 + Node + 红黑树。
六、三大常用辅助类
1. CountDownLatch
- 用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。
- 这种现象只出现一次——计数无法被重置。
代码如下:
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总计数是6 必须要执行的任务的时候使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "go out");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await(); //等待计数器归0,然后再向下执行
System.out.println("close door");
}
}
2. CyclicBarrier
- 它允许一组线程互相等待,直到到达某个公共屏障点 。
代码如下:
public class CyclicBarrierDemo {
public static void main(String[] args) {
//召唤神龙的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙");
});
for (int i = 0; i < 7; i++) {
final int temp = i;
int finalI = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠");
try {
cyclicBarrier.await(); //等待 加法计数器达到7
System.out.println("释放线程" + finalI);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
Thread-1收集1个龙珠
Thread-0收集0个龙珠
Thread-6收集6个龙珠
Thread-5收集5个龙珠
Thread-2收集2个龙珠
Thread-4收集4个龙珠
Thread-3收集3个龙珠
召唤神龙
释放线程5
释放线程2
释放线程4
...
设定值为7,当等待的线程达到7个时,执行特定方法,然后释放7个线程。
3. Semaphore
一个计数信号量。
acquire():从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。
代码如下:
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量 停车位 限流
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
//acquire() 得到车位
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
semaphore.acquire(); 获得,如果已经满了,等待,等到被释放为止。
semaphore.release(); 释放,会将当前的信号量释放+1,然后唤醒等待的线程。
作用:
- 多个共享资源互斥的使用!
- 并发限流,控制最大的线程数。
七、读写锁
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写入
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp + "",temp + "");
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp + "");
},String.valueOf(i)).start();
}}}
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
//存、写
public void put(String key,Object value) {
System.out.println(Thread.currentThread().getName() + "写入");
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入OK");
}
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读取");
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
}
}
3写入
4写入
4写入OK
未使用读写锁时,在线程3 写入的过程中,有其他线程插入。存在线程安全问题。
使用读写锁的代码如下:put 方法,get一样
//读写锁 更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//存、写入的时候,只希望同时只有一个线程写
public void put(String key,Object value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
readWriteLock.writeLock().lock(); 写锁
readWriteLock.readLock().lock(); 读锁
- 独占锁(写锁):一次只能被一个线程占有
- 共享锁(读锁):多个线程可以同时占有
- 读-读: 可以共存
- 读-写: 不能共存
- 写-写: 不能共存
八、阻塞队列
BlockingQueue
三个主要的实现类
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
什么情况会使用 阻塞队列:多线程并发处理,线程池。
四组API
SynchronizedQueue同步队列
- 同步队列的特点:相当于一个容量为1的队列。
- 放入一个元素后,必须取出后,才能再次放入。
public static void main(String[] args) {
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + ": put 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName() + ": put 2");
synchronousQueue.put("2");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"s1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + ": take" + synchronousQueue.take());
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + ": take" + synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"s1").start();
}
s1: put 1
s1: take1
s1: put 2
s1: take2
结论: put 执行完后,必须等待take()方法执行,才能继续put。
- SynchronousQueue 的take是使用了lock锁保证线程安全的。
九、线程池(重要)
使用线程池的好处:
- 降低资源的消耗
- 提高响应的速度
- 方便管理
三大方法
ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单个线程
ExecutorService threadPool = Executors.newFixedThreadPool(5); //创建一个固定数量的线程池
ExecutorService threadPool = Executors.newCachedThreadPool(); //可伸缩的
七大参数
ExecutorService threadPool = new ThreadPoolExecutor(
2, // 线程池核心线程大小
5, //线程池最大线程数量
3, //空闲线程存活时间
TimeUnit.SECONDS, // 空闲线程存活时间单位
new LinkedBlockingDeque<>(3), //等待区空间 3
Executors.defaultThreadFactory(), //线程工厂
new ThreadPoolExecutor.DiscardOldestPolicy()//拒绝策略
);
- 对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。
重点: 线程池使用完,需要关闭线程池。 threadPool.shutdown();
四大拒绝策略
拒绝策略是第七个参数。
* 四个拒绝策略:
* 1. new ThreadPoolExecutor.AbortPolicy() //线程池和等待空间都满了,还有线程进来,不处理,抛出异常
*
* 2. new ThreadPoolExecutor.CallerRunsPolicy() //哪来的 回哪里去。
*
* 3. new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常,
*
* 4. new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早进来的线程竞争。
- 选择maximunPoolSize的大小
- CPU密集型
几核CPU,最大线程就是几,可以保持CPU的效率最高(CPU的最大线程数)
//获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
- IO密集型
判断你的程序中十分耗IO的线程数。一般设置为2倍。
假设:1个程序,15个大型任务 IO十分占用资源
十、四大函数式接口
1. 函数式接口
- 只有一个方法的接口,一个参数,一个返回值
@FunctionInterface
函数式接口:只有一个方法的接口,一个参数,一个返回值
public static void main(String[] args) {
Function function = new Function<String,String>(){
@Override
public String apply(String o) {
return o;
}
};
//Lambda表达式简化
Function<String,String> function1 = (str)->{
return str;
};
}
输出:abc
2. 断定型接口
- 有一个输入参数,返回值只能是布尔值。
断定型接口:有一个输入参数,返回值只能是布尔值。
public static void main(String[] args) {
//判断字符是否为空
// Predicate<String> predicate = new Predicate<>() {
// @Override
// public boolean test(String s) {
// return s.isEmpty();
// }
// };
Predicate<String> predicate = (str)->{return str.isEmpty();};
System.out.println(predicate.test(""));
}
输出:true
3. 消费型接口
- 只有输入,没有返回值
public static void main(String[] args) {
Consumer<String> consumer = new Consumer() {
@Override
public void accept(Object o) {
System.out.println(o);
}
};
//Lambda表达式简化
Consumer<String> consumer1 = (str)->{System.out.println(str);};
consumer.accept("sad");
consumer1.accept("hello");
}
输出:
sad
hello
##4. 供给型接口
- 没有参数,只有返回值
public static void main(String[] args) {
Supplier supplier = new Supplier() {
@Override
public Object get() {
System.out.println("get()");
return 1024;
}
};
//Lambda表达式简化
Supplier supplier1 = ()->{
System.out.println("get");
return 100;
};
supplier.get();
supplier1.get();
}
输出:
get()
get
十一、单例模式
- 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
1.饿汉式 单例模式
- 实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。
- 好处: 没有线程安全问题。
- 坏处: 浪费内存空间。
public class Hungry {
//可能会浪费空间
private byte[] data1 = new byte[1024];
//构造方法
private Hungry() {};
//实例化对象
private final static Hungry HUNGRY = new Hungry();
//获取对象
public static Hungry getInstance() {
return HUNGRY;
}
public static void main(String[] args) {
System.out.println(Hungry.getInstance(););
}
}
2.懒汉式 单例模式
- 用的时候才去检查有没有实例,如果有则返回,没有则新建。
- 有线程安全和线程不安全两种写法,区别就是synchronized关键字。
非线程安全:
public class Lazy {
private Lazy () {}
private static Lazy LAZY;
public static Lazy getInstance() {
if (LAZY == null) {
LAZY = new Lazy();
}
return LAZY;
}
public static void main(String[] args) {
System.out.println(Lazy.getInstance());
}
}
原因: 由于new 操作不是一个原子性操作,可能会发生指令重排。
new 指令:
- 分配内存空间
- 执行构造方法,初始化对象
- 把这个对象执行这个空间
- 多线程
3. 双重检测模式(懒汉式)
public class DoubleLock {
//volatile :防止指令重排
private volatile static DoubleLock LazyMan2;
public DoubleLock (){}
//双重检测模式
public static DoubleLock getInstance() {
if (LazyMan2 == null) {
synchronized (DoubleLock.class) {
if (LazyMan2 == null) {
LazyMan2 = new DoubleLock();
}
}
}
return LazyMan2;
}
}
- 使用synchronized 锁住类,就等于将静态对象锁住,同时用volatile防止指令重排。
- 反射获取对象,破坏单例模式。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//反射获取对象
Constructor<DoubleLock> declaredConstructor = DoubleLock.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
DoubleLock doubleLock = declaredConstructor.newInstance();
DoubleLock instance = DoubleLock.getInstance();
System.out.println(doubleLock);
System.out.println(instance);
}
com.sparks.single.DoubleLock@7c30a502
com.sparks.single.DoubleLock@49e4cb85
4. 静态内部类
//静态内部类 单例
public class Holder {
public Holder(){};
public static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass {
private static final Holder HOLDER = new Holder();
}
}
5. 枚举类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
除了枚举类,其他的单例模式,都会被反射破坏。
十二、CAS
1. 什么是CAS?
- CAS(compare and swap)是所有原子类的底层原理,乐观锁主要采用CAS算法。
- CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
思想: 获取当前变量最新值A(预期值),然后进行CAS操作。此时如果内存中变量的值V(内存值V)等于预期值A,说明没有被其他线程修改过,我就把变量值改成B(更新值);如果不是A,便不再修改。
应用场景: -
- 乐观锁
-
- 并发容器,例如ConcurrentHashMap
-
- 原子类
Unsafe类: Unsafe是CAS核心类。Java无法直接访问底层操作系统,而是通过本地方法访问。JDK中Unsafe类,底层调用本地方法,提供硬件级别原子操作。
2. 存在的问题
1. ABA问题: CAS在操作值的时候会检测值是否发生变化。如果没有变化,则进行更新。但是,如果一个值由A变成了B,又由B变回了A,这个时候CAS检查就会判断为值没有发生变化,但实际上已经发生了变化。
解决方法: 在变量前面追加版本号,每次变量更新的时候,将版本号+1,这时候A->B->A就变成1A->2B->3A,CAS检查值的时候就发现值发生了变化。
2. 循环时间长开销大
- 高并发场景下,如果线程一直无法进行CAS操作,内部是dowhile死循环,会一直自旋,消耗CPU。