多线程与高并发基础

多线程和高并发介绍 多线程和高并发介绍 文章目录多线程和高并发介绍前言一、什么是多线程?1.多线程介绍2.多线程实现原理?3.白话文解释多线程4.多线程存在的问题二、什么是高并发?1.高并发介绍2.如何提升系统的并发能力三、多线程和高并发总结 前言 本文主要是针对多线程和高并发的概念做了简单的描述,介绍了什么是多线程,什么是高并发,并且对多线程和高并发的关系做了比较描述。 一、什么是多线程? 1.多线程介绍 什么是多线程,首先看下百度百科对多线程的定义;多线程(multithreading) 阅读详情

多线程入门

基本概念

什么叫多线程技术?

从软件或硬件上实现多个线程并发执行的技术

并行与并发

  • 并行:同一时刻,多个指令在 多个CPU 上同时执行
  • 并发:同一时刻,多个指令在 单个CPU 上交替执行

进程和线程

  • 进程:运行中的程序就叫进程,进程有以下三个特性
    1. 独立性:是能独立运行的基本单位,也是系统分配资源和调度的独立单位
    2. 动态性:进程是程序的一次执行过程,进程是动态产出,动态消亡的
    3. 并发性:任何进程都可以和其它进程并发执行
  • 线程:是进程中的一条执行路径

单线程和多线程是啥意思?

  • 单线程:一个程序启动的一个进程如果只有一条执行路径,则是单线程程序
  • 多线程:一个程序启动的一个进程如果有多条执行路径,则是多线程程序

Java中多线程的三种实现方式

方式一:继承 Thread 类

步骤:

  • 自定义一个类 MyThread 继承 Thread 类

  • 在 MyThread 类中重写 run() 方法

  • 创建 MyThread 类的对象

  • 启动线程

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程……run");
    }
}

public class Test {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

 

  • 为什么要重写run()方法?

    因为 run() 是用来封装被线程执行的代码。main 方法中的代码是由主线程来执行的

  • run() 方法和 start() 方法的区别?

    run():封装线程执行的代码, 如果直接调用,相当于普通方法的调用

    start():启动线程;然后由 JVM 调用此线程的 run() 方法

方式二:实现 Runnable 接口

步骤:

  • 定义一个类MyRunnable实现Runnable接口

  • 在MyRunnable类中重写run()方法

  • 创建MyRunnable类的对象

  • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数。有两个构造方法:①Thread(Runnable target),②Thread(Runnable target, String name)  其中 name 参数是要给线程起的名字,如果不传,则程序会分配一个

  • 启动线程

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run……");
        // Thread.currentThread().getName() 获取执行当前代码的线程名
    }
}

public class Test {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable, "哈哈哈线程");
        thread.start();
    }
}

 

方式三:实现 Callable 接口

步骤:

  • 定义一个类MyCallable实现Callable接口,需要指定泛型

  • 在MyCallable类中重写call()方法,返回值跟上面泛型一致

  • 创建MyCallable类的对象

  • 把MyCallable对象作为构造方法的参数,创建Future的实现类FutureTask对象,传入泛型需要与 Callable 中的一致

  • 创建Thread类的对象,把FutureTask对象作为构造方法的参数,也可以传入线程名

  • 启动线程

  • 然后可以通过FutureTask对象调用get方法,就可以获取线程结束之后的结果。(get方法一定要放在start之后执行,因为它是获取线程结束之后的结果,如果线程还没有开启或结束,那么get会在那卡着一直等)

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("哈哈哈……" + i);
        }
        return "笑了100次,乐疯了";
    }
}

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable callable = new MyCallable();
        //可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
        //这里的泛型要和 MyCallable 中的泛型一致
        FutureTask<String> task = new FutureTask<>(callable);
        Thread thread = new Thread(task);
        thread.start();
        System.out.println(task.get());
    }
}

 

三种方式的对比

  • 实现Runnable、Callable接口

    • 好处: 扩展性强,实现该接口的同时还可以继承其他的类

    • 缺点: 编程相对复杂

  • 继承Thread类

    • 好处: 编程比较简单,可以直接使用Thread类中的方法

    • 缺点: 可扩展性较差,不能再继承其他的类

设置和获取线程名称

上面的三种方式中,方式二和方式三在创建线程时都可以传入线程名,但是如果是方式一的话则不能了,因为方式一是我们自己自定义的线程类,里面没有含参构造方法,我们必须添加含参构造方法,并在含参构造方法中显示的调用父类的含参构造方法才能给线程命名,代码实现如下:

public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread my1 = new MyThread("哈哈哈线程);

        //static Thread currentThread() 返回对当前正在执行的线程对象的引用
        System.out.println(Thread.currentThread().getName());  // main
    }
}
class MyThread extends Thread {
    public MyThread() {}
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(getName()+" run……");
    }
}

 

还可以创建线程后 通过线程的 setName 方法设置线程名称

通过 getName() 获取当前线程的名字。

注意:在自定义线程类的 run() 方法中可以直接通过 getName() 来获取执行当前方法的线程,但是在 Runnable 实现类的 run() 方法 和 Callable 实现类的 call() 方法中的只能 先通过  Thread.currentThread() 获取到线程再调用 getName() 方法

线程休眠:sleep 方法

static void sleep(long millis)   填入的为毫秒数

作用:使当前正在执行的线程停留

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}

注意点: 当一个类或接口的方法中未抛出异常,那么他的子类或者实现类重写该方法时就不能抛出异常。 如上面:的Runnable接口中的 run 方法就没有抛出异常,那么它的实现类中就不能抛出异常。所以上面 sleep 处的异常只能通过 try-catch 来处理

线程的优先级

线程调度有两种方式:

  • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型: 优先级越高抢到CPU的几率就越高

Java使用的线程调度方式是抢占式调度模型。相关的方法如下:

  • getPriority()    获取线程的优先级
  • setPriority(int newPriority)   设置线程的优先级。默认优先级是5;线程优先级的范围是:1-10

守护线程:daemon thread

