Java高并发下多线程编程

1、创建线程

Java中创建线程主要有三种方式:

  • 继承Thread类创建线程类:
    • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此也把run方法称为执行体。
    • 创建Thread子类实例,即创建了线程对象。
    • 调用线程对象的start方法开启该线程。
//program--Thread
public class Test {
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        new MyThread().start();    

    }
     static class MyThread extends Thread{
        private int ticket = 5;
        public void run(){
            while(true){
                System.out.println("Thread ticket = " + ticket--);
                if(ticket < 0){
                    break;
                }
            }
        }
    }
}
  • 实现Runnable接口创建线程类:
    • 定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体。
    • 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    • 调用线程对象的start方法来启动该线程。
//program--Runnable
public class Test2 {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        MyThread2 mt=new MyThread2();     
        new Thread(mt).start();


    }
    static class MyThread2 implements Runnable{
        private int ticket = 5;
        public void run(){
            while(true){
                System.out.println("Runnable ticket = " + ticket--);
                if(ticket < 0){
                    break;
                }
            }
        }
    }
}  
  • 实现Callable接口和FutureTask类创建线程:
    • 定义Callable接口的实现类,并实现call方法,该call方法将作为线程执行体,并有返回值。
    • 创建Callable实现类实例,使用FutureTask类包装Callable对象,该FutureTask对象封装了该Callable对象的call方法返回值。
    • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    • 调用FutureTask对象的get方法来获取子线程执行结束后的返回值。
  //定义实现Callable接口的的实现类重写call方法。
  public class MyCallableTask implements Callable<Integer>{
      @Override
          public Integer call() throws Exception {
             //TODO 线程执行方法
          }
  }
  
  Callable<Integer> mycallabletask = new MyCallableTask();
 
 //开始线程
  FutureTask<Integer> futuretask= new FutureTask<Integer>(mycallabletask);
  new Thread(futuretask).start();

  //通过futuretask可以得到MyCallableTask的call()的运行结果:
  futuretask.get();

 2、Synchronizer同步器

synchronized关键字以内存中一个对象作为锁(互斥锁),获取到这个对象的线程执行synchronized内部的代码,执行完毕后释放锁。

  • synchronized既可以锁对象(在方法上修饰也属于锁定类),也可以锁定类。若锁定当前类,则当前类只能由一个线程进入执行,其它线程阻塞。
  • synchronized修饰非静态方法和使用synchronized(this)都是锁住这个类的对象,如果多线程访问,对象不同就锁不住,对象固定一个,就可以锁住。
  • 使用synchronized(类名.class)和修饰静态方法,是锁住了代码块。不管多线程访问的时候,对象是否为同一个(能缩小代码段的范围就尽量缩写,能在代码段上加同步就不要在整个方法上加同步,缩小锁的粒度)

基本使用

public class T {   
   private int count = 10;
   private Object o = new Object();
   
   public void m() {
      synchronized(o) { //任何线程要执行下面的代码,必须先拿到o的锁
         count--;
         System.out.println(Thread.currentThread().getName() + " count = " + count);
      }
   }   
}

专门创建一个无用的对象作为锁显得很浪费,可以直接以当前为对象作为锁。

public class T {   
   private int count = 10;   
   public void m() {
      synchronized(this) { //任何线程要执行下面的代码,必须先拿到this的锁
         count--;
         System.out.println(Thread.currentThread().getName() + " count = " + count);
      }
   }   
}

以当前对象作为锁,可以省略synchronized块的定义,直接在方法上修饰。

public class T { 
   private int count = 10;   
   public synchronized void m() { //等同于在方法的代码执行时要synchronized(this)
      count--;
      System.out.println(Thread.currentThread().getName() + " count = " + count);
   } 
}

synchronized修饰静态方法,锁定的是字节码对象。下面代码m方法和mm方法锁定效果一样。

public class T {
 
   private static int count = 10;
   
   public synchronized static void m() { //这里等同于synchronized(yxxy.c_004.T.class)
      count--;
      System.out.println(Thread.currentThread().getName() + " count = " + count);
   }
   
