多线程笔记

1.线程与进程的关系

程序(进程) 由 多个线程组成 而线程是由CPU执行的

  • 正常运行的程序(软件)就是一个独立的进程

  • 线程是属于进程,一个进程中包含多个线程

  • 进程中的线程其实并发和并行同时存在(继续往下看)

1.1 多线程注意事项:

创建一个线程可以看成又有一个Main函数,把run()放到Main函数里面执行,然后两个Main函数在CPU上轮流执行

2.线程三种创建方式

2.1方式一:继承Thread类

2.1.1步骤:

1.定义一个子类继承Thread类,并重写run方法
2.创建Thread的子类对象
3.调用start方法启动线程(启动线程后,会自动执行run方法中的代码)

2.1.2案例:

2.1.3总结:

2.2方式二:实现Runnable接口

2.2.1java Api文档:

2.2.2案例:

2.2.3总结(步骤、优缺点)

2.2.4方式二创建线程匿名内部类写法

我不想写Runnable实现类,于是可以直接创建Runnable接口的匿名内部类对象,传递给Thread对象。

public class ThreadTest2_2 {
    public static void main(String[] args) {
        // 1、直接创建Runnable接口的匿名内部类形式(任务对象)
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程1输出:" + i);
                }
            }
        };
        new Thread(target).start();

        // 简化形式1:
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();

        // 简化形式2:因为Runnable()是一个函数式接口(接口只有一个抽象方法),所以可以使用lambda表达式简化
        new Thread(() -> {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程3输出:" + i);
                }
        }).start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

2.3方式三:利用Callable接口和FutureTask来实现

2.3.1前两种创建方式存在的问题

2.3.2步骤:

2.3.3案例:

Callable类是泛型接口用来声明线程返回结果的类型,不是任务对象

具体代码如下:

先准备一个Callable接口的实现类

import java.util.concurrent.Callable;

/**
 * 1、让这个类实现Callable接口
 */
public class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    // 2、重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果。
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "线程求出了1-" + n + "的和是:" + sum;
    }
}

再定义一个测试类,在测试类中创建线程并启动线程,还要获取返回结果

获取线程执行完毕后返回的结果时假如上面的子线程还没有执行完毕
获取线程执行完毕后返回结果 这里的代码会暂停,等待上面线程执行完毕后才会获取结果

public class ThreadTest3 {
    public static void main(String[] args) throws Exception {
        // 3、创建一个Callable的对象
        Callable<String> call = new MyCallable(100);
        // 4、把Callable的对象封装成一个FutureTask对象(任务对象)
        // 未来任务对象的作用?
        // 1、是一个任务对象,实现了Runnable对象.
        // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
        FutureTask<String> f1  = new FutureTask<>(call);
        // 5、把任务对象交给一个Thread对象
        new Thread(f1).start();


        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2  = new FutureTask<>(call2);
        new Thread(f2).start();


        // 6、获取线程执行完毕后返回的结果。
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕
        // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        String rs = f1.get();
        System.out.println(rs);

        String rs2 = f2.get();
        System.out.println(rs2);
    }
}

2.3.4总结(优缺点):

2.4考考你

看到这里,请你说出三种线程的创建方式和不同点(各自的特点)?

3.常用API

4.线程安全问题

4.1.线程安全问题出现的原因?

4.2案例

你会发现,在这个取钱案例中,两个人把共享账户的钱都取了10万,但问题是只有10万块钱啊!!!

以上取钱案例中的问题,就是线程安全问题的一种体现。

案例代码:

先定义一个共享的账户类

public class Account {
    private String cardId; // 卡号
    private double money; // 余额。

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    // 小明 小红同时过来的
    public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        // 1、判断余额是否足够
        if(this.money >= money){
            System.out.println(name + "来取钱" + money + "成功!");
            this.money -= money;
            System.out.println(name + "来取钱后,余额剩余:" + this.money);
        }else {
            System.out.println(name + "来取钱:余额不足~");
        }
    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}

在定义一个是取钱的线程类

public class DrawThread extends Thread{
    private Account acc;
    public DrawThread(Account acc, String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        // 取钱(小明,小红)
        acc.drawMoney(100000);
    }
}

最后,再写一个测试类,在测试类中创建两个线程对象

public class ThreadTest {
    public static void main(String[] args) {
         // 1、创建一个账户对象,代表两个人的共享账户。
        Account acc = new Account("ICBC-110", 100000);
        // 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万。
        new DrawThread(acc, "小明").start(); // 小明
        new DrawThread(acc, "小红").start(); // 小红
    }
}

4.3.常见线程同步方案:

4.3.1同步代码块

//锁对象:必须是一个唯一的对象(同一个地址)
synchronized(锁对象){
    //...访问共享数据的代码...
}