线程分为:用户线程和守护线程(主线程也属于用户线程)

守护线程,是个服务线程,准确地来说就是服务用户线程。 当用户线程都执行完毕退出了的时候,守护线程也就没必要存在了,那么守护线程也会退出

就比如垃圾回收线程就是典型的守护线程

通过  setDaemon(boolean on) 里面传入 true 可以将线程设为守护线程。

synchronized 同步

先看一个典型的卖票案例:

class SellTicket implements Runnable {
    private int ticket = 100;
    //在SellTicket类中重写run()方法实现卖票,代码步骤如下
    @Override
    public void run() {
        while (true) {
            if(ticket <= 0){
                    //卖完了
                    break;
                }else{
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        //创建SellTicket类的对象
        SellTicket st = new SellTicket();

        //创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
        Thread t1 = new Thread(st,"窗口1");
        Thread t2 = new Thread(st,"窗口2");
        Thread t3 = new Thread(st,"窗口3");

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

用上面这种方式创建的所有线程都共享同一份数据,但如果用第一种创建线程的方式,则要将数据用static修饰才能共享

卖票案例出现的问题:相同的票出现了多次、出现了负数的票

问题产生原因:线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现线程安全问题

这里我们就可以通过 synchronized 来解决上面的问题

同步方法

修饰符 synchronized 返回值类型 方法名(方法参数) {

方法体;

}

锁对象是什么呢? this

class SellTicket implements Runnable {
    private int ticket = 100;
    //在SellTicket类中重写run()方法实现卖票,代码步骤如下
    @Override
    public void run() {
        synchronizedMethod();
    }

    public synchronized void synchronizedMethod() {
        while (true) {
            if(ticket <= 0){
                //卖完了
                break;
            }else{
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
                System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        SellTicket st = new SellTicket();
        Thread t1 = new Thread(st,"窗口1");
        Thread t2 = new Thread(st,"窗口2");
        Thread t3 = new Thread(st,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

静态同步方法:

就是把synchronized关键字加到静态方法上

修饰符 static synchronized 返回值类型 方法名(方法参数) {

方法体;

}

注意:同步的静态方法的锁对象为:类名.class, 即 class对象

同步代码块

以下演示一种错误方法

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("窗口1");
        MyThread t2 = new MyThread("窗口2");
        MyThread t3 = new MyThread("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

package thread.synchronize;

public class MyThread extends Thread {
    public MyThread() {}
    public MyThread(String name) {
        super(name);
    }

    private static int ticket = 100;  //-------------------------------这里要加 static

    @Override
    public void run() {
        while (true) {
            synchronized(this) {   //-------------------每个线程到来,this都是当前线程对象
                if(ticket <= 0){
                    break;
                }else{
                    try {
                      Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                  ticket--;
                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                }
            }
      }
    }
}

为啥上面的有问题?

因为你是用 继承 的方式创建自己的线程类,然后 new 了三个线程,每个线程过来的this都是线程自己本身,那这样的话锁对象不唯一,当然没法锁住。

如果用的是实现 Runnable 的方式的话那是没有问题的(前提是你创建线程是用的同一个 Runnable 对象),请看下面代码 :

public class Demo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        Thread t1 = new Thread(mr,"窗口1");
        Thread t2 = new Thread(mr,"窗口2");
        Thread t3 = new Thread(mr,"窗口3");
    }
}


class MyRunnable implements Runnable {
    private int ticket = 100;
    @Override
    public void run() {
        while (true) {
            synchronized(this) {   //--------- 每个线程到来,this都是同一个 MyRunnable 对象
                if(ticket <= 0){
                    break;
                }else{
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                }
            }
        }
    }
}

原因:每个线程到来,this都是同一个 MyRunnable 对象,所有线程用的同一把锁才能锁住

同步的利与弊:

利:成功解决多线程的数据安全问题

弊:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock是接口不能直接实例化,这里采用它的实现类 ReentrantLock 来实例化。相关方法:

  • ReentrantLock 构造方法,创建 ReentrantLock 实例对象

    方法名说明
    ReentrantLock()创建一个ReentrantLock的实例
  • ReentrantLock实例的 所相关的方法

    方法名说明
    void lock()获得锁
    void unlock()释放锁
  • 加锁解锁方法
方法名说明
newCondition()创建一个 Condition 的实例,通过它可以调用 await、signal方法

例子:

class Ticket implements Runnable {
    //票的数量
    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //synchronized (obj){//多个线程必须使用同一把锁.
            try {
                lock.lock();
                if (ticket <= 0) {
                    //卖完了
                    break;
                } else {
                    Thread.sleep(100);
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            // }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}

synchronized 和 Lock 的对比

以下分别从语法、功能、性能这三个层面将它们进行对比

  • 语法层面

    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现

    • Lock 是接口,源码由 jdk 提供,用 java 语言实现

    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁(为了防止大家忘记释放锁,所以一般推荐使用 try-finally 然后在 finally 块中释放锁)

  • 功能层面

    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能

      • 互斥:多个线程抢同一把锁,只有一个能成功,其它线程都会失败并进入阻塞状态

      • 同步:多个线程同时运行,当某个线程运行到某处时发现需要其它线程的结果,那它就会等待其它线程把结果搞出来后再继续执行 (synchronized可通过wait notify来实现同步) (Lock 可以通过 await signal 这两个都是 condition类 中的方法)

      • 锁重入:持锁的线程可以给持锁的对象重复加锁

    • Lock 提供了许多 synchronized 不具备的功能,例如:

      • 获取等待状态

      • 公平锁

      • 可打断、可超时:Lock提供了等不到锁就打断它不然它继续等 及 一定时间内等不到则不再等待

      • 多条件变量

    • Lock 有适合不同场景的实现,如 ReentrantLock 是可重入锁, ReentrantReadWriteLock 适合读多写少的场景

  • 性能层面

    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不错

    • 在竞争激烈时,Lock 的实现通常会提供更好的性能

关于 Lock 的公平锁:

  • 已经处在阻塞队列中的线程(不考虑超时)始终都是公平的,先进先出

  • 未处于阻塞队列中的线程来争抢锁,如果队列不为空,则老实到队尾等待

  • 非公平锁是指 未处于阻塞队列 中的线程来争抢锁,与队列头唤醒的线程去竞争,谁抢到算谁的

  • 公平锁会降低吞吐量,一般都不用

关于 Lock 条件变量 Condition:

  • ReentrantLock 中的条件变量功能类似于普通 synchronized 的 wait,notify,用在当线程获得锁后,发现条件不满足时,临时等待的链表结构

  • 与 synchronized 的等待集合不同之处在于,ReentrantLock 中的条件变量可以有多个,可以实现更精细的等待、唤醒控制

死锁问题

什么叫死锁:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行

产生原因:同步嵌套

例子:(下面用到了 Lambda 表达式创建线程)

public class Demo {
    public static void main(String[] args) {
        Object objA = new Object();
        Object objB = new Object();

        new Thread(()->{
            while(true){
                synchronized (objA){
                    //线程一
                    synchronized (objB){
                        System.out.println("小康同学正在走路");
                    }
                }
            }
        }).start();

        new Thread(()->{
            while(true){
                synchronized (objB){
                    //线程二
                    synchronized (objA){
                        System.out.println("小薇同学正在走路");
                    }
                }
            }
        }).start();
    }
}

生产者与消费者

生产者消费者模式是一个十分经典的多线程协作的模式,在这种模式下,线程可以看作两类:

  • 生产者线程:用于生产数据
  • 消费者线程:用于消费数据

为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

Object类的等待和唤醒方法(使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法)

方法名说明
void wait()导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify()唤醒正在等待对象监视器的单个线程,如果有多个线程在等待,则随机唤醒一个
void notifyAll()唤醒正在等待对象监视器的所有线程

案例:

生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

  1. 如果没有包子,生产包子,生产完后唤醒消费者消费包子

  2. 如果有包子,就进入等待状态

消费者类(Foodie):实现Runnable接口,重写 run() 方法,设置线程任务

  1. 如果有包子,就消费包子,消费完后唤醒生产者生产包子

  2. 如果没有包子就等待

public class Test {
    public static void main(String[] args) {
        Foodie f = new Foodie();
        Cooker c = new Cooker();
        f.start();
        c.start();
    }
}

class Desk {

    //定义一个标记
    //true 就表示桌子上有汉堡包的,此时允许吃货执行
    //false 就表示桌子上没有汉堡包的,此时允许厨师执行
    public static boolean flag = false;

    //锁对象
    public static final Object lock = new Object();
}

class Cooker extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (!Desk.flag) {  //如果没有包子了则生产
                    System.out.println("厨师正在生产汉堡包");
                    Desk.flag = true;
                    Desk.lock.notifyAll();  // 做好后唤醒消费者吃
                } else {  // 如果还有包子则等待
                    try {
                        Desk.lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

class Foodie extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.flag) { //如果有包子,则开吃
                    System.out.println("吃货在吃汉堡包");
                    Desk.flag = false;
                    Desk.lock.notifyAll(); //吃完后唤醒生产者生产
                } else { //如果没有就等待
                    try {
                        Desk.lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    }
}

 

线程的状态

对于线程的状态,我们可能听过两个版本,一种是说有五种,另一种是说有六种,那到底谁说的才是对的呢?

其实这两种说法都是对的!!

只是他们的划分依据不一样而已,六种状态是Java中线程就是划分为六种,五种状态是根据操作系统层面划分的。

六种状态

六种状态分别是:新建、可运行、终结、阻塞、等待、有限等待

  • 新建

    • 当一个线程对象被创建,但 还未调用 start 方法 时处于新建状态

    • 此时未与操作系统底层线程关联

  • 可运行

    • 调用了 start 方法,就会由新建进入可运行

    • 此时与底层线程关联,由操作系统调度执行

  • 终结

    • 线程内代码已经执行完毕,由可运行进入终结

    • 此时会取消与底层线程关联

注意:新建→可运行→终结:是单向不可逆的过程

  • 阻塞

    • 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间

    • 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态

  • 等待

    • 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间

    • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态

  • 有时限等待

    • 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间

    • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁

    • 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁

    • 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不会释放锁,不需要主动唤醒,超时时间到自然恢复为可运行状态,在等待时同样不占用 cpu

五种状态

五种状态时根据操作系统层面划分的,分为:新建、就绪、运行、终结、阻塞

  • 新建与终结态:与 java 中同名状态类似

  • 运行态:分到 cpu 时间,能真正执行线程内代码的

  • 就绪态:有资格分到 cpu 时间,但还未轮到它的

  • 阻塞态:没资格分到 cpu 时间的

    • 涵盖了 java 状态中提到的 阻塞 、 等待 、 有时限等待

    • 多出了阻塞 I/O,指线程在调用阻塞 I/O 时,实际活由 I/O 设备完成,此时线程无事可做,只能干等(不需要分到CPU就可以完成)

注意:Java中的 RUNNABLE状态 涵盖了就绪、运行、阻塞I/O

wait 和 sleep 比较

共同点

  • wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点

  • 方法归属不同

    • sleep(long) 是 Thread 的静态方法

    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有(每个对象都可以作为锁,获得锁后就可以调用这两个方法)

  • 醒来时机不同

    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来

    • wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去

    • 它们都可以被打断唤醒

  • 锁特性不同(重点)

    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制

    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)

    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,但我不释放锁)

线程池

线程池的作用及设计思路

线程池也是可以看做成一个池子,在该池子中存储很多个线程。

那么线程池有啥用呢?

系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互。当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理对系统资源的消耗,这样就有点"舍本逐末"了。

针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。

线程池的设计思路 :

  1. 准备一个任务容器

  2. 一次性启动多个消费者线程

  3. 刚开始任务容器是空的,所以线程都在wait

  4. 直到一个外部线程向这个任务容器中扔了一个"任务",就会有一个消费者线程被唤醒

  5. 这个消费者线程取出"任务",并且执行这个任务,执行完毕后,继续等待下一次任务的到来

线程池的创建

我们可以使用 Executors类 中所提供的静态方法来创建线程池

static ExecutorService newCachedThreadPool() 创建一个默认的线程池 ​

static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池

默认线程池的创建

static ExecutorService newCachedThreadPool() 创建一个默认的线程池

代码实现:

思路:

  1. 创建一个池子,刚创建时池子中是空的。

  2. 有任务需要执行时,如果池子里没有空闲的线程,则创建线程对象(有则直接拿来用)。ExecutorService 的 submit 方法(池子会自动的帮我们创建对象,任务执行完毕,线程对象归还给池子。)

  3. 所有任务全部执行完毕,关闭线程池。ExecutorService 的 shutdown 方法

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        //Executors --- 可以帮助我们创建线程池对象
        //ExecutorService --- 可以帮助我们控制线程池
        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });
        //Thread.sleep(2000); 如果加上此句,即不让主线程这么块去执行下面这句,
        // 则上面那句创建的线程已经执行好了,放入池中,执行到下面这句的时候就不会创建新的线程了
        executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        Thread.sleep(100);
        //我们可以通过ExecutorService的子类的getPoolSize()获取池中的线程数
        ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
        System.out.println(pool.getPoolSize());
        
        // 当线程池不再使用时,关闭线程池
        executorService.shutdown();
    }
}

 

创建指定上限的线程池

static ExecutorService newFixedThreadPool(int nThreads) : 创建一个指定最多线程数量的线程池

相比于上面的代码只是换了个方法并且指定了线程中线程数量的最大值而已。

注意:这里指定的是最大值而不是初始值,初始值和上面一样是空的

ThreadPoolExecutor类 及 线程池七大参数

查看源码就会发现 newCachedThreadPool() 及 newFixedThreadPool(int nThreads) 里面都是通过 new ThreadPoolExecutor(一堆参数); 来实现的

而这需要传递的一堆参数即为线程池的七大参数,分别是:

  1. corePoolSize 核心线程数目

    • 池中会保留的最多线程数

  2. maximumPoolSize 最大线程数目

    • 核心线程+救急线程的最大数目

      (核心线程:当你这个线程执行完任务后任然需要保留在线程池中的线程;救急线程:当执行完任务后不被保留在线程池中的线程)

  3. keepAliveTime 生存时间

    • 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

  4. unit 时间单位

    • 救急线程的生存时间单位,如秒、毫秒等

  5. workQueue 任务队列

    • 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

    • 一般会设置其能容的量,不然可能导致内存紧张

  6. threadFactory 线程工厂

    • 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。也可以直接传入 Executors.defaultThreadFactory()

  7. handler 拒绝策略:当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略。有四种拒绝策略,分别为:

    1. 丢弃任务并抛出异常  ThreadPoolExecutor.AbortPolicy   这是默认的策略,如果用没有该参数的构造方法,那么就默认为此种策略

    2. 由调用者执行任务 ThreadPoolExecutor.CallerRunsPolicy(线程池已经忙不过来了,调用者你自己去干吧你(就是相当于直接调用任务的run() 方法直接执行))

    3. 直接丢弃任务       ThreadPoolExecutor.DiscardPolicy

    4. 丢弃最早排队任务    ThreadPoolExecutor.DiscardOldestPolicy

      public ThreadPoolExecutor(int corePoolSize,
                                    int maximumPoolSize,
                                    long keepAliveTime,
                                    TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue,
                                    ThreadFactory threadFactory,
                                    RejectedExecutionHandler handler)
      
      // corePoolSize:       -------------------不能小于0
      // maximumPoolSize      ----------------不能小于 corePoolSize
      // keepAliveTime:   --------------------不能小于0
      // unit:           时间单位
      // workQueue:      ------------------------不能为null
      // threadFactory:  创建线程工厂  -----不能为null
      // handler:        任务的拒绝策略  -----不能为null 

       具体使用:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger c = new AtomicInteger(1);
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5,
                10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                r -> new Thread(r, "myThread" + c.getAndIncrement()),  //--------定制线程名
                new ThreadPoolExecutor.AbortPolicy());

    }
}

class Test1 {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5,
                10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                Executors.defaultThreadFactory(),       //-------------------使用默认的ThreadFactory
                new ThreadPoolExecutor.AbortPolicy());

    }
}

 

volatile 关键字解决可见性问题

线程安全主要需要考虑的有三个问题:原子性、可见性、有序性

可见性

  • 起因:由于 编译器优化、或缓存优化、或 CPU 指令重排序优化 导致的对共享变量所做的修改另外的线程看不到

  • 解决:用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。 即强制线程每次在使用的时候,都会看一下共享区域最新的值

如下案例:

public class Test {

    public static void main(String[] args) throws InterruptedException {
        Boy boy = new Boy();
        boy.setName("小明");
        Girl girl = new Girl();
        girl.setName("小红");
        girl.start();
        boy.start();
    }
}

class Money {
    public static int money = 100000;
}

class Boy extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Money.money = 90000;  //  花了1万
        System.out.println("小明花了一万块钱");
    }
}

