java多线程(二)

目录

一.线程池

1.线程池如何减少模式切换

2.创建线程池

(1).使用ThreadPoolExecutor构造方法

(2).使用Executors工厂方法

3.线程池工作流程

二.定时器

实现方法

三.锁的策略

四.锁的升级

五.CAS(compare and swap)

六.CAS应用场景

七.JUC(java.util.concurrent)常见的类


一.线程池

线程池通过预先创建并维护一组线程,能够实现纯用户态操作,可以减少线程创建和销毁时的用户态与内核态切换开销。

1.线程池如何减少模式切换

1.预先创建线程​​:线程池在初始化时就创建了一定数量的线程(核心线程),避免了每次任务到来时都需要新建线程。

2.​​线程复用​​:线程池中的线程在执行完一个任务后不会立即销毁,而是保持存活状态等待下一个任务,避免了频繁的线程创建和销毁。

3.​​减少系统调用​​:每次创建新线程都需要进行系统调用(从用户态切换到内核态),线程池通过复用线程减少了这种切换。

2.创建线程池

(1).使用ThreadPoolExecutor构造方法

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    int corePoolSize,                      // 核心线程数
    int maximumPoolSize,                    // 最大线程数
    long keepAliveTime,                     
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
);

1. corePoolSize(核心线程数)

  •  

    线程池中保持的最小线程数量

  •  

    即使线程空闲也不会被回收(除非设置allowCoreThreadTimeOut)

  •  

    新任务提交时优先创建核心线程

2. maximumPoolSize(最大线程数)

  •  

    线程池允许的最大线程数量

  •  

    当工作队列满时,线程池会创建新线程直到达到此限制

  •  

    必须 ≥ corePoolSize

最大线程数=核心线程数(最先创建的/不会被销毁)+非核心线程数(后来创建的/可能被销毁)

通过最大线程数和核心线程数能够完成自动缩容/扩容的作用

3. keepAliveTime(线程空闲时间)

  •  

    非核心线程允许空闲的最大时间

  •  

    超过此时间且当前线程数>corePoolSize时,线程会被终止

  •  

    可设置unit参数指定时间单位

4. unit(时间单位)

  •  

    keepAliveTime的时间单位

  •  

    常用值:TimeUnit.SECONDS、TimeUnit.MILLISECONDS等

5. BlockingQueue<Runnable> workQueue(工作队列)

  •  

    用于保存等待执行的任务的阻塞队列

  •  

    常用实现类:

    •  

      ArrayBlockingQueue:有界队列

    •  

      LinkedBlockingQueue:无界队列(默认)

    •  

      SynchronousQueue:不存储元素的队列

    •  

      PriorityBlockingQueue:优先级队列

6. threadFactory(线程工厂)=>Thread类的工厂类,会提供多个工厂方法,创建出Thread对象

  •  

    用于创建新线程的工厂

  •  

    可以自定义线程名称、优先级、守护状态等

  •  

    默认使用Executors.defaultThreadFactory()

可以统一设置线程的属性

7. handler(拒绝策略)

  •  

    当线程池和工作队列都饱和时的处理策略

  •  

    内置策略:

    •  

      AbortPolicy:默认,任务队列满,抛出RejectedExecutionException

    •  

      CallerRunsPolicy:由提交任务的线程执行该任务

    •  

      DiscardPolicy:把任务队列中最新的任务丢弃

    •  

      DiscardOldestPolicy:把任务队列中最老的任务(最早被加入,还没执行)丢弃了,空余位置留给新任务

(2).使用Executors工厂方法

// 固定大小线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

// 单线程线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

// 可缓存线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

// 定时任务线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

Executors 是一个工具类/工厂类,提供了一系列静态工厂方法来创建不同类型的 ExecutorService 实例。

ExecutorService 是 Java 并发包 (java.util.concurrent) 中的一个接口,它扩展了更基础的 Executor 接口,提供了更丰富的线程池管理功能。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//接口实现线程池
public class Demo18 {
    public static void main(String[] args) {
        ExecutorService executorService= Executors.newCachedThreadPool();
        for (int i = 0; i <100 ; i++) {
            final int id=i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    String name =Thread.currentThread().getName();
                    System.out.println("hello"+name+id);

                }
            });
        }
    }
}

自主实现线程池

