Java多线程

1. 线程与进程

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,即进程空间或(虚空间)。进程不依赖于线程而独立存在,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
​ 线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源。

区别:

线程 进程
地址空间 共享本进程的地址空间 进程之间是独立的空间
资源 共享本进程的资源(内存,I/O,CPU等) 进程之间的资源是独立的
执行过程 线程不能独立执行,必须存在于应用程序中,执行开销小 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口、执行开销大
并发性 可并发执行 可并发执行
调度和分配的基本单位 拥有资源的基本单位

2. Java中的线程

​ Java中创建线程的两种方式

  • 继承Thread类并重写run()方法

    /**
     * 测试扩展Thread类实现的多线程程序
     */
    public class TestThread extends Thread {
        public TestThread(String name){
           super(name);
        }
        @Override
        public void run() {
           for(int i=0;i<5;i++){
               for(long k=0;k<100000000;k++);
               System.out.println(this.getName()+":"+i);
           }
        }
        public static void main(String[] args){
           Thread t1=new TestThread("李白");
           Thread t2=new TestThread("屈原");
           t1.start();
           t2.start();      
        }
    }
    
  • 通过实现Runnable接口创建

    /**
     * 实现Runnable接口的类
     */
    public class RunnableImpl implements Runnable{
        private Stringname;
        public RunnableImpl(String name) {
           this.name = name;
        }
        @Override
        public void run() {
           for (int i = 0; i < 5; i++) {
               for(long k=0;k<100000000;k++);
               System.out.println(name+":"+i);
           }     
        }
    }
     
    /**
     * 测试Runnable类实现的多线程程序
     */
    public class TestRunnable {
     
        public static void main(String[] args) {
           RunnableImpl ri1=new RunnableImpl("李白");
           RunnableImpl ri2=new RunnableImpl("屈原");
           Thread t1=new Thread(ri1);
           Thread t2=new Thread(ri2);
           t1.start();
           t2.start();
        }
    }
    

3. 线程状态转换

  • 新生状态:线程对象已创建还未执行start()
  • 就绪态:线程有资格运行,等待调度程序调用其start()方法
  • 运行态:调度程序从就绪态池中选取一个线程执行
  • 阻塞态:线程是活的,但此时没有资格运行,阻塞接触后重新进入就绪态
  • 死亡:代码执行完毕或者线程中断执行,一旦线程死亡,不可再执行start()

线程离开运行态:

  • sleep() 线程睡眠
  • yield() 线程让步,让其他相同优先级的线程优先执行
  • join() 线程合并,使当前线程停止执行,让新加入的线程先执行完毕
  • wait() 在对象上调用该方法

4. 同步和锁定

锁的原理:

Java每个对象都有一个内置锁,当程序运行到非静态的synchronized()方法/synchronized()代码块上时,自动获得当前实例(this)的锁,一个对象只有一个锁,所以当一个线程获得锁之后,其他线程都不能进入该实例的synchronized()方法/synchronized()代码块,直到该锁被释放。释放锁是指持锁线程退出了synchronized同步方法/代码块。

note:

  • 不必同步类中的所有方法,类可以同时拥有同步方法和非同步方法
  • 线程访问类中的非同步方法不受限制
  • 线程睡眠不会释放锁

静态方法同步:需要一个应用于整个类对象的锁

public staticsynchronized int setName(String name){
      Xxx.name = name;
}
//等价
public static intsetName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}

调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。

5. Java线程交互

void notify()——唤醒在此对象监视器上等待的单个线程。
void notifyAll()——唤醒在此对象监视器上等待的所有线程。
void wait()——导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法。

