多线程学习笔记(四)

来源:互联网 发布:ubuntu g 升级 编辑:程序博客网 时间:2024/05/18 13:12

多线程并发的所有支持的类都在java.lang.concurrent包中。

1、线程池
    当需要再程序中创建大量生存期很短暂的线程时,应该考虑使用线程池。Java提供Executors工厂类产生线程池。

Executors线程池工具类

    1)public static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。

    2)public static ExecutorService newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于newFixedThreadPool方法是传入的参数为1。

    3)public static ExecutorService newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。

    4)public static ScheduledExecutorService newSingleThreadScheduledExecutor:创建只有一条线程的线程池,他可以在指定延迟后执行线程任务

    5)public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以再指定延迟后执行线程任务,corePoolSize指池中所保存的线程数,即使线程是 空闲的也被保存在线程池内。


例子:

public class ExecutorsTest { public static void main(String[] args) throws InterruptedException, ExecutionException {  //创建一个可重用的,具有固定次数的线程池,若有线程因为异常结束,那么线程池会补充一个新线程// ExecutorService threadPool = Executors.newFixedThreadPool(4);  //创建一个无界线程池,可以进行自动线程回收,若当前没有线程池可用,则创建一个新线程到线程池中,终止并从缓存中移除那些已有60s未被使用的线程,跑空闲线程不会使用任何资源// ExecutorService threadPool = Executors.newCachedThreadPool();  //单个后台线程  ExecutorService threadPool = Executors.newSingleThreadExecutor();  for(int i = 0; i < 10; i++) {// threadPool.execute(new Test());   Future future = threadPool.submit(new Test());   System.out.println("使用的submit方法获取的线程返回值是: " + future.get() );  }  //shutdown方法会等待该线程完成后关闭,因为该方法是无限循环,换言之线程池不会关闭// threadPool.execute(new Runnable() {// public void run() {// while(true) {// System.out.println("我在占用线程资源");// try {// Thread.sleep(100);// } catch (InterruptedException e) {// e.printStackTrace();// }// }// }// });  //如果不写这个,程序不会执行完毕,同时会把所有在线程池的任务执行完再关闭,若有线程是循环的,则不会关闭线程池  threadPool.shutdown(); } static class Test implements Runnable{     private static Map<String, Integer> threadExecutorTime = new HashMap<String, Integer>();  @Override  public void run() {   String threadName = Thread.currentThread().getName();   int index = 1;   if(threadExecutorTime.containsKey(threadName)) {    index = threadExecutorTime.get(threadName) + 1;      }   threadExecutorTime.put(threadName, index);   System.out.println("线程名:" + threadName + " 第 " + index + " 次执行");  } }  static class Task implements Callable<Integer> {  private static Map<String, Integer> threadExecutorTime = new HashMap<String, Integer>();   @Override  public Integer call() throws Exception {   String threadName = Thread.currentThread().getName();   int index = 1;   if(threadExecutorTime.containsKey(threadName)) {    index = threadExecutorTime.get(threadName) + 1;      }   threadExecutorTime.put(threadName, index);   System.out.println("线程名:" + threadName + " 第 " + index + " 次执行");   return index;  } }}

上面例子使用了不同的方式去构建线程池(分别是newFixedThreadPool,newSingleThreadExecutor,newCachedThreadPool)可以复制到IDE上看看不同的效果。

注意:下面例子会复用上面两个静态内部类Test和Task,它们分别实现了Runnable接口和Callable接口。并且只会贴出main方法,减少重复代码篇幅。


真正的线程池接口是ExecutorService或者ScheduledExecutorService,它们之间的区别是ScheduledExecutorService是可以定时周期执行指定的任务

ScheduledExecutorService有以下四个方法:

  public  ScheduledFuture<V>  schedule(Callable<V> callable, long initialDelay, TimeUnit unit)  创建并执行在给定延迟后启用的callable,最后返回ScheduledFuture,只执行一次

  public  ScheduledFuture<?>  schedule(Runnable command, long initialDelay, TimeUnit unit) 创建并执行在给定延迟后启用的command,最后返回ScheduleFuture,只执行一次

  public  ScheduledFuture<?>  scheduleAtFixedRate(Runnable command, long initialDelay,  long delay, TimeUnit unit) 创建并执行在给定延迟initialDelay后启用的command,并定期在initialDelay + delay时间后周期执行

