Java线程

线程的创建

  • 继承Thread类
    Thread类直接继承Object类,并实现了Runnable接口;
    从Thread类派生一个子类,并创建子类的对象;
    子类应该重写Thread类的run方法,写入需要在新线程执行的语句段;
    调用start方法来启动新线程,自动进入run方法;

    TheadTest thread = new ThreadTest(); // ThreadTest类继承自Thread类
    thread.start();
    
  • 通过Runnable接口构造线程

    • Runnable接口
      只有一个run()方法;
      Thread类实现了Runnable接口;
      便于多个线程共享资源;
      Java不支持多继承,如果已继承了某个基类,需要实现Runnabel接口来生成多线程;
      以实现Runnable的对象为参数建立新的线程;

      TheadTest thread = new ThreadTest(); // ThreadTest类实现了Runnable方法
      new Thread(thread).start();
      

线程同步

  • 监视器

    java使用监视器机制,每个对象只有一个锁,利用多线程对锁的争夺实现线程间的互斥,线程A获得对象的锁后,线程B必须等待线程A完成操作释放锁后才能获取该对象的锁。

  • synchronize关键字

    1. 将需要互斥的语句段放入synchronized(object){}语句中,且两处的object是相同的。

      while (t.number<t.size)
      {
          synchronized (t) {
              System.out.println("Producer put tickets "+(++t.number));
              t.available=true;
          }
      }
      
    2. 除了对指定代码段进行同步控制外,还可以定义整个方法在同步控制下执行,只要在方法前加上synchronized关键字即可。

      public synchronized void put() {
          System.out.println("Producer put tickets "+(++number));
          available=true;
      }
      
  • 同步与锁

    1. 只能同步方法,而不同同步变量。
    2. 每个对象只有一个锁。
    3. 类可以同时拥有同步方法与非同步方法,非同步方法可以被多个线程自由访问而不受锁的控制
    4. 两个线程使用相同的实例调用synchronized方法,一次只能有一个线程执行方法,另一个需要等待锁。
    5. 线程睡眠时,它所持的任何锁都不会被释放
    6. 线程可以获得多个锁。
    7. 同步损害并发性,尽可能缩小同步范围。
    8. 使用同步代码块时,需指定在哪个对象上同步,即获得哪个对象的锁。

线程的等待与唤醒