class MyThreadPool{
    private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
    //n表示线程池的线程数目
    public MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread t=new Thread(()->{
                try {
                    while(true){
                        Runnable task=queue.take();
                        task.run();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.setDaemon(true);
            t.start();//调用start才会执行lambda
        }
    }
    //在线程池中添加新任务
    public void submit(Runnable task) throws InterruptedException {
        queue.put(task);
    }
}
public class Demo19 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool=new MyThreadPool(4);//创建线程池,可以存储4个线程
        for (int i = 0; i <10 ; i++) {
            int id=i;
            pool.submit(()->{
                Thread cur=Thread.currentThread();
                System.out.println("hello"+cur.getName()+","+id);
            });
        }
        //确保线程池的线程有足够时间执行完任务,因为线程池中的线程被设置为后台线程,主线程结束进程结束后台进程就会直接结束
        Thread.sleep(1000);
    }
}

3.线程池工作流程

    • 当提交任务时,线程池首先检查核心线程数(corePoolSize)是否已满。未满则创建新线程执行任务。
    • 若核心线程已满,任务会被放入阻塞队列等待。
    • 若阻塞队列也满了,才会判断当前线程数是否小于最大线程数(maximumPoolSize)。若小于,则创建新线程(非核心线程)执行任务。
    • 若队列满且线程数已达最大值,则触发拒绝策略

二.定时器

实现方法

//定时器Timer类
import java.util.Timer;
import java.util.TimerTask;

public class Demo20 {
    public static void main(String[] args) {
        Timer timer=new Timer();//这个类的内部有专门的前台线程执行任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello task1");
            }
        },1000);//TimerTask继承自Runnable接口,并且还是抽象方法,需要实现
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello task2");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello task3");
            }
        },3000);
        timer.cancel();//关闭定时器
    }
}

三.锁的策略

1.乐观锁vs悲观锁

加锁时预测出现竞争的概率大=>悲观锁

加锁时预测出现竞争的概率小=>乐观锁

synchronized既是乐观锁又是悲观锁

2.重量级锁vs轻量级锁

加锁操作开销很大=>重量级

加锁操作开销很小=>轻量级

synchronized既是重量级锁又是轻量级锁

3.挂起等待锁vs自旋锁

遇到锁冲突,把线程阻塞,等待唤醒=>涉及到系统内部调度,开销很大=>挂起等待锁(重量级锁的典型实现)

遇到锁冲突,不着急阻塞,而是进行重试,直到锁释放=>用户态的操作,不涉及内核和线程调度=>自旋锁(轻量级锁的典型实现)(基于cas完成)

synchronized既是挂起等待锁又是自旋锁

4.公平锁vs不公平锁

操作系统内部线程调度随机,不做任何限制=>非公平锁

依赖额外的数据结构记录线程的先后顺=>公平锁

synchronized是非公平锁

5.可重入锁vs不可重入锁

一个线程,一把锁,连续加两次

不死锁=>可重入锁

死锁=>不可重入锁

synchronized是可重入锁

6.读写锁vs普通互斥锁

加锁 ,解锁=>普通互斥锁

加读锁,加写锁,解锁=>读写锁(如果代码逻辑中只进行读操作/写操作,就可以只用读锁/写锁)

                                   读锁和读锁不存在锁竞争

                                    读锁和写锁以及写锁和写锁存在锁竞争

synchronized是普通互斥锁

四.锁的升级

升级过程

Java中的锁升级是指从无锁状态逐步升级到重量级锁的过程,这是Java Synchronized关键字实现锁优化的核心机制。锁升级过程主要分为以下几个阶段:

1. 无锁状态

  • 对象刚创建时,没有任何线程竞争

  • 此时对象的Mark Word中存储的是对象的哈希码等信息

2. 偏向锁(Biased Locking)

  • 进入synchronized代码块,锁会进入偏向模式

  • 没有真正加锁,只是标记

  • 在下面的过程中,如果没有别的线程来争取,就始终保持标记

3. 轻量级锁(Lightweight Locking)(自旋锁)

  • 当有第二个线程尝试获取锁时,偏向锁会升级为轻量级锁

  • JVM会在当前线程的栈帧中创建锁记录(Lock Record)

4. 重量级锁(Heavyweight Locking)

  • 当自旋超过一定次数(默认10次)或等待线程超过一定数量,锁会升级为重量级锁

  • 此时Mark Word中存储的是指向操作系统互斥量(mutex)的指针

  • 未获取到锁的线程会被阻塞,进入内核态的等待队列

  • 重量级锁会导致线程在用户态和内核态之间切换,开销较大

