多线程基础

一、线程与进程

        一个程序的执行,首先把可执行文件放到内存,找到起始(main)的地址,逐步读出指令和数据,进行计算并写回到内存。

1)、什么是线程?

        线程是一个动态概念,线程是CPU调度的基本单位,是可执行的计算单位。

        线程的概念:共享空间,不共享计算。

2)、什么是进程?

        一个程序放到内存里,就叫进程。

        一个程序可以有多个进程,比如我们在电脑上,运行多个QQ程序。

        进程是一个静态概念,进程是一个分配资源的基本单位。

二、线程切换(OS)

        1)、 主要功能是,保存上下文。

        比如:我们有两个线程,一个T1线程,一个T2线程,T1有三个命令,T2也有三个命令,这个时候去访问线程T1去访问执行到第二个命令了,然后要换成T2访问,我们需要吧T1已经访问过的数据,存进缓存中,当T2执行完后,从缓存中拿取T1,在执行T1时我们直接在第二个命令继续执行。

问题:

        1.1)、是不是线程数量越多,执行效率就越高?

                        答:不是,资源都浪费在线程切换上了。比如:十万个活着的线程,来回切换,效率低。

        1.2)、单核CPU多线程执行有没有意义?

                        答:有意义,比如:一个程序在访问是,因为网络原因,CPU不能一直傻傻的等待,可以吧资源让给别人继续访问

        1.3)、工作线程数(线程池中线程数量)设置多少合适?

                答:我们也可以用一个公式来计算,如下:、

                

W:50%、C:50%,Ucpu期望:50% (1+1)两个线程。 

w/c这个是我们是没有办法知道的,主要还是要压测来测试,多少线程能够充分利用CPU。

三、启动线程的三种方式(实现多线程的四种方法)

1)、启动线程三种方式

        1.1)、Thread方法

        1.2)、Runnable方法

        1.3)、可以使用Lambda表达式,或使用线程池Executors.newCachedThrad

2)、继承Thread方法实现多线程

        重写Thread中的run方法,来实现多线程,这种方式我们一般是不用的,因为java类是单继承的,使用继承方式不利于扩展性的。

public class DemoThread extends Thread{

    @Override
    public void run() {
        int i = 10;
        for (int i1 = 0; i1 < i; i1++) {
            try {
                // 睡眠 一秒钟
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("i的值" + i1 + "子线程名称1:"+Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        // 创建线程类对象
        DemoThread domeThread1 = new DemoThread();
        // 启动线程
        domeThread1.start();

        
        int a = 10;
        for (int i = 0; i < a; i++) {
            System.out.println(">>>>>>>>a的值" + i + "主线程名称:"+ Thread.currentThread().getName());
        }
    }
}

3)、实现Runnabel接口实现多线程

        重写Runnabel中的run方法,但是要启动线程,需要通过 Thread来启动Runnabel线程来,实现多线程。

        注:Runnabel接口是没有返回值的。

public class DemoRunnable implements Runnable{

    @Override
    public void run() {
        int i = 10;
        for (int i1 = 0; i1 < i; i1++) {
            System.out.println("=====i的值" + i1 + "子线程名称" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {

        DemoRunnable domeRunnable1 = new DemoRunnable();
        // 通过new Thread 来启动
        Thread thread = new Thread(domeRunnable1);
        // 启动线程
        thread.start();

       /**
         *  另一种实现方式
         */
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 10;
                for (int i1 = 0; i1 < i; i1++) {
                    System.out.println("=====i的值" + i1 + "子线程名称" + Thread.currentThread().getName());
                }
            }
        }).start();

       /**
         *  java8 写法
         */
        new Thread(() -> {
            int i = 10;
            for (int i1 = 0; i1 < i; i1++) {
                System.out.println("=====i的值" + i1 + "子线程名称" + Thread.currentThread().getName());
            }
        }).start();

        int a = 10;
        for (int i = 0; i < a; i++) {
            System.out.println(">>>>>>a的值" + a + "主线程名称" + Thread.currentThread().getName());
        }
    }
}

4)、实现Callable接口实现多线程

        通过Callable实现,重写call方法,使用Callable配合Future一起使用,这我用的是FutureTask,它的父类实现了RunnableFuture接口,RunnableFuture接口继承了Runnable, Future<V>这连个,感兴趣的可以去看下源码。

        启动线程,我们还是用 THread来启动。