使用同步代码块,来解决前面代码里面的线程安全问题。我们只需要修改DrawThread类中的代码即可。

注意:锁对象建议用this,this正好代表共享资源也就是当前对象(这里是账户)!

如果锁对象定义成字符串虽然可以达到效果,但作用范围太大,

比如:

取钱,锁对象定义成字符串,则效果是只能有一个账户取钱,别的不同账户也无法取

// 小明 小红线程同时过来的
public void drawMoney(double money) {
    // 先搞清楚是谁来取钱?
    String name = Thread.currentThread().getName();
    // 1、判断余额是否足够
    // this正好代表共享资源!
    synchronized (this) {
        if(this.money >= money){
            System.out.println(name + "来取钱" + money + "成功!");
            this.money -= money;
            System.out.println(name + "来取钱后,余额剩余:" + this.money);
        }else {
            System.out.println(name + "来取钱:余额不足~");
        }
    }
}
1)同步锁,锁对象的选择

说明:

同步锁,如果类中只有一个静态方法,锁是当前类的Class对象。因为静态方法调用是:类名.静态方法(),而类名.class文件是唯一的

如果类中有多个静态方法,则建议使用不同的锁对象

使用独立的锁对象: 对于每一个静态同步方法,或者每一组相互关联的方法,分配不同的锁对象。这种做法可以确保互不干扰的方法可以同时并发执行,而不会因为共享同一个锁而被迫串行化。

import java.util.concurrent.locks.ReentrantLock;

public class MyClass {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public static void method1() {
        lock1.lock();
        try {
            // 关键代码
        } finally {
            lock1.unlock();
        }
    }

    public static void method2() {
        lock2.lock();
        try {
            // 关键代码
        } finally {
            lock2.unlock();
        }
    }
}
2)synchronized修饰符本身的特性:
  1. synchronized关键字不能被继承 即父类方法是同步方法 子类方法继承后默认不是同步方法
  2. synchronized不能修饰接口方法 因为接口是特殊的抽象类 不能新建实例 实例锁应归实现其的类所有
  3. synchronized不能修饰构造方法(但可在内部使用synchronized代码块来同步

4.3.2同步方法

1)同步方法如何保证线程安全

其实同步方法,就是把整个方法给锁住,一个线程调用这个方法,另一个线程调用的时候就执行不了,只有等上一个线程调用结束,下一个线程调用才能继续执行。

2)同步方法锁对象的原理

synchronized 方法的锁对象是当前同步方法所在类的实例对象(即 this)。这是通过 synchronized 关键字隐式指定的

  • 当线程执行到 drawMoney 方法时,它会尝试获取 this 对象的锁。
  • 如果锁已经被其他线程持有,尝试进入方法的线程将被阻塞,直到锁被释放。
  • 当方法执行完毕,当前线程会释放锁,其他等待的线程可以继续尝试获取锁。
3)总结

4.3.3 Lock锁

1)简介

Lock锁是JDK5版本专门提供的一种锁对象,通过这个锁对象的方法来达到加锁,和释放锁的目的,使用起来更加灵活。格式如下:

1.首先在成员变量位子,需要创建一个Lock接口的实现类对象(这个对象就是锁对象)
	private final Lock lk = new ReentrantLock();
2.在需要上锁的地方加入下面的代码
	 lk.lock(); // 加锁
	 //...中间是被锁住的代码...
	 lk.unlock(); // 解锁

2)常用API

3)改写上面取钱案例

使用Lock锁改写前面DrawThread中取钱的方法,代码如下:

解释:

锁对象创建要写成 private final 保证了每个类创建实例加载时都自动携带对应方法的锁

加锁解锁逻辑放在try catch finally中保证锁一定能释放

// 创建了一个锁对象
private final Lock lk = new ReentrantLock();

public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        try {
            lk.lock(); // 加锁
            // 1、判断余额是否足够
            if(this.money >= money){
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "来取钱后,余额剩余:" + this.money);
            }else {
                System.out.println(name + "来取钱:余额不足~");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lk.unlock(); // 解锁
        }
    }
}

4.4多线程同步主流的四种方式

多线程同步的四种方式(史上最详细+用例) - Chilk - 博客园 (cnblogs.com)icon-default.png?t=N7T8https://www.cnblogs.com/Chlik/p/13556720.html

5.线程通信

5.1什么是线程通信

5.2 线程的等待与唤醒

5.2.1注意:

1)线程的等待与唤醒需要用锁对象调用,同步锁方法可用this 关键字是对当前实例对象的引用,也就是同步锁的对象。当一个线程调用 synchronized 方法时,它会自动获取该实例对象的锁。

2)先唤醒别人,再等待自己

(否则,自己都休眠了,还怎么唤醒别人)

5.3 案例:

接下来,我们学习一下线程通信。