class Girl extends Thread {
    @Override
    public void run() {
        while (Money.money == 100000) {
            //盯着这钱看还是不是十万,如果还是那就啥也不干
        }
        System.out.println("钱已经不是十万了");
    }
}

上面的代码运行后发现,代码在小明花了一万块钱后卡住不动了,说明小红线程在循环中没有出来,说明小明线程对共享数据是不可见的。

但如果先启动 boy 再启动 girl ,并且在启动 girl 的时候先 sleep 10 毫秒让 boy 先执行完就不会这样了。为啥呢?就是因为编译器的优化,如果 girl 先启动,那么运行一段时间后,girl 循环的那段代码会变成会被 JIT 即时编译器当成热点代码,money就直接放到cpu缓存了,然后就不会再重新去读共享数据了。所以这里是 编译器的优化导致 的可见性问题,要如何解决呢?在money的数据类型前加上 volatile 关键字即可。

原子性问题

  • 起因:多线程下,不同线程的 指令发生了交错 导致的共享变量的读写混乱

  • 解决:用 悲观锁或乐观锁 解决,volatile 并不能解决原子性

举个例子:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyAtomThread atom = new MyAtomThread();
        for (int i = 0; i < 100; i++) {
            new Thread(atom).start();
        }
    }
}


class MyAtomThread implements Runnable {
    private volatile int count = 0; //送冰淇淋的数量
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            count++;
            System.out.println("已经送了" + count + "个冰淇淋");
        }
    }
}

