Java并发体系-第三阶段-JUC并发包-[1]

news2024/12/30 2:23:15

AtomicXXXFieldUpdater

算是一个小补充

简介

public class AtomicIntegerFieldUpdaterTest {

    public static void main(String[] args) {
        AtomicIntegerFieldUpdater<Test> updater = AtomicIntegerFieldUpdater.newUpdater(Test.class, "value");
        Test ts = new Test();

        IntStream.rangeClosed(0, 2).forEach(item -> {            
        	new Thread(() -> {
                int value = updater.getAndIncrement(ts);
                System.out.println("oldV: " + value);
            }).start();
        });
    }
 }

class Test {
    volatile int value;
}

1、以AtomicIntegerFieldUpdater为例,看上面代码。Test类的value属性被volatile修饰了,但是volatile只能保证可见性和有序性。在以往的文章里我们讲过是可以通过volatile+CAS同时解决可见性,有序性,原子性。

2、JUC提供了一种新的功能来保证原子性,AtomicXXXFieldUpdater修饰的类对应的字段,在进行更新时同样可以保证原子性。

使用场景

  • 想让类的属性操作具备原子性,

  • 但是不想使用锁。

  • 大量需要原子类型修饰的对象,相比较比较耗费内存

举个例子:

如果你想要保证原子性,一般是使用AtomicStampedReference<Node>来包装Node对象。

import org.apache.lucene.util.RamUsageEstimator;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @Author: youthlql-吕
 * @Date: 2020/10/11 16:51
 * <p>
 * 功能描述: 计算对象内存大小
 */
public class Calculate_Java_Object_Size {

    public static void main(String[] args) {
        Node node = new Node();
        //计算指定对象本身在堆空间的大小,单位字节
        long b = RamUsageEstimator.shallowSizeOf(node);
        System.out.println(b);

        AtomicStampedReference<Node> nodeAtomicStampedReference = new AtomicStampedReference<Node>(node,0);
        long c = RamUsageEstimator.shallowSizeOf(nodeAtomicStampedReference);
        System.out.println(c + b);
    }

    /**
     * 功能描述: Api说明
     */
    public static void Api(Object o){
        //下面三个方法参数都是 Object类型

        //计算指定对象及其引用树上的所有对象的综合大小,单位字节
        long a = RamUsageEstimator.sizeOf(o);
        System.out.println(a);

        //计算指定对象本身在堆空间的大小,单位字节
        long b = RamUsageEstimator.shallowSizeOf(o);
        System.out.println(b);

        //计算指定对象及其引用树上的所有对象的综合大小,返回可读的结果,如:2KB
        String c = RamUsageEstimator.humanSizeOf(o);
        System.out.println(c);
    }
}

class Node {
    Node pre;
    Node next;
    Integer value;
}

输出结果:

24

40

可以看出如果用了AtomicStampedReference<Node>,会多出16个字节。如果对象有10000个,那么会多出很多字节。生产过程中的内存都是很贵的。为了减少内存消耗,同时可以保证原子性,就可以使用AtomicXXXFieldUpdater

CountDownLatch

简介

  • countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。(重点理解:等待一词)
  • 是通过一个 state(相当于计数器)的东西来实现的,计数器的初始值是 线程的数量或者任务的数量
  • 每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
  • CountDownLatch的方便之处在于,你可以在一个线程中使用, 也可以在多个线程上使用,一切只依据状态值,这样便不会受限于任何的场景。

使用场景一

需求

  • 可能刚从数据库读取了一批数据
  • 利用并发处理这批数据
  • 当所有的数据处理完成后,再去执行后面的操作

解决方案

  • 第一种:可以利用 join 的方法,但是在线程池中,比较麻烦。
  • 第二种:利用线程池的awaitTermination,阻塞一段时间。
    • 当使用awaitTermination时,主线程会处于一种等待的状态,等待线程池中所有的线程都运行完毕后才继续运行。
  • 第三种:利用CountDownLatch,每当任务完成一个,就计数器减一。
package _05_AQS;

import java.util.concurrent.CountDownLatch;

/**
 * @Author: youthlql-吕
 * @Date: 2019/9/26 10:05
 * <p>
 * 功能描述:
 */
public class Video32 {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
             new Thread(() ->{
                System.out.println("\t\t" + Thread.currentThread().getName() + "处理完毕~~~");
                countDownLatch.countDown();
                System.out.println("非调用者线程-" + Thread.currentThread().getName() + "-还可以干点其他事");
             }, Country.forEach_Country(i + 1).getCountryName()).start();
        }

        countDownLatch.await();
        System.out.println("-----------------------------");
        System.out.println("\t 所有任务都已经处理完毕,可以往后执行了!");

    }
}

enum Country{

    ONE(1,"1号任务"),
    TWO(2,"2号任务"),
    THREE(3,"3号任务"),
    FOUR(4,"4号任务"),
    FIVE(5,"5号任务"),
    SIX(6,"6号任务");

    private Integer index;
    private String countryName;


    public static Country forEach_Country(Integer index){
        Country[] values = Country.values();
        for (Country c: values) {
            if(c.getIndex() == index){
                return c;
            }
        }
        return null;
    }

    Country(Integer index, String countryName) {
        this.index = index;
        this.countryName = countryName;
    }

    public Integer getIndex() {
        return index;
    }

    public String getCountryName() {
        return countryName;
    }

}

结果

		1号任务处理完毕~~~
		5号任务处理完毕~~~
非调用者线程-5号任务-还可以干点其他事
		6号任务处理完毕~~~
		3号任务处理完毕~~~
非调用者线程-3号任务-还可以干点其他事
		4号任务处理完毕~~~
		2号任务处理完毕~~~
非调用者线程-4号任务-还可以干点其他事
非调用者线程-6号任务-还可以干点其他事
非调用者线程-1号任务-还可以干点其他事
-----------------------------
	 所有任务都已经处理完毕,可以往后执行了!
