总结多线程的知识点,很适合初学者的一篇文档

总结多线程的知识点,很适合初学者的一篇文档

1. 进程

一个正在运行的程序就是一个进程,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。例如,正在运行的微信、qq就是进程。

2. 线程

具体执行任务的最小单位,一个进程中至少包含一个线程。例如:浏览器开了4个页面,这四个页面是浏览器的执行的4个线程。

3. 两者的联系

  • 一个进程至少有一个线程,这个线程是主线程,也就是运行起来就执行的线程。
  • 线程之间是共享内部资源(由进程申请)的。
  • 线程之间可以通信:进行数据传递,多数为主线程和子线程

每个线程都是有独立的工作内存,这个不可以共享。但是也有工共的共享内存资源

4. 创建子线程的原因

主线程中如果有耗时的任务执行,例如:上传、下载视频等。这些操作会阻塞主线程,任务就会停止卡顿,等完成任务后才能执行,用户体验较差。为了不阻塞主线程,所以将耗时任务放到子线程里完成任务。

5. 线程上下文切换

计算机中有个内核(Kenel),它是管理计算机的资源。一个内核运行的一个主线程。假如一个计算机同时运行的微信、qq,一会运行微信、一会运行qq。cpu里有个时钟,会发送时钟脉冲,它的执行速度是纳米级别,特别特别快,它会分出一个极短的时间片给微信使用,一个极短的时间片给qq使用。速度极快,咱们感觉不到,只会觉得微信,qq能同时运行。但其实整个过程中,它会一会切换到微信、一会切换到qq,这个切换过程就是线程上下文切换。微信不能直接切换到qq,qq不能直接切换到微信。在计算机内核中有两种状态,一种是用户态、一种是内核态。只有kenel调度微信、qq。微信不能直接调度qq,所以中间有一个状态让两者进行切换。假如现在kenel运行qq的线程,qq是第三方应用,所以运行qq的状态是用户态;qq因为不能直接转换为微信,所以运行qq的状态(“用户态”)要转换成操作系统的内核态,并且把qq里的信息保存起来,内核态调度微信的线程,又切换到用户态…然后以此类推。每一次上下文切换都伴随的用户态与内核态的切换,这是非常消耗资源的操作。所以一个kenel运行的多个运行程序越少越好。

6. 创建线程的方法

三种方法

1、继承Thread类重写run()方法

2、 实现Runnable接口 new Thread时 new一个实现Runnable类的实例开启start()

3、使用lambda表达式,方便去操作

Thread源码

/**
** 方法被native修饰 说明此方法是本地方法
** Thread 类中是用start0: 向系统申请资源,开辟资源
*/
private native void start0();
/**start ()调度系统资源,开启线程,执行run()方法*/
    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * ......
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
 public synchronized void start(){
    
    ...}

6.1 继承Thread类

package com.xinzhi.build;

/**
 * @author ch
 * @date 2020/7/7
 */
public class UseThread {
    
    
    /***
     * 主线程打印 1234 主线程开辟了一个新的线程  在子线程里打印 5
     */

    public static void main(String[] args) {
    
    
     
        System.out.println("主线程----1");
        System.out.println("主线程----2");
        /**开辟新的线程*/
        new MyTask().start();
        System.out.println("主线程----3");
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("主线程----4");

    }

    static class MyTask extends Thread {
    
    
        @Override
        public void run() {
    
    
            /** run() 里的方法体为一个任务 */
            System.out.println("通过继承实现多线程!---子线程---5");
        }
    }
}


----
 主线程----1
 主线程----2
 主线程----3    
通过继承实现多线程!---子线程---5
 主线程----4   

6.2 实现Runnable接口(无返回值的接口)

package com.xinzhi.build;

/**
 * @author ch
 * @date 2020/7/7
 */
public class UseRunnable {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("主线程---1");
        /**开启线程使用Thread里的start */
        new Thread(new MyTask()).start();
        try {
    
    
            Thread.sleep(100);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("主线程---2");
    }

    static class MyTask implements Runnable {
    
    

        @Override
        public void run() {
    
    
            System.out.println("实现Runnable接口开启多线程----子线程--3");
        }
    }
}

-----
主线程---1
实现Runnable接口开启多线程----子线程--3
主线程---2    

