多线程详解

151 阅读22分钟

多线程详解

线程简介

多任务

多任务是操作系统可以执行多个程序的能力。操作系统使用硬件时钟为每个程序配置时间片段。

多线程

多线程是在程序内部实现“多任务”。

普通方法调用和多线程

程序 - 进程 - 线程

  • Process 与 Thread

    • 说起进程,就不得不说程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念

    • 进程则是执行程序的一次执行过程,他是一个动态的概念。是系统资源分配的单位。

    • 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是 CPU 调度和执行的单位

      注意:很多多线程是模拟出来的,真正的多线程是指多个 cpu,即多核,如服务器。如果是模拟出来的多线程,即在同一个 cpu 的情况下,在同一个时间点,cpu 只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

总结

  • 线程就是独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc 线程
  • main() 称之为主线程,为系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如 cpu 调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当对造成数据不一致

线程创建

三种创建方式

  1. 继承 Thread

    • 自定义线程类继承 Thread

    • 重写 run() 方法,编写线程执行体

    • 创建线程对象,调用 start() 方法启动线程

      public class TestThread extends Thread{
          @Override
          public void run(){
              //run 方法线程体
              for (int i = 0; i < 20; i ++){
                  System.out.println("这是线程---" + i);
              }
          }
      
          public static void main(String[] args){
      
              //创建一个线程对象,调用start()方法开启线程
              TestThread testThread = new TestThread();
              testThread.start();
      
              //main 主线程
              for (int i = 0; i < 200; i ++){
                  System.out.println("这是主线程---" + i);
              }
          }
      }
      

      结果:交替运行

 > 注意,线程开启不一定立即执行,由 cpu 调度执行
  1. 实现 Runnable 接口

    • 定义 MyRunnable 类实现 Runnable 接口

    • **实现 run() ** 方法,编写线程执行体

    • 创建线程对象,调用 start 方法启动线程

      public class TestThread3 implements Runnable {
          @Override
          public void run() {
              //run 方法线程体
              for (int i = 0; i < 20; i++) {
                  System.out.println("这是线程---" + i);
              }
          }
      
          public static void main(String[] args){
      
              //创建 runnable 接口的实现类对象
              TestThread3 testThread = new TestThread3();
      
              //创建线程对象,通过线程对象来开启我们的线程,代理
              Thread thread = new Thread(testThread);
      
              //调用线程对象的 start() 方法
              thread.start();
      
              //main 主线程
              for (int i = 0; i < 200; i ++){
                  System.out.println("这是主线程---" + i);
              }
          }
      }
      
  2. 实现 Callable 接口

静态代理

真实对象和代理对象都要是实现同一个接口,代理对象要代理真实角色,代理对象可以做很多真实对象做不了的是,真实对象专注自己的事情

实现Runnable接口的形式创建多线程,可以发现,代理角色Thread类不需要我们创建,我们只需要写委托对象实现Runnable接口.把委托对象的引用传递给Thread,借助Thread对象来开启线程即可

小结

  • 继承 Thread
    • 子类继承 Thread 类具备多线程能力
    • 启动线程:子类对象.start()
    • 不推荐使用:避免 OPP 单继承局限性
  • 实现 Runnable 接口
    • 实现接口 Runnable 具备多线程能力
    • 启动线程:传入目标对象 + Thread 对象.start()
    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

番外:Lamda 表达式

概述

  • λ 希腊字母表中排序第十一位,英文名称为 Lambda
  • 避免匿名内部类定义过多
  • 其实实质属于函数式编程的概念
(params) -> expression[表达式]
(params) -> statement[语句]
(params) -> {statements}

例如:

new Thread(()->System.out.println("hello world")).start();
  • 理解 Functional Interface (函数式接口) 是学习 Java8 lambda 表达式的关键所在

  • 函数式接口的定义:

    • 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。

      @FunctionalInterface
      public interface Runnable{
      	public abstract void run();
      }
      
    • 对于函数式接口,我们可以通过 lambda 表达式来创建该接口对象

推导过程

package com.dedezhang.lambda;

/**
 * @program: demo->TestLambda1
 * @description: 推导 lambda 表达式
 * @author: dedezhang
 * @create: 2020-03-22 00:30
 **/
public class TestLambda1 {

