21_多线程

1. 什么是进程

  • 正在运行的程序, 是系统进行资源分配的基本单位
  • 目前操作系统都是支持多进程, 可以同时执行多个进程, 通过进程ID区分
  • 单核CPU在同一时刻, 只能运行一个进程; 宏观并行, 微观串行

2. 什么是线程

  • 线程, 又称轻量级进程(Light Weight Process)
  • 进程中的一条执行路径, 也是CPU的基本调度单位
  • 一个进程由一个或多个线程组成, 彼此间完成不同的工作, 同时执行, 称为多线程

3. 进程和线程的区别

  1. 进程是操作系统资源分配的基本单位, 而线程是CPU的基本调度单位
  2. 一个程序运行后至少有一个进程
  3. 一个进程可以包含多个线程, 但是至少需要有一个线程, 否则这个进程是没有意义的
  4. 进程间不能共享数据段地址, 但同进程的线程之间可以

4. 线程的组成

  • 任何一个线程都具有基本的组成部分:
    • CPU时间片: 操作系统(OS)会为每个线程分配执行时间
    • 运行数据:
      • 堆空间: 存储线程需使用的对象, 多个线程可以共享堆中的对象
      • 栈空间: 存储线程需使用的局部变量, 每个线程都拥有独立的栈
    • 线程的逻辑代码

5. 线程的特点

  1. 线程抢占式执行
    • 效率高
    • 可防止单一线程长时间独占CPU
  2. 在单核CPU中, 宏观上同时执行, 微观上顺序执行

6. 创建线程

  • 创建线程三种方式
    1. 继承Thread类, 重写run方法
    2. 实现Runnable接口
    3. 实现Callable接口
  • 获取和修改线程名称
    1. 获取线程ID和线程名称
      • 在Thread的子类中调用this.getId()和this.getName
      • 使用Thread.currentThread().getId()和Thread.currentThread().getName()
    2. 修改线程名称
      • 调用线程对象的setName()方法
      • 使用线程子类的构造方法赋值

方式一: 继承Thread类, 重写run方法

package com.thread.demo01;

public class MyThread extends Thread {
    
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //方式一: this.getId()获取线程Id,this.getName()获取线程名称
            //System.out.println("线程id: " + this.getId() + " 线程名称: " + this.getName() + " 子线程:......" +i);
            //方式二(推荐): Thread.currentThread() 获取当前线程
            System.out.println("线程id: " + Thread.currentThread().getId() + " 线程名称: " + Thread.currentThread().getName() + " 子线程:......" + i);
        }
    }
}

启动多个线程

package com.thread.demo01;
public class TestThread {

    public static void main(String[] args) {

        //1. 创建线程对象
        MyThread myThread = new MyThread("我的子线程1");
        //2. 启动线程,不能用run()方法
        //myThread.setName("我的子线程1"); //启动线程前设置线程名称
        myThread.start();
        System.out.println(myThread.getId());

        MyThread myThread1 = new MyThread("我的子线程2);
        //myThread1.setName("我的子线程2"); //启动线程前设置线程名称
        myThread1.start();
        System.out.println(myThread1.getId());

        //主线程执行
        for (int i = 0; i < 50; i++) {
            System.out.println("主线程:......" + i);
        }
    }
}
案例一: 实现四个窗口同时各卖票100张
public class SellTicket extends Thread{

    public SellTicket() {
    }
    public SellTicket(String name){
        super(name);
    }

    private int ticket = 100;//票

    @Override
    public void run() {
        while (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "卖的第" + (101 - ticket) + "张票");
            ticket--;
        }
    }
}
public class TestTicket {

    public static void main(String[] args) {

        //1. 创建四个卖票窗口
        SellTicket st1 = new SellTicket("窗口一");
        SellTicket st2 = new SellTicket("窗口二");
        SellTicket st3 = new SellTicket("窗口三");
        SellTicket st4 = new SellTicket("窗口四");

        //2. 开启多线程
        st1.start();
        st2.start();
        st3.start();
        st4.start();




    }
}

方式二: 实现Runnable接口

Thread的构造方法,可传入Runnable target, String name ,即Runnable接口类型的对象和字符串类型的名称

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals)
public class MyRunnable implements Runnable{


    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "子线程......" + i);
        }
    }
}
public class TestRunnable {

    public static void main(String[] args) {

        //创建MyRunnable对象, 表示线程要执行的功能
        MyRunnable runnable = new MyRunnable();

        //2. 创建线程对象
        Thread thread = new Thread(runnable, "我的线程一");
        Thread thread1 = new Thread(runnable, "我的线程二");

        //3. 启动多线程
        thread.start();
        thread1.start();

        for (int i = 0; i < 50; i++) {
            System.out.println("主线程" + i);
        }
    }
}

方式二优化: 使用匿名内部类

public class TestRunnable1 {