   public static void mm() {
      synchronized(T.class) { 
     //考虑一下这里写synchronized(this)是否可以? // 不可以,静态方法不能访问非静态对象 count --; } } }

下面代码可以演示到多线程运行时出现的重复问题,若取消synchronized关键字的注释,可以避免这个问题。

public class T implements Runnable {
 
   private int count = 10;
   
   public /*synchronized*/ void run() { 
      count--;
      System.out.println(Thread.currentThread().getName() + " count = " + count);
   }
   
   public static void main(String[] args) {
      T t = new T();
      for(int i=0; i<5; i++) {
         new Thread(t, "THREAD" + i).start();
      }
   }
   
}

一个同步方法可以调用另外一个同步方法(包括子类同步方法调用父类同步方法)

已取得锁的当前线程再次申请获得同一个锁是可行的,也就是说synchronized获得的锁是可以重入的。

所以下面代码m1内调用m2是可以执行的。

import java.util.concurrent.TimeUnit;

public class T {
    synchronized void m1() {
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
    }

    synchronized void m2() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2");
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1, "t1").start();
    }
}

程序在执行过程中,如果出现移除,默认情况锁会被释放。

在并发处理的过程中,有异常要多加小心,可能会发生不一致的情况。比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时第一个线程未修改完的数据。

因此要非常小心的处理同步业务逻辑中的异常。

当可能抛出异常可以catch来避免上述错误(如回滚数据操作)

import java.util.concurrent.TimeUnit;
 
public class T {
   int count = 0;
   synchronized void m() {
      System.out.println(Thread.currentThread().getName() + " start");
      while(true) {
         count ++;
         System.out.println(Thread.currentThread().getName() + " count = " + count);
         try {
            TimeUnit.SECONDS.sleep(1);
            
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         
         if(count == 5) {
            int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
            System.out.println(i);
         }
      }
   }
   
   public static void main(String[] args) {
      T t = new T();
      Runnable r = new Runnable() {
 
         @Override
         public void run() {
            t.m();
         }
         
      };
      new Thread(r, "t1").start();
      
      try {
         TimeUnit.SECONDS.sleep(3);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      
      new Thread(r, "t2").start();
   }
   
}

3、死锁

线程1需要按顺序获取锁A和锁B来执行,线程2需要按顺序获取锁B和锁A来执行,当线程1获取了锁A未获取锁B时线程2并发,线程2执行获取了锁B,此时就发生死锁,线程1无法获取锁B,线程2也无法获取锁A来继续执行。

下面程序可以模拟死锁:

public class T {
 
    Object a = new Object();
    Object b = new Object();
 
    public void m1() {
        synchronized (a) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (b) {
                System.out.println("success1");
            }
        }
    }
 
    public void m2() {
        synchronized (b) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (a) {
                System.out.println("success2");
            }
        }
    }
 
    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1, "t1").start();
        new Thread(t::m2, "t2").start();
    }
}

synchronized同步代码块中的代码越少越好。

import java.util.concurrent.TimeUnit;
 
 
public class T {
   
   int count = 0;
 
