• 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();
            }
        }
    }
    
  • 相关阅读:
    mysql 基础整合大全
    django 完整日志配置
    rest_framework视图基类与五个扩展类
    Nginx + uWSGI 配置django---终极版
    小程序 textarea ios兼容解决
    小程序调取数字键盘,没有小数点解决办法
    消息框-提示框
    h5页面长按保存图片
    解决浏览器自动填充input
    angularJS(2)
  • 原文地址:https://www.cnblogs.com/MRASdoubleZ/p/14503013.html
Copyright © 2020-2023  润新知