首先,什么是线程通信呢?

  • 当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,以相互协调,避免无效的资源挣抢。

线程通信的常见模式:是生产者与消费者模型

  • 生产者线程负责生成数据

  • 消费者线程负责消费生产者生成的数据

  • 注意:生产者生产完数据后应该让自己等待,通知其他消费者消费;消费者消费完数据之后应该让自己等待,同时通知生产者生成。

比如下面案例中,有3个厨师(生产者线程),两个顾客(消费者线程)。

接下来,我们先分析一下完成这个案例的思路

1.先确定在这个案例中,什么是共享数据?
    答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。
​
2.再确定有那几条线程?哪个是生产者,哪个是消费者?
    答:厨师是生产者线程,3条生产者线程; 
       顾客是消费者线程,2条消费者线程
       
3.什么时候将哪一个线程设置为什么状态
    生产者线程(厨师)放包子:
         1)先判断是否有包子
         2)没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待
         3)有包子时,不做包子了,直接唤醒别人、然后让自己等待
            
    消费者线程(顾客)吃包子:
         1)先判断是否有包子
         2)有包子时,顾客开始吃包子, 吃完之后把别人唤醒,然后让自己等待
         3)没有包子时,不吃包子了,直接唤醒别人、然后让自己等待

按照上面分析的思路写代码。先写桌子类,代码如下:

public class Desk {
    private List<String> list = new ArrayList<>();

    // 放1个包子的方法
    // 厨师1 厨师2 厨师3
    public synchronized void put() {
        try {
            String name = Thread.currentThread().getName();
            // 判断是否有包子。
            if(list.size() == 0){
                list.add(name + "做的肉包子");
                System.out.println(name + "做了一个肉包子~~");
                Thread.sleep(2000);

                // 唤醒别人, 等待自己
                this.notifyAll();
                this.wait();
            }else {
                // 有包子了,不做了。
                // 唤醒别人, 等待自己
                this.notifyAll();
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 吃货1 吃货2
    public synchronized void get() {
        try {
            String name = Thread.currentThread().getName();
            if(list.size() == 1){
                // 有包子,吃了
                System.out.println(name  + "吃了:" + list.get(0));
                list.clear();
                Thread.sleep(1000);
                this.notifyAll();
                this.wait();
            }else {
                // 没有包子
                this.notifyAll();
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

再写测试类,在测试类中,创建3个厨师线程对象,再创建2个顾客对象,并启动所有线程

public class ThreadTest {
    public static void main(String[] args) {
        //   需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
        //      2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。
        Desk desk  = new Desk();

        // 创建3个生产者线程(3个厨师)
        new Thread(() -> {
            while (true) {
                desk.put();
            }
        }, "厨师1").start();

        new Thread(() -> {
            while (true) {
                desk.put();
            }
        }, "厨师2").start();

        new Thread(() -> {
            while (true) {
                desk.put();
            }
        }, "厨师3").start();

        // 创建2个消费者线程(2个吃货)
        new Thread(() -> {
            while (true) {
                desk.get();
            }
        }, "吃货1").start();

        new Thread(() -> {
            while (true) {
                desk.get();
            }
        }, "吃货2").start();
    }
}

运行结果如下:你会发现多个线程相互协调执行,避免无效的资源挣抢。

厨师1做了一个肉包子~~
吃货2吃了:厨师1做的肉包子
厨师3做了一个肉包子~~
吃货2吃了:厨师3做的肉包子
厨师1做了一个肉包子~~
吃货1吃了:厨师1做的肉包子
厨师2做了一个肉包子~~
吃货2吃了:厨师2做的肉包子
厨师3做了一个肉包子~~
吃货1吃了:厨师3做的肉v

6. 线程池(重要)

6.1 什么是线程池

6.2 线程池的原理

但是任务总有被执行完的时候,假设第1个线程的任务执行完了,那么第1个线程就空闲下来了,有新的任务时,空闲下来的第1个线程可以去执行其他任务。依此内推,这3个线程可以不断的复用,也可以执行很多个任务。

6.3 线程池的创建

在JDK5版本中提供了代表线程池的接口ExecutorService,

而ExecutorService接口下有一个实现类叫ThreadPoolExecutor类,使用ThreadPoolExecutor类就可以用来创建线程池对象。

6.3.1 方案一:ThreadPoolExecutor构造器,来创建线程池对象

6.3.2 线程池注意事项(面试题)

1)临时线程什么时候创建?

新任务提交时,发现核心线程都在忙任务队列满了、并且还可以创建临时线程,此时会创建临时线程。

2)什么时候开始拒绝新的任务?

核心线程和临时线程都在忙、任务队列也满了、新任务过来时才会开始拒绝任务。

6.3.3 方案二:Executors工厂方法进行配置,来创建线程池对象

6.4 考考你:

不知道,请你重新看上面笔记

6.5 线程池执行Runnable任务

6.5.1 API

6.5.2 案例:

先准备一个线程任务类

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        // 任务是干啥的?
        System.out.println(Thread.currentThread().getName() + " ==> 输出666~~");
        //为了模拟线程一直在执行,这里睡久一点
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

下面是执行Runnable任务的代码,注意阅读注释,对照着前面的7个参数理解。

ExecutorService pool = new ThreadPoolExecutor(
    3,	//核心线程数有3个
    5,  //最大线程数有5个。   临时线程数=最大线程数-核心线程数=5-3=2
    8,	//临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
    TimeUnit.SECONDS,//时间单位(秒)
    new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
    Executors.defaultThreadFactory(), //用于创建线程的工厂对象
    new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);

Runnable target = new MyRunnable();
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
//下面4个任务在任务队列里排队
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);

//下面2个任务,会被临时线程的创建时机了
pool.execute(target);
pool.execute(target);
// 到了新任务的拒绝时机了!
pool.execute(target);

执行上面的代码,结果输出如下

注意:核心线程和临时线程都在忙、任务队列也满了、新任务过来时才会开始拒绝任务。

6.5.3 拒绝策略

6.6 考考你

6.6.1 线程池如何处理Runnable任务?

(啥?你说你不知道???,重新看6.5笔记理解代码过程,要能够独立描述过程)

6.7 线程池执行Callable任务

以前Runable ,Callble任务对象都要用Thread类来启动 现在用线程池来启动

案例:

先准备一个Callable线程任务

public class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    // 2、重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果。
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;
    }
}

再准备一个测试类,在测试类中创建线程池,并执行callable任务。

public class ThreadPoolTest2 {
    public static void main(String[] args) throws Exception {
        // 1、通过ThreadPoolExecutor创建一个线程池对象。
        ExecutorService pool = new ThreadPoolExecutor(
            3,
            5,
            8,
            TimeUnit.SECONDS, 
            new ArrayBlockingQueue<>(4),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy());

        // 2、使用线程处理Callable任务。
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));

        // 3、执行完Callable任务后,需要获取返回结果。
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}

执行后,结果如下图所示

6.8 考考你

1.线程池如何处理Callable任务,并得到任务执行完后返回的结果?

(啥?你说你不知道???,重新看6.7笔记理解代码过程,要能够独立描述过程)

6.9 线程池工具类 (Executors)大型并发不建议使用

6.9.1 API

6.9.2 案例:

public class ThreadPoolTest3 {
    public static void main(String[] args) throws Exception {
        // 1、通过Executors创建一个线程池对象。
        ExecutorService pool = Executors.newFixedThreadPool(17);
        // 老师:核心线程数量到底配置多少呢???
        // 计算密集型的任务:核心线程数量 = CPU的核数 + 1
        // IO密集型的任务:核心线程数量 = CPU核数 * 2

        // 2、使用线程处理Callable任务。
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}

Executors底层还是使用的 new ThreadPoolExecutor构造器

6.9.3 Executors存在的问题

6.10 考考你

(啥?你说你不知道???,重新看6.9笔记理解代码过程,要能够独立描述过程)

6.11 核心线程数量配置多少?

17=16+1

6.12 并发和并行

6.12.1线程与进程的关系

程序(进程) 由 多个线程组成 而线程是由CPU执行的

