多线程知识总结(VIP典藏版)

发布于:2022-12-29 ⋅ 阅读:(624) ⋅ 点赞:(0)

目录

一、概念

1、摩尔定律

2、并行与并发

3、线程与进程

a、进程

b、线程

c、多线程

二、线程的创建方式

三、线程的状态

1、状态转换图

2、状态流程

​3、常见方法

四、线程同步

1、同步方法

2、同步代码块

3、死锁

4、Lock锁

五、线程通信

六、线程池

1、线程池概述

2、线程池的使用

3、Runnable实现类代码

4、线程池测试类

七、参考资料


一、概念

1、摩尔定律

提到多线程好多书上都会提到摩尔定律,它是由英特尔创始人之一 Gordon Moore 提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔 18-24 个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔 18-24 个月翻一倍以上。这一定律揭示了信息技术进步的速度。

可是从 2003 年开始 CPU 主频已经不再翻倍,而是采用多核,而不是更快的主频。摩尔定律失效。那主频不再提高,核数增加的情况下要想让程序更快就要用到并行或并发编程。

2、并行与并发

        如果 CPU 主频增加程序不用做任何改动就能变快。但核多的话程序不做改动,不一定会变快。

        CPU 厂商生产更多的核的 CPU 是可以的,一百多核也是没有问题的,但是软件还没有准备好,不能更好的利用,所以没有生产太多核的 CPU。随着多核时代的来临,软件开发越来越关注并行编程的领域。但要写一个真正并行的程序并不容易。

        并行和并发的目标都是最大化 CPU 的使用率,并发可以认为是一种程序的逻辑结构的设计模式。可以用并发的设计方式去设计模型,然后运行在一个单核的系统上。可以将这种模型不加修改的运行在多核系统上,实现真正的并行,并行是程序执行的一种属性真正的同时执行,其重点的是充分利用 CPU 的多个核心。

        多线程开发的时候会有一些问题,比如安全性问题,一致性问题等,重排序问题,因为这些问题大家在写代码的时候会加锁等等。这些基础概念大家都懂,本文不再描述。 本文主要分享造成这些问题的原因和 Java 解决这些问题的底层逻辑。

3、线程与进程

a、进程

        是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

看完之后,是不是感觉很抽象?很懵bi?懵bi就对了,说明你和我智商一样高....~开个玩笑~

不妨先憋弃上面的概念,放松一下大脑,双击打开LOL,秒选德马打野,输了直接退出游戏并且保持微笑,然后正襟危坐心平气和的看宜春写的博客....

这个时候的你不仅仅是愉快的撸了一把游戏,而且还亲自体验撸了一把进程...其实在你双击打开LOL的时候就已经创建了进程,此话怎讲?众所周知,我们的电脑安装的软件比如:LOL、微信、谷歌等等都是存储在我们的硬盘上的,硬盘上的数据可以说是永久存储(ORM),当我们双击LOL的时候,LOL程序执行就进入了内存中,所有的程序必须进入内存中才能执行,内存属于临时存储(RAM),而进入内存的程序都可以叫做是进程,把LOL程序退出的时候,LOL程序就会退出内存,进程也就随之销毁了!因此说各位撸了一把进程也不为过吧。

 上面主要是通过抽象的描述了进程,其实进程是可以很直观的看的到的,我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程:

b、线程

同样的,我们先来看线程的概念

线程是进程中的一个执行单位,负责当前进程中程序的执行。一个进程中至少有一个线程,也就是说一个进程可以有多个线程的,而多个线程的进程运用程序就叫做多线程程序

线程的概念稍微好理解很多,但是想更深层次的去理解光靠上面一段文字的概述是完全不够的!

这不打LOL的过程中,属实卡的一批,果然花高价998买的6手戴尔笔记本打LOL属实像极了爱情。这个时候不得不双击打开电脑安全管家进行杀毒,果然2500天没有进行过病毒查杀,我天。。。其实我相信很多人都用过电脑管家或者手机管家之类的安全软件,我们都很清楚我们开启病毒查杀之后一般要几分钟扫描查杀,这个时候我们是可以让它后台进行的,我们不会等而是开启另一个垃圾清理的功能,这个时候我们也不会等而是再去启动电脑加速功能。等到 这些操作都完成之后果断退出电脑管家,继续LOL,果然高价998买的6手戴尔笔记本再怎么杀毒打LOL还是照样的卡....