6.3 使用lambda表达式

package com.xinzhi.build;

/**
 * @author ch
 * @date 2020/7/7
 */
public class UseLambdaRunnable {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("主线程---1");
        Runnable task = () -> System.out.println("实现Runnable接口开启多线程----子线程--3");
        /**开启线程使用Thread里的start */
//        new Thread(() -> System.out.println("实现Runnable接口开启多线程----子线程--3")).start();
        new Thread(task).start();
        try {
    
    
            Thread.sleep(100);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("主线程---2");
    }
}

7. 有返回值的线程(实现Callable接口)

package com.xinzhi.build;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author ch
 * @date 2020/7/7
 */
public class UseCallable {
    
    
    public static void main(String[] args) throws Exception {
    
    
        System.out.println(2);
        /**FutureTask 未来任务:某个任务完成后,过后还想拿到结果就用FutureTask*/
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyTask());
        System.out.println(3);
        new Thread(futureTask).start();
        System.out.println(4);
        /**get 阻塞方法*/
        int result = futureTask.get();
        System.out.println(5);
        System.out.println("result = "+result);
        System.out.println(6);
    }

    static class MyTask implements Callable<Integer> {
    
    

        @Override
        public Integer call() throws Exception {
    
    
            Thread.sleep(1000);
            return 1;
        }
    }
}

----
 2
 3
 4
 //停止一秒
 5
 result = 1
 6
package com.xinzhi.build;

import java.util.concurrent.FutureTask;

/**
 * @author ch
 * @date 2020/7/7
 */
public class UseLambdaCallable {
    
    

    public static void main(String[] args) throws Exception {
    
    
        System.out.println("ch");
        FutureTask<String> futureTask = new FutureTask<String>(() ->{
    
    
            Thread.sleep(2000);
            return "person";
        });
        System.out.println("gm");
        new Thread(futureTask).start();
        String res = futureTask.get();
        System.out.println("hh");
        System.out.println("res = " + res);
        System.out.println("qq");
    }
}
----
 ch
 gm
  //
 hh
 res = person
 qq

8. 守护线程

Java提供两种类型的线程: 用户线程守护程序线程

扫描二维码关注公众号,回复: 16671621 查看本文章

用户线程是高优先级线程。JVM将在终止任务之前等待任何用户线程完成其任务。

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。守护线程是低优先级线程,其唯一作用是为用户线程提供服务。由于守护线程旨在为用户线程提供服务,并且仅在用户线程运行时才需要,因此它们都不会退出JVM,直到所有用户线程执行完成。

8.1 创建守护线程

调用Thread.setDaemon()

package com.xinzhi.build;
/**
 * @author ch
 * @date 2020/7/7
 */
public class DaemonThread {
    
    
    public static void main(String[] args) {
    
    
        Thread daemon = new Thread(() -> {
    
    
            int count = 5;
            while (count >= 0) {
    
    
                try {
    
    
                    Thread.sleep(400);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("--用户线程--");
                count --;
            }
            System.out.println("用户线程结束");
        });
        //设置为守护线程: 守护的主线程
        daemon.setDaemon(true);
        //开启线程
        daemon.start();
    }
}

9. 线程安全

9.1 cpu缓存多核架构

一个cpu可能有多个核。一个电脑可以有多cpu。

CPU的处理速度是很快的,内存的速度次之,硬盘速度最慢。

磁盘是顺序读取,主存(内存条)是随机读取。读取主存的内容,需要地址。

cpu处理内存数据中,内存运行速度太慢,就会拖累cpu的速度。为了解决这样的问题,cpu设计了多级缓存策略。

计算机一般是三级缓存架构,离计算机cpu核越近的缓存,容量越小,速度越快。

例如: 访问主存中的一个变量a,先从一级缓存找变量a,在一级缓存中找不到然后去二级缓存找,在二级缓存中找不到去三级缓存中找,三级找不到去主存找。然后主存将变量一级一级的往上放。不管是三级与二级缓存、还是一级与二级缓存、三级与主存之间的交互过程,都有最小的单位。缓存的最小单位是缓存行(64个字节 == 8个long)

数据只要存到多级缓存区里,就会有数据一致性的问题。在cpu里,通过数据一致性协议保障数据一致性。

cpu模型
在这里插入图片描述
JMM内存模型

在这里插入图片描述

java内存模型–Jmm,每个线程被开辟了,都会给线程开辟一个工作内存,每个工作内存都是互相屏蔽的,不能相互访问。工作内存想要改变一些数据,是需要从主存中获取相应数据。

9.2 导致问题

9.2.1 cpu内存模型导致的问题
  • 伪共享

缓存中的数据与主内存的数据不是实时同步的,各个CPU间缓存的数据也不是实时同步的,在同一时间点,各个CPU所看到的的同一内存地址的数据可能是不一致的。

