美文网首页
Java-09 并发编程

Java-09 并发编程

作者: 哆啦_ | 来源:发表于2020-06-30 09:01 被阅读0次

    线程的创建

    创建并开启一个新的线程

    第一种方法

        // 创建线程
        Thread thread = new Thread(new Runnable(){
          @Override
          public void run() { 
            System.out.println("新线程任务-----------");
          }
        });
        thread.setName("lwy");
        // 开启线程
        thread.start();
    
    

    Thread调用start方法之后,内部会调用run方法。

    注意: 直接调用run方法并不能开启新线程,只是在当前线程执行run里面的任务而已。 调用start方法才能开启新线程(start内部有一个native的start0方法,会向内核申请开启新线程)

    第二种方法: 创建一个线程类(继承自Thread)

    public class MyThread extends Thread {
      @Override
      public void run() {
        System.out.println("自己的线程类");
      }
    }
    
        Thread thread = new MyThread();
        thread.start();
    

    多线程的内存布局

    PC寄存器:每个线程都有自己的pc寄存器

    java虚拟机栈:每个线程都有自己的java虚拟机栈

    堆(Heap):多个线程共享

    方法区: 多个线程共享方法区

    本地方法栈: 每个线程都有自己的本地方法栈

    线程的状态

    java中线程一共有6种状态。可以通过getState方法获取

        public enum State {
            // 新建(还未启动)
            NEW,
            // 可运行状态(正在JVM中运行。或者正在等待操作系统的其他资源(比如处理器))
            RUNNABLE,
            BLOCKED,
            WAITING,
            TIMED_WAITING,
            TERMINATED;
        }
    
    1. NEW: 新建(还未启动

    2. RUNNABLE : 可运行状态(正在JVM中运行。或者正在等待操作系统的其他资源(比如处理器))

    3. BLOCKED: 阻塞状态,正在等待监视器锁(内部锁)

    4. WAITING: 等待状态,在等待另一个线程

    5. TIMED_WAITING:定时等待状态,

    6. TERMINATED: 终止状态,已经执行完毕

    BLOCK跟WATING的区别:

    一个线程如果正在执行任务,会消耗CPU时间片。BLOCK状态是等待锁,
    类似于一直执行while(锁没有被释放); ,会消耗时间片。而WAITING状态就是等待其他线程,处于休眠,CPU不会分配时间片,也不会执行其他代码

    线程的方法

    sleep、interrupt

    可以通过Thread.sleep方法来暂停线程,进入WATING状态。
    在暂停期间,若调用线程对象的interrupt方法中断线程,会抛出java.lang.InterruptedExpection异常

        Thread thread = new Thread(() -> {
          System.out.println(1);
          try {
            Thread.sleep(3000);
          } catch (Exception e) {
            e.printStackTrace();
          }
          System.out.println(2);
        });
        
        thread.start();
        
        try {
          Thread.sleep(1000);
        } catch (Exception e) {
    
        }
        System.out.println(3);
    
        thread.interrupt();
    
    

    打印:

    1
    3
    java.lang.InterruptedException: sleep interrupted
            at java.base/java.lang.Thread.sleep(Native Method)
            at Thread.Main.lambda$0(Main.java:12)
            at java.base/java.lang.Thread.run(Thread.java:835)
    2
    

    join、isAlive

    A.join: 等线程A执行完毕之后,当前线程再继续执行任务。可以传参制定最长等待时间

        Thread thread = new Thread(()-> {
          System.out.println(1);
          try {
            Thread.sleep(3000);
          } catch (Exception e) {
            e.printStackTrace();
          }
          System.out.println(2);
        });
        thread.start();
    
        System.out.println(3);
    

    打印

    3
    1
    2
    

    加入等待之后:

        Thread thread = new Thread(()-> {
          System.out.println(1);
          try {
            Thread.sleep(3000);
          } catch (Exception e) {
            e.printStackTrace();
          }
          System.out.println(2);
        });
        thread.start();
        try {
        // 等待线程thread执行完毕之后再往下执行
          thread.join();
        } catch (Exception e) {
        }
        System.out.println(3);
    

    打印

    1
    2
    3
    

    A.isAlive: 查看线程A是否活着

    线程安全问题

    多个线程可能会共享(访问)同一个资源,比如访问同一个对象,同一个变量,同一个文件。当多个线程访问同一块资源是,很容易引发数据错乱和数据安全问题,成为线程安全问题

    什么时候下会出现线程安全问题?

    • 多个线程共享同一个资源,且至少一个线程正在进行写操作

    线程同步

    • 可以使用线程同步技术来解决线程安全问题
      • 同步语句(Synchronized Statement)
      • 同步方法(Synchronized Method)

    线程同步 - 同步语句

      public boolean saleTicket() {
        synchronized (this) {
          if (tickets < 0)
            return false;
          tickets--;
          String name = Thread.currentThread().getName();
          System.out.println(name + "买了一张票, 还剩" + tickets + "张");
          return tickets > 0;
        }
      }
    
    • synchronized(obj)的原理

      • 每个对象都有一个与他相关的内部锁(intrinsic lock),或者叫做监视器锁(monitor lock)
      • 第一个执行到同步语句的线程会获得obj的内部锁,在执行同步语句结束后会释放该锁
      • 只要一个线程持有了内部锁,那么其他线程在同一时刻无法再获得该锁
      • 当他们试图获取此锁时,会进入BLOCKED状态

    线程同步 - 同步方法

    public synchronized boolean saleTicket() {
        if (tickets < 0)
          return false;
        tickets--;
        String name = Thread.currentThread().getName();
        System.out.println(name + "买了一张票, 还剩" + tickets + "张");
        return tickets > 0;
      }
    
    • synchronized 不能修饰构造方法

    • 修饰实例方法时跟同步语句是等价的

    • 静态方法的话等价于synchronized (Class对象)

    public synchronized static void test() {
    
      }
      public static void test1() {
        // Station.class: 类对象 每一个类都只有一个类对象
        synchronized (Station.class) {
    
        }
      }
    
    

    死锁(Deadlock)

    什么是死锁?

    两个或多个线程永远阻塞,相互等待对方的锁

    
        new Thread(() -> {
          synchronized ("1") {
            System.out.println("1");
            try {
              Thread.sleep(100);
            } catch (Exception e) {
              e.printStackTrace();
            }
            synchronized ("2") {
              System.out.println("1 - 2");
            }
          }
        });
    
        new Thread(() -> {
          synchronized ("2") {
            System.out.println("2");
            try {
              Thread.sleep(100);
            } catch (Exception e) {
              e.printStackTrace();
            }
            synchronized ("1") {
              System.out.println("2 - 1");
            }
          }
        });
    

    注:同步语句的obj这里传的是字面量,由于SCP的存在,同一字面量就是同一个对象

    线程间通信

    可以使用Object.waitObject.notify,Object.notifyAll方法实现线程间通信

    若想在线程A中陈工调用obj.wait,obj,notify,obj.notifyAll方法,线程A必须要持有obj的内部锁

    obj.wait:释放obj的内部锁,当前线程进入WATINGTIMED_WAITING状态

    obj.notifyAll:唤醒所有因为obj.wait进入WATINGTIMED_WAITING状态的线程

    obj.notify:随机唤醒一个因为obj.wait进入WATINGTIMED_WAITING状态的线程

    注意:

    1. 调用waitnotify,notifyAll的obj是同一个obj
    2. 调用waitnotify,notifyAll的线程必须持有obj的内部锁

    可重入锁(ReentrantLock)

    可重入锁具有跟同步语句,同步方法一样的一些基本功能,但功能更加强大

    什么是可重入?

    同一个线程可以重复获取同一个锁。

    其他地方叫做递归锁

     private ReentrantLock lock = new ReentrantLock();
    
      public boolean saleTicket() {
        try {
        // lock():必须获得此锁,如果锁被其他线程获取 将一直等待直到获得此锁
          lock.lock();
          if (tickets < 0)
            return false;
          tickets--;
          String name = Thread.currentThread().getName();
          System.out.println(name + "买了一张票, 还剩" + tickets + "张");
          return tickets > 0;
        } finally {
          lock.unlock();
        }
      }
    
    • ReentrantLock.lock:获得此锁,

      • 如果此锁没有被另一个线程持有,则将锁的持有计数设为1.并且此方法立即返回。
      • 如果当前线程已经持有此锁,则将此锁的持有计数加一,并且此方法立即返回。
      • 如果此锁被另一个线程持有,那么在获得此锁之前,此线程将一直处于休眠状态,直到获得此锁。此时锁的持有计数被设为1(虽然被设为1,但是此线程并没有持有该锁,而是在等待获取该锁)。
      • 所以,调用了几次lock方法,对应的就要有几次的unlock方法
    • ReentrantLock.tryLock: 仅在锁没有被其他线程持有的情况下,才会获得此锁

      • 如果此锁没有被其他线程持有,则将锁的持有计数设为1,并且立即返回
      • 如果当前线程已经持有此锁,则将锁的持有计数加一,并且立即返回
      • 如果锁被另一个线程持有,此方法立即返回false
      public boolean saleTicket() {
        boolean flag = false;
        try {
          flag = lock.tryLock();
          if (tickets < 0)
            return false;
          tickets--;
          String name = Thread.currentThread().getName();
          System.out.println(name + "买了一张票, 还剩" + tickets + "张");
          return tickets > 0;
        } finally {
          if (flag) {
            lock.unlock();
          }
        }
      }
    
    • ReentrantLock.unlock:尝试释放此锁

      • 如果当前线程持有此锁,则将持有计数减一
      • 如果持有计数为0,释放此锁
      • 如果当前线程没有持有此锁,则抛出异常
    • ReentrantLock.isLocked:查看此锁是否被任意线程持有

    其实synchronized也是可重入的

      public static void main(String[] args) {
        synchronized("1") {
          synchronized("1") {
            System.out.println("123456");
          }
        }
      }
      
      // 打印:123456
    

    假设synchronized不是可重入锁,那么在第一个synchronized中,获得了"1"的内部锁,在第二个synchronized中会发现“1”的内部锁已经被持有,然后会等待,但是“1”本身就是被当前的线程持有,所以打印语句永远不会被执行。
    但是现在打印了,说明synchronized是可重入锁,可以多次获得该锁

    synchronized在不同语言实现不同,有可能在其他语言就不是可重入锁(递归锁),再像上面那样写的话可能就会出错

    线程池

    线程对象占用大量的内存,在大型项目中,频繁的创建和销毁线程对象将产生大量的内存管理开销。

    使用线程池可以最大程度的减少线程创建、销毁带来的开销

    线程池由工作线程(Worker Thread)组成

    • 普通线程:执行完一个工作之后,生命周期就结束了

    • 工作线程:可以执行多个任务(没有工作时就在等待,有任务了就开始干活)

      • 先将任务添加到队列(Queue)中,再从队列中取出任务提交到池中
    • 常用的线程池类型是固定线程池(Fixed Thread Pool)
      • 具有固定适量的正在运行的线程
        // 创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        pool.execute(() -> {
          System.out.println(11 + "_" + Thread.currentThread().getName());
        });
        pool.execute(() -> {
          System.out.println(22 + "_" + Thread.currentThread().getName());
        });
        pool.execute(() -> {
          System.out.println(33 + "_" + Thread.currentThread().getName());
        });
        pool.execute(() -> {
          System.out.println(44 + "_" + Thread.currentThread().getName());
        });
    
        // 关闭线程池
        // pool.shutdown();
        
        /*
         * 11_pool-1-thread-1 
         * 22_pool-1-thread-2 
         * 33_pool-1-thread-3 
         * 44_pool-1-thread-4
         */
    

    相关文章

      网友评论

          本文标题:Java-09 并发编程

          本文链接:https://www.haomeiwen.com/subject/yiolfktx.html