执行代码,发现本来最后的一次输出应该为 10000,结果有时候是,有时候不是,这里就是因为原子性问题的产生。因为这里的 count++ 并不是一个原子性操作,在这里,可以把这里的整个过程看成三步:

  1. 从共享数据中读取数据到本线程栈中.
  2. 修改本线程栈中变量副本的值
  3. 会把本线程栈中变量副本的值赋值给共享数据.

当一个线程执行完第1步读取到数为100时,这时候,另一个线程执行了两次循环中的操作,把数据改成了102,然后该线程继续执行2、3步,又把数据改成了101,那岂不是就因此产生错误了。

那么该如何解决?

方法一:悲观锁

class MyAtomThread implements Runnable {
    private volatile int count = 0; //送冰淇淋的数量
    private final Object obj = new Object();

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //1,从共享数据中读取数据到本线程栈中.
            //2,修改本线程栈中变量副本的值
            //3,会把本线程栈中变量副本的值赋值给共享数据.
            synchronized (obj) {
                count++;
                System.out.println("已经送了" + count + "个冰淇淋");
            }
        }
    }
}

方法二:乐观锁

这里需要用到 AtomicInteger 类,这里先只写解决方法,下节再详谈  AtomicInteger 和乐观锁

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyAtomThread atom = new MyAtomThread();
        for (int i = 0; i < 100; i++) {
            new Thread(atom).start();
        }
    }
}