    public static void main(String[] args) {

        //1. 创建Runnable对象, 以及使用匿名内部类
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + "子线程......" + i);
                }
            }
        };

        //2. 创建线程对象
        Thread thread = new Thread(runnable, "我的线程1");
        Thread thread1 = new Thread(runnable, "我的线程2");

        //3. 启动多线程
        thread.start();
        thread1.start();

        for (int i = 0; i < 50; i++) {
            System.out.println("主线程" + i);
        }

    }
    
}
案例二: 实现四个窗口共同卖票100张
public class Ticket implements Runnable{

    private int ticket = 100;


    @Override
    public void run() {
        while (ticket > 0){
            System.out.println(Thread.currentThread().getName() + "售出第" + (101-ticket) + "张票");
            ticket--;
        }

    }
    
}
public class TestTicket {

    public static void main(String[] args) {

        Ticket ticket = new Ticket();

        Thread wd1 = new Thread(ticket, "窗口一: ");
        Thread wd2 = new Thread(ticket, "窗口二: ");
        Thread wd3 = new Thread(ticket, "窗口三: ");
        Thread wd4 = new Thread(ticket, "窗口四: ");

        wd1.start();
        wd2.start();
        wd3.start();
        wd4.start();
    }
}
案例三: 实现两人共用一张银行卡, 一人存钱, 另一人取钱
public class BankCard {//创建银行卡类

    private double money;//余额

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}
public class AddMoney implements Runnable{//创建存钱类, 实现Runnable接口

    private BankCard card;

    public AddMoney(BankCard card) {
        this.card = card;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            card.setMoney(card.getMoney() + 1000);
            System.out.println(Thread.currentThread().getName() + "存的第" + (i+1) + "笔钱,金额为1000元, 现卡上余额: " + card.getMoney() + "元");
        }

    }
}
public class SubMoney implements Runnable{//创建取钱类, 实现Runnable接口

    private BankCard card;

    public SubMoney(BankCard card) {
        this.card = card;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (card.getMoney() >= 1000){
                card.setMoney(card.getMoney() - 1000);
                System.out.println(Thread.currentThread().getName() + "取了第" + (i+1) + "笔钱,金额为1000元, 现卡上余额: " + card.getMoney() + "元");
            }else{
                System.out.println("余额不足...");
                i--;
            }
        }

    }
}
public class TestBankCard {

    public static void main(String[] args) {

        //1. 创建银行卡对象
        BankCard card = new BankCard();

        //2. 创建存钱对象
        AddMoney addMoney = new AddMoney(card);
        //3. 创建取钱对象
        SubMoney subMoney = new SubMoney(card);

        //4. 创建两个线程
        Thread boy = new Thread(addMoney, "男孩");
        Thread girl = new Thread(subMoney, "女孩");

        //5. 开启线程
        boy.start();
        girl.start();



    }
}

7. 线程的状态

(1) 基本
  1. New(新创建): 线程对象被创建, 即为新创建状态. 只在堆中开辟内存, 与常规对象无异
  2. Runnable(可运行): 调用start()后进入可运行状态, 等待被OS选中并分配时间片后开始运行, 时间片到期后又重新进入可运行状态,等待被OS选中
  3. Terminated(终止状态): 主线程main()或独立线程run()结束, 进入终止状态, 并释放持有的时间片
    • 正常终止: run()方法正常退出
    • 意外终止: 因为一个没有捕获的异常终止了run()方法
(2) 阻塞
  • Blocked(阻塞): 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有, 则该线程进入阻塞状态. 当所有其他线程释放该锁, 并且线程调度器允许本线程持有它的时候, 该线程将变为非阻塞状态
  • Waiting(等待): 当线程等待另一个线程通知调度器一个条件时, 它自己进入等待状态, 调用以下方法时会出现这种情况
    • Object.wait
    • Thread.join
    • java.util.concurrent库中的Lock或Condition
  • Timed Waiting(计时等待): 有几个方法有一个超时参数. 调用它们导致线程进入计时等待状态. 这一状态将一直保持到超时期满或者接收到适当的通知. 带有超时参数的方法有:
    • Thread.sleep
    • Object.wait
    • Thread.join
    • Lock.tryLock
    • Condition.await的计时版

8. 常见方法

  • 休眠
    • public static void sleep(long millis)
    • 当前线程主动休眠 millis 毫秒
  • 放弃
    • public static void yield()
    • 当前线程主动放弃时间片, 回到就绪状态, 竞争下一次时间片
  • 加入
    • public final void join()
    • 允许其它线程加入到当前线程中
  • 优先级
    • 线程对象.setPriority
    • 线程优先级为1-10. 默认为5, 优先级越高, 表示获取CPU机会越多
  • 守护线程
    • 线程对象.setDaemon(true); 设置为守护线程
    • 线程有两类: 用户线程(前台线程), 守护线程(后台线程)
    • 如果程序中所有前台线程都执行完毕了, 后台线程会自动结束
    • 垃圾回收器线程属于守护线程

(1)sleep()方法 休眠