    //3、静态内部类
    static class Like2 implements ILike{
        @Override
        public void lambda() {
            System.out.println("i like lambda2");
        }
    }

    public static void main(String[] args){
        ILike like = new Like();
        like.lambda();

        like = new Like2();
        like.lambda();


        //4、局部内部类
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }

        like = new Like3();
        like.lambda();

        //5、匿名内部类,没有类的名称,必须借助接口或者父类
        like = new ILike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };

        //6、用 lambad 简化
        like = ()->{
                System.out.println("i like lambda5");
        };
        like.lambda();
    }

}

//1、定义一个函数式表达式
interface ILike{
    void lambda();
}

//2、实现类
class Like implements ILike{

    @Override
    public void lambda() {
        System.out.println("i like lambda");
    }
}

简化

表达式可以进一步简化:

package com.dedezhang.lambda;

/**
 * @program: demo->TestLambda
 * @description:
 * @author: dedezhang
 * @create: 2020-03-22 00:45
 **/
public class TestLambda2 {
    public static void main(String[] args) {

        //1、lambda 表达式简化
        ILove love = (int a)->{
            System.out.println("i love you ---" + a);
        };

        //简化1、参数类型简化
        love = (a)-> {
            System.out.println("i love you ---" + a);
        };

        //简化2、简化括号(只有一个参数的情况下)
        love = a-> {
            System.out.println("i love you ---" + a);
        };
        
        //简化3、去掉花括号(实现只有一行的情况下)
        love = a-> System.out.println("i love you ---" + a);

        love.love(2);
    }

}

interface ILove{
    void love(int a);
}

线程状态

五大状态

  • 创建状态
  • 就绪状态
  • 运行状态
  • 阻塞状态
  • 死亡状态

  • 线程方法
方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程
boolean isAlive() 测试线程是否处于活动状态

线程停止

  • 不推荐使用 JDK 提供的 stop()destroy() 方法/
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量,例 当 flag = false ,则终止线程;

例如:

package com.dedezhang.state;

/**
 * @program: demo->TestStop
 * @description: 测试 stop
 * 1、建议线程正常停止--->利用次数,不建议死循环
 * 2、建议使用标志位--->设置一个标志
 * 3、不要使用 stop 或 destroy 等过时或者 JDK 不建议使用的方法
 * @author: dedezhang
 * @create: 2020-03-22 01:17
 **/
public class TestStop implements Runnable{

    /**
     * 1、设置一个标识位
     */
     private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag){
            System.out.println("run....Thread" + i++);
        }

    }

    /**
     * 2、设置一个公开的方法停止线程,转换标识符
     */
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {
        TestStop testStop = new TestStop();

        new Thread(testStop).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main" + i);
            if (i == 900){
                //调用 stop 方法切换标志位,让线程停止
                testStop.stop();
                System.out.println("该线程已停止");
            }
        }
    }
}

线程休眠

  • sleep() 指定当前线程阻塞的毫秒数
  • sleep 存在异常 InterruptedException
  • sleep 时间达到后线程进入就绪状态
  • sleep 可以模拟网络延时,倒计时等
  • 每个对象都有一个锁,sleep 不会释放锁

模拟网络延时的作用:放大问题的发生性

例子:

package com.dedezhang.state;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @program: demo->TestSleep
 * @description: 测试线程休眠
 * 模拟倒计时
 * @author: dedezhang
 * @create: 2020-03-22 14:31
 **/
public class TestSleep {

    public static void tenDown() throws InterruptedException {
        int num = 10;

        while (true){
            Thread.sleep(1000);
            System.out.println(num--);
            if (num == 0){
                break;
            }
        }
    }

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

        //模拟倒计数
        tenDown();

        //打印当前系统时间
        Date startTime = new Date(System.currentTimeMillis());

        while (true){
            Thread.sleep(1000);
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
            startTime = new Date(System.currentTimeMillis());
        }

    }
}