class MyAtomThread implements Runnable {
    private AtomicInteger ac = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            int count = ac.incrementAndGet();
            System.out.println("已经送了" + count + "个冰淇淋");
        }
    }
}

AtomicInteger 及 乐观锁(CAS)

java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新 基本类型 、原子更新 数组 、原子更新 引用 和原子更新 属性 (字段)。

本次我们只讲解使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:

AtomicBoolean: 原子更新布尔类型

AtomicInteger: 原子更新整型

AtomicLong: 原子更新长整型

以上3个类提供的方法几乎一模一样,所以本节仅以AtomicInteger为例进行讲解,AtomicInteger的常用方法如下:

public AtomicInteger():	   			    初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue):    初始化一个指定值的原子型Integer

int get():   			 				 获取值
int getAndIncrement():      			 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet():     				 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data):				 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):   			 以原子方式设置为newValue的值,并返回旧值。

减则是 decrement

CAS:

有3个操作数(内存值V,旧的预期值A,要修改的值B)

在即将修改的时候读到原本的内存值,然后将 旧的预期值A 设为该值,

然后将修改的值设为B,在要提交更新的时候再次读取内存值,然后进行判断

  • 当旧的预期值A == 内存值 此时修改成功,将V改为B
  • 当旧的预期值A != 内存值 此时修改失败,不会做出修改

如果没有修改的话,会重新获取现在的最新值然后将旧的预期值A设为此次获取到的值,然后继续按照整个步骤操作,这里的操作就叫自选

简单概括此过程为:开始获取到值的时候保存一份,然后要修改的时候再读取一次内存中的值看跟刚刚保存的是否一致,如果不是,说明有人已经改过,需要重新获取再来一遍,如果还出现这样的问题,继续自旋

悲观锁和乐观锁的对比

悲观锁与乐观锁的相同点:在多线程情况下,都可以保证共享数据的安全性。那他们具体有什么不同的呢?以下就是悲观锁和乐视锁各自的特点:

  • 悲观锁的代表是 synchronized 和 Lock 锁

    • 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】

    • 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能

    • 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会

  • 乐观锁的代表是 AtomicInteger,使用 cas 来保证原子性

    • 其核心思想是:无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功

    • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换

    • 它需要多核 cpu 支持,且线程数不应超过 cpu 核数(如果没有多核CPU支持,虽然不会有阻塞状态,但还是会涉及线程上下文切换)

为啥叫 “悲观” “乐观” 呢?

  • 悲观锁总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每次操作共享数据之前,都会上锁。

  • 乐观锁是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。

并发工具类 —— Hashtable

Hashtable出现的原因 :在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。原因如下:

  1. Hashtable采取悲观锁synchronized的形式保证数据的安全性

  2. 只要有线程访问,会将整张表全部锁起来,同一时刻,只能有一个线程操作它

也正因为它效率低的原因,所以它很少被使用到

并发工具类 —— ConcurrentHashMap

ConcurrentHashMap出现的原因 :在集合类中 HashMap 是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。

基于以上两个原因我们可以使用JDK1.5以后所提供的 ConcurrentHashMap。

ConcurrentHashMap 1.7原理

  • 数据结构: Segment(大数组) + HashEntry(小数组) + 链表 ,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突。

  • 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了

  • 扩容:每个小数组的扩容相对独立,小数组在 超过 扩容因子时会触发扩容,每次扩容翻倍

  • Segment[0] 原型:首次创建其它小数组时,会以此原型为依据,数组长度,扩容因子都会以原型为准

具体的一些细节在这里就不讲了,毕竟它已经过时了,如果有兴趣网上找ConcurrentHashMap1.7的帖子学习即可。