public class SleepThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "--------------" + i);
            try {
                SleepThread.sleep(2000);//睡眠两秒钟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class TestSleep {

    public static void main(String[] args) {

        SleepThread s1 = new SleepThread();
        SleepThread s2 = new SleepThread();

        s1.start();
        s2.start();
    }
}

(2)yield()方法 让步

public class YieldThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "-------------" + i);
            //放弃CPU
            YieldThread.yield();
        }
    }
}
public class TestYield {

    public static void main(String[] args) {

        YieldThread y1 = new YieldThread();
        YieldThread y2 = new YieldThread();

        y1.start();
        y2.start();
    }   
}

(3)join()方法 加入

public class JoinThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "子线程------------" + i);
            try {
                JoinThread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class TestJoin {

    public static void main(String[] args) {

        JoinThread j1 = new JoinThread();
        j1.start();
        try {
            j1.join();//加入到当前线程main, 并阻塞当前线程main, 直到加入的线程j1执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //主线程
        for (int i = 0; i < 30; i++) {
            System.out.println(Thread.currentThread().getName() + "主线程: " + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

(4)优先级 Priority

public class PriorityThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "子线程:------ " + i);
        }
    }
}
public class TestPriority {

    public static void main(String[] args) {

        PriorityThread p1 = new PriorityThread();
        p1.setName("p1");
        PriorityThread p2 = new PriorityThread();
        p2.setName("p2");
        PriorityThread p3 = new PriorityThread();
        p3.setName("p3");
        p1.start();
        p2.start();
        p3.start();
        p1.setPriority(1);//优先级最低
        p2.setPriority(3);
        p3.setPriority(10);//优先级最高


        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "主线程:------ " + i);
        }
    }
}

(5)守护线程 Daemon

public class DaemonThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("子线程: " + Thread.currentThread().getName() + "-----------" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class TestDaemon {

    public static void main(String[] args) {

        //1. 创建线程(默认为前台线程)
        DaemonThread d1 = new DaemonThread();
        //2. 设置为守护线程(后台线程) 当主线程执行完毕后,守护线程(后台线程)会自动结束
        d1.setDaemon(true);
        d1.start();


        for (int i = 0; i < 10; i++) {
            System.out.println("主线程: " + Thread.currentThread().getName() + "-------" + i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


}

9. 线程安全

  • 多线程安全问题
    • 当多线程并发访问临界资源时, 如果破坏原子操作, 可能会造成数据不一致
    • 临界资源: 共享资源(同一个对象), 一次仅允许一个线程使用, 才可保持其正确性
    • 原子操作: 不可分割的多步操作, 被视作一个整体, 其顺序和步骤不可打乱或缺省

(1) 同步方式1

  • 同步代码块:

    synchronized(临界资源对象){ //对临界资源对象加锁

    ​ //代码(原子操作)

    }

    注: 每个对象都有一个互斥锁标记, 用来分配给线程的. 只有拥有对象互斥锁标记的线程, 才能进入对该对象加锁的同步代码块.

    线程退出同步代码块时, 会释放相应的互斥锁标记

import java.util.Arrays;

public class ThreadSafe {

    private static int index = 0;
    public static void main(String[] args) throws InterruptedException {

        //1. 创建数组
        String[] s = new String[5];

        //2. 创建两个操作
        Runnable runnableA = new Runnable() {
            @Override
            public void run() {
                //同步代码块
                synchronized (s){
                    s[index] = "hello";
                    index++;
                }
            }
        };
        Runnable runnableB = new Runnable() {
            @Override
            public void run() {
                //同步代码块
                synchronized (s){
                    s[index] = "world";
                    index++;
                }
            }
        };
        //3. 创建两个线程
        Thread thread1 = new Thread(runnableA, "A");
        Thread thread2 = new Thread(runnableB, "B");
        //4. 启动
        thread1.start();
        thread2.start();
        //5. 加入线程
        thread1.join();
        thread2.join();

        System.out.println(Arrays.toString(s));
    }
}
优化案例二(1): 使用同步代码块

实现四个窗口共同卖票100张, 不能出现四个窗口同时卖出一张票的情况

public class Ticket implements Runnable{

    private int ticket = 100;//票
    //创建锁
    //private Object obj = new Object();


    @Override
    public void run() {
        while (true){
            synchronized (this){//this---当前对象,这里放入obj和this都可以
                if (ticket <= 0){
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "卖的第" + (101-ticket) + "张票");
                ticket--;
            }

        }
    }
}
public class TestSafeTicket {

    public static void main(String[] args) {

        //1. 创建票对象
        Ticket ticket = new Ticket();

        //2. 创建四个线程对象
        Thread win1 = new Thread(ticket, "窗口一");
        Thread win2 = new Thread(ticket, "窗口二");
        Thread win3 = new Thread(ticket, "窗口三");
        Thread win4 = new Thread(ticket, "窗口四");

        //3. 启动
        win1.start();
        win2.start();
        win3.start();
        win4.start();

    }
}
优化案例三: 使用同步代码块

两人用同一张银行卡,一人存钱,另一人取钱, 不能出现存钱后卡上没钱或卡上没钱还能取出的情况

public class BankCard {

    private double money;//余额

    public double getMoney() {
        return money;
    }

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

    public static void main(String[] args) {

        //1. 创建银行卡对象
        BankCard bankCard = new BankCard();

        //2. 创建存钱取钱操作
        Runnable addMoney = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    synchronized (bankCard){
                        bankCard.setMoney(bankCard.getMoney() + 1000);
                        System.out.println(Thread.currentThread().getName() + "第" + i + "次存了1000元" + "现卡上余额: " + bankCard.getMoney());
                    }

                }

            }
        };
        Runnable subMoney = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    synchronized (bankCard){
                        if (bankCard.getMoney() >= 1000){

                            bankCard.setMoney(bankCard.getMoney()-1000);
                            System.out.println(Thread.currentThread().getName() + "第" + i + "次取了1000元" + "现卡上余额: " + bankCard.getMoney());
                        }else{
                            System.out.println("余额不足...");
                            i--;
                        }
                    }

                }
            }
        };

        //3. 创建两个线程
        Thread boy = new Thread(addMoney, "jack");
        Thread girl = new Thread(subMoney, "rose");

        boy.start();
        girl.start();

    }
}