非调用者线程-2号任务-还可以干点其他事

因为countdown只会阻塞调用者,其它线程干完任务就可以干其他事。这里的调用者线程就是main线程。

使用场景二

需求

  • 多个线程协同工作
  • 多个线程需要等待其他线程的工作之后,再进行其后续工作。
  • 被唤醒后继续执行其他操作
public class CountDownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " Do some initial working.");
            try {
                Thread.sleep(1000);
                latch.await();
                System.out.println(Thread.currentThread().getName() + " Do other working.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " Do some initial working.");
            try {
                Thread.sleep(1000);
                latch.await();
                System.out.println(Thread.currentThread().getName() + " Do other working.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            System.out.println("asyn prepare for some data.");
            try {
                Thread.sleep(2000);
                System.out.println("Data prepare for done.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                latch.countDown();
            }
        }).start();

    }

}

结果

Thread-0 Do some initial working.
Thread-1 Do some initial working.
asyn prepare for some data.
Data prepare for done.
Thread-0 Do other working.
Thread-1 Do other working.

总体来说意思都差不多

常用API

构造方法只有一个

  • CountDownLatch(int count) :构造一个以给定计数

实例方法

  • public void await()
    • 当前线程等到锁存器计数到零
    • 可以被 打断
  • public boolean await(long timeout,TimeUnit unit)
    • 等待一段时间
    • timeout - 等待的最长时间 , unit - timeout参数的时间单位
    • 如果 指定的等待时间过去,则返回值false
    • 如果 计数达到零,则方法返回值为true
  • public void countDown()
    • 减少锁存器的计数, 如果计数达到零,释放所有等待的线程
  • public long getCount()
    • 返回当前计数

给离散的平行任务增加逻辑层次关系

需求

  • 并发的从很多的数据库读取大量数据
  • 在读取数据的过程中,某个表可能会出现: 数据丢失、数据精度丢失、数据大小不匹配。
  • 需要进行对数据的各个情况进行检测,这个检测是并发的完成的
  • 所以需要控制如果一个表所有的情况检测完成,再进行后续的操作

解决

  • 利用 CountDownLatch的计数器
  • 每当一个检测完成,计数器减一
  • 如果计数为0,执行后面操作
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: youthlql-吕
 * @Date: 2020/10/11 21:05
 * <p>
 * 功能描述:
 */
public class test {
    private static final Random RANDOM = new Random();

    public static void main(String[] args) throws Exception {
        //2个事件请求,这里只演示校验数据行,和数据schema
        Event[] events = {new Event(1), new Event(2)};

        ExecutorService service = Executors.newFixedThreadPool(5);

        for (Event event : events) {
            //2个事件请求中,可能涉及多个表,可以再分成多个表
            List<Table> tables = capture(event);
            for (Table table : tables) {
                TaskBatch taskBatch = new TaskBatch(2);
                TrustSourceColumns sourceColumns = new TrustSourceColumns(table, taskBatch);
                TrustSourceRecordCount recordCount = new TrustSourceRecordCount(table, taskBatch);
                service.submit(sourceColumns);
                service.submit(recordCount);
            }
        }
    }

    static class Event {
        private int id;

        Event(int id) {
            this.id = id;
        }
    }

    interface Watcher {
        void done(Table table);
    }

    static class TaskBatch implements Watcher {

        private final CountDownLatch latch;

        TaskBatch(int size) {
            this.latch = new CountDownLatch(size);
        }

        @Override
        public void done(Table table) {
            latch.countDown();
            if (latch.getCount() == 0) {
                System.out.println("The table " + table.tableName + " finished work , " + table.toString());
            }
        }
    }

    static class Table {
        String tableName;
        long sourceRecordCount;
        long targetCount;
        String columnSchema = "columnXXXType = varchar";

        String targetColumnSchema = "";

        public Table(String tableName, long sourceRecordCount) {
            this.tableName = tableName;
            this.sourceRecordCount = sourceRecordCount;

        }

        @Override
        public String toString() {
            return "Table{" +
                    "tableName='" + tableName + '\'' +
                    ", sourceRecordCount=" + sourceRecordCount +
                    ", targetCount=" + targetCount +
                    ", columnSchema='" + columnSchema + '\'' +
                    ", targetColumnSchema='" + targetColumnSchema + '\'' +
                    '}';
        }
    }

    private static List<Table> capture(Event event) {
        List<Table> list = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            list.add(new Table("table-" + event.id + "-" + i, i * 1000));
        }
        return list;
    }

    //校验数据行数是否一致
    static class TrustSourceRecordCount implements Runnable {

        private final Table table;

        private final TaskBatch taskBatch;

        TrustSourceRecordCount(Table table, TaskBatch taskBatch) {
            this.table = table;
            this.taskBatch = taskBatch;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(RANDOM.nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            table.targetCount = table.sourceRecordCount;

            taskBatch.done(table);

        }

    }
    //校验数据列属性以及对应的表
    static class TrustSourceColumns implements Runnable {

        private final Table table;

        private final TaskBatch taskBatch;

        TrustSourceColumns(Table table, TaskBatch taskBatch) {
            this.table = table;
            this.taskBatch = taskBatch;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(RANDOM.nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            table.targetColumnSchema = table.columnSchema;

            taskBatch.done(table);
        }

    }
}

结果

The table table-1-1 finished work , Table{tableName='table-1-1', sourceRecordCount=1000, targetCount=1000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'}
The table table-1-0 finished work , Table{tableName='table-1-0', sourceRecordCount=0, targetCount=0, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'}
The table table-2-0 finished work , Table{tableName='table-2-0', sourceRecordCount=0, targetCount=0, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'}
The table table-1-2 finished work , Table{tableName='table-1-2', sourceRecordCount=2000, targetCount=2000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'}
The table table-1-3 finished work , Table{tableName='table-1-3', sourceRecordCount=3000, targetCount=3000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'}
The table table-2-1 finished work , Table{tableName='table-2-1', sourceRecordCount=1000, targetCount=1000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'}
The table table-2-2 finished work , Table{tableName='table-2-2', sourceRecordCount=2000, targetCount=2000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'}
The table table-2-3 finished work , Table{tableName='table-2-3', sourceRecordCount=3000, targetCount=3000, columnSchema='columnXXXType = varchar', targetColumnSchema='columnXXXType = varchar'}

利用CountDownLatch实现回调函数

实现:

  • 在每个线程使计数器减一的时候,利用getCount判断,当前是否所有线程任务执行完成
public class CyclicBarrierTest {
    static class MyCountDownLatch extends CountDownLatch{
        private final Runnable runnable;

        public MyCountDownLatch(int count,Runnable runnable) {
            super(count);
            this.runnable = runnable;
        }

        @Override
        public void countDown() {
            super.countDown();
            if (getCount()==0){
                this.runnable.run();
            }
        }

    }

    public static void main(String[] args) {

        final MyCountDownLatch latch = new MyCountDownLatch(2, ()->{
            System.out.println("All of work finish done. This is call back.");
        });

        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    latch.countDown();
                    System.out.println(getName() +  " finished.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName() +  " finished.");
            }
        }.start();

    }
}

CyclicBarrier

引出

  • 栅栏类似于闭锁(CountDownLatch),它能阻塞一组线程直到某个事件的发生(其他线程完成调用countDown)。栅栏与闭锁的关键区别在于, 所有的线程必须同时到达栅栏位置,才能继续执行(所有线程到达栅栏初等待指令一起执行)。闭锁用于等待事件,而 栅栏用于等待其他线程。(不理解看下面的例子)

  • CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。 当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。 如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。

package _05_AQS;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @Author: youthlql-吕
 * @Date: 2019/9/26 10:34
 * <p>
 * 功能描述:
 */
public class Video33 {

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> System.out.println("收集到7颗龙珠,召唤神龙"));

        for (int i = 0; i < 7; i++) {
             final int temp = i + 1;
             new Thread(() ->{
                System.out.println(Thread.currentThread().getName() + "\t收集到第" + temp + "颗龙珠");
                 try {
                     int await = cyclicBarrier.await();
                     System.out.println("还剩几个:" + await);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 } catch (BrokenBarrierException e) {
                     e.printStackTrace();
                 }
             },"线程" + String.valueOf(i)).start();
        }
    }
}

结果:

线程0	收集到第1颗龙珠
线程2	收集到第3颗龙珠
线程3	收集到第4颗龙珠
线程1	收集到第2颗龙珠
线程4	收集到第5颗龙珠
线程5	收集到第6颗龙珠
线程6	收集到第7颗龙珠
收集到7颗龙珠,召唤神龙
还剩几个:0
还剩几个:6
还剩几个:3
还剩几个:4
还剩几个:5
还剩几个:1
还剩几个:2

可以明显的看到cyclicBarrier会阻塞所有线程,和countdownlatch不一样。

API使用

构造方法

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
  • parties 是参与线程的个数
  • 第二个构造方法有一个 Runnable 参数,这个参数的意思是 *最后一个到达线程要做的任务

重要方法

public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
  • 线程调用 await() 表示自己已经到达栅栏
  • BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时

其他方法

public void reset()
  • 将屏障重置为初始状态。 如果任何一方正在等待屏障,他们将返回 BrokenBarrierException
  • 这样就可以重复利用这个屏障

CyclicBarrier 与 CountDownLatch 区别

  • CountDownLatch 是一次性的。 CyclicBarrier 是可循环利用的
  • CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束CyclicBarrier 参与的线程职责是一样的

牛客-京东2019春招Java开发笔试卷-T27

https://blog.csdn.net/liangyihuai/article/details/83106584

从jdk作者设计的目的来看,javadoc是这么描述它们的:

  • CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

  • CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.

  • 从javadoc的描述可以得出: CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行; CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

  • 对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。 而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须互相等待,然后继续一起执行。 CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。 按照这个题目的描述等所有线程都到达了这一个阀门处,再一起执行,此题强调的是,一起继续执行,我认为 选B 比较合理!

  • 像上文中CountDownLatch的例子,main线程在等待,其余6个线程任务做完之后,main线程才苏醒干后面的事。因为countdown只会阻塞调用者,其它线程干完任务就可以干其他事。这里的调用者线程就是main线程。

  • 像上文中CyclicBarrier的例子,是7个线程互相等待对方,7个任务都完成后,执行注册的回调任务。

Exchanger

简介

  • 用于两个工作线程之间交换数据的封装工具类
  • 简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则 *第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据

Exchanger<v></v> 泛型类型,其中 V 表示可交换的数据类型

简单的应用

import java.util.concurrent.*;

/**
 * @Author: youthlql-吕
 * @Date: 2020/10/11 21:05
 * <p>
 * 功能描述:
 */
public class ExchangerTest {

    public static void main(String[] args) {
        final Exchanger<String> exchanger = new Exchanger<>();


        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " start . ");
            try {

                /**
                 * 如果这里睡200ms的话,应该是B线程先拿出数据,然后B线程等待A线程。因为是B先给的数据,
                 * 所以最后A线程会先拿到B给的数据,也就是先打印
                 */
                TimeUnit.MILLISECONDS.sleep(200);
                String exchange = exchanger.exchange("I am come from T-A");
                System.out.println(Thread.currentThread().getName() + " get value : " + exchange);
                System.out.println(Thread.currentThread().getName() + " end . ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"A").start();


        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " start . ");
            try {
                String exchange = exchanger.exchange("I am come from T-B");
                System.out.println(Thread.currentThread().getName() + " get value : " + exchange);
                System.out.println(Thread.currentThread().getName() + " end . ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

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

结果:

A start . 
B start . 
A get value : I am come from T-B
B get value : I am come from T-A
B end . 
A end . 

方法介绍

public V exchange(V x) throws InterruptedException
  • 等待另一个线程到达此交换点,然后将给定对象传输给它,接收其对象作为回报。
  • 可以被打断
  • 如果已经有个线程正在等待了,则直接交换数据

数据的分析

import java.util.concurrent.*;


/**
 * @Author: youthlql-吕
 * @Date: 2020/10/11 21:05
 * <p>
 * 功能描述:
 */
public class test {
    public static void main(String[] args) {


        final Exchanger<Object> exchanger = new Exchanger<>();

        new Thread(() -> {
            Object Aobj = new Object();
            System.out.println("A将会发送:" + Aobj);
            try {
                Object Robj = exchanger.exchange(Aobj);
                System.out.println("A接收的:" + Robj);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }, "A").start();

        new Thread(() -> {
            Object Bobj = new Object();
            System.out.println("B将会发送:" + Bobj);
            try {
                Object Robj = exchanger.exchange(Bobj);
                System.out.println("B接收的:" + Robj);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

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

结果

A将会发送:java.lang.Object@7be2d776
B将会发送:java.lang.Object@5eca8d3a
B接收的:java.lang.Object@7be2d776
A接收的:java.lang.Object@5eca8d3a

从这个例子可以看出一个很严重的问题:发送的对象和接收的对象是同一个对象,可能会用严重的线程安全问题。

ReentrantLock

简介

当多个线程需要访问某个公共资源的时候,我们知道需要通过加锁来保证资源的访问不会出问题。java提供了两种方式来加锁:

  • 一种是关键字: synchronized,一种是concurrent包下的基于API实现的。
  • synchronized是 JVM底层支持的,而concurrent包则是 jdk实现。

公平锁和非公平锁

  • 当有线程竞争锁时,当前线程会首先尝试获得锁而不是在队列中进行排队等候,这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来

  • 默认情况下为非公平锁。

  • 锁的存储结构就两个东西:“双向链表” + “int类型状态”。ReenTrantLock的实现是一种自旋锁, 通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

构造方法

public ReentrantLock()
public ReentrantLock(boolean fair)
  • fair:该参数为true时,会尽力维持公平

获得锁

public void lock()
public void lockInterruptibly()  throws InterruptedException
public boolean tryLock()
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

lock

  • 正常的获取锁,如果没有获得到锁,就会被阻塞

lockInterruptibly

  • 获取锁,如果没有获得到锁,就会被阻塞
  • 可以被打断

tryLock

  • 如果获得到锁,返回true
  • 如果没有获得到锁,返回false
  • timeout:表示等待的时间
  • tryLock()在获取的锁的时候,不会考虑此时是否有其他线程在等待,会破坏公平。
  • 如果你希望遵守公平设置此锁,然后用 tryLock(0, TimeUnit.SECONDS) 这几乎是等效的(它也检测中断)。

释放锁

public void unlock()
  • 尝试释放此锁。
  • 必须是锁的持有者才能释放锁

锁的调试

protected Thread getOwner()
public final boolean hasQueuedThreads()
public final int getQueueLength()
protected Collection<Thread> getQueuedThreads()

getOwner

  • 返回持有锁的线程

hasQueuedThreads

  • 是否有线程在等待获取锁

getQueueLength

  • 获取等待锁的线程数目

getQueuedThreads

  • 返回正在等待的线程集合

Lock和synchronized的区别

底层实现

  • Lock基于 AQS实现,通过state和一个CLH队列来维护锁的获取与释放
  • synchronized需要通过 monitor,经历一个从用户态到内核态的转变过程,更加耗时

其他区别

synchronizedLock
是java内置关键字,在jvm层面是个java类
无法判断是否获取锁的状态可以判断是否获取到锁
会自动释放锁需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁
线程会一直等待下去如果尝试获取不到锁,线程可以不用一直等待就结束

总结来说
synchronized的锁可重入、不可中断、非公平。而Lock锁可重入、可判断、可公平(两者皆可)。

Semaphore

简介

  • Semaphore是一种在多线程环境下使用的设施,该设施负责协调各个线程,以保证它们能够正确、合理的使用公共资源的设施,也是操作系统中用于控制进程同步互斥的量。

  • Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数

  • Semaphore用于限制可以访问某些资源(物理或逻辑的)的线程数目,他维护了一个许可证集合,有多少资源需要限制就维护多少许可证集合,假如这里有N个资源,那就对应于N个许可证,同一时刻也只能有N个线程访问。

  • 一个线程获取许可证就调用acquire方法,用完了释放资源就调用release方法。

除了JDK定义的哪些锁,Semaphore也可以定义锁。Semaphore可以做的功能相当的多,比如秒杀限流

用Semaphore自定义Lock

public class SemaphoreTest {

    public static void main(String[] args) {
        final SemaphoreLock lock = new SemaphoreLock();
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + " get the lock.");
                    Thread.sleep(3000);
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    System.out.println(Thread.currentThread().getName() + " release the lock.");
                    lock.unlock();
                }
            }).start();
        }

    }
        static class SemaphoreLock{
        private final Semaphore semaphore = new Semaphore(1);

        public void lock() throws InterruptedException {
            semaphore.acquire();
        }

        public void unlock()  {
            semaphore.release();
        }
    }
}

结果

Thread-0 get the lock.
Thread-0 release the lock.
Thread-1 get the lock.
Thread-1 release the lock.

跟synchronized的区别

  • 可以看出最大的区别就是可以控制多个线程访问多份资源,而不是只用一个线程访问一份资源

用Semaphore做限流

Semaphore可以控制多个线程访问多份资源,而不是只用一个线程访问一份资源,所以在限流方面也有应用。

package _05_AQS;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * @Author: youthlql-吕
 * @Date: 2019/9/26 10:58
 * <p>
 * 功能描述:
 */
public class Video34 {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < 10; i++) {
             new Thread(() ->{
                 try {
                     semaphore.acquire();
                     System.out.println(Thread.currentThread().getName() + "\t 进入抢购秒杀页面,准备抢小米9");
                     //停3秒后离开
                     try {
                         TimeUnit.SECONDS.sleep(3);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                     System.out.println(Thread.currentThread().getName() + "\t 离开抢购秒杀页面,成功抢到小米9");
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }finally {
                     semaphore.release();
                 }
             },"用户" + String.valueOf(i)).start();
        }
    }
}

结果:

用户0	 进入抢购秒杀页面,准备抢小米9
用户2	 进入抢购秒杀页面,准备抢小米9
用户1	 进入抢购秒杀页面,准备抢小米9
用户1	 离开抢购秒杀页面,成功抢到小米9
用户2	 离开抢购秒杀页面,成功抢到小米9
用户0	 离开抢购秒杀页面,成功抢到小米9
用户4	 进入抢购秒杀页面,准备抢小米9
用户3	 进入抢购秒杀页面,准备抢小米9
用户5	 进入抢购秒杀页面,准备抢小米9
用户3	 离开抢购秒杀页面,成功抢到小米9
用户4	 离开抢购秒杀页面,成功抢到小米9
用户6	 进入抢购秒杀页面,准备抢小米9
用户8	 进入抢购秒杀页面,准备抢小米9
用户5	 离开抢购秒杀页面,成功抢到小米9
用户7	 进入抢购秒杀页面,准备抢小米9

代码里限制了资源为3个,所以同一时间段只能有3个线程进来抢购小米9手机。

常用API

构造方法

public Semaphore(int permits)
public Semaphore(int permits , boolean fair)
  • permits:同一时间可以访问资源的线程数目
  • fair:尽可能的保证公平

重要方法

public void acquire() throws InterruptedException
public void release()
  • acquire()获取一个许可证,如果许可证用完了,则陷入阻塞。可以被打断。

  • release()释放一个许可证

    acquire(int permits)
    public void release(int permits)

acquire多个时,如果没有足够的许可证可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,直到发生两件事情之一:

  • 一些其他线程调用此信号量的一个release方法,当前线程旁边将分配许可证,并且可用许可证的数量满足此请求;
  • 要么一些其他线程interrupts当前线程。

release多个时,会使许可证增多,最终可能超过初始值

public boolean tryAcquire(int permits)
public boolean tryAcquire(int permits,
                          long timeout,
                          TimeUnit unit)
                   throws InterruptedException
  • 尝试去拿,拿到返回true

其他方法

public int availablePermits()
  • 返回此信号量中当前可用的许可数。

    protected Collection<Thread> getQueuedThreads()
    public final int getQueueLength()

  • getQueuedThreads返回正在阻塞的线程集合

  • getQueueLength返回阻塞获取的线程数

    public void acquireUninterruptibly()
    public void acquireUninterruptibly(int permits)

  • 可以不被打断获取许可证

    public int drainPermits()

  • 获取当前全部的许可证目标

Condition

简介(挺有意思)

  • Condition主要是用于线程通信的,也就是和Object类的wait,notify有同样的功能。不过Condition的功能更加多样,Conditon可以绑定锁,实现选择性唤醒。
  • Condition和Lock搭配使用; condition必须使用lock.newCondition();来创建condition。必须存放在Lock中。否则抛出异常。
package _07_LockAndCondition;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: youthlql-吕
 * @Date: 2019/9/26 15:41
 * <p>
 * 功能描述: A线程打印3次,B线程打印6次,C线程9次,持续两轮
 */
public class ConditionDemo {

    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(() ->{
            for (int i = 0; i < 2; i++) {
                shareResource.print3();
            }
        },"A").start();

        new Thread(() ->{
            for (int i = 0; i < 2; i++) {
                shareResource.print6();
            }
        },"B").start();

        new Thread(() ->{
            for (int i = 0; i < 2; i++) {
                shareResource.print9();
            }
        },"C").start();


    }
}

//以前是一个lock只有一个钥匙,现在是一个lock多个钥匙
class ShareResource{
    //标志位,可取值为1,2,3
    private volatile Integer flag = 1;
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void print3(){
        lock.lock();
        try {
            while(flag != 1){
                c1.await();
            }
            for (int i = 0; i < 3 ; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + (i+1));
            }
            flag = 2;
            c2.signal();//唤醒2号线程
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print6(){
        lock.lock();
        try {
            while(flag != 2){
                c2.await();
            }
            for (int i = 0; i < 6 ; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + (i+1));
            }
            flag = 3;
            c3.signal();//唤醒3号线程
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public void print9(){
        lock.lock();
        try {
            while(flag != 3){
                c3.await();
            }
            for (int i = 0; i < 9 ; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + (i+1));
            }
            flag = 1;
            c1.signal();//唤醒1号线程
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


}

结果:

A	1
A	2
A	3
B	1
B	2
B	3
B	4
B	5
B	6
C	1
C	2
C	3
C	4
C	5
C	6
C	7
C	8
C	9
A	1
A	2
A	3
B	1
B	2
B	3
B	4
B	5
B	6
C	1
C	2
C	3
C	4
C	5
C	6
C	7
C	8
C	9

Condition版生产者消费者

/**
 * @Author: youthlql-吕
 * @Date: 2019/9/26 15:03
 * <p>
 * 功能描述: 使用Lock和Condition实现
 */
public class Producer_04 {
    public static void main(String[] args) {
        Consumer4 consumer = new Consumer4();

        //生产者线程A
        new Thread(() ->{
            for (int i = 0; i < 10; i++) {
                try {
                    consumer.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产者A").start();

        new Thread(() ->{
            for (int i = 0; i < 10; i++) {
                try {
                    consumer.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费者B").start();

        new Thread(() ->{
            for (int i = 0; i < 10; i++) {
                try {
                    consumer.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产者C").start();

        new Thread(() ->{
            for (int i = 0; i < 10; i++) {
                try {
                    consumer.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费者D").start();
    }
}
class Consumer4{
    private volatile Integer num = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public  void increment() throws InterruptedException {
        lock.lock();
        try {
            while(num != 0){
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + "\t" + num);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public  void decrement() throws InterruptedException {
        lock.lock();
        try {
            while(num == 0){
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + "\t" + num);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

ReentrantReadWriteLock读写锁

读写锁的目的是为了让读 — 读之间不加锁

冲突策略
读 — 读并行化
读 — 写串行化
写 — 读串行化
写 — 写串行化
package _05_AQS;

import java.util.*;

/**
 * @Author: youthlql-吕
 * @Date: 2019/9/26 10:58
 * <p>
 * 功能描述:
 */
public class Video34 {

    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);

    private static final Lock readLock = lock.readLock();
    private static final Lock writeLock = lock.writeLock();

    private static final List<String> data = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->write(),"write-1").start();

        Thread.sleep(1000);
        new Thread(()->read(),"read-1").start();

        new Thread(()->read(),"read-2").start();

    }

    public static void write(){
        try{
            writeLock.lock();
            data.add("数据XXX");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("写锁释放");
            writeLock.unlock();
        }
    }

    public static void read(){
        try{
            readLock.lock();
            for (String datum : data) {
                System.out.println(Thread.currentThread().getName() + " 读入:"+datum);
            }
            Thread.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("读锁释放");
            readLock.unlock();
        }
    }
}

结果:

写锁释放
read-1 读入:数据XXX
read-2 读入:数据XXX
读锁释放
读锁释放

可以看到读写-串行化,读读可以并发(你是怎么看出来的。。。加个时间呀)。

StampedLock读写锁

ReadWriteLock出现的问题

1、深入分析ReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写线程去抢锁,这是一种悲观的读锁,会出现写饥饿。

2、有100个线程访问某个资源,如果有99线程个需要读锁,1个线程需要写锁,此时,写的线程很难得到执行。

StampedLock改进

3、StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入 。这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁

4、乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

用StampedLock去悲观的读

StampedLock可以完全实现ReadWriteLock的功能。

public class StampedLockDemo1 {


    private static final StampedLock stampedLock = new StampedLock();

    private static Integer DATA = 0;

    public static void write() {
        long stamp = -1;
        try {
            stamp = stampedLock.writeLock();// 获取写锁
            DATA++;
            System.out.println("写-->" + DATA);
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public static void read() {
        long stamp = -1;

        try {
            stamp = stampedLock.readLock();// 获取悲观读锁
            System.out.println("读-->" + DATA);
        } finally {
            stampedLock.unlockRead(stamp); // 释放悲观读锁
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        //写任务
        Runnable writeTask = () -> {
            for (; ; ) {
                write();
            }
        };
        //读任务
        Runnable readTask = () -> {
            for (; ; ) {
                read();
            }
        };

        //一个线程写,9个线程读
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(writeTask);//写线程要写最后


    }
}

独线程一旦先执行,写线程很难再获得锁

输出结果:

读–>0
读–>0
读–>0
读–>0
读–>0
读–>0
读–>0
读–>0
读–>0
读–>0
读–>0
读–>0
写–>1

用StampedLock去乐观的读

只需要改一下read()

public class StampedLockDemo2 {
    private static final StampedLock stampedLock = new StampedLock();

    private static Integer DATA = 0;

    public static void write() {
        long stamp = -1;
        try {
            stamp = stampedLock.writeLock();// 获取写锁
            DATA++;
            System.out.println("写-->" + DATA);
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public static void read() {
        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁

        //在这块可能会有写锁抢锁,修改数据,所以用validate检查乐观读锁后是否有其他写锁发生
        /**
         * 1、在这块可能会有写锁抢锁,修改数据,所以用validate检查乐观读锁后是否有其他写锁发生
         * 判断执行读操作期间,是否存在写操作,如果存在则validate返回false
         * 2、如果有写锁抢锁,修改了数据,那么就要获取悲观锁。因为写锁在修改数据的过程中,你不能直接
         * 去读,只能老老实实拿到读锁再去读,才不会发生线程安全问题
         */
        if (!stampedLock.validate(stamp)) {//检查乐观读锁后是否有其他写锁发生
            try {
                stamp = stampedLock.readLock();// 获取悲观读锁
                System.out.println("悲观读-->" + DATA);
                return;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }
        System.out.println("乐观读-->" + DATA);

    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        //写任务
        Runnable writeTask = () -> {
            for (; ; ) {
                write();
            }
        };
        //读任务
        Runnable readTask = () -> {
            for (; ; ) {
                read();
            }
        };

        //一个线程写,9个线程读
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(writeTask);


    }
}

悲观读–>704
写–>705
写–>706
写–>707
悲观读–>707
写–>708
悲观读–>708
写–>709
写–>710
悲观读–>710
乐观读–>710
乐观读–>690
乐观读–>690

ForkJoin框架

思想:分而治之。将一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。

举例说明

1、我们举个例子:如果要计算一个超大数组的和,最简单的做法是用一个循环在一个线程内完成:

2、还有一种方法,可以把数组拆成两部分,分别计算,最后加起来就是最终结果,这样可以用两个线程并行执行:

3、如果拆成两部分还是很大,我们还可以继续拆,用4个线程并行执行:

这就是Fork/Join任务的原理:判断一个任务是否足够小。如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。

编码实现

整个任务流程如下所示

  • 首先继承任务,覆写任务的执行方法
  • 通过判断阈值,判断该线程是否可以执行
  • 如果不能执行,则将任务继续递归分配,利用fork方法,并行执行
  • 如果是有返回值的,才需要调用join方法,汇集数据。

主要的两个类:

  • RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)
  • RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)

RecursiveTask

public class ForkJoinRecursiveTask  {
	/* 
	   1、分到哪种程度可以不用分了
	   2、也就是设置一个任务处理最大的阈值
	*/
    private final static int MAX_THRESHOLD = 3;


    public static void main(String[] args) {
        final ForkJoinPool joinPool = new ForkJoinPool();
        ForkJoinTask<Integer> future = joinPool.submit(new CalculatedRecursiveTask(0, 1000));
        try {
            Integer integer = future.get();
            System.out.println("执行结果:" + integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }


    private static class CalculatedRecursiveTask extends RecursiveTask<Integer> {


        private final int start;//任务开始的上标
        private final int end;//任务开始的下标

        private CalculatedRecursiveTask(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected Integer compute() {
            if (end - start <= MAX_THRESHOLD) {//如果自己能处理,就自己计算
                return IntStream.rangeClosed(start, end).sum();
            } else {//自己处理不了,拆分任务
                int middle = (end + start) / 2;
                CalculatedRecursiveTask leftTask = new CalculatedRecursiveTask(start, middle);
                CalculatedRecursiveTask rightTask = new CalculatedRecursiveTask(middle + 1, end);
				
                //分别去执行
                leftTask.fork();
                rightTask.fork();
				
                //把返回结果合起来
                return leftTask.join() + rightTask.join();
            }
        }
    }
}

这个是有返回值的

RecursiveAction

这个是没有返回值的

public class ForkJoinRecursiveAction {

    private final static int MAX_THRESHOLD = 3;//设置一个任务处理最大的阈值

    private final static AtomicInteger SUM = new AtomicInteger();


    public static void main(String[] args) throws InterruptedException {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
		
        forkJoinPool.submit(new CalculateRecursiveAction(0,1000));
		
        //任务执行需要事件,这里可以等一下
        forkJoinPool.awaitTermination(10, TimeUnit.SECONDS);

        System.out.println("执行结果为:" + SUM);
    }

    private static class CalculateRecursiveAction extends RecursiveAction{

        private final int start;
        private final int end;

        private CalculateRecursiveAction(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected void compute() {
            if ((end-start)<=MAX_THRESHOLD){
                SUM.addAndGet(IntStream.rangeClosed(start,end).sum());
            }else {
                int middle = (start+end)/2;
                CalculateRecursiveAction leftAction = new CalculateRecursiveAction(start,middle);
                CalculateRecursiveAction rightAction = new CalculateRecursiveAction(middle+1,end);
                leftAction.fork();
                rightAction.fork();
            }
        }
    }
}

原理

整个流程需要三个类完成

1、ForkJoinPool

  • 既然任务是被逐渐的细化的,那就需要把这些任务存在一个池子里面,这个池子就是ForkJoinPool。

  • 它与其它的ExecutorService区别主要在于它使用**“工作窃取”**,那什么是工作窃取呢?

  • 工作窃取:一个大任务会被划分成无数个小任务,这些任务被分配到不同的队列,这些队列有些干活干的块,有些干得慢。于是干得快的,一看自己没任务需要执行了,就去隔壁的队列里面拿去任务执行。

2、ForkJoinTask

ForkJoinTask就是ForkJoinPool里面的每一个任务。他主要有两个子类:RecursiveActionRecursiveTask。然后通过fork()方法去分配任务执行任务,通过join()方法汇总任务结果。

小总结

  • Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。
  • ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。
  • 使用Fork/Join模式可以进行并行计算以提高效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/600906.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Maven处理依赖冲突

1.java常用的包依赖异常有&#xff1a; 1&#xff09;AbstractMethodError 2&#xff09;NoClassDefFoundError 3&#xff09;ClassNotFoundException 4&#xff09;LinkageError Maven会根据pom文件中的groupId、artifactId、version来判断jar是否冲突 如果出现了同名不…

开发软件必须写代码?来看smardaten如何零代码开发学生管理系统

一、前言 互联网产品在我们的生活中无处不在&#xff0c;但你知道开发一个这样的产品需要的成本有多大吗&#xff1f; 传统的产品研发模式是&#xff1a;功能需求&#xff0c;需要调研&#xff0c;画原型&#xff0c;开发&#xff0c;测试&#xff0c;上线&#xff0c;跟踪运…

基于Python+OpenCV的图像搜索引擎(CBIR+深度学习+机器视觉)含全部工程源码及图片数据库下载资源

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 数据预处理2. 定义图像描述符3. 索引化数据集4. 设计搜索引擎内核5. 执行搜索 系统测试1. 处理数据集2. 执行搜索 工程源代码下载其它资料下载 前言 本项目旨在开发一套完整高效的图像搜索引擎&#xff0c;为用…

python程序大全(7)——一元一次、一元二次方程解及函数解析

&#x1f3c6;一、前言 从1月到6月一直没更新&#xff0c;学习太忙辣。马上就要暑假了&#xff0c;今天是六一儿童节&#xff0c;所以抽出空来更新更新。 本文讲述的是1元1次方程&#xff0c;1元2次方程的python解法。只用给出一般形式的系数和常数&#xff0c;自动给出方程的…

企业为什么要进行思维与创新内训?有什么好处?

产品思维和创新在现代产品开发和管理中具有重要作用。 产品思维是指在设计和开发产品过程中&#xff0c;综合考虑用户需求、市场趋势、技术发展等多方面因素&#xff0c;以实现产品的有效性、可用性、价值和竞争力。 创新则是通过引入新的想法、方法或技术&#xff0c;创造出新…

table表格排序,@sort-change=“sortChange“ 取消排序

table表格排序&#xff0c;sort-change"sortChange" 取消排序 点击的单个进行排序时,要求isAsc对应当前字段的排序顺序;值ascending,descending,null三种情况;若指定了列对应的prop,没有指定order的话,默认ascending; desc降序&#xff0c;asc升序&#xff0c;当点升…

《水经注地图服务》下载与安装步骤

概述 《水经注地图服务》&#xff08;WeServer&#xff09;是一款可快速发布全国乃至全球海量卫星影像的地图发布服务产品&#xff0c;该产品完全遵循OGC相关协议标准&#xff0c;是一个基于若干项目成功经验总结的产品。它可以轻松发布100TB级海量卫星影像&#xff0c;从而使…

【漏洞复现】海康威视iVMS综合安防系统任意文件上传漏洞复现 (在野0day)

文章目录 前言声明一、产品简介二、漏洞概述三、影响范围四、漏洞验证五、漏洞利用六、修复建议 前言 海康威视iVMS综合安防系统存在任意文件上传漏洞 &#xff0c;攻击者可通过构造特定Payload实施对目标的攻击。 声明 本篇文章仅用于漏洞复现与技术研究&#xff0c;请勿利用…

chatgpt赋能python:Python中出现NaN的原因及相关处理方法

Python中出现NaN的原因及相关处理方法 介绍 Python是一种灵活、可扩展的编程语言&#xff0c;它已经成为科学计算、数据分析、人工智能等领域的重要工具。然而&#xff0c;在Python编程过程中&#xff0c;会出现一些比较特殊的问题&#xff0c;其中之一就是NaN。 NaN&#x…

基于html+css的图展示105

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

chatgpt赋能python:如何用Python编写抽奖程序

如何用Python编写抽奖程序 抽奖活动是很多企业和组织引发用户关注和参与的一种有效手段。而Python作为一种简单易学&#xff0c;功能强大&#xff0c;受欢迎的编程语言&#xff0c;它可以很好地帮助我们实现抽奖程序的编写。本篇文章将介绍如何用Python编写抽奖程序&#xff0…

本科毕业设计-软件工程-汽车销售客户关系管理系统

仅供学习参考&#xff0c;严禁盗用&#xff0c;商用&#xff01;&#xff01;&#xff01; 摘 要 随着国家的快速发展&#xff0c;人民对物质生活的需求也在逐渐增加&#xff0c;其中汽车需求是当前人民最主要的增长需求之一。随着汽车市场的不断扩大&#xff0c;汽车销售公司…

光栅尺磁栅尺编码器4倍频脉冲计数器Modbus RTU模块 支持PNP和NPN输入

1、 信号输入 1路光栅尺磁栅尺编码器信号输入&#xff0c;可接NPN和PNP信号&#xff0c;通过命令设置输入类型。 2、 通讯协议 通讯接口&#xff1a; 1路标准的RS-485通讯接口。 通讯协议&#xff1a;支持两种协议&#xff0c;命令集定义的字符协议和MODBUS RTU通讯协议。模块自…

Debian11之基于kubeadm安装K8S(v1.26.0) 集群

Debian10之基于kubeadm安装K8S(v1.26.0) 集群 参考文章 原文链接&#xff1a;https://blog.csdn.net/qq_30818545/article/details/128056996 版权声明&#xff1a;本文为CSDN博主「大能嘚吧嘚」的原创文章&#xff0c;遵循CC 4.0 BY-SA版权协议&#xff0c;转载请附上原文出…

游戏封包加密方案解析

当下游戏市场已全面回暖&#xff0c;暑期档临近更将迎来大量新游上线&#xff0c;如此关键节点&#xff0c;游戏厂商应当更加注重游戏安全。 FairGuard发现近期游戏黑灰产攻击角度愈发刁钻&#xff0c;除了常见的内存修改外挂、注入挂&#xff0c;针对游戏封包破解的「脱机挂」…

Springboot项目开发常遇到的问题

一、Springboot&#xff0c;修改默认端口&#xff0c;无效 Springboot是一个轻量级Web开发工具。里面内嵌了tomcat&#xff0c;所以我们不需要安装tomcat了。但是多个项目放在一起的时候&#xff0c;总不能都访问8080端口吧。所以我们需要修改默认端口。 默认是8080&#xff0…

jQuery 基础知识

1.jQuery的使用 要想使用 jQuery 的话&#xff0c;我们必须先要官网上下载&#xff08; http://jquery.com/ &#xff09;3.7 到 4.0的开发版本就可以&#xff0c;下载到文件夹以后桌面都可以 &#xff0c;然后拖动到代码编辑器根目录下即可 在需要使用 jQuery 的页面引入 j…

使用chartgtp写Android代码

<LinearLayout android:layout_width"match_parent" android:layout_height"match_parent" android:orientation"horizontal"> <TextView android:id"id/姓名" …

FME教程:批量提取面要素图形的拐点坐标到Excel,其他类型图形的坐标提取、输出可参考本文方法

目录 一、提取成果 二、实现过程 1.读取数据 2.分离内外边界 3.提取坐标 4.获取边界序号 5.坐标处理 6.数据输出 三、总结 今天给大家介绍使用FME提取几何图形拐点坐标&#xff0c;并输出到Excel中的案例。这里以shapefile格式&#xff0c;且内部存在环洞的面要素为例…

linux nohup命令如何使用?

Linux nohup 命令 nohup 英文全称 no hang up&#xff08;不挂起&#xff09;&#xff0c;用于在系统后台不挂断地运行命令&#xff0c;退出终端不会影响程序的运行。 nohup 命令&#xff0c;在默认情况下&#xff08;非重定向时&#xff09;&#xff0c;会输出一个名叫 nohup…