  • cpu指令重排问题

多核多线程,指令逻辑无法分辨因果关联,可能出现乱序执行,导致程序结果出现错误。

9.2.2.java内存模型导致问题
  • 线程安全
package com.xinzhi.ticket;

/**
 * @author ch
 * @date 2020/7/7
 */
public class Ticket implements Runnable {
    
    
    private static Integer count = 50;

    String name;

    public Ticket(String name) {
    
    
        this.name = name;
    }

    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(new Ticket("一号窗"));
        Thread t2 = new Thread(new Ticket("二号窗"));
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
    
    
        while (count > 0) {
    
    
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(name + "出票一张,还剩" + count-- + "张!");
        }
    }
}

---
一号窗出票一张,还剩50张!
二号窗出票一张,还剩50张!
一号窗出票一张,还剩49张!
二号窗出票一张,还剩49张!
二号窗出票一张,还剩48张!
一号窗出票一张,还剩47张!
一号窗出票一张,还剩46张!
二号窗出票一张,还剩46张!
    。。。。。。

解释: 这是java内存模型导致的,数据在主存里存的,多线程操作主存里的数据,不能直接在主存中操作,要copy到工作内存中。操作完然后在赋给主存里。

线程安全常发生在:多个线程操作同一个资源

9.3 解决线程安全问题

第一种方式—synchronized(参数就是一个监听器)

package com.xinzhi.ticket;

/**
 * @author ch
 * @date 2020/7/7
 */
/**
 * 解决线程安全的第一个方法
 * @author Admintor*/
public class SolveThread1 implements Runnable {
    
    
   /**票数*/
    private static Integer count = 50;
    /**监视器:任何对象都可以*/
    private static final Object monitor = new Object();

    String name;

    public SolveThread1(String name) {
    
    
        this.name = name;
    }

    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(new SolveThread1("一号窗"));
        Thread t2 = new Thread(new SolveThread1("二号窗"));
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
    
    
        while (count > 0) {
    
    
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            /**jdk内置的一把锁 synchronized锁加监视器*/
            synchronized (SolveThread1.monitor){
    
    
                System.out.println(name + "出票一张,还剩" + count-- + "张!");
            }
        }
    }
}

第二种方式—ReentrantLock

package com.xinzhi.ticket;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author ch
 * @date 2020/7/7
 */

/**
 * 解决线程安全的第二个方法
 *
 * @author Admintor
 */
public class SolveThread2 implements Runnable {
    
    
    private static Integer count = 50;

    /**
     * ReentrantLock可重入锁,显示锁
     */
    private static ReentrantLock lock = new ReentrantLock();

    String name;

    public SolveThread2(String name) {
    
    
        this.name = name;
    }

    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(new SolveThread2("一号窗"));
        Thread t2 = new Thread(new SolveThread2("二号窗"));
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
    
    
        while (count > 0) {
    
    
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
           lock.lock();
            try {
    
    
                System.out.println(name + "出票一张,还剩" + count-- + "张!");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }
    }
}

10. Java中的锁

10.1 synchronized(内置锁)介绍

java1.6之前synchronized是重量级锁,在java1.6之后对synchronized锁升级。

synchronized同步块使用了monitorenter和monitorexit指令实现同步

synchronized 有三种方式来加锁

  1. 实例方法(普通方法),作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

反编译命令: javap -v .className

10.2 synchronized锁升级

使用 Synchronized 是能够实现线程同步的,即加锁。并且实现的是悲观锁,在操作同步资源的时候直接先加锁。在jdk1.6之后,会根据线程竞争的激烈程度,一级一级的将锁升级。有4种锁状态,级别由低到高依次为:无锁状态偏向锁状态轻量级锁状态重量级锁状态