线程礼让——yield()

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞

  • 将线程从运行状态转为就绪状态

  • 让 cpu 重新调度,礼让不一定成功,看 cpu 心情

    package com.dedezhang.state;
    
    /**
     * @program: demo->TestYield
     * @description: 测试礼让程序
     * @author: dedezhang
     * @create: 2020-03-22 15:38
     **/
    public class TestYield {
    
        public static void main(String[] args) {
            MyYield myYield = new MyYield();
    
            new Thread(myYield,"a").start();
            new Thread(myYield,"b").start();
        }
    }
    
    class MyYield implements Runnable{
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "线程开始执行");
            Thread.yield();//礼让
            System.out.println(Thread.currentThread().getName() + "线程停止执行");
        }
    }
    
    #运行结果:
    #a线程开始执行
    #b线程开始执行
    #b线程停止执行
    #a线程停止执行
    
    

线程强制执行——join()

  • Join 合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞

  • 可以理解为插队

  • 不推荐使用

    package com.dedezhang.state;
    
    import org.omg.PortableServer.THREAD_POLICY_ID;
    
    /**
     * @program: demo->TestJoin
     * @description: 测试 Join 方法
     * @author: dedezhang
     * @create: 2020-03-22 15:48
     **/
    public class TestJoin implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("线程vip" + i);
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            //启动我们的线程
            TestJoin testJoin = new TestJoin();
            Thread thread = new Thread(testJoin);
            thread.start();
            //主线程
            for (int i = 0; i < 500; i++) {
                if (i == 200){
                    thread.join();//插队
                }
                System.out.println("main" + i);
            }
        }
    }
    
    

线程状态观测——Thread.State

  • Thread.State 线程状态可以处于以下状态之一:

    • NEW : 尚未启动的线程处于此状态
    • RUNNABLE : 在 Java 虚拟机中执行的线程处于此状态
    • BLOCKED : 被阻塞等待监视器锁定的线程处于此状态
    • WAITING : 正在等待另一个线程执行特定动作的线程处于此状态
    • TIME_WAITING : 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
    • TERMINATED : 已退出的线程处于此状态

    一个线程可以在给定时间点处于一个状态,这些状态是不反应任何操作系统线程状态的虚拟机状态

    package com.dedezhang.state;
    
    /**
     * @program: demo->TestState
     * @description: 观测线程的状态
     * @author: dedezhang
     * @create: 2020-03-22 16:07
     **/
    public class TestState {
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(()->{
                for (int i = 0; i < 5; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("/////");
            });
    
            //观测状态
            Thread.State state = thread.getState();
            System.out.println(state);
    
            //观察启动后
            thread.start();//启动线程
            state = thread.getState();
            System.out.println(state);
    
            while (state != Thread.State.TERMINATED){ //只要线程不终止,就一直输出状态
                Thread.sleep(100);
                state = thread.getState();//更新线程状态
                System.out.println(state);
            }
    
        }
    
    }
    
    

线程优先级——priority

  • Java 提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
  • 线程的优先级用数字表示,范围从 1~10
    • Thread.MIN_PRIORITY = 1;
    • Thread.MAX_PRIORITY = 10;
    • Thread.NORM_PRIORITY = 5;
  • 使用以下方式来改变或获取优先级
    • getPriority setPriority(int xxx);
package com.dedezhang.state;

import org.omg.PortableServer.THREAD_POLICY_ID;

/**
 * @program: demo->TestPriority
 * @description: 测试优先级
 * @author: dedezhang
 * @create: 2020-03-23 23:41
 **/
public class TestPriority {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "---" + Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();

        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);
        Thread t5 = new Thread(myPriority);

        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(Thread.MAX_PRIORITY);
        t4.start();

        t5.setPriority(6);
        t5.start();
    }
}

class MyPriority implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---" + Thread.currentThread().getPriority());
    }
}

  • 小结:
    • 优先级低只是意味着获得调度的概率低,应不识优先级低就不会被调用,这都是看 cpu 的调度
    • 一定是先设置优先级再启动线程

守护 (daemon) 线程

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如:后台记录操作日志,监控内存,垃圾回收等待
package com.dedezhang.state;

import java.util.HashMap;

/**
 * @program: demo->TestDaemon
 * @description: 测试守护线程
 * @author: dedezhang
 * @create: 2020-03-23 23:58
 **/
public class TestDaemon {

    public static void main(String[] args) {
        God god = new God();
        Human human = new Human();

        Thread thread = new Thread(god);
        /**
         * 设置位守护线程
         */
        thread.setDaemon(true);
        thread.start();

        new Thread(human).start();
    }
}

