juc小结

本文详细解析了线程的6个状态,比较Synchronized和Lock的区别,包括锁的判断、释放和公平性,以及通过例子展示了生产者消费者问题的JUC版本。还介绍了线程池、Volatile、单例模式和并发工具如Semaphore、CyclicBarrier。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程的6个状态:

1.初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2.运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。

Synchronized 和 Lock区别

Synchronized 是java内置的关键字;Lock是类
Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁
Synchronized 会自动释放锁,Lock必须要手动释放锁,不释放会死锁
Synchronized 如果线程1(获得锁,阻塞)线程2(等待),lock不一定会等待下去
Synchronized 可重入锁,不可以中断,是非公平的,Lock 可重入锁,可判断锁,非公平(可以自己设置)
Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码
注意:Synchronized锁的是调用者对象或class,集群环境下synchronized会失效。

生产者和消费者问题

Synchronized版本:
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(()->{for(int i=0;i<10;i++) {
            try {
                data.increment();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        },"A").start();
        new Thread(()->{for(int i=0;i<10;i++) {
            try {
                data.decrement();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }},"B").start();
    }
}
class Data{
    //数字  资源类
    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        while(number!=0){   //如果这里使用if会存在虚假唤醒
            //等待操作
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程 我+1完毕了
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        while(number==0){  //如果这里使用if会存在虚假唤醒
            //等待操作
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        //通知其他线程  我-1完毕了
        this.notifyAll();
    }
}
JUC版本的生产者和消费者问题

juc版通过Condition还可以精准的通知和唤醒的指定的线程!

/**
 * A 执行完 调用B
 * B 执行完 调用C
 * C 执行完 调用A
 */
public class Test {
    public static void main(String[] args) {
        Data3 data3 = new Data3();
        new Thread(()->{
            for(int i=0;i<10;i++){
                data3.printA();
            }
        },"A").start();
        new Thread(()->{
            for(int i=0;i<10;i++){
                data3.printB();
            }
        },"B").start();
        new Thread(()->{
            for(int i=0;i<10;i++){
                data3.printC();
            }
        },"C").start();
    }
}

class Data3{
    //资源类
    private Lock lock=new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int number = 1; //1A 2B 3C

    public void printA(){
        lock.lock();
        try {
            //业务 判断 -> 执行 -> 通知
            while(number!=1){
                //等待
                condition1.await();
            }
            //操作
            System.out.println(Thread.currentThread().getName()+",AAAAA");
            //唤醒指定的线程
            number=2;
            condition2.signal(); // 唤醒2

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB(){
        lock.lock();
        try {
            //业务 判断 -> 执行 -> 通知
            while (number!=2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+",BBBBB");
            //唤醒3
            number=3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            //业务 判断 -> 执行 -> 通知
            while(number!=3){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+",CCCCC");
            //唤醒1
            number=1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

常用辅助类:

这里只介绍最常用的三种,另外两种可以 点击查看
Semaphore是经典的并发工具。
CountDownLatch是一个非常简单但非常常见的实用程序,用于阻塞直到给定数量的信号、事件或条件成立。
ACyclicBarrier是可重置的多路同步点,在某些并行编程风格中很有用。
APhaser提供了一种更灵活的屏障形式,可用于控制多个线程之间的分阶段计算。
AnExchanger允许两个线程在一个集合点交换对象,并且在多个管道设计中很有用。

CountDownLatch

减法计数器: 等待计数器为0,就唤醒,再继续向下运行

public static void main(String[] args) throws InterruptedException {
        //总数是6
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" Go out");
                countDownLatch.countDown(); //每个线程都数量-1
            },String.valueOf(i)).start();
        }
        countDownLatch.await();  //等待计数器归零  然后向下执行
        System.out.println("close door");
    }
CyclickBarrier

加法计数器

public static void main(String[] args) {
        //主线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("召唤神龙~");
        });
        for (int i = 1; i <= 7; i++) {
            //子线程
            int finalI = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 收集了第 {"+ finalI+"} 颗龙珠");
                try {
                    cyclicBarrier.await(); //加法计数 等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
Semaphore

作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!
semaphore.acquire()得到资源,如果资源已经使用完了,就等待资源释放后再进行使用!
semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!
代码案例(抢车位:3个车位 6辆车):

public static void main(String[] args) {
    //停车位为3个
    Semaphore semaphore = new Semaphore(3);
    for (int i = 1; i <= 6; i++) {
        int finalI = i;
        new Thread(() -> {
            try {
                semaphore.acquire(); //得到
                //抢到车位
                System.out.println(Thread.currentThread().getName() + " 抢到了车位{" + finalI + "}");
                TimeUnit.SECONDS.sleep(2); //停车2s
                System.out.println(Thread.currentThread().getName() + " 离开车位");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();//释放
            }
        }, String.valueOf(i)).start();
    }
}

读写锁:

官方文档 点击查看
先对于不加锁的情况:
如果我们做一个我们自己的cache缓存。分别有写入操作、读取操作;
我们采用五个线程去写入,使用十个线程去读取。
我们来看一下这个的效果,如果我们不加锁的情况!


public class Test {
    public static void main(String[] args) {
        MyCacheeadWriteLock mycache = new MyCacheeadWriteLock();
        //开启5个线程 写入数据
        for (int i = 1; i <=5 ; i++) {
            int finalI = i;
            new Thread(()->{
                mycache.put(String.valueOf(finalI),String.valueOf(finalI));
            }).start();
        }
        //开启10个线程去读取数据
        for (int i = 1; i <=10 ; i++) {
            int finalI = i;
            new Thread(()->{
                String o = mycache.get(String.valueOf(finalI));
            }).start();
        }
    }
}

class MyCacheeadWriteLock{
    private volatile Map<String,String> map=new HashMap<>();

    //使用读写锁
    private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
    //普通锁
    private Lock lock=new ReentrantLock();

    public void put(String key,String value){
        //写入
        System.out.println(Thread.currentThread().getName()+" 线程 开始写入");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName()+" 线程 写入OK");
    }

    public String get(String key){
        //得到
        System.out.println(Thread.currentThread().getName()+" 线程 开始读取");
        String o = map.get(key);
        System.out.println(Thread.currentThread().getName()+" 线程 读取OK");
        return o;
    }
}

运行结果:
在这里插入图片描述
使用读写锁运行结果:


public class Test {
    public static void main(String[] args) {
        MyCacheeadWriteLock mycache = new MyCacheeadWriteLock();
        //开启5个线程 写入数据
        for (int i = 1; i <=5 ; i++) {
            int finalI = i;
            new Thread(()->{
                mycache.put(String.valueOf(finalI),String.valueOf(finalI));
            }).start();
        }
        //开启10个线程去读取数据
        for (int i = 1; i <=10 ; i++) {
            int finalI = i;
            new Thread(()->{
                String o = mycache.get(String.valueOf(finalI));
            }).start();
        }
    }
}

class MyCacheeadWriteLock{
    private volatile Map<String,String> map=new HashMap<>();
    //使用读写锁
    private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
    //普通锁
    private Lock lock=new ReentrantLock();
    public void put(String key,String value){
        //加锁
        readWriteLock.writeLock().lock();
        try {
            //写入
            //业务流程
            System.out.println(Thread.currentThread().getName()+" 线程 开始写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName()+" 线程 写入OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock(); //解锁
        }
    }
    public String get(String key){
        //加锁
        String o="";
        readWriteLock.readLock().lock();
        try {
            //得到
            System.out.println(Thread.currentThread().getName()+" 线程 开始读取");
            o = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 线程 读取OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
        return o;
    }
}

运行结果如下:
在这里插入图片描述
在这里插入图片描述

线程池

线程池:三大方法、7大参数、4种拒绝策略

线程池的好处

1.降低资源消耗
通过重复利用已创建的线程 从而降低线程的创建和销毁造成的消耗
2.提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行
3.方便管理
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一的分配,调优和监控

线程池创建

Executors类中提供(三大方法)

阿里巴巴开发手册明确规定:不推荐使用原因Executors类创建线程
newSingleThreadExecutor(),newFixedThreadPool(),newCachedThreadPool()可用来创建线程池。而这三个方法皆调用了ThreadPoolExecutor类中的构造方法
在这里插入图片描述

ThreadPoolExecutor类

七大参数

public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue, //等待提交到线程池的任务队列
                              ThreadFactory threadFactory, //创建线程的方式,一般默认
                              RejectedExecutionHandler handler  //拒绝策略) {
 this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
}
拒绝策略

AbortPolicy: 默认的拒绝策略,如果maximumPoolSize+workQuery.size()已经饱和,就丢掉超额的部分,并抛出RejectedExcutionException异常
DiscardPolicy: 如果饱和就丢掉超额的部分,但不抛出异常
DIscardOldestPolicy: 如果队列已经饱和就删除最早进入队列的任务,将新任务追加到队尾
CallerRunPolicy: 将任务分给调用线程来执行
在这里插入图片描述

代码示例:

 public static void main(String[] args) throws InterruptedException, IOException {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, //线程池的核心线程数量
                3,//线程池的最大线程数
                5, //当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                TimeUnit.SECONDS, //时间单位
                new LinkedBlockingDeque<>(3), //等待提交到线程池的任务队列
                Executors.defaultThreadFactory(),   //创建线程的方式,一般默认
                new ThreadPoolExecutor.AbortPolicy());//拒绝策略
        try {
            for (int i = 1; i <= 10; i++) {
                poolExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //线程用完 程序结束 关闭线程池
            poolExecutor.shutdown();
        }
    }

核心线程数为2个,队列长度为3个,当来了第6个人的时候,则判断是否达到最大线程数。如果到达最大线程数则按照拒绝策略执行

最大线程如何定义?
     1.cpu密集型: 电脑的核数是几核就选择几(maximumPollSize = Runtime.getRuntime().availableProcessors(););
     2.IO密集型 maximumPollSize > 判断程序中十分占IO的线程

Volatile

1.保证可见性


// 如果不加volatile 程序会死循环
// 加了volatile是可以保证可见性的
private volatile static Integer number = 0;
public static void main(String[] args) {
    //main线程
    //子线程1
    new Thread(()->{
        while (number==0){
            System.out.println("num=====>"+number);
        }
    }).start();
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //子线程2
    new Thread(()->{
        while (number==0){
        }

    }).start();
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    number=1;
    System.out.println(number);
}

2.不保证原子性
原子性:不可分割;
线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。
原子性操作类:java.util.concurrent.atomic
在这里插入图片描述

3.避免指令重排

什么是指令重排?

我们写的程序,计算机并不是按照我们自己写的那样去执行的
源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性!

int x=1; //1
int y=2; //2
x=x+5;   //3
y=x*x;   //4

//我们期望的执行顺序是 1->2->3->4  可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的

玩转单例模式

饿汉式
/**
 * 饿汉式单例
 */
public class Hungry {

    /**
     * 可能会浪费空间
     */
    private byte[] data1=new byte[1024*1024];
    private byte[] data2=new byte[1024*1024];
    private byte[] data3=new byte[1024*1024];
    private byte[] data4=new byte[1024*1024];

    private Hungry(){

    }
    private final static Hungry hungry = new Hungry();

    public static Hungry getInstance(){
        return hungry;
    }
}
DCL懒汉式
//懒汉式单例模式
public class LazyMan {

    private static boolean key = false;

    private LazyMan(){
        synchronized (LazyMan.class){
            if (key==false){
                key=true;
            }
            else{
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }
        System.out.println(Thread.currentThread().getName()+" ok");
    }
    private volatile static LazyMan lazyMan;

    //双重检测锁模式 简称DCL懒汉式
    public static LazyMan getInstance(){
        //需要加锁
        if(lazyMan==null){
            synchronized (LazyMan.class){
                if(lazyMan==null){
                    lazyMan=new LazyMan();
                    /**
                     * 1、分配内存空间
                     * 2、执行构造方法,初始化对象
                     * 3、把这个对象指向这个空间
                     *
                     *  就有可能出现指令重排问题
                     *  比如执行的顺序是1 3 2 等
                     *  我们就可以添加volatile保证指令重排问题
                     */
                }
            }
        }
        return lazyMan;
    }
    //单线程下 是ok的
    //但是如果是并发的
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        //Java中有反射
//        LazyMan instance = LazyMan.getInstance();
        Field key = LazyMan.class.getDeclaredField("key");
        key.setAccessible(true);
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true); //无视了私有的构造器
        LazyMan lazyMan1 = declaredConstructor.newInstance();
        key.set(lazyMan1,false);
        LazyMan instance = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(lazyMan1);
        System.out.println(instance == lazyMan1);
    }
}

因为反射的存在,所以单例模式不安全。可以使用枚举,我们就可以防止反射破坏了

//enum 是什么? enum本身就是一个Class 类
public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        //java.lang.NoSuchMethodException: com.ogj.single.EnumSingle.<init>()

        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

CAS

什么是cas?

点击查看

//CAS : compareAndSet 比较并交换
public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(2020);

    //boolean compareAndSet(int expect, int update)
    //期望值、更新值
    //如果实际值 和 我的期望值相同,那么就更新
    //如果实际值 和 我的期望值不同,那么就不更新
    System.out.println(atomicInteger.compareAndSet(2020, 2021));
    System.out.println(atomicInteger.get());

    //因为期望值是2020  实际值却变成了2021  所以会修改失败
    //CAS 是CPU的并发原语
    atomicInteger.getAndIncrement(); //++操作
    System.out.println(atomicInteger.compareAndSet(2020, 2021));
    System.out.println(atomicInteger.get());
}

概括:
CAS(Compare And Swap)比较并替换,是线程并发运行时用到的一种技术,比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。
缺点:
循环会耗时;
一次性只能保证一个共享变量的原子性;
它会存在ABA问题

ABA问题?

描述:
线程1:期望值是1,要变成2;
线程2:两个操作:
1、期望值是1,变成3
2、期望是3,变成1
所以对于线程1来说,A的值还是1,所以就出现了问题,骗过了线程1;
在这里插入图片描述

怎么解决ABA问题?

juc版乐观锁

public static void main(String[] args) {
        //integer使用了对象缓存机制,超出了-128~127重新创建对象
        //AtomicStampedReferencer如果泛型是一个包装类,注意对象引用问题
//        AtomicStampedReference<Object> objectAtomicStampedReference = new AtomicStampedReference<>(2020,2021);
        //每次被动过版本号加一
        //正常业务操作这里比较的是一个个对象
        AtomicStampedReference<Integer> objectAtomicStampedReference = new AtomicStampedReference<>(1,1);

        //乐观锁的原理相同
        new Thread(()->{
            int stamp = objectAtomicStampedReference.getStamp();//获得版本号
            System.out.println("a1->"+stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            objectAtomicStampedReference.compareAndSet(1,2,objectAtomicStampedReference.getStamp(), objectAtomicStampedReference.getStamp()+1);
            System.out.println("a2->"+objectAtomicStampedReference.getStamp());
            objectAtomicStampedReference.compareAndSet(2,1,objectAtomicStampedReference.getStamp(), objectAtomicStampedReference.getStamp()+1);
            System.out.println("a3->"+objectAtomicStampedReference.getStamp());
        },"a").start();
        new Thread(()->{
            int stamp = objectAtomicStampedReference.getStamp();//获得版本号
            System.out.println("b1->"+stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            objectAtomicStampedReference.compareAndSet(2,6,stamp,stamp+1);
            System.out.println("b2->"+objectAtomicStampedReference.getStamp());

        },"b").start();
    }

种锁的理解

公平锁,非公平锁
公平锁:非常公平,不能够插队,必须先来后到
非公平锁:非常不公平,可以插队,可以改变顺序

死锁

死锁测试,怎么排除死锁:
1.使用jps定位进程号
命令:jps -l
2.使用jstack 进程进程号 找到死锁信息
在这里插入图片描述

3.杀进程
linux: kill -9 进程id
windows: taskkill /pid 进程id /F

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值