  • 正常运行的程序(软件)就是一个独立的进程

  • 线程是属于进程,一个进程中包含多个线程

  • 进程中的线程其实并发和并行同时存在

6.12.2 什么是并发?

进程中的线程由CPU负责调度执行,但是CPU同时处理线程的数量是有限的,为了保证全部线程都能执行到,CPU采用轮询机制为系统的每个线程服务由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。(简单记:并发就是多条线程交替执行)

6.12.3  什么是并行?

并行指的是,多个线程同时被CPU调度执行。

如下图:

4核CPU可同时处理4个线程

6.12.4 多线程到底怎么执行的?

并发和并行是同时执行的

6.13 线程的生命周期

6.13.1 线程的6种状态

NEW: 新建状态,线程还没有启动
RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态
BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态
WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态
TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态
TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。

这几种状态之间切换关系如下图所示

注意:sleep不会释放锁, wait会把锁释放

6.14 乐观锁,悲观锁,共享锁的特点以及使用场景

乐观锁,悲观锁:

多线程并发控制:悲观锁与乐观锁的区别解析_乐观锁与悲观锁对线程的影响-CSDN博客

Java 多线程之悲观锁与乐观锁 - 灰色飘零 - 博客园 (cnblogs.com)

乐观锁,悲观锁,共享锁:

多线程中的锁(悲观锁、乐观锁、共享锁)_乐观锁即可以用于多线程也可以用于多进程-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值