   synchronized void m1() {
      //do sth need not sync
      try {
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
      count ++;
      
      //do sth need not sync
      try {
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
   
   void m2() {
      //do sth need not sync
      try {
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
      //采用细粒度的锁,可以使线程争用时间变短,从而提高效率
      synchronized(this) {
         count ++;
      }
      //do sth need not sync
      try {
         TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
}

4、锁对象状态对锁的影响

锁定某对象o,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变。所以应该避免将锁定对象的引用变成另外的对象。当锁定对象改变后,多个线程间获取的锁对象就有可能不一样了,使得同步代码块失效。

下面代码说明了这个问题,锁定对象改变了,线程t2进入同步代码块执行,但若锁定对象不变,线程t2将不能进入同步代码块执行。

import java.util.concurrent.TimeUnit; 
public class T {   
   Object o = new Object(); 
   void m() {
      synchronized(o) {
         while(true) {
            try {
               TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());            
         }
      }
   }
   
   public static void main(String[] args) {
      T t = new T();
      //启动第一个线程
      new Thread(t::m, "t1").start();
      
      try {
         TimeUnit.SECONDS.sleep(3);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      //创建第二个线程
      Thread t2 = new Thread(t::m, "t2");
      
      t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
      
      t2.start();
      
   }
}

不要以String字符串类型作为锁对象。由于String类型的特殊性(常量池),表面上变量名不同的两个String对象可能指向的是同一个地址。

在下面的程序中,m1和m2其实锁定的是同一个对象。

这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁。

public class T {
   
   String s1 = "Hello";
   String s2 = "Hello";
 
   void m1() {
      synchronized(s1) {
         
      }
   }
   
   void m2() {
      synchronized(s2) {
         
      }
   }
}

5、volatile关键字

volatile关键字,可以使一个变量在多个线程间可见。

在下面的代码中,running是存在于堆内存的t对象中,当线程t1开始运行的时候,会把running值从内存中读到(拷贝到)t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行。而使用volatile关键字,将会强制所有线程都去堆内存中读取变量running的值。

下面代码可以说明volatile关键字的效果,当running变量可以被其他线程改变时,while代码块才会结束并打印"m end"。

import java.util.concurrent.TimeUnit;
 
public class T {
   /*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
   void m() {
      System.out.println("m start");
      while(running) {        
          // 若上面几行代码取消注释,有可能在cpu空闲时去读取堆内存中的running变量的值从而结束while代码块
      }
      System.out.println("m end!");
   }
   
   public static void main(String[] args) {
      T t = new T();
      
      new Thread(t::m, "t1").start();
      
      try {
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      
      t.running = false; 
   } 
}

6、volatile不能替代synchronized

运行下面的程序输出count的值会出现不到10000的结果,原因是因为虽然volatile可以保证每个线程读取count的值是同步的,但不能保证/要求线程写入count的值时一定是根据此时的count值+1的操作。即读操作和写操作是复合操作,不构成原子性操作。

解决这个问题可以使用synchronized关键字修饰m方法即可以保证可见性和原子性同步。

import java.util.ArrayList;
import java.util.List;
 
public class T {
   volatile int count = 0;
 
   void m() {
      for (int i = 0; i < 10000; i++) count++;
   }
 
   public static void main(String[] args) {
      T t = new T();
 
      List<Thread> threads = new ArrayList<Thread>();
 
      for (int i = 0; i < 10; i++) {
         threads.add(new Thread(t::m, "thread-" + i));
      }
 
      threads.forEach((o) -> o.start());
 
      threads.forEach((o) -> {
         try {
            o.join();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      });
 
      System.out.println(t.count); 
 
   }
 
}

7、若仅在数据进行简单的运算时需要保证原子性,可以使用AtomicXXX类

AtomicXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的,即多个AtomicXXX类的方法连续被调用的时候不能保证原子性。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
 
 
public class T {
   /*volatile*/ //int count = 0;
   
   AtomicInteger count = new AtomicInteger(0); 
 
   /*synchronized*/ void m() { 
      for (int i = 0; i < 10000; i++)
         //if count.get() < 1000
         count.incrementAndGet(); //count++
   }
 
   public static void main(String[] args) {
      T t = new T();
 
      List<Thread> threads = new ArrayList<Thread>();
 
      for (int i = 0; i < 10; i++) {
         threads.add(new Thread(t::m, "thread-" + i));
      }
 
      threads.forEach((o) -> o.start());
 
      threads.forEach((o) -> {
         try {
            o.join();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      });
 
      System.out.println(t.count);
   }
}

8、volatile与synchronized

1)基本概念

线程安全有两个方面:执行控制和内存可见。

  • 执行控制的目的是控制代码执行(顺序)及是否可以并发执行。
  • 内存可见控制的是线程执行结果在内存中对其它线程的可见性,根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。

 

volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

 

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

2)volatile与synchronized两者区别

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
  • volatile的性能比synchronized的性能要高,可以用volatile的时候尽量避免使用synchronized。

9、ReentrantLock类(重入锁)

ReentrantLock可以用于替代synchronized,前者可以完成后者可完成的功能且更灵活(但性能没有明显区别)。但使用ReentrantLock必须手动释放锁,使用synchronized锁定的话如果遇到异常,JVM会自动释放锁,但是ReentrantLock必须手动释放锁,因此经常在finally块中进行锁的释放。ReentrantLock是Java并发包中互斥锁,它有公平锁和非公平锁两种实现方式。

1)基本使用

下面的代码可以演示使用重入锁完成使m1方法与m2方法互斥的功能:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class T {
    Lock lock = new ReentrantLock(); // 创建锁

    void m1() {
        try {
            lock.lock(); //synchronized(this) 申请并锁定锁lock
            for (int i = 0; i < 10; i++) {
                TimeUnit.SECONDS.sleep(1);

                System.out.println(i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 解锁
        }
    }

    void m2() {
        lock.lock(); // 申请并锁定锁lock
        System.out.println("m2 ...");
        lock.unlock(); // 解锁
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m1).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(t::m2).start();
    }
}

2)tryLock

使用ReentrantLock可以调用tryLock方法尝试进行锁定。线程可以根据tryLock方法的返回值判断是否锁定并以此决定是否继续等待或执行其他操作。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class T {
    Lock lock = new ReentrantLock();

    void m1() {
        try {
            lock.lock();
            for (int i = 0; i < 10; i++) {
                TimeUnit.SECONDS.sleep(1);

                System.out.println(i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行
     * 可以根据tryLock的返回值来判定是否锁定
     * 也可以指定tryLock的时间,由于tryLock(time)抛出异常,所以要注意unclock的处理,必须放到finally中
     */
    void m2() {    
        boolean locked = false;
        try {
            locked = lock.tryLock(5, TimeUnit.SECONDS);
            System.out.println("m2 ..." + locked);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(locked) lock.unlock();
        }

    }

    public static void main(String[] args) {
        T rl = new T();
        new Thread(rl::m1).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(rl::m2).start();
    }
}

 3)lockInterruptibly

调用lockInterruptibly方法申请锁的线程可以对线程interrupt方法做出响应,即若主线程想调用interrupt方法打断某一个线程的执行,通常来讲通过lock方法申请锁却因为未申请到锁而阻塞的线程不能被打断,而通过lockInterruptibly方法申请锁的线程阻塞时可以对主线程调用interrupt方法作出响应而被打断。

简言之:lockInterruptibly方法的作用是使在一个线程在等待锁的过程中,可以被打断。

下面的代码可以演示当线程t1霸占锁使得线程t2一直等待获得锁而阻塞,t2使用lockInterruptibly方法代替lock方法来声明申请锁,主线程可以通过调用线程t2对象的interrupt方法打断线程t2的执行——“别等了,哥们”

线程t1在获取/申请锁的过程中不响应中断(lock方法),而t2在获取/申请锁的过程响应中断(lockInterruptibly方法)

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        Thread t1 = new Thread(()->{
            try {
                lock.lock();
                System.out.println("t1 start");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                System.out.println("t1 end");
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
            } finally {
                lock.unlock();
            }
        });
        t1.start();
        Thread t2 = new Thread(()->{
            boolean bLock =false;
            try {
                //lock.lock();
                lock.lockInterruptibly(); //可以对interrupt()方法做出响应
                bLock = true;
                System.out.println("t2 start");
                TimeUnit.SECONDS.sleep(5);
                System.out.println("t2 end");
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
            } finally {
                if(bLock){
                    lock.unlock();
                }
            }
        });
        t2.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.interrupt(); //打断线程2的等待
    }
}

4)ReentrantLock可以被指定为公平锁

ReentrantLock在创建时可以被指定为公平锁,而synchronized是非公平锁。

公平锁:某个对象的锁对所有线程都是公平的,先到先得。每次加锁前都会检查队列里面有没有排队等待的线程,有则排队等待,没有才会尝试获取锁。


非公平锁:当一个线程采用非公平锁这种方式获取锁时,该线程会首先去尝试获取锁而不是等待。如果没有获取成功,那么它才会去队列里面等待。

下面代码使用公平锁,结果应该是两个线程交替打印,若创建重入锁时没有传入true,则打印结果无法预测。

import java.util.concurrent.locks.ReentrantLock;

public class T extends Thread {

    private static ReentrantLock lock=new ReentrantLock(true); //参数为true表示为公平锁,请对比输出结果
    public void run() {
        while (true){
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"获得锁");
            }finally{
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        T rl=new T();
        Thread th1=new Thread(rl);
        Thread th2=new Thread(rl);
        th1.start();
        th2.start();
    }
}

5)ReentrantLock(重入锁)小结

  • ReentrantLock可以完成synchronized关键字同样的功能,且更灵活。
  • ReentrantLock必须手动操作锁(申请/释放)。
  • 使用ReentrantLock可以调用tryLock方法尝试申请锁定,并根据方法返回值判断申请锁定的状态而据此选择执行不同的操作。
  • ReentrantLock可以通过lockInterruptibly方法使在一个线程在等待锁的过程中响应中断。
  • ReentrantLock在创建时可以被指定为公平锁。
  • ReentrantLock和synchronized应该根据具体情况选择使用。

10、ThreadLocal类(线程局部变量)

ThreadLocal是使用空间换时间,synchronized是使用时间换空间。简言之:ThreadLocal中存放的数据每个线程独立一份,各个线程之间的ThreadLocal互不影响。

当单个线程可以独立维护一个变量,不需要或不应该被其他线程修改这个变量,则可以使用TreadLocal,比如在Hibernate中session就存在与ThreadLocal中,避免synchronized的使用。

注意:ThreadLocal可能会导致内存泄漏。

面代码结果应该是线程t1通过get方法不能获取Person对象:

import java.util.concurrent.TimeUnit;

public class T {
    //volatile static Person p = new Person();
    static ThreadLocal<Person> tl = new ThreadLocal<>();

    public static void main(String[] args) {

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(6);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
           // tl.set(new Person());
            System.out.println(tl.get());
        }).start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tl.set(new Person());
        }).start();
    }