锁升级的特点

  1. ​不可逆性​​:锁只能升级不能降级(从JDK 15开始,HotSpot实现了锁降级)

  2. ​自适应自旋​​:JVM会根据历史情况动态调整自旋次数

  3. ​锁消除​​:针对synchronized进行的编译器优化,会对不需要的锁进行消除(比如某个变量只在一个线程中使用)

  4. 锁的粒度:取决于加锁和解锁之间有多少逻辑

  5. ​锁粗化​​:对连续多次加锁解锁操作合并为一次范围更大的锁操作

 锁升级机制是为了在减少锁开销和提高并发性能之间找到平衡:

  • 无竞争时使用偏向锁,几乎无开销

  • 轻度竞争时使用轻量级锁,避免线程阻塞

  • 重度竞争时使用重量级锁,避免CPU空转

这种机制使得Synchronized在低竞争和高竞争环境下都能有较好的性能表现。

五.CAS(compare and swap)

与加锁不同的解决线程安全的另一种思路,是通过"原子性"来防止插队

CAS不是函数而是一条"cpu指令"

主要进行比较一个内存和寄存器的值,如果内容相同,把内存器和另一个寄存器的值交换

CAS 操作包含三个操作数:

  1. ​内存位置(V)​
  2. ​预期原值(A)​

  3. ​新值(B)​

操作逻辑:

  •  

    当且仅当 V 的值等于 A 时,才会将 V 的值更新为 B

  •  

    无论是否更新成功,都会返回 V 的旧值

AtomicInteger atomicInt = new AtomicInteger(0);

// CAS 操作
boolean success = atomicInt.compareAndSet(0, 1); // 如果当前值是0,则更新为1

ABA问题

CAS循环检测,判定是否出现其他线程穿插执行了的判定依据=>检测值没有改变    (×)

用下面的代码进行讲解为什么上面的结论不正确

AtomicInteger atomicInt = new AtomicInteger(100);

// 线程1:100 -> 50
atomicInt.compareAndSet(100, 50);

// 线程2:50 -> 100
atomicInt.compareAndSet(50, 100);

// 线程3:认为值还是100(实际上已经发生过穿插)
atomicInt.compareAndSet(100, 200); // 会成功,但可能不符合业务预期

解决方法

引入"版本号"概念,约定版本号只能加不能减,每次修改值都会原子的把版本号+1

再次使用aba判定时,不是拿数值判定是否有插队,而是拿版本号判定

value=1000;
version=1
oldVersion=version
if(CAS(version,oldVersion,通过原子的方式进行version+1和value的修改))
//版本相同则进行修改,不同则认为失败.原子方式修改需要AtomicStampedReference的引用

六.CAS应用场景

原子类/自旋锁=>内部方法基于cas实现

使用原子类解决线程安全问题(与synchronized作用类似)

import java.util.concurrent.atomic.AtomicInteger;

//使用原子类解决线程安全问题
public class java22 {
    //private static int count=0;
    private static AtomicInteger count=new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for (int i = 0; i <5000 ; i++) {
                //count++;java中不支持运算符重载=>不支持让各种运算符对某个类的对象使用
                count.getAndIncrement();//先获取旧值在自增

                //++count
                //count.incrementAndGet();
                //count--
                //count.getAndDecrement();
                //--count
                //count.decrementAndGet();
                //count+=n
                //count.addAndGet(n);
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <5000 ; i++) {
                //count++;
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count"+count);
    }
}

如何理解内部方法基于CAS实现,起到维护线程安全的作用,用下面代码解释

class AtomicInteger{
    private int value;
    public int getAndIncrement(){
        int oldValue=value;
        //value 是内存的值,将其放到寄存器当中为oldValue
        //在这个时候可能会有其他线程穿插进来执行,就可能出现线程不安全问题,但是下面的CAS能够识别是否存在穿插
        while (CAS(value,oldValue,oldValue)!=true){
            //此处while循环进入的条件是当value与oldValue值不同时进入
            oldValue=value;//此时更新oldValue的值
        }
        return oldValue;
    }
}

图解

自旋锁基于CAS实现伪代码

//自旋锁基于cas实现伪代码
public class Demo23 {
    private Thread owner=null;//owner是一个标记,用于记录当前哪个线程持有锁
    //通过CAS看是否锁被某个线程持有
     //CAS中如果owner!=null,说明锁被其他线程持有,返回false =>!false=>true 继续循环等待
     //owner==null 说明锁被释放,将当前线程赋给owner,返回true=>!true=>false 退出循环
    while (!CAS(this.owner,null,Thread.currentThread())){

    }
    public void unlock(){
        this.owner=null;
    }
}

七.JUC(java.util.concurrent)常见的类

1.callable接口

类似于Runnable void run表示任务的过程没有返回值

callable中有一个T call方法,同样描述一个任务,要求返回一个值

通过代码区分