其实清楚线程必然涉及到CPU的相关概念了,将上面文字所描述的用图片概括,大致为:

 

c、多线程

多线程就是多个线程同时运行 或 交替运行

  • 单核CPU:交替运行。
  • 多核CPU:同时运行。

       其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

二、线程的创建方式

1)继承Thread

2)实现Runnable接口

3)实现Callable接口

class MyThread1 implements Runnable{
    @Override
    public void run() {
        System.out.println("thread-1");
    }
}

class MyThread2 extends Thread{
    @Override
    public void run() {
        System.out.println("thread-2");
    }
}
//实现Callable接口方式
class MyThread3 implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("thread-3");
        return 123;
    }
}
public class TestCreate {

    public static void main(String[] args) {
        
        new Thread(new MyThread1()).start();
        new MyThread2().start();
        
        // 实现Callable接口
        FutureTask<Integer> task = new FutureTask<>(new MyThread3());
        new Thread(task).start();

        // 获取线程的返回值
        try {
            Integer i = task.get();
            System.out.println(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

三、线程的状态

1、状态转换图

线程有6个状态:

状态(E) 状态(C) 描述
NEW 新创建 当使用new操作创建一个线程时(new Thread(x)),此时处于新建态
Runnable 就绪·(可运行) 调用start()方法后,进入可运行状态
Blocked 阻塞 竞争不到对象锁而进入阻塞,获得锁进入可运行态
Waiting 等待 一个线程等待另一个线程通知调度器一个条件时,它使自己进入等待状态【生产者-消费者】,比如调用 wait方法或 join方法,或等待Lock或Condition,就会出现这种情况
Timed waiting 计时等待 这一状态将一直保持到超时期满或接到适当的通知
Terminated 被终止 run 方法正常结束,或run 方法出现异常而结束

2、状态流程


3、常见方法

方法 方法名 作用
静态方法 Thread.sleep(int time) 使当前线程进入休眠,不会放对象锁
静态方法 Thread.yield() 使当前线程让出CPU—>进入就绪态
实例方法 t1.join() 类似插队,其他线程需要等待t1结束后,才能继续
实例方法 t1.get/setPriority(int n) 用于获取当前和设置线程的优先级
实例方法 t1.getState() 获取线程状态
实例方法 t1.setDaemon(true) 设置当前为守护线程

四、线程同步

1、同步方法

    使用synchronized关键字,它包括两种用法:synchronized 方法和synchronized块.

同步方法:

public synchronized void method(){}

    同步方法锁的是对象本身this

    缺陷:若将一个大的方法申明为synchronized将会影响效率

2、同步代码块

    同步块:synchronized(obj){}

    obj 称之为同步监视器
        obj可以是任何对象,但是推荐使用共享资源作为同步监视器
        同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class

    同步监视器的执行过程
        第一个线程访问,锁定同步监视器,执行其中代码.
        第二个线程访问,发现同步监视器被锁定,无法访问.
        第一个线程访问完毕,解锁同步监视器.
        第二个线程访问,发现同步监视器没有锁,然后锁定并访问

示例:银行取钱

/**
 * 2夫妻同时到银行取钱
 *
 * 同步块:
 * @author a_apple
 * @create 2020-05-16 21:25
 */
//账户
class Account{
    int money; //余额
    String name; //卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//银行:模拟取款
class Darwing extends Thread{

    Account account;
    //取了多少钱
    int drawMoney;
    //手里多少钱
    int nowMoney;

    public Darwing(String name, Account account, int drawMoney) {
        super(name);
        this.account = account;
        this.drawMoney = drawMoney;
    }

    // synchronized方法:默认锁的是this。这里如果锁run()方法的话,锁的对象就是Drawing对象,相当于锁住银行了
    @Override
    public void run() {
       takeMoney();
    }

    public void takeMoney(){
        // 锁的对象是变化的量,需要增删改的量
        synchronized (account){
            //余额不足
            if(account.money<drawMoney){
                System.out.println(this.getName()+"余额不足...");
                return;
            }

            try {
                //将2个线程都堵在这里,放大问题
                // you,girlFriend线程都发现有100,但是钱被girl取了,变为0
                // 当you醒过来以为还有100,就也取了50
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //取钱
            account.money -= drawMoney;
            nowMoney += drawMoney;

            System.out.println(this.getName()+"取了:"+drawMoney);
            System.out.println(account.name+"余额有:"+account.money);
            System.out.println(this.getName()+"手里有:"+nowMoney+"\n");
        }
    }
}

public class UnsafeBank {

    public static void main(String[] args) {
        Account account = new Account(100, "结婚基金");

        Darwing you = new Darwing("you",account,50);
        Darwing girl = new Darwing("girl",account,100);

        you.start();
        girl.start();
    }
}

3、死锁

形成条件:线程互相持有对方需要的资源—>循环等待

    互斥条件:一个资源每次只能被一个进程使用。
    请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
    循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何解决:破坏其中一个或多个条件即可
 

/**
 * 测试死锁
 *      多个线程互相拥有对方需要的资源,形成僵持
 * @author a_apple
 * @create 2020-05-16 22:54
 */
class Player extends Thread {

    private String lock1;
    private String lock2;

    Player(String lock1, String lock2, String name) {
        super(name);
        this.lock1 = lock1;
        this.lock2 = lock2;
    }

    @Override
    public void run() {
        try {
            //确保2线程都启动
            Thread.sleep(1000);
            noDeadLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //死锁:持有锁,并想获得对方的锁
    void deadLock() throws InterruptedException {
        synchronized (lock1) {
            System.out.println(this.getName() + " have " + lock1);
            System.out.println(this.getName() + " need " + lock2);

            //保证2线程都启动
            Thread.sleep(1000);

            synchronized (lock2) {
                System.out.println(this.getName() + " get " + lock2);
            }
        }
    }

    //解决死锁:持有锁,等待对方放弃锁
    void noDeadLock() throws InterruptedException {
        synchronized (lock1) {
            System.out.println(this.getName() + " have " + lock1);
            System.out.println(this.getName() + " need " + lock2);

            Thread.sleep(1000);
        }
        //放弃lock1,请求lock2
        synchronized (lock2) {
            System.out.println(this.getName() + " get " + lock2);
        }
    }
}

public class TestDeadLock {

    private static String lock1 = new String("lock-V");
    private static String lock2 = new String("lock-H");

    public static void main(String[] args) {
        Player a = new Player(lock1, lock2, "A");
        Player b = new Player(lock2, lock1, "B");

        a.start();
        b.start();
    }
}

4、Lock锁

    JDK1.5 开始提供的显示锁。使用Lock对象
    java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
    锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
    ReentrantLock(可重入锁)类实现了Lock,它拥有与synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

示例:买票

/**
 * 使用Lock锁
 * @author a_apple
 * @create 2020-05-21 9:53
 */
class Tickets2{
    // 票的数量
    private int num = 20;

    private Lock lock = new ReentrantLock();
    // 买票的方法
    public void sale(){
        lock.lock();
        try{
            if(num<=0){
                return;
            }
            System.out.println(Thread.currentThread().getName()+"卖出了第"+num--+"张票");
        }finally {
            lock.unlock();
        }
    }
}
public class SaleTicket2 {

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

        Tickets2 res = new Tickets2();

        // 确保线程启动
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            for (int i = 0; i < 8; i++) {
                res.sale();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 8; i++) {
                res.sale();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 8; i++) {
                res.sale();
            }
        },"C").start();
    }
}

输出:

A卖出了第20张票
A卖出了第19张票
A卖出了第18张票
A卖出了第17张票
A卖出了第16张票
A卖出了第15张票
A卖出了第14张票
A卖出了第13张票
B卖出了第12张票
B卖出了第11张票
B卖出了第10张票
B卖出了第9张票
B卖出了第8张票
B卖出了第7张票
B卖出了第6张票
B卖出了第5张票
C卖出了第4张票
C卖出了第3张票
C卖出了第2张票
C卖出了第1张票

五、线程通信

多个线程互相协作,共同完成任务。

涉及的方法

wait() 使当前线程进入阻塞,并释放锁
wait(int time) 等待指定的毫秒数
notify() 唤醒一个等待该(对象锁)线程并使该线程开始执行
notifyAll() notifyAll 会唤醒所有等待该(对象锁)线程,

注意:

     wait()、notify()、notifyAll()是继承自Object的本地final方法
    上面的方法需要配合synchronized关键字使用,即放在同步方法或代码块中使用。【说明当前线程已经获得锁。

生产者-消费者问题

    生产者—>缓冲区 缓冲区满–>等待消费
    消费者<—缓冲区 缓冲区空–>等待生产

package pers.xu.multithread.kuangshen.pc;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * 生产者--消费者
 *
 *      生产者--->缓冲区   缓冲区满-->通知消费者消费
 *      消费者<---缓冲区   缓冲区空-->通知生产者生产
 *
 * 实例:生产者,消费者,产品,缓冲区
 * @author a_apple
 * @create 2020-05-17 10:03
 */

class Product{
    //产品编号
    int i;
    public Product(int i) {
        this.i = i;
    }
}

class ProductBuffer{
    // 缓冲区大小 5
    List<Product> buffer = new ArrayList<>();
    private int maxCapacity = 5;

    // 将产品放入缓冲区
    public void push(Product product){
        // 实际操作的是buffer,所以这里使用buffer锁
        synchronized (buffer){
            // 缓冲区满-->放弃锁-->阻塞
            while (maxCapacity == buffer.size()){
                try {
                    System.out.println("缓冲区已满...");
                    buffer.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //放入
            buffer.add(product);
            System.out.println(Thread.currentThread().getName()+"生产了:"+product.i);

            //通知消费
            buffer.notifyAll();
        }
    }

    // 从缓冲区获取商品   synchronized锁的this==缓冲区
    public Product pop(){
        synchronized (buffer){
            // 缓冲区空-->等待
            while(buffer.size()==0){
                try {
                    //进入阻塞-->放弃buffer锁
                    System.out.println("---->缓冲区空了<----");
                    buffer.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //随机取出一个
            Product product = buffer.remove(new Random().nextInt(buffer.size()));
            System.out.println(Thread.currentThread().getName()+"-->消费了:"+product.i);

            //通知生产
            buffer.notifyAll();
            //返回产品
            return product;
        }
    }
}

public class TestPC {

    public static void main(String[] args) {

        ProductBuffer buffer = new ProductBuffer();

        // 生产者-1   
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                //放入10个产品
                buffer.push(new Product(i));
                //sleep(1000): 每生产一个就停一下
            }
        },"A").start();

        // 生产者-2
        new Thread(()->{
            for (int i = 11; i <= 20; i++) {
                //放入10个产品
                buffer.push(new Product(i));
            }
        },"C").start();

        // 消费者-1
        new Thread(()->{
            for (int i = 1; i <= 10; i++) {
                buffer.pop();
            }
        },"B").start();

        // 消费者-2
        new Thread(()->{
            for (int i = 11; i <= 20; i++) {
                buffer.pop();
            }
        },"D").start();
    }
}

结果

A生产了:1
A生产了:2
A生产了:3
A生产了:4
A生产了:5
缓冲区已满...
B-->消费了:1
B-->消费了:2
B-->消费了:5
B-->消费了:3
B-->消费了:4
---->缓冲区空了<----
C生产了:11
C生产了:12
C生产了:13
C生产了:14
C生产了:15
缓冲区已满...
B-->消费了:12
B-->消费了:15
B-->消费了:13
B-->消费了:14
B-->消费了:11
A生产了:6
A生产了:7
A生产了:8
A生产了:9
A生产了:10
D-->消费了:9
D-->消费了:10
D-->消费了:6
D-->消费了:7
D-->消费了:8
---->缓冲区空了<----
C生产了:16
C生产了:17
C生产了:18
C生产了:19
C生产了:20
D-->消费了:20
D-->消费了:19
D-->消费了:16
D-->消费了:18
D-->消费了:17

六、线程池

在java中只要说到池,基本都是一个套路,啥数据库连接池、jdbc连接池等,思想基本上就是:一个容纳多个要使用资源的容器,其中的资源可以反复使用,省去了频繁创建线程对象的操作,无需反复创建资源而消耗过多资源。

1、线程池概述

线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

2、线程池的使用

Java里面线程池的最顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不操作这一步)。

3、Runnable实现类代码

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();

        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 调用MyRunnable中的run()

        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池
        //service.shutdown();
    }
}

4、线程池测试类

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();

        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 调用MyRunnable中的run()

        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池
        //service.shutdown();
    }
}

以上只是简单的使用线程池,仅仅是入门阶段!道阻且长,路还很长....

七、参考资料

一文解读多线程 (转) - 朱志勇 - 博客园

Java-多线程基础总结_昫 灬的博客-CSDN博客

2020-7-15 多线程总结_小宋想站起来的博客-CSDN博客

一文入门多线程_「已注销」的博客-CSDN博客

一文入门多线程知识_xmurphymurphy的博客-CSDN博客

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到