        注:Callable接口是有返回值的,它是一个泛型。

public class DemoCallable implements Callable<Integer> {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 线程实现类
        DemoCallable domeCallable1 = new DemoCallable();
        // 创建FutureTask对象
        FutureTask<Integer> t1 = new FutureTask<>(domeCallable1);
        // 通过 Thread来启动
        Thread thread = new Thread(t1);
        // 启动线程
        thread.start();
        // 获取返回值
        System.out.println("-----子线程:" + t1.get());
        int a = 10;
        for (int i = 0; i < a; i++) {
            System.out.println(">>>>>a的值"+i+"主线程名称:"+Thread.currentThread().getName());
        }
    }

    @Override
    public Integer call() throws Exception {
        // 随机数
        return new Random().nextInt(11);
    }
}

4)、线程池实现多线程        

public class ThreadPoolTaskExecutorDemo {

    Logger logger = LoggerFactory.getLogger(ThreadPoolTaskExecutorDemo.class);

    /**
     * 线程池
     */
    @Resource
    ThreadPoolTaskExecutor threadPoolTaskExecutor;


    public static void main(String[] args) {
        // 此方法是没有返回值
        CompletableFuture.runAsync(() -> { "待处理的逻辑" });
    }

}

线程池的七大参数:

corePoolSize:核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务来时,才创建线程去执行任务。默认情况下,线程池的线程数量为0,只有任务来时,才会创建线程,当线程池中的线程数量达到corePoolSize,就会把达到的放到缓存队列当中。
maximumPoolSize:最大核心线程数。表明线程池中能创建的最大数量,此值必须大于等于1
keepAliveTime:空闲的线程保留时间。意思是线程被用过之后,一定时间没有再用后,就会自动放回线程池。
TimeUnit unit:空闲线程保留时间的单位
BlockingQueue:阻塞队列,存储等待执行的任务。参数有ArrayBlockingQueue、
LinkedBlockingQueue、SynchronousQueue可选。
ThreadFactory :线程工程,创建线程的,一般默认即可
RejectedExecutionHandler :拒绝策略。队列已满,而且任务量大于最大线程的异常处理策略。

线程池的配置信息如下:

@Configuration
public class AsyncConfiguration {

    /**
     *  核心线程数
     */
    private int bookCorePoolSize = 3;
    /**
     * 最大线程数
     */
    private int bookMaxPoolSize = 10;
    /**
     * 队列容量
     */
    private int bookQueueCapacity = 100;
    /**
     * 线程活跃时间(秒)
     */
    private int bookKeepAliveSeconds = 100;
    /**
     * 默认线程名称
     */
    private String bookThreadNamePrefix = "priceRelation-sync-%d";

    /**
     * 接口的线程池
     *
     * @return TaskExecutor taskExecutor接口
     * @since JDK 1.8
     */
    @Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        //newFixedThreadPool
        //多线程线程池,内部线程是异步执行的
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数:一般线程池会至少保持这么多的线程数量
        executor.setCorePoolSize(bookCorePoolSize);
        // 设置最大线程数:线程池最多有这么多的线程数量
        executor.setMaxPoolSize(bookMaxPoolSize);
        // 设置队列容量
        executor.setQueueCapacity(bookQueueCapacity);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(bookKeepAliveSeconds);
        // 设置默认线程名称
        executor.setThreadNamePrefix(bookThreadNamePrefix);
        // 设置拒绝策略
        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
        //AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException
        //CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
        //DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
        //DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.initialize();
        return executor;
    }

}

四、锁的概念(Synchronized)

1)、Synchronized是一个安全的锁,锁的是谁,如下:

        锁基本概念:总而言之使用Synchronized锁,同一时刻只有一个线程在里面。不会让其他的线程进行访问。

public static void main(String[] args) {
        // 这个时候,锁就是,o
        Object o = new Object();
        synchronized(o){
            System.out.println("数据输出");
        }
    }

    public synchronized void m(){
        // 就相当于、锁就是 this
        synchronized (this){
            System.out.println("数据输出");
        }
    }

    public static synchronized void n(){
        // 相当于、锁就是类名
        synchronized (ThreadPoolTaskExecutorDemo.class){
            System.out.println("数据输出");
        }
    }

2)、不持有锁的线程该咋办?

        2.1)、自选等待(忙等待)、轻量级锁

        2.2)、等待队列(需要等着OS调度)、重量级锁

        轻量级的锁,并不比重量级的锁快?

        比如:两三个线程使用轻量级的锁,这个时候速度相对比较快,省略了进入队列,和OS的调度然后再抢锁,循环是消耗CPU的。

        比如:有一万个线程在循环的抢锁,是比较占用CPU资源的,这个时候我们就要用到重量级的锁,让锁的线程在队列里等待,这个等待是不消耗CPU资源的。