    static class Person {
        String name = "test";
    }
}

11、高并发下线程安全的单例模式

单例模式的两个特点:

  • 在任何情况下,单例类永远只有一个实例存在
  • 单例需要有能力为整个系统提供这一唯一实例

下面的程序使用了静态内置类的方式来实现单例模式,可以实现懒加载而且线程安全。

import java.util.Arrays;
public class T {
    private T() {
        System.out.println("single");
    }
    private static class Inner {
        private static T s = new T();
    }
    public static T getSingle() {
        return Inner.s;
    }
    public static void main(String[] args) {
        Thread[] ths = new Thread[200];
        for(int i=0; i<ths.length; i++) {
            ths[i] = new Thread(()->{
                System.out.println(T.getSingle());
            });
        }
        Arrays.asList(ths).forEach(o->o.start());
    }
}

12、并发容器 

存在于java.util.concurrent包中。

同步容器类:

  • Vector Hashtable :早期使用synchronized实现
  • ArrayList HashSet :未考虑多线程安全(未实现同步)
  • HashSet/Hashtable和StringBuilder/StringBuffer
  • Collections.synchronized***工厂方法使用的也是synchronized

使用早期的同步容器以及Collections.synchronized**方法的不足之处,本文略表。

示例

有N张火车票,每张票都有一个编号,同时有10个窗口对外售票,实现程序如下:

实现一:

以下是最基本的实现,弊端为ArrayList的remove方法不是同步的,可能重复remove同一张票;判断剩余票数的代码也不是同步的,可能卖出超过10000张票:

/**
 * 有N张火车票,每张票都有一个编号
 * 同时有10个窗口对外售票
 * 请写一个模拟程序
 *
 * 分析下面的程序可能会产生哪些问题?
 * 重复销售?超量销售?
 *
 */
import java.util.ArrayList;
import java.util.List;

public class T {
    static List<String> tickets = new ArrayList<>();

    static {
        for(int i=0; i<10000; i++) tickets.add("票编号:" + i);
    }

    public static void main(String[] args) {
        for(int i=0; i<10; i++) {
            new Thread(()->{
                while(tickets.size() > 0) {
                    try{
                        System.out.println("销售了--" + tickets.remove(0));
                    }catch (Exception e){}

                }
            }).start();
        }
    }
}

 实现二

使用Vector或者Collections.synchronizedXXX这种同步容器来实现同步。

仍然存在弊端,因为判断条件的代码和操作容器的代码分离了,虽然Vector的remove方法是同步的,但判断是否还有票的代码不是同步的,可能售出超过10000张票(若打开while块内线程sleep的语句,可以模拟这种出错,原因是多个线程进入到while块同时执行remove操作),即从判断到操作容器这两步中间可能会出现并发问题,因为两步操作不同步。解决方法呼之欲出了,使判断(size>0)和操作容器(remove方法)的代码同步,即实现原子性。 但在判断和操作外部加synchronized可实现原子性,但这种实现的效率并不特别高。

import java.util.Vector;
import java.util.concurrent.TimeUnit;