//上帝
class God implements Runnable{

    @Override
    public void run() {
        while (true){
            System.out.println("上帝守护你");
        }
    }
}

//人类
class Human implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("you alive");
        }
        System.out.println("you die");
    }
}

线程同步

处理多线程问题是,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池行程队列,等待前面线程使用完毕,下一个线程再使用

  • 并发:同一个对象多个线程同时操作
    • 处理多线程问题是,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池行程队列,等待前面线程使用完毕,下一个线程再使用
  • 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问的正确性,在访问时加入**锁机制 synchronized ** ,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 再多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

队列和锁

同步方法

  • 由于我们可以通过 private 关键字来保证数据只能被方法访问,所有我们只需要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种方法:

    • synchronized 方法和 synchronized
    • 同步方法:public synchronized void method(int args){}
  • synchtonized 方法控制对“对象”的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程就会被阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

    • 缺陷:若将一个大的方法声明为 synchronized 将会影响效率
  • 同步方法弊端:

    • 方法里面需要修改的内容才需要锁,锁太多会浪费资源
    package com.dedezhang.syn;
    
    /**
     * @program: demo->UnsafeBuyTicket
     * @description:
     * @author: dedezhang
     * @create: 2020-03-24 00:20
     **/
    public class UnsafeBuyTicket {
    
        public static void main(String[] args) {
            BuyTicket buyTicket = new BuyTicket();
    
            new Thread(buyTicket,"小明").start();
            new Thread(buyTicket,"小红").start();
            new Thread(buyTicket,"小刚").start();
    
        }
    }
    
    class BuyTicket implements Runnable{
    
        //票
        private int ticketNum = 10;
    
        //停止标志位
        boolean flag = true;
    
        @Override
        public void run() {
            //买票
            while (flag){
                try {
                    buy();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        //synchronized 同步方法,锁是this
        private synchronized void buy() throws InterruptedException {
            //判单是否有票
            if (ticketNum <= 0){
                flag = false;
                return;
            }
            
    
            System.out.println(Thread.currentThread().getName() + "买到了票" + ticketNum--);
        }
    }
    
    

同步块

  • 同步块:synchronized(Obj){}
  • Obj 称之为同步监视器
    • Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this ,就是这个对象本身,或者是 class
  • 同步监视器的执行过程
    1. 第一个线程访问,锁定同步监视器,执行其中代码
    2. 第二个线程访问,发现同步监视器被锁定,无法访问
    3. 第一个线程访问完毕,解锁同步监视器
    4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
  • 锁的对象是变化的量,需要增删改的对象
//同步块
synchronized (account){
    //判断是否有钱
    if (account.money - drawingMoney < 0){
        System.out.println("余额不足");
        return;
    }

    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    //余额
    account.money = account.money - drawingMoney;
    System.out.println( "已取:" + drawingMoney + "," + account.name + "余额:" + account.money);
}

了解 GUA

package com.dedezhang.syn;

import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @program: demo->TestGUC
 * @description: 测试GUC安全类型的集合
 * @author: dedezhang
 * @create: 2020-03-24 01:16
 **/
public class TestGUC {
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size());
    }
}

死锁

  • 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有两个以上对象的锁时,就可能会发生“死锁”的问题
  • 死锁:
package com.dedezhang.syn;

import org.omg.PortableServer.THREAD_POLICY_ID;

/**
 * @program: demo->DeadLock
 * @description: 死锁
 * 多个线程互相抱着对方需要的资源,然后形成僵持
 * @author: dedezhang
 * @create: 2020-03-24 01:32
 **/
public class DeadLock {

    public static void main(String[] args) {
        Makeup girl1 = new Makeup(0,"小红");
        Makeup girl2 = new Makeup(1,"小芳");

        girl1.start();
        girl2.start();
    }
}

//口红
class Lipstick{

}

//镜子
class Mirror{}

//化妆
class Makeup extends Thread{
    //需要的资源只有一份,用 static 来保证只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;

    String girl;

    Makeup(int choice,String girl){
        this.choice = choice;
        this.girl = girl;
    }

    @Override
    public void run() {
        //化妆
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //化妆 互相持有对方的锁
    private void makeup() throws InterruptedException {
        if (choice == 0){
            synchronized (lipstick){
                System.out.println(Thread.currentThread().getName() + "获得口红的锁");
                Thread.sleep(1000);

            }
        }else {
            synchronized (mirror){
                System.out.println(Thread.currentThread().getName() + "获得镜子的锁");
                Thread.sleep(2000);
                synchronized (lipstick){    //两秒后想获得口红
                    System.out.println(this.getName() + "获得口红的锁");
                }
            }
        }
    }
}

  • 解决方式:不抱着对方的锁
	if (choice == 0){
            synchronized (lipstick){
                System.out.println(Thread.currentThread().getName() + "获得口红的锁");
                Thread.sleep(1000);
            }
            synchronized (mirror){  //一秒后想获取镜子
                System.out.println(this.getName() + "获得镜子的锁");
            }
        }else {
            synchronized (mirror){
                System.out.println(Thread.currentThread().getName() + "获得镜子的锁");
                Thread.sleep(2000);
            }
            synchronized (lipstick){    //两秒后想获得口红
                System.out.println(this.getName() + "获得口红的锁");
            }
        }

避免死锁的方法

  • 产生死锁的四个必要条件

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

    以上四个死锁必要条件,只要想办法破其中一种或多个条件就可以避免死锁发生

Lock(锁)

  • 从 jdk 5.0 开始,Java 提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁对象使用 Lock 对象来充当

  • java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前,应先获得 Lock 对象

  • ReentrantLock (可重入锁) 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock ,可以显示加锁、释放锁

    classA{
    	private final ReentrantLock lock = new TeenTrantLock();
    	public void m(){
    		lock.lock();
    		try{
    			//保证线程安全的代码
    		}finally{
    			lock.unlock();
    			//如果同步代码有异常,要将 unlock() 写入 finally 语句块
    		}
    	}
    }
    
    

    例如:

    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @program: demo->TestLock
     * @description:测试Lock锁
     * @author: dedezhang
     * @create: 2020-03-24 23:34
     **/
    class TestLock2 implements Runnable{
        int ticketNum = 10;
    
        //定于 Lock 锁
        private final ReentrantLock lock = new ReentrantLock();
    
        @Override
        public void run() {
            while (true){
                /**
                 * 加锁
                 */
                try{
                    lock.lock();
                    if (ticketNum > 0){
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "抢到了" + ticketNum --);
                    }else {
                        break;
                    }
                }finally {
                    /**
                     * 解锁
                     */
                    lock.unlock();
                }
            }
        }
    }
    
    

synchronized 与 Lock 锁的对比

  • Lock 是显示锁(手动开启和关闭锁),synchronized 是隐式锁,出了作用域就自动释放
  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁
  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:
    • Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)