(2) 同步方式2

  • 同步方法:

    synchronized 返回值类型 方法名称(形参列表0) {//对当前对象(this)加锁

    ​ //代码(原子操作)

    }

    注:

    ​ 只有拥有对象互斥锁标记的线程, 才能进入对该对象加锁的同步 方法中.

    ​ 线程退出同步方法时, 会释放相应的互斥锁标记

优化案例二(2): 使用同步方法
public class Ticket1 implements Runnable{

    private int ticket1 = 100;


    @Override
    public void run() {
        while (true){
            if (!sell()){
                break;
            }
        }

    }

    public synchronized boolean sell(){//锁是this 代表当前对象ticket1, 如果是静态方法, 锁则是类Ticket1.class
        if (ticket1 > 0){
            System.out.println(Thread.currentThread().getName() + "卖的第" + (101-ticket1) + "张票");
            ticket1--;
        }
        return true;
    }
}
public class TestSafeTicket1 {

    public static void main(String[] args) {

        //1. 创建票对象
        Ticket1 ticket1 = new Ticket1();

        Thread w1 = new Thread(ticket1, "窗口1");
        Thread w2 = new Thread(ticket1, "窗口2");
        Thread w3 = new Thread(ticket1, "窗口3");
        Thread w4 = new Thread(ticket1, "窗口4");

        w1.start();
        w2.start();
        w3.start();
        w4.start();


    }
}

(3) 同步规则

  • 注意:
    • 只有在调用包含同步代码块的方法, 或同步方法时, 才需要对象的锁标记
    • 如调用不包含同步代码块的方法, 或普通方法时,则不需要锁标记, 可直接调用
  • 已知JDK中线程安全的类:
    • StringBuffer
    • Vector
    • Hashtable
    • 以上类中的公开方法, 均为synchronized修饰的同步方法

(4) 经典问题: 死锁

  • 当第一个线程拥有A对象锁标记, 并等待B对象锁标记, 同时第二个线程拥有B对象锁标记, 并等待A对象锁标记时, 产生死锁
  • 一个线程可以同时拥有多个对象的锁标记, 当线程阻塞时, 不会释放已经拥有的锁标记, 由此可能造成死锁
public class MyLock {

    //两个锁: 代表两根筷子,同时拿到这两根筷子才能吃饭
    public static Object lock1 = new Object();
    public static Object lock2 = new Object();

}
public class Boy extends Thread{

    @Override
    public void run() {
        synchronized (MyLock.lock1){
            System.out.println("男孩拿到了筷子A");
            synchronized (MyLock.lock2){
                System.out.println("男孩拿到了筷子B");
                System.out.println("男孩可以吃饭了");
            }
        }
    }
}
public class Girl extends Thread{

    @Override
    public void run() {
        synchronized (MyLock.lock2){
            System.out.println("女孩拿到了筷子B");
            synchronized (MyLock.lock1){
                System.out.println("女孩拿到了筷子A");
                System.out.println("女孩可以吃饭了");
            }
        }
    }
}
public class TestDeadLock {

    public static void main(String[] args) {

        //1. 创建两个线程对象男孩, 女孩
        Boy boy = new Boy();
        Girl girl = new Girl();

        //2. 女孩线程启动
        girl.start();
        //3. 休息0.5秒钟(如果他们两个同时启动就会造成死锁)
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //4. 最后启动男孩线程
        boy.start();


    }
}