  public  ScheduledFuture<?>  scheduleWithFixedDelay(Runnable command, long initialDelay,  long delay, TimeUnit unit)创建并执行在给定延迟initialDelay后启用的command,并定期在delay时间后周期执行

明显我们一般使用scheduleAtFixedRate和scheduleWithFixedDelay方法作为周期性触发的方法,定期清除无用的数据和文件都是非常有用的方法,并用来替代有缺陷的Timer类。当线程执行时间大于周期时间时候,它并不会在指定间隔开辟一个新线程并发执行这个任务,而是等待线程执行完毕。ScheduledFuture类是Future类的子类,可以使用getDelay方法获取延迟的时间。

ScheduledExecutorService的schedule方法使用:

public static void main(String[] args) throws InterruptedException, ExecutionException {  //ScheduledExecutorService是ExecutorService类的子类,所以会有execute等方法,当然也增加了一些延迟执行的方法// ScheduledExecutorService scheduledThreadPool = Executors.newSingleThreadScheduledExecutor();  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);  for(int i = 0; i < 10; i++) {   //延迟100ms后执行Test方法// ScheduledFuture scheduledFuture = scheduledThreadPool.schedule(new Test(), 100, TimeUnit.MILLISECONDS);   ScheduledFuture scheduledFuture = scheduledThreadPool.schedule(new Task(), 100, TimeUnit.MILLISECONDS);   //若是调用Runnable的实现方法类,则get方法返回为null   System.out.println(scheduledFuture.get());  }   scheduledThreadPool.shutdown(); }

ScheduledExecutorService的scheduleAtFixedRate方法和scheduleWithFixedDelay方法使用:

public static void main(String[] args) throws InterruptedException, ExecutionException {  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);  //延迟100ms后执行Test方法,以后每次执行时间为100+1000ms// ScheduledFuture scheduledFuture = scheduledThreadPool.scheduleAtFixedRate(new Test(), 100, 1000,TimeUnit.MILLISECONDS);  //延迟100ms后执行Test方法,以后每次执行时间为1000ms  ScheduledFuture scheduledFuture = scheduledThreadPool.scheduleWithFixedDelay(new Test(), 100, 1000,TimeUnit.MILLISECONDS);  //若是调用Runnable的实现方法类,则get方法返回为null  System.out.println("方法延迟的时间: " + scheduledFuture.getDelay(TimeUnit.MILLISECONDS));  //调用shutdown方法不会等待schedule方法延迟执行,而是直接关闭// scheduledThreadPool.shutdown(); }

ExecutorService中有两个执行方法:

     1)public void execute(Runnable command) 

     2) * public Future<T> submit(Callable<T> task) 

           * public Future<?> submit(Runnable task)

           * public Future<T> submit(Runnable task, T result) result返回Runnable的中run方法的结果

    一个是execute方法,一个是submit方法,这两个方法都可以接受所需要被线程池执行的方法,它们的区别主要是submit有返回值,execute没有,同时submit方法可以在调用过程中取消执行该方法。还有一个比较重要的点是submit方法可以使用Future对象的get方法获取执行的方法抛出的Exception,而execute方法只是单纯将有Exception的线程取消并打印到console上。

    从上面两个方法可以看到执行的方法需要实现Runnable接口或者Callable接口。


Runnable & Callable 接口

使用Runnable接口需要实现run方法,使用Callable接口需要实现call方法,并指定call方法的返回值,如:

public class Task implements Callable<Integer> {  @Override  public Integer call() throws Exception {   return null;  } }

若用submit方法执行实现Runnable接口方法的类,获取的Future对象的get方法是返回null的,就是说实现Runnable接口的类的方法用execute或者submit都是没有返回值(sumbit方法中带有result参数的是可以使用Runnable接口的实现类通过此路径返回结果,不过我们最好还是使用Callable接口返回结果,更符合逻辑)。而实现Callable接口方法的类,则可以获取线程执行之后的返回值。


2、Lock

Lock的工作与synchronized类似,都是提供同步原语,但为什么只用synchronized不足够,还需要Lock呢。因为第一个,如果在同步块中因为其它原因如调用了sleep方法被阻塞了,但又没有释放锁,那么就会影响其它线程进入同步块工作(synchronized同步不支持interrupt),其次,Lock可以进行读写锁分离,而synchronized不支持。但Lock有个缺点,需要在finally块中手动释放锁,如果没有主动释放锁,可能导致死锁现象。


关于Lock的包都在java.util.concurrent.locks包中

1)Lock

可以看到,Lock是一个接口,并且有以下一些方法:

public interface Lock {     void lock(); //若锁已被其他线程获取,则进行等待     void lockInterruptibly() throws InterruptedException; //线程在等待锁的过程中可被interrupt打断,但拿到锁运行的线程不能使用interrupt打断     boolean tryLock(); //尝试获取锁,获取成功返回true,获取失败返回false,立刻返回不会阻塞     boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //可以设置设置时间限期获取锁,如果在一定时间内拿不到锁,则返回false。     void unlock(); //释放锁     Condition newCondition(); //获取当前锁的Condition对象 }

lock,tryLock,lockInterruptibly方法是用来获取锁的,unLock方法是用来释放锁的,newCondition方法获取Condition对象用于线程间协作。


2)ReentrantLock 可重入锁