3)、Synchronized底层原理(重要部分)

        3.1)、 什么是CAS?

        多线程循环相加处理、按理来说,我们要获取100万,但是执行下面代码,我们确只能拿到几万的数据,这是因为,比如:一个线程获取的数据为9,另一个线程获取的数据也是9,然后线程一回写为10,线程二回写把线程一的值10,又更新了一次,还是10,所以我们拿到的数据不足100万。

        注:这添加volatile是不可以的,volatile最主要的功能是两个,

        一个是:线程可见性、一个是:指令重排序


private static /*volatile*/ int n = 0;


/**
     *  不加锁执行运行,效果是达不到100万的,会造成数据不一致
     * @param args
     */
    public static void main(String[] args) {
        // 启动100个线程
        Thread[] threads = new Thread[100];
        Object o = new Object();
        for (int i= 0; i< threads.length;i++){
            threads[i] = new Thread(() -> {
                for (int j = 0; j< 10000;j++){
                    n++;
                }
            });
        }
        Arrays.stream(threads).forEach((t) -> t.start());
        System.out.println(n);

    }

3.1.1)、如何解决这个问题呢?

        方法一:添加synchronized 锁

        比如:线程一拿到8,线程二也拿到8,我们上了锁以后,线程才能执行++操作,只有线程一把锁释放了,其它线程才能执行。

 private static /*volatile*/ int n = 0;


    /**
     *   synchronized 加锁,这里的锁就是 Object 的 o 、是可以实现的但是效率比较低
     * @param args
     */
    public static void main(String[] args) throws InterruptedException {
        // 启动100个线程
        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);
        Object o = new Object();
        for (int i= 0; i< threads.length;i++){
            threads[i] = new Thread(() -> {
               synchronized (o){
                   for (int j = 0; j< 10000;j++){
                       n++;
                   }
                   latch.countDown();
               }
            });
        }
        Arrays.stream(threads).forEach((t) -> t.start());
        latch.await();
        System.out.println(n);

    }

        方法二:使用 AtomicInteger 称之为:自旋锁,也叫 CSA。

 // 轻量级、无锁、自旋锁
    public static AtomicInteger m = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {

        // 启动100个线程
        Thread[] threads = new Thread[100];
        // 新型的锁,等待所有线程执行完后,在执行主线程
        CountDownLatch latch = new CountDownLatch(threads.length);
        
        for (int i= 0; i< threads.length;i++){
            threads[i] = new Thread(() -> {
                for (int j = 0; j< 10000;j++){
                    // 安全递增,保证数据一致性(自选锁/无锁)
                    m.incrementAndGet();
                }
                //标记已经完成一个任务
                latch.countDown();
            });
        }
        Arrays.stream(threads).forEach((t) -> t.start());
        // 释放锁
        latch.await();
        System.out.println(m);
    }

3.1.2)、CAS 的实现如图:

        我们先读一个值,比如为0,我需要把你改成1,在回写的过程中,我需要判断你是否还是0,如果是直接更新为1。如果不是,值为2了,在把值2获取出来,进行操作改成3,回写然后再比较你是否还是2,如果是直接更新,如果不是直接重复之前的操作。称之为自旋锁。

        ABA问题:

        读值为0,线程一进行++操作改为1,然后回写,这个时候线程二把这个值0,改成了1,然后又改回成1。我们看到的值,就不是原来的0了,只是值是0而已。

        解决方法:

        第一个:给当前值加一个版本号,1.0,如果有人改过版本号++

        第二个:放一个布尔类型。

lock  cmpxchg  指令(汇编语言)

        cmpxchg  指令 cas 如何保证数据的一致性,非原子性,lock 指令执行 原子性

3.1.3)、锁升级过程

        mark word 里面记录了,除了锁的信息,还有GC的信息。

      无锁(new)--》  偏向锁 ---》轻量级锁、自旋锁 ---》重量级锁

        偏向锁:不用抢锁,偏向于第一个进入的线程。只有一个线程时,效率是最高的时候。70%~80%是只有一个线程在跑的。

        轻量级锁:只有有一个线程来抢锁,就会从偏向锁升级为轻量级锁

        重量级锁:等待时间过长,线程过多,这个时候就会进入队列等待,升级为重量锁。

        比如:CPU最多管理5个线程,然后我们有20个,这个时候就会进入队列等待,不消耗CPU的资源。

3.1.4)、锁消除 lock  eliminate

     

3.1.5)、锁粗化lock  coarsening

4)、Volatile的概念

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值