(5) 线程通信

  • 等待

    • public final void wait()
    • public final void wait(long timeout)
    • 必须在对obj加锁的同步代码块中. 在一个线程中, 调用obj.wait() 时, 此线程会释放其拥有的所有锁标记. 同时此线程阻塞在obj的等待队列中. 释放锁, 进入等待队列
  • 通知

    • public final void notify()
    • public final void notifyAll()
  • 多存多取问题分析: 线程被重新唤醒后没有对标记flag进行判断

    • 解决: 只需把if语句改成while, 每次线程被唤醒后重新判断一下标记
  • 全部等待问题分析: 唤醒出现问题. notify 只能随机唤醒一个线程

    • 解决: notify改为notifyAll

银行卡类

public class BankCard {

    //余额
    private double money;
    //标记
    private boolean flag = false;//true 表示有钱可以取不能存, false表示没钱可以存不能取

    //存钱
    public synchronized void deposit(double m){//this
        while (flag){//有钱
            try {
                this.wait();//进入等待队列 调用wait()方法的对象一定是锁,this代表锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        money = money + m;
        System.out.println(Thread.currentThread().getName() + "存了" + m + "元, 当前余额为: " + money);
        //修改标记
        flag = true;
        //唤醒取钱线程
        this.notifyAll();
    }
    //取钱
    public synchronized void draw(double m){//this
        while (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        money = money - m;
        System.out.println(Thread.currentThread().getName() + "取了" + m + "元, 当前余额为: " + money);
        //修改标记
        flag = false;
        //唤醒存钱线程
        this.notifyAll();
    }
}

存钱

public class Deposit implements Runnable{

    private BankCard bankCard;
    public Deposit(BankCard bankCard){
        this.bankCard = bankCard;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            bankCard.deposit(1000);
        }

    }
}

取钱

public class Draw implements Runnable{

    private BankCard bankCard;

    public Draw(BankCard bankCard){
        this.bankCard = bankCard;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            bankCard.draw(1000);
        }
    }
}

(6) 经典问题: 生产者, 消费者

若干个生产者在生产产品, 这些产品将提供给若干个消费者去消费, 为了使生产者和消费者能并发执行, 在两者之间设置一个能存储多个产品的缓冲区, 生产者将生产的产品放入缓冲区中, 消费者从缓冲区中取走产品进行消费, 显然生产者和消费者之间必须保持同步, 即不允许消费者到一个空的缓冲区中取产品, 也不允许生产者向一个满的缓冲区中放入产品

面包类

public class Bread {

    private int id;//产品id
    private String productName;//产家名称

    public Bread(int id, String productName) {
        this.id = id;
        this.productName = productName;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }
}

存放面包的容器

public class BreadCon {

    //存放面包的数组
    private Bread[] cons = new Bread[6];
    //存放面包的位置
    private int index = 0;

    //存放面包
    public synchronized void input(Bread b){ // 锁this
        //判断容器有没有满
        while (index >= 6){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        cons[index] = b;
        System.out.println(Thread.currentThread().getName() + "生产了" + b.getId() + "号产品");
        index++;
        //唤醒
        this.notifyAll();

    }

    //取出面包
    public synchronized void output(){ // 锁this
        while (index <= 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        index--;
        Bread b = cons[index];
        System.out.println(Thread.currentThread().getName() + "消费了" + b.getProductName() + "生产的" + b.getId() + "号产品");
        cons[index] = null;
        //唤醒
        this.notifyAll();
    }
}

生产类

public class Product implements Runnable{
    private BreadCon con;

    public Product(BreadCon con) {
        this.con = con;
    }

    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            con.input(new Bread(i, Thread.currentThread().getName()));
        }
    }
}

消费类

public class Consume implements Runnable{
    private BreadCon con;

    public Consume(BreadCon con) {
        this.con = con;
    }

    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            con.output();
        }
    }
}

测试

public class Test {

    public static void main(String[] args) {
        //容器
        BreadCon con = new BreadCon();

        //生产和消费
        Product product = new Product(con);
        Consume consume = new Consume(con);

        //创建线程对象
        Thread p1 = new Thread(product, "1号厂家");
        Thread c1 = new Thread(consume, "1号消费者");
        Thread p2 = new Thread(product, "2号厂家");
        Thread c2 = new Thread(consume, "2号消费者");

        //启动线程
        p1.start();
        c1.start();
        p2.start();
        c2.start();
        
    }
}

10. 线程池

  • 问题:
    • 线程是宝贵的内存资源, 单个线程约占1MB空间, 过多分配易造成内存溢出
    • 频繁的创建及销毁线程会增加虚拟机回收频率, 资源开销, 造成程序性能下降
(1) 线程池
  • 线程容器, 可设定线程分配的数量上限
  • 将预先创建的线程对象存入池中, 并重用线程池中的线程对象
  • 避免频繁的创建和销毁
(2) 线程池原理
  • 将任务提交给线程池, 由线程池分配线程, 运行任务, 并在当前任务结束后复用线程