ConcurrentHashMap 1.8原理

  • 数据结构: Node 数组 + 链表或红黑树 ,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。

  • 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容

  • 扩容条件:Node 数组 满 3/4 时就会扩容(1.7是要超过factory ,这个满3/4就行)

  • 扩容单位:以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode

  • 扩容时并发 get

    • 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞

    • 如果链表长度超过 1,则需要对节点进行复制(创建新节点),怕的是节点迁移后 next 指针改变

    • 如果链表最后几个元素扩容后索引不变,则节点无需复制

  • 扩容时并发 put

    • 如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞

    • 如果 put 的线程操作的链表还未迁移完成,即链表头节点不是 ForwardingNode,则可以并发执行

    • 如果 put 的线程操作的链表已经迁移完成,即链表头结点是 ForwardingNode,则可以协助扩容

    • 在迁移时是节点是会重新计算哈希值的

  • 与 1.7 相比是懒惰初始化

  • capacity 代表预估的元素个数,capacity / factory 来计算出初始数组大小,需要贴近 。由于它是满就扩容,所以如果 capacity = 12,factory = 3 / 4 那么 初始数组大小会是32

  • loadFactor 只在计算初始数组大小时被使用,之后扩容固定为 3/4

  • 超过树化阈值时的扩容问题,如果容量已经是 64,直接树化,否则在原来容量基础上做 3 轮扩容

并发工具类——CountDownLatch

使用场景: 让某一条线程等待其他线程执行完毕之后再执行

相关方法:

方法解释
public CountDownLatch(int count)参数传递线程数,表示等待线程数量
public void await()让线程等待,当计数器变成0的时候,会自动唤醒这里等待的线程
public void countDown()当前线程执行完毕,让计数器 -1

以下这个案例能很好的帮助我们理解,里面的妈妈线程在等三个孩子线程执行完吃饺子的代码后才执行自己收拾碗筷的代码

public class Test {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(3);
        MotherThread motherThread = new MotherThread(countDownLatch);
        ChileThread1 chileThread1 = new ChileThread1(countDownLatch);
        ChileThread2 chileThread2 = new ChileThread2(countDownLatch);
        ChileThread3 chileThread3 = new ChileThread3(countDownLatch);
        motherThread.start();
        chileThread1.start();
        chileThread2.start();
        chileThread3.start();
    }
}

class MotherThread extends Thread {
    private CountDownLatch countDownLatch;
    public MotherThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        //1.等待
        try {
            //当计数器变成0的时候,会自动唤醒这里等待的线程。
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //2.收拾碗筷
        System.out.println("妈妈在收拾碗筷");
    }
}

class ChileThread1 extends Thread {

    private CountDownLatch countDownLatch;
    public ChileThread1(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        //1.吃饺子
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "在吃第" + i + "个饺子");
        }
        //2.吃完说一声
        //每一次countDown方法的时候,就让计数器-1
        countDownLatch.countDown();
    }
}

class ChileThread2 extends Thread {
    private CountDownLatch countDownLatch;
    public ChileThread2(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        //1.吃饺子
        for (int i = 1; i <= 15; i++) {
            System.out.println(getName() + "在吃第" + i + "个饺子");
        }
        //2.吃完说一声
        //每一次countDown方法的时候,就让计数器-1
        countDownLatch.countDown();
    }
}

class ChileThread3 extends Thread {
    private CountDownLatch countDownLatch;
    public ChileThread3(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        //1.吃饺子
        for (int i = 1; i <= 20; i++) {
            System.out.println(getName() + "在吃第" + i + "个饺子");
        }
        //2.吃完说一声
        //每一次countDown方法的时候,就让计数器-1
        countDownLatch.countDown();
    }
}

并发工具类——Semaphore

使用场景 : 可以控制访问特定资源的线程数量 。

方法解释
public Semaphore(int permits)创建管理员对象,设置最多的通行证数(即最多允许多少线程执行)
public void  acquire()获取通行证
public void release()当前线程执行完毕,释放通行证

例子:某通道一次性只允许两辆车通过

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable myRunnable = new MyRunnable();
        for (int i = 0; i < 10; i++) {
            new Thread(myRunnable).start();
        }
    }
}

class MyRunnable implements Runnable {
    //1.创建通行证管理员对象
    private Semaphore semaphore = new Semaphore(2);