6. 线程的调度

  1. 休眠

    Thread.sleep(long millis)
    Thread.sleep(long millis, int nanos)
    

    线程休眠,会将CPU资源交给其他线程,但不会释放锁,以便线程可以轮换执行,休眠一定时间后,会再次进入就绪态

  2. 优先级

    Thread.setPriority(10);
    

    线程的优先级无法保证线程的执行顺序,但是优先级较高的线程获取资源的概率较大,线程优先级默认为5

    在父线程中开启子线程,子线程的优先级会继承父线程的优先级

  3. 让步

    Thread.yield()
    

    线程让步的意思是当前线程暂停执行,让出CPU资源,并执行其他线程,但并不能指定是哪个线程,yield()是静态方法

  4. 合并

    void join()——等待该线程终止。   
    void join(longmillis)——等待该线程终止的时间最长为 millis毫秒。   
    void join(longmillis,int nanos)——等待该线程终止的时间最长为 millis毫秒 + nanos 纳秒。
    

    将几个并行的线程合并为一个单线程执行,当前线程会暂停执行,知道新加入的线程执行完毕

  5. 守护线程

    public final void setDaemon(boolean on)--将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java虚拟机退出。
    

    调用该方法可以将线程设置为守护线程,必须在启动前调用

    JVM的垃圾回收,内存管理,操作系统的线程很多都是守护线程

    JRE判断程序是否执行完毕的标准是所有的前台线程执行完毕,而不管后台线程的状态

  6. 同步方法

     synchronized 关键词修饰方法
    

    synchronized只能标记非抽象的方法,不能标识成员变量。

  7. 同步块

     synchronized 关键字修饰代码块
    

    在同步方法或同步代码块中应该尽可能减少使用sleep()和yield(),使用sleep()的话,该方法占着对象锁,其他线程无法访问该资源,使用yield()时,其他线程也无法访问同步方法或者同步代码块。

  8. 生产者消费者模型

  9. 死锁

    发生死锁的原因:两个对象的锁互相等待

  10. volatile关键字

    volatile可以用在变量前面,防止两个线程同时操作数据引起其中一个线程读取到脏数据的情况

    volatile的同步性比较差,但是其开销较低

  11. 线程池

    线程池:在一块内存空间内,存放未死亡的线程,线程调度由池管理器管理。可以将常用的一些线程存放在线程池中,当有线程任务时,直接在线程池中取出,执行完成后放回线程池,可以节省反复创建相同线程的时间和空间开销,节省资源。

    • 固定大小的线程池

      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
       
      /**
       * Java线程:线程池
       */
      public class Test {
          public static void main(String[] args) {
             //创建一个可重用固定线程数的线程池
             ExecutorService pool =Executors.newFixedThreadPool(2);
             //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
             Thread t1 = new MyThread();
             Thread t2 = new MyThread();
              //将线程放入线程池中进行执行
              pool.execute(t1);
              pool.execute(t2);
              //关闭线程池
              pool.shutdown();
          }
      }
      class MyThread extends Thread{
          public void run(){
             System.out.println(Thread.currentThread().getName()+"正在执行...");
          }
      }
      
    • 单任务线程池

      ExecutorService pool=Executors.newSingleThreadExecutor();
      

      单任务线程池中只有一个线程,当需要反复执行一个线程时,避免重复新建该线程,可以用线程池实现

    • 可变大小的线程池

      ExecutorService pool=Executors.newCachedThreadPool();
      

      可以根据需要改变大小的线程池

    • 延迟连接池

      在给定延迟后运行命令或者定期地执行

      ScheduledExecutorServicepool=Executors.newScheduledThreadPool(2);
      pool.schedule(thread, 10, TimeUnit.MILLISECONDS);
      

      创建一个单任务执行线程池,它可安排在给定延迟后运行命令或者定期地执行

      ScheduledExecutorServicepool=Executors.newSingleThreadScheduledExecutor();
      

      自定义线程池

      /*
      * corePoolSize : 池中所保存的线程数,包括空闲线程
      * maximumPoolSize : 池中允许的最大线程数
      * keepAliveTime : 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间
      * unit -keepAliveTime : 参数的时间单位
      * workQueue : 执行前用于保持任务的队列
      */
      public ThreadPoolExecutor(int corePoolSize,
                               int maximumPoolSize,
                               long keepAliveTime,
                                TimeUnit unit,
                               BlockingQueue<Runnable> workQueue)
      
    1. 锁对象

      Java5以后提供了锁对象,用来控制需要并发访问的资源

      主要有三个接口:

      java.util.concurrent.locks.Condition//替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。
      java.util.concurrent.locks.Lock//实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作
      java.util.concurrent.locks.ReadWriteLock//维护了一对相关的锁定,一个用于只读操作,另一个用于写入操作
      
      Lock myLock=new ReentrantLock();
      ReadWriteLock myRWLock=new ReentrantReadWriteLock();//读写锁
      @Override
      public void run() {
          //获取锁
          myLock.lock();
      	//锁定对象
          //释放锁
          myLock.unlock();
          //获取锁
      	myRWLock.readLock().lock();
      	//只能进行读操作
          //释放锁
          myRWLock.readLock().unlock();
              //获取锁
      	myRWLock.writeLock().lock();
      	//只能进行写操作
          //释放锁
          myRWLock.writeLock().unlock();
      }  
      
    2. 信号量

      信号量是一个计数器,可以监控有多少数目的线程等待获取资源,通过信号量可以得知可用资源的数目,但仅仅只能获取到数目,不能知道具体是哪些资源

    3. 阻塞队列

      java.util.concurrent.BlockingQueue
      

      指定长度的队列,队列如果满了,添加进队列的操作会阻塞,知道队内元素出队

    4. 阻塞栈

      java.util.concurrent.BlockingDeque
      

      指定长度的栈,栈如果满了,添加进栈的操作也会被阻塞,与阻塞队列不同的是,栈是先进后出的,队列是先进先出的