(3) 创建线程池
  • 常用的线程池接口和类(所在包java.util.concurrent):
    • Executor: 线程池的顶级接口
    • ExecutorService: 线程池接口, 可通过submit(Runnable task)提交任务代码
    • Executors工厂类: 通过此类可以获得一个线程池
    • 通过newFixedThreadPool(int nThreads) 获取固定数量的线程池. 参数: 指定线程池中线程的数量
    • 通过newCachedThreadPool() 获得动态数量的线程池, 如不够则创建新的, 没有上限
public class Demo01 {

    public static void main(String[] args) {
        //1. 创建线程池
        //1.1 创建固定线程个数的线程池
        //ExecutorService es = Executors.newFixedThreadPool(4);
        //1.2 创建缓存线程池, 线程个数由任务个数决定
        ExecutorService es = Executors.newCachedThreadPool();
        //1.3 创建单线程池
        //Executors.newSingleThreadExecutor();
        //1.4 创建调度线程池
        //Executors.newScheduledThreadPool(3);
        //2. 创建任务
        Runnable runnable = new Runnable() {
            private int ticket = 100;
            @Override
            public void run() {
                while (true){
                    if (ticket <= 0){
                        break;
                    }
                    System.out.println(Thread.currentThread().getName() + "卖的第" + (101-ticket) + "张票");
                    ticket--;
                }
            }
        };
        //3. 提交任务
        for (int i = 0; i < 4; i++) {
            es.submit(runnable);
        }
        //4. 关闭线程池
        es.shutdown();//等待所有任务执行完毕后才会结束,关闭线程池
        //es.shutdownNow();//试图停止所有正在执行的活动任务, 暂停处理正在等待的任务,并返回等待执行的任务列表

    }
}
(4) Callable接口

public interface Callable{

​ public V call() throws Exception;

}

  • JDK5加入, 与Runnable接口类似, 实现后代表一个线程任务
  • Callable具有泛型返回值, 可以声明异常

使用Callable实现1-100的和

public class Demo02 {

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

        //功能需求: 使用Callable实现1-100的和
        //1. 创建Callable对象
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName() + "开始计算: ");
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        //2. 把Callable对象转成可执行的任务
        FutureTask<Integer> task = new FutureTask<>(callable);

        //3. 创建线程
        Thread thread = new Thread(task);

        //4. 启动线程
        thread.start();

        //5. 获取结果(等待call方法执行完毕,才会返回)
        Integer sum = task.get();
        System.out.println("结果是: " + sum);

    }
}

使用线程池计算1-100的和

public class Demo03 {

    public static void main(String[] args) throws Exception{
        
        //1. 创建线程池
        ExecutorService es = Executors.newFixedThreadPool(1);
        //2. 提交任务Future:表示将要执行完任务的结果
        Future<Integer> future = es.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName() + "开始计算: ");
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    sum += i;
                    Thread.sleep(10);
                }
                return sum;
            }
        });
        //3. 获取任务结果,等待任务执行完毕才会返回
        System.out.println(future.get());

        //4. 关闭线程池
        es.shutdown();
    }
}
(5) Future接口
  • Future: 表示将要完成任务的结果
  • 表示ExecutorService.submit()所返回的状态结果, 就是call()的返回值
  • 方法: V get() 以阻塞形式等待Future中的异步处理结果(call()的返回值)
  • 需求: 使用两个线程, 并发计算1~50, 50~100的和,再进行汇总
public class Demo04 {

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

        //1. 创建线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //2. 提交任务
        Future<Integer> future1 = es.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 50; i++) {
                    sum += i;
                }
                System.out.println("1~50计算完毕");
                return sum;
            }
        });
        Future<Integer> future2 = es.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 51; i <= 100; i++) {
                    sum += i;
                }
                System.out.println("51~100计算完毕");
                return sum;
            }
        });

        //3. 获取结果
        int sum = future1.get() + future2.get();
        System.out.println("结果是: " + sum);

        //4. 关闭线程池
        es.shutdown();

    }
}
(6) 线程的同步和异步
  • 同步
    • 形容一次方法调用, 同步一旦开始, 调用者必须等待该方法返回, 才能继续
    • 注: 单条执行路径
  • 异步
    • 形容一次方法调用, 异步一旦开始, 像是一次消息传递, 调用者告知之后立刻返回. 二者竞争时间片, 并发执行
    • 注: 多条执行路径

11. Lock接口

  • JDK5加入, 与synchronized比较, 显示定义, 结构更灵活
  • 提供更多实用性方法, 功能更强大, 性能更优越
  • 常用方法:
    • void lock() //获取锁, 如锁被占用, 则等待
    • boolean tryLock() //尝试获取锁(成功返回true, 失败返回false, 不阻塞)
    • void unlock() //释放锁