无锁:如果没有线程调用,为无锁状态

偏向锁:如果只有一个线程调用,为偏向锁状态,只有一个线程时可以直接掉用

轻量级锁:如果有多个线程竞争,升级为轻量级锁,第一个线程获取锁成功后,记录偏向的线程。第二个进程获取如果成功说明第一个线程结束,如果获取失败会进入自旋,通过自旋的方式不断获取锁,默认获取10次,如果10次后还没有获取成功,锁升级为重量级锁 重量级锁

重量级锁:线程为阻塞状态,为抢到锁的都会被阻塞,进入阻塞对列。

10.3 Lock(显示锁)

Lock接口有几个重要方法:

// 获取锁 
void lock()
//仅在调用时锁为空闲状态才获取该锁,可以响应中断 
  boolean tryLock() 
//如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁 
boolean tryLock(long time, TimeUnit unit) 
// 释放锁
void unlock()

获取锁的两种方式

/**第一种写法*/
            lock.lock();
            try {
    
    
                System.out.println(name + "出票一张,还剩" + count-- + "张!");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
            /**第二种写法*/
            //tryLock 可以传入参数:时间、单位
            if (lock.tryLock()) {
    
    
                try {
    
    
                    System.out.println(name + "出票一张,还剩" + count-- + "张!");
                } catch (Exception e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    lock.unlock();
                }
            }else{
    
    
                System.out.println("其他操作");
            }
        }

10.4 锁分类

10.4.1 乐观锁

总觉得自己能获取(操作)资源,如果操作失败了就去排队

10.4.2 悲观锁

觉得自己操作不了资源,首先去排队

10.4.3 读写锁

读锁 :只读 ReadLock

写锁 :只写 WriteLock

10.4.4 可重入锁

获取到一个方法的锁时,此方法内的所有锁的都可以获取到 ReentrantLock

10.4.5 不可重入锁

获取到一个方法的锁时,此方法内的所有锁不可以获取

10.4.6 公平锁

所有线程来了,都得排队

10.4.7 非公平锁

“插队”现象:新的线程来了,先不排队,先尝试抢一下资源,抢不上在去排队

10.4.8 自旋锁

当获取资源时一直在自旋,自旋次数可以自己定,在jvm得轻量级锁得自旋次数是10次。自旋次数到10次还没有获取资源由乐观锁变为悲观锁。

11. volatile 关键字

Java虚拟机提供的轻量级的同步机制

  • 保证可见性(一个线程修改了主内存的值,其他线程立即得知改变),线程访问此变量会从主内存

  • 中去读写,而不是线程自己的缓存中

  • 不保证原子性

  • 禁止指令重排

12. 线程的生命周期

new 新建-----》》 就绪 runnable ----(调度线程) 运行 running ------(调用sleep方法 ) 阻塞 blocked -----就绪runnable -----重新调度 running ----死亡 dead

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C2r6kfgq-1594463801310)(C:\Users\Admintor\AppData\Roaming\Typora\typora-user-images\image-20200708021453399.png)]
10.4.4 可重入锁

获取到一个方法的锁时,此方法内的所有锁的都可以获取到 ReentrantLock

10.4.5 不可重入锁

获取到一个方法的锁时,此方法内的所有锁不可以获取

10.4.6 公平锁

所有线程来了,都得排队

10.4.7 非公平锁

“插队”现象:新的线程来了,先不排队,先尝试抢一下资源,抢不上在去排队

10.4.8 自旋锁

当获取资源时一直在自旋,自旋次数可以自己定,在jvm得轻量级锁得自旋次数是10次。自旋次数到10次还没有获取资源由乐观锁变为悲观锁。

11. volatile 关键字

Java虚拟机提供的轻量级的同步机制

  • 保证可见性(一个线程修改了主内存的值,其他线程立即得知改变),线程访问此变量会从主内存

  • 中去读写,而不是线程自己的缓存中

  • 不保证原子性

  • 禁止指令重排

12. 线程的生命周期

new 新建-----》》 就绪 runnable ----(调度线程) 运行 running ------(调用sleep方法 ) 阻塞 blocked -----就绪runnable -----重新调度 running ----死亡 dead

猜你喜欢

转载自blog.csdn.net/weixin_47294072/article/details/107288980