public class T {
    static Vector<String> tickets = new Vector<>();
   static Object o = new Object();
    static {
        for(int i=0; i<1000; i++) {
            tickets.add("票 编号:" + i);
        }
    }
    public static void main(String[] args) {

        for(int i=0; i<10; i++) {
            new Thread(()->{
                // synchronized (o) //虽然vector remove是同步的,但是 romve与size非原子性,所以加上synchronized实现原子性,但效率并不特别高
                {
                    while(tickets.size() > 0) {
                        try {
                            TimeUnit.MILLISECONDS.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("销售了--" + tickets.remove(0));
                    }
                }
            }).start();
        }
    }
}

实现三:

使用并发容器ConcurrentLinkedQueue(并发链表队列)提高并发性。其中poll方法是i同步的,操作代码并没有加锁,但可以实现高效率线程安全操作。

原理是队列的特性:队列操作一般不允许有值为null的元素(即使容器允许null值也应该做判断,避免传入null值),若没有元素则返回null,先取出后判断,并不是先判断后取出,所以完美解决了判断和操作两个部分的同步问题。这也是并发容器的优势所在。

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;

public class T {
    static Queue<String> tickets = new ConcurrentLinkedQueue<>();
    static {
        for(int i=0; i<1000; i++) {
            tickets.add("票 编号:" + i);
        }
    }
    public static void main(String[] args) {

        for(int i=0; i<10; i++) {
            new Thread(()->{
                while(true) {
                    String s = tickets.poll();
                    if(s == null) {
                        break;
                    } else {
                        System.out.println("销售了--" + s);
                    }
                }
            }).start();
        }
    }
}

1)并发容器介绍

ConcurrentMap(还有ConcurrentSet等)

以下程序可以演示高并发下不同并发Map容器的添加效率。

小关键:这里的CountDownLatch主要是为了限制主线程等待添加操作完成后再继续执行:

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CountDownLatch;

public class T {
    public static void main(String[] args) {
        Map<String, String> map = new ConcurrentHashMap<>();
//        Map<String, String> map = new ConcurrentSkipListMap<>(); //高并发并且排序

//        Map<String, String> map = new Hashtable<>();
//        Map<String, String> map = new HashMap<>(); //Collections.synchronizedXXX
        //TreeMap
        Random r = new Random();
        Thread[] ths = new Thread[100];
        CountDownLatch latch = new CountDownLatch(ths.length);
        long start = System.currentTimeMillis();
        for(int i=0; i<ths.length; i++) {
            ths[i] = new Thread(()->{
                for(int j=0; j<10000; j++) {
                    map.put("a" + r.nextInt(100000), "a" + r.nextInt(100000));
                }
                latch.countDown();
            });
        }

        Arrays.asList(ths).forEach(t->t.start());
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}

ConcurrentHashMap

上述代码执行结果可以发现,在并发情况下使用ConcurrentHashMap的效率比HashTable高,原因是,HashTable在添加操作时回锁定整个容器,只响应一个线程的添加操作;而ConcurrentHash则是将容器分段(默认16段,1.8之后底层改变,CAS替代分段锁),并发操作时只锁定其中一段。

ConcurrentSkipListMap

在高并发且需要对元素排序的情况下,可以使用ConcurrentSkipListMap提高效率,跳表可以理解为“多链链表”,是一种用空间换时间的数据结构,通过在每个节点中增加了向前的指针,从而提升查找的效率。

2)CopyOnWrite容器(写时复制列表容器)

CopyOnWriteArrayList

写时复制容器(copy on write)当添加/删除等修改元素操作发生时,将逐一复制原列表值到新容器,修改操作(即写的操作)完成后再将原容器的引用调整至新容器,从而实现读取数据的线程安全。

主要是读写分离的思想:在写的过程中引用并未指向新容器,所以读操作仍然在旧容器中读取,待写操作完成后才更新新容器的引用。

CopyOnWriteArrayList的实现原理是,在一个线程开始遍历(创建Iterator对象)时,内部会创建一个“快照”数组,遍历基于这个快照Iterator进行,在遍历过程中这个快照数组不会改变,也就不会抛出ConcurrentModificationException。如果在遍历的过程中有其他线程尝试改变数组的内容,就会拷贝一份新的数据进行变更,而后面再来访问这个数组的线程,看到的就是变更过的数组。

其实CopyOnWirteArrayList主要就是解决了并发环境下修改操作和对容器遍历操作的冲突(修改时另一线程开始遍历容器会抛出ConcurrentModificationException)。

多线程环境下,写时效率低,读时效率高,适合写少读多的环境。

下面的程序可以演示CopyOnWirteArrayList的读/写的效率:

3)ConcurrentQueue(并发队列容器)

在并发环境下用得较多的容器

4)ConcurrentLinkedQueue(并发链表队列)

是无界队列,容量取决于内存。

下面的代码演示基本使用ConcurrentLinkedQueue,poll和peek方法的区别是poll方法将返回并移除元素,peek方法是获取元素但不移除。另外有ConcurrentLinkedDeque并发双向链表队列。

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class T {
    public static void main(String[] args) {
        Queue<String> strs = new ConcurrentLinkedQueue<>();

        for(int i=0; i<100; i++) {
            strs.offer("a" + i);  //add
        }

        System.out.println(strs);

        System.out.println(strs.size());

        System.out.println(strs.poll());
        System.out.println(strs.size());

        System.out.println(strs.peek());
        System.out.println(strs.size());

        //双端队列Deque
    }
}

5)BlockingQueue(阻塞式队列)

BlockingQueue(阻塞式队列)其实是Java对生产者/消费者模式的实现。

  • LinkedBlockingQueue:链表实现,无界
  • ArrayBlockingQueue:数组实现,有界

LinkedBlockingQueue

其中LinkedBlockingQueue是使用链表实现的阻塞式无界队列,put方法在容器已满时将等待,而take方法在容器为空时将等待(下文例题中有实现这种生产者/消费者模式的容器的程序)

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

public class T {

    static BlockingQueue<String> strs = new LinkedBlockingQueue<>();