可重入锁:当线程请求由自己持有的对象锁时,若该锁是重入锁,则请求成功,否则阻塞。可重入锁避免死锁。ReentranLock和synchronized都是可重入锁,我们来看个例子:

public class Test {  public static void main(String[] args) {   final Test test = new Test();  for(int i = 0; i < 5; i++) {   new Thread(new Runnable() {    public void run() {     test.sayHelloTo(Thread.currentThread().getName());    }   }).start();  }  }  public synchronized void sayHello(String name) {  System.out.println("同步方法sayHello " + name); }  //同步方法sayHelloTo public synchronized void sayHelloTo(String name) {  System.out.println("同步方法sayHelloTo " + name);  //在同步方法中继续调用同步方法不会导致死锁,因为synchronized是重入锁  sayHello(name); }}

结果:



我们看到,在同步方法sayHelloTo方法中调用同步方法sayHello不会导致死锁,是因为synchronized是可重入锁,当当前持有锁的线程再去获取锁则可以立刻获得。


现在我们来看回ReentrantLock,那如何使用Lock和ReentrantLock来完成同步呢?代码块如下:

Lock lock = ...;lock.lock();try{    //处理任务}catch(Exception ex){     }finally{    lock.unlock();   //释放锁}

我们看到lock.lock()方法是不会抛异常的,但是为防止程序异常导致不释放锁,所以需要try-catch-finally结构来释放锁。现在看一个具体的例子:
public class ReentrantLockTest { //所有线程共用一个锁才能达到进入共享变量的效果 private Lock lock = new ReentrantLock(); //ReentrantLock是Lock的实现类 //向List加值 private List<Integer> arrayList = new ArrayList<Integer>(); public static void main(String[] args) {   final ReentrantLockTest test = new ReentrantLockTest();                  new Thread(){             public void run() {                 test.insert(Thread.currentThread());             };         }.start();                  new Thread(){             public void run() {                 test.insert(Thread.currentThread());             };         }.start(); }  public void insert(Thread thread) {  lock.lock(); //直接lock// lock.tryLock();  //如果不能获得锁会报IllegalMonitorStateException   try {// lock.tryLock(100, TimeUnit.MILLISECONDS); //一定时间内获取锁// lock.lockInterruptibly(); //如果线程对被阻塞的线程调用interrupt方法,则会中断阻塞            System.out.println(thread.getName()+"得到了锁");            for(int i=0;i<5;i++) {                arrayList.add(i);            }        } catch (Exception e) {            e.printStackTrace();        }finally {            System.out.println(thread.getName()+"释放了锁");            lock.unlock();        } }}

使用了lock.lock()方法后的结果:

使用了lock.tryLock()方法后的结果,因为不能获得锁,所以立刻报出异常。


3)ReadWriteLock & ReentrantReadWriteLock

ReadWriteLock 是一个接口,并定义了两个方法readLock()和writeLock(),分别是获取读锁和写锁;ReentrantReadWriteLock是实现类,那我们来看一下具体的用法,分别进行读操作和写操作:

public class ReadWriteLockTest { private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //使用读写锁 private Lock readLock = readWriteLock.readLock(); //读锁 private Lock writeLock = readWriteLock.writeLock();  //写锁  private SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss"); //获取读锁并进行读操作 public void read() {  readLock.lock();   try {   System.out.println(format.format(new Date()) + "----ReadLock---- is doing something...");   TimeUnit.SECONDS.sleep(3);  } catch(Exception e) {   e.printStackTrace();  } finally {   readLock.unlock();  } } //获取写锁并进行读操作 public void write() {  writeLock.lock();   try {   System.out.println(format.format(new Date()) + "----WriteLock---- is doing something...");   TimeUnit.SECONDS.sleep(3);  } catch(Exception e) {   e.printStackTrace();  } finally {   writeLock.unlock();  } } public static void main(String[] args) {  final ReadWriteLockTest test = new ReadWriteLockTest();   //使用了3个线程分别进行读操作和写操作  for(int i = 0; i < 3; i++)  new Thread(new Runnable() {   public void run() {    test.read();    //test.write();   }  }).start();  }

进行读操作结果:


可以看到是同时进行的,所以所读锁是互不排斥,一旦有写锁加入,则需要变成顺序进行,即读写互斥或者谢谢互斥。


进行写操作结果:



每个写操作是顺序执行的,读写操作,写读操作我就一一列举了,写操作需要所有读操作完成才会进行或者写操作完成才能进行读操作,当然读操作间是互不互斥的。



0 0
原创粉丝点击