线程协作

线程通信

  • 应用场景:生产者和消费者问题

    • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费
    • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
    • 如果仓库中有产品,则消费者可以将产品取走消费,否者停止消费并等待,直到仓库中再次放入产品为止
  • 这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件

    • 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,有需要马上通知消费者消费
    • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
    • 在生产者消费者问题中,仅有 synchronized 是不够的:
      • synchronized 可阻止并发更新同一个共享资源,实现了同步
      • synchronized 不能用来实现不同线程之间的消息传递(通信)
  • Java 提供了几个方法解决线程之间的通信问题

    方法名 作用
    wait() 表示线程一直等待,直到其他线程通知,与 sleep 不同,会释放锁
    wait(long timeout) 指定等待的毫秒数
    notify() 唤醒一个处于等待状态的线程
    notifyAll() 唤醒同一个对象上所有调用 wait() 方法的线程,优先级别高的线程优先调度

    注意:均是 Object 类的方法,都只能在同步方法或者同步代码块中使用, 否者会抛出异常 lllegaMonitorStateException

管程法

​ 并发协作模型“生产者/消费者模式”——管程法

  • 生产者:负责生产数据的模块(可能是方法、对象、线程、进程)

  • 消费者:负责处理数据的模块(可能是方法、对象、线程、进程)

  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”

  • 生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

  • 示例:

package com.dedezhang.gaoji;
  