​ 为了协调不同线程的工作,需要在线程间建立沟通渠道,通过线程的对话来解决线程间的同步问题

  • wait()方法

    如果当前状态不适合本线程执行,正在执行同步代码synchronized的某个线程A调用改方法(在对象X上),该线程暂停执行进入对象X的等待池,并释放已获得的对象X的锁。线程A要一直等到其他线程在对象X上调用notify或notifyAll方法,才能重新获得对象X的锁继续执行(从wait语句后继续执行)。

  • notify()方法

    随机唤醒一个等待的线程,本线程继续执行。

    线程被唤醒后,还要等待唤醒消息者释放监视器。

    被唤醒的线程开始执行时,一定要判断当前状态是否适合自己运行。

  • notifyAll()方法

    唤醒所有等待的线程,本线程继续执行。

    示例:

    //要求:每存入一张票,就售出一张票,售出后,再存入
    class Tickets {
        int number = 0; //票号
        int size;   //总票数
        int i=0;    //售票序号
        boolean available=false;    //表示目前是否有票可售
    
        public Tickets(int size) {
            this.size = size;
        }
    
        public synchronized void put() {
            if (available) {
                try {
                    wait(); //如果还有存票,则存盘线程等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Producer put tickets "+(++number));
            available=true;
            notify();   //存盘后唤醒售票线程开始售票
        }
    
        public synchronized void sell() {
            if(!available) {
                try {
                    wait(); //如果没有存票,则售票线程开始等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            System.out.println("Consumer buys ticket "+(number));
            available=false;
            notify();   //售票后唤醒存票线程开始存票
            if (number==size)
                number=size + 1;    //设置结束标志,number>size表示售票结束
        }
    }
    
    class Producer extends Thread {
        Tickets t=null;
    
        public Producer(Tickets t) {
            this.t = t;
        }
    
        @Override
        public void run() {
            while (t.number<t.size)
            {
                t.put();
            }
        }
    }
    
    class Consumer extends Thread {
        Tickets t=null;
    
        public Consumer(Tickets t) {
            this.t = t;
        }
    
        @Override
        public void run() {
            while (t.i<t.size) {
                t.sell();
            }
        }
    }
    
    public class ProducerAndConsumer {
    
        public static void main(String[] args) {
            Tickets t = new Tickets(10);
            new Consumer(t).start();
            new Producer(t).start();
        }
    }
    

后台线程

后台线程也叫守护线程,通常是为了辅助其他线程而运行的线程。

一个进程中只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中的所有前台线程都已经结束,那么无论是否还有未结束的后台线程,这个进程都会结束。

“垃圾回收”便是一个后台线程。

  • 设置后台线程

    线程对象在启动start方法之前,调用setDaemon(true)方法,该线程便成为后台线程。

线程的生命周期与死锁

  • 线程的生命周期状态图

  • 死锁

    线程在运行的过程中,某个步骤需要满足一些条件才能进行下去,如果条件不满足,线程将在这个步骤上出现阻塞。

  • 结束线程的生命

    1. 通常,可通过控制run方法中循环条件的方式来结束一个线程。
    2. 用stop()方法结束线程的生命,不推荐。

线程的调度

  • 线程调度

    单CPU的系统中,多个线程共享CPU,任何时间点实际只能有一个线程在运行,控制多个线程在一个CPU上以某种顺序运行称为线程调度。

    Java虚拟机使用固定优先级算法调度线程。

  • 线程的优先级

    1. 线程优先级范围1~10,默认为5。

    2. 线程A运行过程中创建的新的线程对象B,初始化状态具有和线程A相同的优先级。如果A是个后台线程,则B也是后台线程。

    3. 线程创建后,可通过setPriority(int priority)方法改变优先级。

  • yield()方法

    把线程让给同优先级的线程执行,如果不存在同优先级的线程,则仍由自己继续执行。

  • 假设某线程正在运行,只有出现以下情况之一,才会使其暂停运行

    1. 一个更高优先级的线程变为就绪状态
    2. 输入输出、调用sleep、wait、yield方法使其阻塞
    3. 对于支持时间分片的系统,时间片的时间期满

线程安全与线程兼容与对立

  • 线程安全的定义

    当多个线程访问同一对象时,如果不考虑这些线程在运行时环境的调度与交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的接口,那这个对象是线程安全的。

  • 实现线程安全的方法

    1. 不可变的对象

      1.final修饰
      2.Sting类是常量类 Sting s="string"
      3.枚举类型 public enum Color {...}
      4.Number的子类,如Long,Double
      5.BigInteger,BigDecimal(数值类型的高精度实现)
      
    2. 绝对线程安全

      符合前面定位的线程是绝对线程安全的

    3. 相对线程安全

      使用同步手段保证调用的正确性

  • 线程兼容与线程对立

    1. 线程兼容

      对象本身不是线程安全的,但是可以在调用端正确地使用同步手段来保证对象在并发环境中可以安全使用

    2. 线程对立

      无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码

线程安全的实现方式-互斥同步

  • 操作系统中同步的互斥实现方式

    临界区(Critical Section),互斥量(Mutex),信号量(Semaphone)

  • Java中同步的互斥实现方法

    1. Synchronized关键字

      经过编译后,会在同步块前后形成monitorenter和monitorexit两个字节码

    2. 重入锁ReentrantLock(Java.uitl.concurrent)

      相比采用Synchronized,重入锁可实现:等待可中断、公平锁、锁可以绑定多个条件

      Synchronized表现为原生语法层面的互斥锁,而RenetrantLock表现为API层面的互斥锁

      ReentrantLock的性能更高

      ReentrantLock lock = new ReentrantLock();
      public void write() {
          lock.lock();
          try {
              ...
          } finally {
              lock.unlock()
          }
          ...
      }
      
      public void read() {
          lock.lockInterruptibly(); //可以响应中断
          try {
              ...
          } finally {
              lock.unlock();
          }
          ...
      }
      

线程的安全实现-非阻塞同步

  • 阻塞同步:互斥同步存在的问题是进行线程阻塞和唤醒所带来的的性能问题,这种同步方式成为阻塞同步(Blocking Synchronization)。这是一种悲观并发策略。

  • 非阻塞同步:基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程征用共享数据,则操作成功;否则就是产生了冲突,采取不断重试直到成功为止的策略,这种策略不需要把线程挂起,称为非阻塞同步。

  • 使用硬件处理器指令进行不断重试策略

    1. 测试并设置(Test-and-Set)
    2. 获取并增加(Fetch-adn-Increment)
    3. 交换(Swap)
    4. 比较并交换(Compare-and-Swap,简称CAS)
    5. 加载链接,条件存储(Load-Linked,Store-conditional,简称LL, SC)

    Java中以实现非阻塞同步的类,例如AtomicInteger,AtomicDouble等。

线程的安全实现-无同步方案

  • 可重入代码

    也叫纯代码,相对线程安全来说,可以保证线程安全。可以在代码执行过程中断它,转而去执行另一端代码,而在控制权返回后,原来的程序不会出现任何错误。

  • 线程本地存储

    如果一段代码中所需要的的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一线程中执行,如果能保证,就可以把共享数据的可见范围限定在同一个线程之类,这样无需同步也能保证线程之间不出现数据争用的问题。

线程的安全实现-无同步方案

  • 可重入代码

    也叫纯代码,相对线程安全来说,可以保证线程安全。可以在代码执行过程中断它,转而去执行另一端代码,而在控制权返回后,原来的程序不会出现任何错误。

  • 线程本地存储

    如果一段代码中所需要的的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一线程中执行,如果能保证,就可以把共享数据的可见范围限定在同一个线程之类,这样无需同步也能保证线程之间不出现数据争用的问题。

    示例代码:

    package com.company;
    
    public class SequenceNumber {
        private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){
            public Integer initialValue() {
                return 0;
            }
        };
        public int getNextNum() {
            seqNum.set(seqNum.get()+1);
            return seqNum.get();
        }
        public static void main(String[] args) {
            SequenceNumber sn = new SequenceNumber();
            TestClient t1 = new TestClient(sn);
            TestClient t2 = new TestClient(sn);
            TestClient t3 = new TestClient(sn);
            t1.start();
            t2.start();
            t3.start();
        }
    
        private static class TestClient extends Thread {
            private SequenceNumber sn;
            public TestClient(SequenceNumber sn) {
                this.sn=sn;
            }
    
            @Override
            public void run() {
                for(int i=0;i<3;i++) {
                    System.out.println("thread["+Thread.currentThread().getName()+"]sn["+sn.getNextNum()+"]");
                }
            }
        }
    }
    

    结果如下:

    thread[Thread-1]sn[1]
    thread[Thread-2]sn[1]
    thread[Thread-0]sn[1]
    thread[Thread-2]sn[2]
    thread[Thread-1]sn[2]
    thread[Thread-2]sn[3]
    thread[Thread-0]sn[2]
    thread[Thread-0]sn[3]
    thread[Thread-1]sn[3]
    

    我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,3个TestClient,共享1个SequenceNumber实例,但是不会相互影响。

  • 锁优化

    1. 自旋锁

      互斥同步存在问题:挂起线程和恢复线程都需要转入内核态中完成,给操作系统的并发性能带来很大压力

      自旋锁:让后面请求锁的线程“稍等一会”,但不放弃处理器的执行时间,看持有锁的线程是否很快会释放锁。为了让线程等待,需要让线程执行一个忙循环(自旋),这项技术就是自旋锁。Java中自旋次数默认10次。

    2. 自适应锁

      锁自选的时间不再固定,由前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定。

    3. 锁消除

      定义:Java即时编译器在运行时,对一些代码要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

      判定依据:如果在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步枷锁自然无需进行。

    4. 锁粗化

      通常将同步块的作用范围限制小,另一种情况是,如果连续操作都对同一个对象反复枷锁,甚至在循环体中,频繁的互斥同步会导致不必要的性能损耗,扩大锁的同步块范围即可。

    5. 偏向锁

      目的:消除数据无竞争情况下的同步,提高程序运行的性能。偏向锁即在无竞争的情况下把整个同步都消除掉,连CAS操作都不做。

      偏向:意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,则持有偏向锁的线程永远不需要再进行同步。

posted @ 2020-09-10 00:28  hunter-w  阅读(112)  评论(0编辑  收藏  举报