    static Random r = new Random();

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    strs.put("a" + i); //如果满了,就会等待
                    TimeUnit.MILLISECONDS.sleep(r.nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "p1").start();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                for (;;) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " take -" + strs.take()); //如果空了,就会等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "c" + i).start();

        }
    }
}

ArrayBlockingQueue

底层使用数组实现,容量有限制,offer方法可以向容器添加元素,并返回是否添加成功的布尔值(若容器已满则不添加元素并返回false,而使用add方法则会抛出异常),且offer方法可以传入参数设置时间间隔,在此段时间间隔内不断添加,超时则放弃添加操作并返回false。而put方法在容器已满时将阻塞。

import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class T {
    static BlockingQueue<String> strs = new ArrayBlockingQueue<>(10);
    static Random r = new Random();
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            strs.put("a" + i);
        }

        strs.put("aaa"); //满了就会等待,程序阻塞
        //strs.add("aaa");
        //strs.offer("aaa");
        //strs.offer("aaa", 1, TimeUnit.SECONDS);
        System.out.println(strs);
    }
}

LinkedTransferQueue

TransferQueue有transfer方法(将元素放入容器),这个方法的作用是当多个消费者线程等待获取队列中的元素时,此时生产者再生产一个元素,不放入队列中,而是可以直接交给消费者线程,但使用了transfer方法,若没有消费者线程等待获取元素,使用transfer方法的线程将阻塞直至消费者线程出现。可以提高并发效率。

import java.util.concurrent.LinkedTransferQueue;

public class T {
    public static void main(String[] args) throws InterruptedException {
        LinkedTransferQueue<String> strs = new LinkedTransferQueue<>();

      /*new Thread(() -> {
         try {
            System.out.println(strs.take());
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }).start();*/
        //strs.transfer("aaa");
        strs.put("aaa");
        new Thread(() -> {
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

SynchronousQueue(同步队列)

是一种特殊的TransferQueue,特殊在容量为0

不能调用add方法,只能调用put方法(将阻塞等待消费者线程)

若没有消费者线程等待获取容器中的值,则会抛出异常IllegalStateException:Queue full

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;

public class T { //容量为0
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> strs = new SynchronousQueue<>();

        new Thread(()->{
            try {
                System.out.println(strs.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        strs.put("aaa"); //阻塞等待消费者消费
        //strs.add("aaa");
        System.out.println(strs.size());
    }
}

阻塞式队列添加方法小结

  • put方法:容器已满时阻塞,等待容器变为未满时再尝试添加(同步队列中put方法将阻塞等待消费者线程)
  • offer方法:容器已满时将放弃添加操作,但可以设置一定时间内不断尝试添加,超时放弃
  • add方法:容器已满时添加将抛出IllegalStateException:Queue full异常
  • transfer方法:TransferQueue特有,阻塞等待消费者线程获取元素的添加元素方法,直接将元素交由消费者线程消费

6)DelayQueue(延迟队列)

同样,DelayQueue也是一个线程安全的无界队列。

特点是当队列中的元素到达延迟时间时才能被取出,队列元素会按照最终执行时间(阻塞结束后到被执行的时间)在队列中进行排序,头部为最终执行时间最长的元素。

可以使用延迟队列来执行定时任务。

DelayQueue是一个无界阻塞队列,该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。例如,size 方法同时返回到期和未到期元素的计数。此队列不允许使用 null 元素。

下面的代码演示了使用DelayQueue,其中DelayQueue存放的元素需要实现Delayed接口以实现元素延迟计时等功能(如下MyTask类实现了Delay接口)

import java.util.Calendar;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class T {

    static BlockingQueue<MyTask> tasks = new DelayQueue<>();

    static Random r = new Random();

    static class MyTask implements Delayed {
        long runningTime;

        MyTask(long rt) {
            this.runningTime = rt;
        }

        @Override
        public int compareTo(Delayed o) {
            if(this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS))
                return -1;
            else if(this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS))
                return 1;
            else
                return 0;
        }

        @Override
        public long getDelay(TimeUnit unit) {

            return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }


        @Override
        public String toString() {
            return "" + runningTime;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        long now = System.currentTimeMillis();
        MyTask t1 = new MyTask(now + 1000);
        MyTask t2 = new MyTask(now + 2000);
        MyTask t3 = new MyTask(now + 1500);
        MyTask t4 = new MyTask(now + 2500);
        MyTask t5 = new MyTask(now + 500);

        tasks.put(t1);
        tasks.put(t2);
        tasks.put(t3);
        tasks.put(t4);
        tasks.put(t5);

        System.out.println(tasks);

        for(int i=0; i<5; i++) {
            System.out.println(tasks.take());
        }
    }
}

13、Executor(执行器)

1)Executor接口

Executor是一个接口,内部定义了execute(Runnable command)方法,实现类需要实现这个方法编写需要实现的具体任务。简言之,Executor的实现类是用于执行某个任务的。

import java.util.concurrent.Executor;

public class T implements Executor{

    public static void main(String[] args) {
        new T().execute(()->System.out.println("hello executor"));
    }

    @Override
    public void execute(Runnable command) {
        //new Thread(command).run();
        command.run();

    }
}

2)ExecutorService接口

ExecutorService是一个继承Executor的接口,除继承execute方法外,还定义了一系列其他关于执行任务的方法(如submit方法),其中submit方法可以传入Callable和Runnable接口类型的参数。

Callable接口

与Runnable接口非常相似,Runnable接口内定义了run方法,而Callable接口内部定义了call方法与Runnable接口的区别在于,Runnable接口的run方法没有返回值且不能抛出异常,而Callable接口定义的call方法可以有返回值且可以抛出异常。

3) Executors类(工具类)

Executors是简化使用Executor接口常见实现类的工具类。其中定义了一些使用的方法比如创建线程池等。

14、ThreadPool(线程池)

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。使用线程池可以达到线程的重用,提高性能。

下面的程序演示了创建一个固定线程数量的线程池并直接向线程池派发任务并执行(把任务扔进线程池中,线程池中的数个线程将抢着执行任务)。

其中shutdown方法的作用是关闭线程池,若线程仍在执行任务则等待线程全部空闲再关闭,有shutdownNow方法可以强制关闭线程池。

isTerminated方法作用是检测此时线程池内任务是否被执行完毕(全部线程空闲)。

isShutdown方法注意是检测该线程池是否执行了shutdown方法。

1)Future

在Java中,如果需要设定代码执行的最长时间,即超时,可以用Java线程池ExecutorService类配合Future接口来实现。 Future接口是Java标准API的一部分,在java.util.concurrent包中。Future接口是Java线程Future模式的实 现,可以来进行异步计算。

 

Future模式可以这样来描述:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时 间之后,我就便可以从Future那儿取出结果。就相当于下了一张订货单,一段时间后可以拿着提订单来提货,这期间可以干别的任何事情。其中Future 接口就是订货单,真正处理订单的是Executor类,它根据Future接口的要求来生产产品。

Future接口提供方法来检测任务是否被执行完,等待任务执行完获得结果,也可以设置任务执行的超时时间。这个设置超时的方法就是实现Java程序执行超时的关键。

在Future接口中声明了5个方法:

  • cancel方法:用于取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false
    • 参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务
    • 如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;
    • 如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;
    • 如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true
  • isCancelled方法:表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回true
  • isDone方法:表示任务是否已经完成,若任务完成,则返回true
  • get()方法:用来获取执行结果(Callable的返回值),这个方法会产生阻塞,会一直等到任务执行完毕才返回
  • get(long timeout, TimeUnit unit)方法:用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null

2)FutureTask

FutureTask实现了Future接口。
不直接构造Future对象,也可以使用ExecutorService.submit方法来获得Future对象,submit方法即支持以 Callable接口类型,也支持Runnable接口作为参数,具有很大的灵活性。
下面的程序演示了两种获取Future对象的方式并通过讲task对象传入线程构造函数开启线程使用,其中FutureTask的泛型表示获取值的类型。

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class T {
    public static void main(String[] args) throws InterruptedException, ExecutionException {

        FutureTask<Integer> task = new FutureTask<>(()->{
            TimeUnit.MILLISECONDS.sleep(500);
            return 1000;
        }); //new Callable () { Integer call();}

        new Thread(task).start();

        System.out.println(task.get()); //阻塞

        //*******************************
        ExecutorService service = Executors.newFixedThreadPool(5);
        Future<Integer> f = service.submit(()->{
            TimeUnit.MILLISECONDS.sleep(500);
            return 1;
        });
        System.out.println(f.get());
        System.out.println(f.isDone());
    }
}

3)FutureTask配合线程池实现并行计算

下面程序使用FutureTask配合固定线程数的线程池实现了并行计算1-200000范围内得素数并对比了串行计算和并行计算的效率。

其中不均分计算范围是因为数值越大计算量越大。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class T {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        long start = System.currentTimeMillis();
        getPrime(1, 200000);
        long end = System.currentTimeMillis();
        System.out.println(end - start);

        final int cpuCoreNum = 4;

        ExecutorService service = Executors.newFixedThreadPool(cpuCoreNum);

        MyTask t1 = new MyTask(1, 80000); //1-5 5-10 10-15 15-20
        MyTask t2 = new MyTask(80001, 130000);
        MyTask t3 = new MyTask(130001, 170000);
        MyTask t4 = new MyTask(170001, 200000);

        Future<List<Integer>> f1 = service.submit(t1);
        Future<List<Integer>> f2 = service.submit(t2);
        Future<List<Integer>> f3 = service.submit(t3);
        Future<List<Integer>> f4 = service.submit(t4);

        start = System.currentTimeMillis();
        f1.get();
        f2.get();
        f3.get();
        f4.get();
        end = System.currentTimeMillis();
        System.out.println(end - start);
        service.shutdown();
    }

    static class MyTask implements Callable<List<Integer>> {
        int startPos, endPos;

        MyTask(int s, int e) {
            this.startPos = s;
            this.endPos = e;
        }

        @Override
        public List<Integer> call() throws Exception {
            List<Integer> r = getPrime(startPos, endPos);
            return r;
        }

    }

    static boolean isPrime(int num) {
        for(int i=2; i<=num/2; i++) {
            if(num % i == 0) return false;
        }
        return true;
    }

    static List<Integer> getPrime(int start, int end) {
        List<Integer> results = new ArrayList<>();
        for(int i=start; i<=end; i++) {
            if(isPrime(i)) results.add(i);
        }

        return results;
    }
}

4)FixedThreadPool(固定线程数线程池)

最基本的线程池,线程数固定。

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

public class T {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(5); //execute submit
        for (int i = 0; i < 6; i++) {
            service.execute(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            });
        }
        System.out.println(service);

        service.shutdown();
        System.out.println(service.isTerminated());
        System.out.println(service.isShutdown());
        System.out.println(service);

        TimeUnit.SECONDS.sleep(5);
        System.out.println(service.isTerminated());
        System.out.println(service.isShutdown());
        System.out.println(service);
    }
}