/**

- @program: demo->TestPC

- @description: 生产者/消费者模型——管程法

- @author: dedezhang

- @create: 2020-03-25 00:17
  **/
  public class TestPC {

  public static void main(String[] args) {
      SynContainer container = new SynContainer();

  new Productor(container).start();
  new Consumer(container ).start();
  

  }
  }

//生产者
class Productor extends Thread{
    SynContainer container;

public Productor(SynContainer container){
    this.container = container;
}

//生产

@Override
public void run() {
    for (int i = 0; i < 100; i++) {
        try {
            container.push(new Chicken(i));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生产了" + i + "只鸡");
    }
}


}

//消费者
class Consumer extends Thread{
    SynContainer container;

public Consumer(SynContainer container){
    this.container = container;
}

//消费
@Override
public void run() {
    for (int i = 0; i < 100; i++) {
        try {
            container.pup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("消费了第" + i + "只鸡");
    }
}


}

//产品
class Chicken{
    //产品编号
    int id;

public Chicken(int id){
    this.id = id;
}


}

//缓冲区
class SynContainer{

//需要一个容器大小
Chicken[] chickens = new Chicken[10];
//容器计数器
int count = 0;

//生产者放入产品
public synchronized void push(Chicken chicken) throws InterruptedException {
    //如果容器满了,就需要等待消费者消费
    if (count == chickens.length){
        //通知消费者消费,生产等待
        this.wait();
    }

    //如果没满,就需要丢入产品
    chickens[count] = chicken;
    count ++;

    //通知消费者消费
    this.notifyAll();
}


//消费者消费产品
public synchronized Chicken pup() throws InterruptedException {
    //判断能否消费
    if (count == 0){
        //等待生产者生产,消费者等待
        this.wait();
    }

    //如果可以消费
    count --;
    Chicken chicken = chickens[count];

    //吃完了,通知生产者生产
    this.notifyAll();

    return chicken;
}


}

信号灯法

并发协作模型“生产者/消费者模式”——信号灯法

  • 通过标志位解决
  • 示例:
package com.dedezhang.gaoji;

/**
 * @program: demo->TestPC2
 * @description: 测试生产者/消费者问题——信号灯法
 * @author: dedezhang
 * @create: 2020-03-25 00:43
 **/
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();

        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生产者——演员
class Player extends Thread{
    TV tv;
    public Player(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0){
                this.tv.play("中国好声音");
            }else {
                this.tv.play("广告");
            }
        }
    }
}

//消费者——观众
class Watcher extends Thread{
    TV tv;
    public Watcher(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            this.tv.watch();
        }
    }
}

//产品——节目
class TV {

    //演员表演的时候,观众等待
    //观众观看的时候,演员等待
    String voice;//节目
    
    //标志位
    boolean flag = true;

    //表演
    public synchronized void play(String voice){

        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("演员表演了:" + voice);

        //通知观众观看
        this.notifyAll();
        this.voice = voice;

        this.flag = !this.flag;
    }

    //观看
    public synchronized void watch() {

        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了:" + voice);

        //观看完了,通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}

使用线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放池中,可以避免频繁创建销毁、实现重复利用
  • 好处:
    • 降低相应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
    • 便于线程管理()
  • 管理参数
    • corePoolSize :核心池的大小
    • maximumPoolSize :最大线程数
    • keepAliveTime :线程没有任务时最多保持多长时间后会终止
  • JDK 5 起提供了线程池相关 API : ExecutorServiceExecutors
    • ExecutorService :真正的线程池接口,常见子类: ThreadPoolExecutor
      • void execute(Runnable command) :执行任务,没有返回值,一般用来执行 Runnable
      • <T> Furure<T> submit(Callable<T> rask) :执行任务,有返回值.一般用来执行 Callable
      • void shutdown() :关闭线程池
    • Executors :工具类,线程池的工厂类,用于创建并返回不同类型的线程池

示例:

package com.dedezhang.gaoji;

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

/**
 * @program: demo->TestPool
 * @description: 测试线程池
 * @author: dedezhang
 * @create: 2020-03-25 01:13
 **/
public class TestPool {

    public static void main(String[] args) {
        //1、创建服务,创建线程池
        //new FixedThreadPool 参数为线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);

        //执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //2、关闭连接
        service.shutdown();
    }

}

class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
#结果
#pool-1-thread-2
#pool-1-thread-3
#pool-1-thread-1

高级主题