(1) 重入锁
  • ReentrantLock: Lock接口的实现类, 与synchronized一样具有互斥锁功能
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyList {

    private Lock lock = new ReentrantLock();
    private String[] str = {"A", "B", "", "", ""};
    private int count = 2;

    public void add(String value){
        lock.lock();
        try{
            str[count] = value;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        } finally {
            lock.unlock();
        }
    }

    public String[] getStr(){
        return str;
    }
}
public class TestMyList {

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

        MyList list = new MyList();
        Runnable runnable1 = new Runnable() {
            @Override
            public void run() {
                list.add("hello");
            }
        };
        Runnable runnable2 = new Runnable() {
            @Override
            public void run() {
                list.add("world");
            }
        };

        Thread t1 = new Thread(runnable1);
        Thread t2 = new Thread(runnable2);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(Arrays.toString(list.getStr()));
    }
}

使用重入锁实现4个窗口共同卖100张票

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

public class Ticket implements Runnable{

    private int ticket = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {

        while (true){
            lock.lock();
            try {

                if (ticket <= 0){
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "卖的第" + (101-ticket) + "张票");
                ticket--;

            } finally {
                lock.unlock();
            }
        }
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestTicket {

    public static void main(String[] args) {

        //1. 创建票对象
        Ticket ticket = new Ticket();

        //2. 创建线程池对象
        ExecutorService es = Executors.newFixedThreadPool(4);

        //3. 提交任务
        for (int i = 0; i < 4; i++) {
            es.submit(ticket);
        }
		//4. 关闭线程池
        es.shutdown();



    }
}
(2) 读写锁
  • ReentrantReadWriteLock
    • 一种支持一写多读的同步锁, 读写分离, 可分别分配读锁, 写锁
    • 支持多次分配读锁, 使多个读操作可以并发执行
  • 互斥规则
    • 写-写: 互斥
    • 读-写: 互斥, 读阻塞写, 写阻塞读
    • 读-读: 不互斥, 不阻塞
    • 在读操作远远高于写操作的环境中, 可在保障线程安全的情况下, 提高运行效率
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

public class ReadWriteLock {

    //创建读写锁
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    //获取读锁
    private ReadLock readLock = rwl.readLock();
    //获取写锁
    private WriteLock writeLock = rwl.writeLock();

    private String value;

    //读取
    public String getValue(){
        //使用读锁上锁
        readLock.lock();

        try {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("读取: " + this.value);
            return this.value;
        } finally {
            readLock.unlock();
        }
    }

    //写入
    public void setValue(String value){
        //使用写锁上锁
        writeLock.lock();

        try {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.value = value;
            System.out.println("写入: " + value);
        } finally {
            writeLock.unlock();
        }
    }

}

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestReadWriteLock {

    public static void main(String[] args) {
        //1. 创建读写锁对象
        ReadWriteLock rwl = new ReadWriteLock();
        //2. 创建线程池
        ExecutorService es = Executors.newFixedThreadPool(20);
        //3. 创建写任务
        Runnable write = new Runnable() {
            @Override
            public void run() {
                rwl.setValue("张三: " + new Random().nextInt(100));
            }
        };
        //4. 创建读任务
        Runnable read = new Runnable() {
            @Override
            public void run() {
                rwl.getValue();
            }
        };
        Long start = System.currentTimeMillis();//获取系统此刻时间(开始)
        //5. 分配2个线程来 写任务
        for (int i = 0; i < 2; i++) {
            es.submit(write);
        }
        //6. 分配18个线程来 读任务
        for (int i = 0; i < 18; i++) {
            es.submit(read);
        }
        //7. 关闭
        es.shutdown();
        while (!es.isTerminated()) {//空转

        }
        Long end = System.currentTimeMillis();//获取系统此刻时间(结束)
        System.out.println("用时: " + (end - start));//3007  如果使用用互斥锁时间为: 20048
    }
}

12. 线程安全的集合

  • Collection体系集合中, 除Vector以外的线程安全集合
  • Map安全集合体系
(1)CopyOnWriteArrayList
  • 线程安全的ArrayList, 加强版的读写分离
  • 写有锁, 读无锁, 读写之间不阻塞, 优于读写锁
  • 写入时, 先copy一个容器副本, 再添加新元素, 最后替换引用
  • 使用方式和ArrayList无异
public class Demo02 {

    public static void main(String[] args) {

        //1. 创建集合
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        //2. 使用多线程操作
        ExecutorService es = Executors.newFixedThreadPool(5);
        //3. 提交任务
        for (int i = 0; i < 5; i++) {
            es.submit(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10; j++) {
                        list.add(Thread.currentThread().getName() + "---" + new Random().nextInt(1000));
                    }
                }
            });
        }
        //4. 关闭线程池
        es.shutdown();
        while (!es.isTerminated()){}
        //5. 打印结果
        System.out.println("元素个数: " + list.size());
        for (String s : list) {
            System.out.println(s);
        }

    }
}
(2)CopyOnWriteArraySet
  • 线程安全的Set, 底层使用CopyOnWriteArrayList实现
  • 唯一不同在于, 使用addIfAbsent() 添加元素, 会遍历数组
  • 如存在元素, 则不添加(扔掉副本),重复依据 equals()方法