//run方法
public class Demo24 {
    private static int result=0;//用来将run方法无法返回的值取出来
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(new Runnable() {
            int sum=0;
            @Override
            public void run() {

                for (int i = 0; i < 4; i++) {
                    sum+=i;
                }
                result=sum;//通过成员变量解决线程间数据交互的问题,该变量能够被各种线程各种逻辑获取
            }
        });
        t.start();
        t.join();
        System.out.println(result);
    }
}

使用callcable中的call方法就可以解决上面的情况

public static void main(String[] args) throws ExecutionException, InterruptedException {
    Callable<Integer> callable=new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            int sum=0;
            for (int i = 0; i < 1000; i++) {
                sum+=i;
            }
            return sum;
        }
    };
    //Thread无法直接接受callable对象作为参数
    //所以需要搭配FutureTask(类似于取餐号码牌)
    FutureTask<Integer> futureTask=new FutureTask<>(callable);
    Thread t=new Thread(futureTask);
    t.start();
    //get方法能够拿到call的返回结果
    //如果call没有执行完,get会阻塞等待
    System.out.println(futureTask.get());
    }
}

2.ReentrantLock(可重入锁)

是一种锁的经典用法,在synchronized出现之前一直使用这种方式加锁

import java.util.concurrent.locks.ReentrantLock;

public class Demo25 {
    private static int sum=0;
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock locker=new ReentrantLock();
        Thread t1=new Thread(()->{
            for (int i = 0; i <5000 ; i++) {
                try{
                    locker.lock();
                    sum++;
                }finally{
                    locker.unlock();
                }
                //. 防止锁泄漏(避免死锁)
                //如果 lock()之后代码抛出异常,而 unlock()没有被执行,锁将永远不会被释放,导致其他线程无法获取锁(死锁)。
                //finally块确保无论是否发生异常,锁都会被释放。
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                try{
                    locker.lock();
                    sum++;
                }finally{
                    locker.unlock();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(sum);
    }
}

相对于synchronized优势

1.ReentrantLock支持trylock行为,加锁不成可以直接返回,不像lock会阻塞等待

2.ReentrantLock支持公平锁(内部维护队列记录加锁顺序,会按照顺序从队列中取)

ReentrantLock locker=new ReentrantLock(true);

3.等待通知机制

synchronized 搭配wait notify,notify只能随机唤醒一个线程

ReentrantLock搭配Condition类,唤醒功能更丰富,能够指定唤醒

八.信号量(Semaphore)

就是一个计数器,描述了"可用资源"的个数,最少为0=>可当作锁来使用

分为两个操作一个是申请资源(p操作),一个是释放资源(v操作)

Semaphore semaphore=new Semaphore(许可证数量)

p操作=>semaphore.acquire()

v操作=>semaphore.release()

p v操作本质上是操作计数器的值=>原子的过程

主要用于固定资源个数的场景,也能充当锁,如下所示

package thread;

import java.util.concurrent.Semaphore;

public class Demo26 {
    public  static int count=0;
    public static void main(String[] args) throws InterruptedException {
        //充当锁
        Semaphore semaphore=new Semaphore(1);
        Thread t1=new Thread(()->{
            for (int i = 0; i <10000 ; i++) {
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                semaphore.release();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <10000 ; i++) {
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                semaphore.release();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

CountDownLatch

CountDownLatch是Java并发包(java.util.concurrent)中的一个同步辅助类,它允许一个或多个线程等待其他线程完成操作后再继续执行。其核心思想基于​​计数器机制​​,通过递减计数来实现线程间的协调。内部维护一个计数器,初始值为设定的正整数。当线程调用countDown()时,计数器减1;调用await()的线程会被阻塞,直到计数器变为0

用下面的例子解释,在运动会赛跑中,有3个选手比赛,在程序中我们设置为3个并发执行的线程,只有全部的线程执行结束也就是所有运动员到达终点比赛才会结束,那我们如何才能知道这几个线程什么时候才能全部结束,这时就会使用CountDownLatch

package thread;

import java.util.concurrent.CountDownLatch;

public class Demo {
    public static int id;
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch=new CountDownLatch(3);
        for (int i = 1; i <= 3; i++) {
            final int id=i;
            Thread t=new Thread(()->{
                System.out.println("运动员"+id+"开始比赛");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("运动员"+id+"到达终点");
                //撞线操作
                latch.countDown();
            });
            t.start();
        }
        latch.await();//等待全部线程结束后
        System.out.println("比赛结束");
    }
}

ConCurentHashMap

每个链表的头节点作为锁对象,多线程使用哈希表尽量使用ConCurentHashMap,原因如下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值