5)CachedPool(缓存线程池)

当任务需要时就自动创建新线程(不限制线程数量),线程默认超过60s空闲则销毁。

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

public class T {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool();
        System.out.println(service);
        for (int i = 0; i < 2; i++) {
            service.execute(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            });
        }
        System.out.println(service);

        TimeUnit.SECONDS.sleep(80);

        System.out.println(service);


    }
}

6)SingleThreadPool(单线程线程池)

这种线程池的作用是保证多个任务顺序执行。

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

public class T {
    public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        for(int i=0; i<5; i++) {
            final int j = i;
            service.execute(()->{

                System.out.println(j + " " + Thread.currentThread().getName());
            });
        }

    }
}

7)ScheduledPool(定时任务线程池)

下面程序演示了使用固定频率执行任务。

import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class T {
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
        service.scheduleAtFixedRate(()->{
            try {
                TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
        }, 0, 500, TimeUnit.MILLISECONDS);

    }
}

8)WorkStealingPool(工作窃取线程池)

一般来讲每个线程维护一个任务队列,每个线程只执行分配给自身的任务,而使用工作窃取线程池当有空闲线程时空闲线程将主动窃取另外线程的任务来执行。

WorkStealingPool底层是由ForkJoinPool线程池实现的。

注意产生的是守护线程。

import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class T {
    public static void main(String[] args) throws IOException {
        ExecutorService service = Executors.newWorkStealingPool();
        System.out.println(Runtime.getRuntime().availableProcessors());

        service.execute(new R(1000));
        service.execute(new R(2000));
        service.execute(new R(2000));
        service.execute(new R(2000)); //daemon
        service.execute(new R(2000));

        //由于产生的是精灵线程(守护线程、后台线程),主线程不阻塞的话,看不到输出
        System.in.read();
    }

    static class R implements Runnable {

        int time;

        R(int t) {
            this.time = t;
        }

        @Override
        public void run() {

            try {
                TimeUnit.MILLISECONDS.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(time  + " " + Thread.currentThread().getName());

        }

    }
}

9)ForkJoinPool(分叉合并线程池)

  • fork:分支/切分
  • join:合并

任务的切分(切分子任务到多小)和合并可以由开发者指定,而ForkJoinPool将根据切分和合并的规则来创建线程并由ForkJoinPool维护线程。下面程序演示了计算长度为1000000的,内部存放随机数值的数组的和。其中使用static inner class是为了防止包可见导致命名冲突。

import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class T {
    static int[] nums = new int[1000000];
    static final int MAX_NUM = 50000;
    static Random r = new Random();

    static {
        for(int i=0; i<nums.length; i++) {
            nums[i] = r.nextInt(100);
        }

        System.out.println(Arrays.stream(nums).sum()); //stream api
    }

   /*
   static class AddTask extends RecursiveAction {

      int start, end;

      AddTask(int s, int e) {
         start = s;
         end = e;
      }
      @Override
      protected void compute() {

         if(end-start <= MAX_NUM) {
            long sum = 0L;
            for(int i=start; i<end; i++) sum += nums[i];
            System.out.println("from:" + start + " to:" + end + " = " + sum);
         } else {

            int middle = start + (end-start)/2;

            AddTask subTask1 = new AddTask(start, middle);
            AddTask subTask2 = new AddTask(middle, end);
            subTask1.fork();
            subTask2.fork();
         }


      }

   }
   */

    static class AddTask extends RecursiveTask<Long> {

        private static final long serialVersionUID = 1L;
        int start, end;

        AddTask(int s, int e) {
            start = s;
            end = e;
        }

        @Override
        protected Long compute() {

            if(end-start <= MAX_NUM) {
                long sum = 0L;
                for(int i=start; i<end; i++) sum += nums[i];
                return sum;
            }

            int middle = start + (end-start)/2;

            AddTask subTask1 = new AddTask(start, middle);
            AddTask subTask2 = new AddTask(middle, end);
            subTask1.fork();
            subTask2.fork();

            return subTask1.join() + subTask2.join();
        }

    }

    public static void main(String[] args) throws IOException {
        ForkJoinPool fjp = new ForkJoinPool();
        AddTask task = new AddTask(0, nums.length);
        fjp.execute(task);
        long result = task.join();
        System.out.println(result);

        //System.in.read();

    }
}

15、ThreadPoolExecutor

其实所有线程池的底层都是使用ThreadPoolExecutor作为支撑的,可以自己自定义线程池,指定不同的特定策略(最小/最大线程数、使用什么任务队列和执行策略等)。

1)扩展-ParallelStream

默认使用多线程并行计算。

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class T {
    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>();
        Random r = new Random();
        for(int i=0; i<10000; i++) nums.add(1000000 + r.nextInt(1000000));

        //System.out.println(nums);

        long start = System.currentTimeMillis();
        nums.forEach(v->isPrime(v));
        long end = System.currentTimeMillis();
        System.out.println(end - start);

        //使用parallel stream api

        start = System.currentTimeMillis();
        nums.parallelStream().forEach(T::isPrime);
        end = System.currentTimeMillis();

        System.out.println(end - start);
    }

    static boolean isPrime(int num) {
        for(int i=2; i<=num/2; i++) {
            if(num % i == 0) return false;
        }
        return true;
    }
}

 

posted @ 2021-05-17 16:36  钟齐峰  阅读(411)  评论(0编辑  收藏  举报