(3)Queue接口(队列)
  • Collection的子接口, 表示队列FIFO(First In First Out)先进先出
  • 常用方法:
    • 抛出异常:
      • boolean add(E e) //顺序添加一个元素(到达上限后, 再附加则会抛出异常)
      • E remove() //获得第一个元素并移除(如果队列没有元素时, 则抛异常)
      • E element() //获得第一个元素但不移除(如果队列没有元素时, 则抛异常)
    • 返回特殊值: 推荐使用
      • boolean offrt (E e) //顺序添加一个元素(到达上限后, 再添加则会返回false)
      • E poll() //获得第一个元素并移除 (如果队列没有元素时,则返回null)
      • E peek() //获得第一个元素但不移除(如果队列没有元素时, 则返回null)
public class Demo04 {

    public static void main(String[] args) {

        //1. 创建队列
        Queue<String> queue = new LinkedList<>();// LinkedList是线程不安全的集合,不能使用多线程
        //2. 入队
        queue.offer("香蕉");
        queue.offer("苹果");
        queue.offer("橘子");
        queue.offer("橙子");
        System.out.println("入队完毕, 元素个数: " + queue.size());
        System.out.println(queue.peek());//获取队列第一个元素但不移除
        System.out.println("----------------------");
        //3. 出队
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            System.out.println(queue.poll());//获得第一个元素并移除
        }
        System.out.println("出队完毕, 元素个数: " + queue.size());
    }
}

(4)ConcurrentLinkedQueue
  • 线程安全, 可高效读写的队列, 高并发下性能最好的队列
  • 无锁, CAS比较交换算法, 修改的方法包含三个核心参数(V,E,N)
  • V: 要更新的变量, E: 预期值, N: 新值
  • 只有当V=E时, V=N; 否则表示已被更新过, 则取消当前操作
public class Demo05 {

    public static void main(String[] args) throws Exception{
        //1. 创建线程安全集合
        ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
        //2. 入队操作
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    queue.offer(i);
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 5; i < 10; i++) {
                    queue.offer(i);
                }
            }
        });
        //3. 启动线程
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        //4. 出队操作
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            System.out.println(queue.poll());
        }
        System.out.println("出队成功, 元素个数: " + queue.size());

    }
}
(5)BlockingQueue接口
  • Queue的子接口, 阻塞的队列, 增加了两个线程状态为无限期等待的方法
  • 方法:
    • void put(E e) //将指定元素插入此队列中, 如果没有可用空间, 则等待
    • E take() //获取并移除此队列头部元素, 如果没有可用元素, 则等待
  • 可用于解决生产者, 消费者问题
(6)ArrayBlockingQueue
  • 数据结构实现, 有界队列. (手工固定上限)
public class Demo06 {

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

        //1. 创建一个有界队列, 添加数据
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
        //2. 添加元素
        queue.put("aaa");
        queue.put("bbb");
        queue.put("ccc");
        queue.put("ddd");
        queue.put("eee");
        System.out.println("已经添加了5个元素");
        //queue.put("fff");//无法添加,因为队列此时满了
        //System.out.println("已经添加了6个元素");
        //3. 删除元素
        queue.take();//删除第一个
        System.out.println(queue.toString());


    }
}

使用阻塞队列实现生产者和消费者

public class Demo07 {

    public static void main(String[] args) {

        //1. 创建队列
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(6);
        //2. 创建两个线程
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 30; i++) {
                    try {
                        queue.put(i);
                        System.out.println(Thread.currentThread().getName() + "生产了第" + i + "个面包");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "张三");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 30; i++) {
                    try {
                        queue.take();
                        System.out.println(Thread.currentThread().getName() + "消费了第" + i + "个面包");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }


                }
            }
        }, "李四");
        //3. 启动线程
        t1.start();
        t2.start();
    }
}
(7)LinkedBlockingQueue

链表结构实现, 有界队列. (默认上限Integer.MAX_VALUE)

(8)ConcurrentHashMap
  • 初始容量默认为16段(Segment), 使用分段锁设计(JDK1.7之前), (JDK1.8之后使用无锁, CAS比较交换算法)
  • 不对整个Map加锁, 而是为每个Segment加锁
  • 当多个对象存入同一个Segment时, 才会互斥
  • 最理想状态为16个对象分被存入16个Segment, 并行数量16
  • 使用方式与HashMap无异
public class Demo08 {

    public static void main(String[] args) {

        //1. 创建集合
        ConcurrentHashMap<String, String> hashMap = new ConcurrentHashMap<>();
        //2. 使用多线程添加数据
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10; j++) {
                        hashMap.put(Thread.currentThread().getName()+ "---" + j, j + "");
                        System.out.println(hashMap);
                    }
                }
            }).start();
        }
    }
}
原文地址:https://www.cnblogs.com/MRASdoubleZ/p/14503013.html