    @Override
    public void run() {
        //2.获得通行证
        try {
            semaphore.acquire();
            //3.开始行驶
            System.out.println("获得了通行证开始行驶");
            Thread.sleep(2000);
            System.out.println("归还通行证");
            //4.归还通行证
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Java多线程并发编程系列-01 线程基本概念、创建、使用 本文介绍了并发编程中线程的基本概念与使用方式。首先阐述了进程与线程的区别,指出线程是CPU调度的基本单位,而进程是资源分配的单位。接着分析了多线程的优势(提高CPU利用率)和局限性(线程安全问题、切换开销)。文章对比了串行、并行和并发的概念,并详细讨论了同步/异步、阻塞/非阻塞四种组合模式。在实践部分,介绍了三种创建线程的方法:继承Thread类、实现Runnable接口、实现Callable接口配合FutureTask,以及线程池方式。最后解析了Java线程的6种状态(NEW、RUNNABLE、BLOCK 阅读详情

相关推荐

基础多线程高并发问题的解决方案

在学习多线程的时候,会知道在java的实际开发中经常出现高并发的问题,也就是同一资源或者说网站被多线程或者说多用户同时访问导致的线程不安全,易引起网站挂掉的情况称之为高并发。 在基础学习中,我们简单的从多线程的角度去了解最基础的并发解决方案。 还是以多窗口卖票为例 一个Thread的子类完成卖票方法。 这里在测试类中可以开启多个线程来访问同一个成员变量t,会导致卖票数量不精确种种情况。 用第一个解决方案: 同步代码块synchronized(){} 其中()内的参数必须是唯一性。字节码对象也可以,一个是字符

weixin_49323255的博客 1636

多线程高并发

1.JMM数据原子操作 read(读取)∶从主内存读取数据 load(载入):将主内存读取到的数据写入工作内存 use(使用):从工作内存读取数据来计算 assign(赋值):将计算好的值重新赋值到工作内存中 store(存储):将工作内存数据写入主内存 write(写入):将store过去的变量值赋值给主内存中的变量 lock(锁定):将主内存变量加锁,标识为线程独占状态 unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量 2.来看volatile关键字 (1)启动两个线程 p

ZHANGLIZENG的博客 2255

【多线程】Java高并发基础

一、摩尔定律的失效和多线程的发展 摩尔定律的思想是,预计18个月会将芯片的性能提高一倍。但是在2004年,Intel CEO宣布Intel彻底取消4G Hz计划,至此摩尔定律在芯片上的发展已经失效了。如果计算机没有办法继续提高单个CPU的性能,那么就在一个CPU里面塞很多的核进去,因此多核CPU兴起。在单核时代,多线程就有很广泛的应用,这时候多线程大多用于降低阻塞。多线程的作用不只是用作并行计算,

不忘初心 2514

【Java进阶】多线程&高并发(一)<线程概述>

在Java中,创建一个线程就是创建一个Thread类(子类)的对象(实例)Thread类有两个常用的构造方法:Thread()和Thread(Runnable),对应的创建线程的两种方式,这两种创建线程的方式没有本质的区别① 定义Thread类的子类② 定义一个Runnable接口的实现类。

行为产生结果 1751

【Java多线程】高并发修炼基础之高并发必须了解的概念

- **`单核CPU(单处理器)上`,只可能存在`并发`而不可能存在并行。** - **`并行在多处理器(多CPU)系统中存在,而并发可以在单处理器和多处理器系统中都存在`**,并发能够在单处理器系统中存在是因为 "并发是并行的假象",**`并行要求程序能够同时执行多个操作`**,而 **`并发只是要求程序假装同时执行多个操作`(每个小时间片执行一个操作,多个操作快速切换执行)** - **并发是`“伪并行”`,看似并行,而实际上还是一个`CPU(单处理器)`执行一切事物,只是`切换的太快`,我们没法察觉

墩墩分墩 642

Unity AR开发实战:从Vuforia图片识别到实体识别全流程解析

增强现实(AR)技术通过计算机视觉算法将虚拟信息叠加到现实世界,其核心原理在于对现实环境的感知与理解。在移动端AR开发中,图像识别与物体识别是实现虚实融合的关键技术,它们决定了虚拟内容能否准确、稳定地与现实对象对齐。Vuforia作为成熟的商业AR SDK,为开发者提供了从平面图像识别(Image Target)到三维实体识别(Model Target)的完整解决方案,极大降低了计算机视觉技术的应用门槛。在工程实践中,Vuforia通过封装复杂的特征提取与匹配算法,使开发者能够专注于交互设计与内容制作,广泛

weixin_30972263的博客 303

精通 Java 多线程:从基础到高并发应用的优化与设计

Java 多线程编程是高性能、可扩展应用开发的重要组成部分。从线程的创建、同步到并发优化,开发者需要掌握多种工具和技术,灵活应对高并发场景中的各种挑战。通过线程池的使用、无锁编程和合适的数据结构,能够有效提升系统的性能,避免资源竞争和死锁问题。掌握这些基础和高级概念,不仅能让你在多线程编程中游刃有余,也能为你设计和优化高并发、高可用的系统提供强大的支持。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1441

C语言头文件完全解析(连“#”我都给你讲明白)

C语言头文件完全解析 引言 本文将尽量详细的介绍c语言的头文件所包含的知识,如有错误,还望指正。 萌新们肯定疑惑为什么每次打代码都要写一个所谓的头文件(如:#include<stdio.h>),这里先简单让大家有个了解,你所用的 printf("%d",a)就是这个 stdio里的东西,如果没有这个头文件,编译器都不知道 printf是个什么东东。所以头文件就是一个准备好的大仓库,你可...

CRAZY_eyes的博客 7万+

Java高并发编程之第一阶段,多线程基础深入浅出

汪文君高并发编程第一阶段01讲-课程大纲及主要内容介绍 汪文君高并发编程第一阶段02讲-简单介绍什么是线程 汪文君高并发编程第一阶段03讲-创建并启动线程 汪文君高并发编程第一阶段04讲-线程生命周期以及start方法源码剖析 汪文君高并发编程第一阶段05讲-采用多线程方式模拟银行排队叫号 汪文君高并发编程第一阶段06讲-用Runnable接口将线程的逻辑执行单元从控制中抽取出来 汪文君高并发编程第一阶段07讲-策略模式在Thread和Runnable中的应用分析 汪文君高并发编程第一阶段08讲-构造Thr

清心寡欲,做一个专注的程序员 1万+

echarts的坐标轴处理算法

参考原文:https://www.cnblogs.com/vicky-li/p/11653415.html。2、可用于双轴的情况,指定splitNumber,保证左右两边的线没有交叉显示的情况。1、设置echarts显示的最大最小值,让数据显示比较有起伏。处理echarts的一些问题。

HYeeee的博客 801

AFE芯片做什么用的?AFE(Analog Front End) 模拟前端

AFE采样精度的影响因素非常多,需要综合考虑各种因素,并根据实际应用场景来选择合适的参数和方法。通过减少噪声、提高采样频率、调节放大器增益、降低电源噪声、优化传输线路、进行温度补偿、选择合适的ADC分辨率、根据信号类型选择合适的AFE参数、进行器件匹配和校准等方法,可以提高AFE的采样精度,从而更好地满足各种应用需求。版权声明:本文为优快云博主「czf.Forver」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

学海无涯的专栏 4万+

多线程与高并发基础(基础篇一)

多线程与高并发基础 一、程序、进程、线程、纤程(协程)的定义 程序:可执行文件 进程:程序中资源分配的最小单元 纤程:程序中资源调度的最小单元 携程:用户管理的纤程 二、单核系统做多线程设计是否有意义 有 纤程分为io密集型和CPU密集型,对于IO密集型纤程,cpu多数时间处于空闲状态,适当的多线程设计可提供cpu利用率,充分压榨cpu性能,提高处理能力。 三、是不是线程数设计越多越好? 不是 cpu只负责操作指令的执行,操作系统负责任务的调度。OS在多个线程间切换会消耗一定的性能,线程数过度会消耗过多性能

weixin_42533448的博客 351

cursor AI代码编辑器IDE对接deepseek模型api;字节ai编辑器trae

参考: https://www.cursor.com/ https://platform.deepseek.com/https://www.cursor.com/下载安装后,注册账号登录即可 再高级里选择设置 再模型这注册模型: 模型名称一定要填正确,url地址填好,最后填上秘钥验证

weixin_42357472的博客 1万+

java多线程写入mysql_高并发下Java多线程编程基础

Java线程同步与异步线程池无锁化的实现方案分布锁的实现方案分享的目的:进一步掌握多线程编程和应用的技巧,希望对大家在平时的开发中应对高并发编程有所帮助Java线程同步与异步1. 同步相关的方法有wait, notify, notifyAll2. 关键字synchronized3. JDK锁的框架AQS (AbstractQueuedSynchronizer)4. AQS的实现类java.util...

weixin_36040903的博客 853

PCL 最小生成树用于法向量定向【2025最新版】

PCL 最小代价生成树用于点云法向量的定向。博客长期更新,本文最近更新时间为:2025年9月15日。

点云侠的博客 2068

ICP-10111气压传感器原理图设计,已量产(压力传感器)

在最近的室内导航定位项目中,我们需要一款能捕捉厘米级垂直高度变化的气压传感器 —— 毕竟室内环境里,哪怕是单个台阶的高度差都得精准识别。对比了几款器件后,最终选定了 ICP-10111:它 ±1Pa 的压差精度刚好能把高度差分辨到 8.5cm,0.4Pa 的低压力噪声能让数据输出足够稳定,再加上 0.5Pa/℃的低温漂特性,长期工作的可靠性也能满足需求。下面聊聊这套适配 ICP-10111 的硬件电路设计细节。

不脱发的程序猿 334

高并发篇_1 多线程和高并发基础

高并发篇_1 多线程和高并发基础 田超凡 1 进程和线程 进程是系统运行程序的基本单位,一个进程至少有一个组成,一个进程中只有一条主线程,其他线程是子线程。 CPU通过划分时间片从而实现多线程并发交替执行(CPU上下文切换执行),并不是每一时刻多线程同时执行的 线程工作内容的不同导致对CPU的消耗程度是不一样的,常见的线程工作模式分为CPU密集型线程和IO密集型线程两类: CPU密集型线程指的是线程运行过程中需要频繁消耗CPU进行运算,IO密集型线程指的是线程运行过程中需要频繁进行IO操作(频繁

田宝宝的博客 575

GlobalBuildingAtlas 建筑物白模数据下载

GlobalBuildingAtlas基于OSM开源数据构建全球建筑物3D白模,提供在线浏览和下载服务。官方资源包括GitHub项目地址和TUM数据库链接,支持行政区划检索建筑物数据。

一醉千秋的专栏 541

多线程与高并发(1)- 线程基础、并发特性、锁、JUC工具

文章目录第一章、线程基础知识一、基础概念1、什么是程序?2、什么是进程?3、什么是线程?4、什么是线程的切换(Context Switch)?5、单核CPU 设定多线程是否有意义?6、工作线程数是不是设置的越大越好?7、工作线程数(线程池中线程数量)设多少合适?二、创建线程的3 种方法1、继承Thread类 重写run方法2、实现Runnable接口 重写run方法3、实现Callable 重写call方法,配合FutureTask4、基于线程池构建线程三、线程状态四、线程的常用方法1、获取当前线程2、线程

qq_32091929的博客 1622

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

【多线程与高并发】- 线程基础与状态

多线程也是一项比较重要的内容,虽然CRUD不太会接触到,但是,在一些相关场景可能会有某些问题是由于线程导致的。简单理解就是一个进程中包含了许多个线程。

qq_43843951的博客 21万+

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

《Java多线程与高并发实战从零基础到工程应用》

Java提供优先级控制:`MAX_PRIORITY`(10)、`NORM_PRIORITY`(5)、`MIN_PRIORITY`(1),但受限于操作系统调度策略。- 新建(New)→ 就绪(Runnable)→ 运行(Running)→ 阻塞/等待(Blocked/Waiting)→ 终止(Terminated)。- 关键方法:`start()`启动线程,`join()`等待线程结束,`sleep()`临时阻塞。- 线程 vs 进程:线程是进程的执行单元,共享进程资源,轻量级切换。

ZefCaEAU的博客 415

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

01、多线程高并发基础

本篇文章主要讲一些和线程,进程,的基础常识问题。

qq_21451945的博客 925

Java多线程编程详解:从基础到高并发实践

Java多线程编程核心与实践 摘要:本文系统讲解了Java多线程编程的核心概念与实现方法。首先比较线程与进程的区别,分析多线程的应用价值;详细介绍Thread类继承、Runnable接口实现和Callable+Future三种创建方式;解析线程六大生命周期状态及其转换;探讨synchronized、ReentrantLock等同步机制及生产者-消费者模型;提供高并发场景下的线程池配置、并发集合使用等优化策略;最后剖析JVM线程模型。通过代码示例和实践方案,帮助开发者掌握从基础到高并发的多线程编程技术。

专注于与编程相关的知识内容分享 580

多线程与高并发——线程基础、方法

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。

weixin_43711204的博客 458

解密Java多线程从并发基础到高并发架构的实战演进

线程池通过Executor框架实现,能有效管理线程资源,避免频繁创建和销毁线程的开销。在多线程环境下,多个线程可能同时访问共享资源,导致数据不一致的问题。原子类(如AtomicInteger)和volatile关键字也是保证线程安全的重要工具,它们能处理简单的原子操作和内存可见性问题。此外,基本的线程控制方法如start()、run()、sleep()和join()为线程提供了基本的操作能力。通过性能监控工具(如JStack和VisualVM)分析线程状态,优化锁粒度和减少竞争,可以实现高效的多线程应用。

mrLiangshuang的博客 164
上一篇: Spring Cloud集成Nacos作为注册中心
下一篇: Java多线程下——各类锁的详解
java_lujj
博客等级 码龄4年 18粉丝 152原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值