多线程并发相关概念整理

编码习惯问题:在创建线程和线程组的时候取个好听民办自,对于计算机名字不重要但出问题的时候对于我们来说如果是一串thread-0,thread-1之类的一定会头疼,如果是HttpHandler,FTPService这样的名字就会好很多。

并发和并行

进程是线程的容器,线程是轻量级的进程,使用多线程而不是多进程去进行并发程序的设计,是因为线程间切换和调度的成本远小于进程。

lock重入锁对一个线程连续两次获得同一把锁是允许的。如不允许这样操作,那么同一个线程在第二次获得锁,将会和自己产生死锁。但多次获得锁的同一线程释放也得多次。

使用重入锁手动锁定和释放对逻辑处理更灵活。

 

 

 

wait和notify方法不能随便调用,必须包含在对应的synchronized语句中,因为需要获得目标对象的一个监视器。

 

 

挂起suspend()和继续执行resume()方法也不推荐使用因为如果先resume()后再挂起suspend()则被挂起的线程可能很难被继续执行而且挂起的线程jstack命令查看还是runnable,影响我们对系统当前判断。

 

join方法本质让基金业调用线程wait方法在当前线程对象实例上

核心代码如下:

 

可以看到他让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出前调用notifyAll方法通知所有的等待线程继续执行。所以我们需要注意:不要在应用程序,在Thread对象实例上使用类似wait方法或者notify方法等,因为这很有可能影响系统api工作或被系统api影响。

 

yield方法会使当前线程让出CPU但是让出cpu后该线程还是会进行cpu资源争夺,但是是否能被再次分配到就不一定了。

 

设置守护线程(Daemon)必须在线程start()之前设置否则会出现异常设置守护线程失败线程正常执行只是当做用户线程而已。

 

公平锁:不会产生饥饿现象,使用synchroized的话是非公平锁,重入锁允许我们进行公平性设置。但公平锁要求系统维护一个有序队列,所以公平锁实现成本比较高。

 

重入锁配合condition的await和singal方法使用。

LockSupport:线程阻塞工具,可以在线程内任意位置让线程阻塞。

为每个线程准备一个许可,若许可可用则park()会立即返回并消费这个许可。反之则阻塞。unpark()方法让许可变得可用,许可不能累加,永远只有一个。

总结一下,LockSupport比Object的wait/notify有两大优势

①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。

②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。

即一个线程先park则阻塞有了unpark才能继续执行,一个线程先unpark拿到许可,再park则不会阻塞会继续执行。

处于park的线程不像suspend()会是runnable状态,而是waiting状态标注park()方法引起

在LockSupport.park()方法不会抛出InterruptedException异常他只会默默返回但是我们可以从Thread.interrupted()等方法中获得中断标记。

 

线程池相关:

newScheduledThreadPool()返回一个ScheduledExecutorService对象,其中的scheduleAtFixedRate()方法意为调度任务频率一定即以上一个任务开始执行时间为起点,之后的preiod时间调用下一次任务。而scheduleWithFixedDelay()方法则是在上一个任务结束后经过delay时间进行任务调度。

若任务遇到异常那么后续所有的子任务都会停止调度,所以必须保证异常被及时处理,为周期性任务稳定调度提供条件。

 

核心线程池内部实现:corePoolSize:线程池中线程数量

maximumPoolSize线程池最大线程数量

keepAliveTime当线程数量超过corePoolSize时,多余空闲线程的存活时间。超过corePoolSize的空闲线程多长时间会被销毁。
unit:keepAliveTime的单位

workQueue:任务队列,被提交但尚未被执行的任务

threadFactory:线程工厂,创建线程使用一般使用默认的即可

handler:拒绝策略,任务太多来不及处理时,如何拒绝任务。

workQueue分类:被提交但未执行的任务队列。分为以下:

1.直接提交队列:SynchronousQueue,没有容量的一种特殊BlockingQueue。每个插入操作都需要一个相应删除操作,同样的每个删除都要等待对应插入操作。不会真实保存提交任务而是直接将新任务提交给线程执行,若没空进程则创建。若数量到了最大值就使用拒绝策略所以一般使用该队列的,都设置很大的maximumPoolSize,否则很容易出现执行拒绝策略。例如newCachedThreadPool。

2.有界的任务队列:ArrayBlockingQueue,使用时构造函数必须指明该队列最大容量是多少。当有新任务来执行,若线程池实际线程数小于corePoolSize则优先创建新线程,若大于corePoolSize则将新任务加入等待队列。若等待队列满了则总线程数不大于maximumPoolSize则创建新进程执行任务。大于maximumPoolSize执行拒绝策略。可见除了系统非常繁忙,否则都确保核心线程数维持在corePoolSize。

3.无界的任务队列:通过LinkedBlockingQueue实现。除非系统资源耗尽否则无界队列任务不存在任务入队失败的情况。有新任务,线程数小于corePoolSize,会生成新线程执行任务。但当达到corePoolSize后就不增加了。后续有新任务又没空闲线程时,进入队列等待。若任务创建和处理速度差异很大,则无界队列快速增长,直至耗尽系统内存。例如newFixedThreadPool和newSingleThreadExecutor都是corePoolSize和maximumPoolSize一样大道理如上。

4.优先任务队列:带有执行优先级的队列。通过PriorityBlockingQueue实现。控制任务执行先后顺序,是一种特殊的无界队列。上面三中都是先进先出而这种可以根据任务自身的优先级顺序先后执行,确保系统性能同时,也有很好的质量保证。

 

拒绝策略:可以说是系统超负荷运行时的补救措施。通常由于压力太大引起,即线程池线程用完不能为新任务服务,等待队列也排满,放不下新任务。

jdk内置如下四种拒绝策略:

AbortPolicy策略:直接抛出异常,阻止系统正常工作。

CallerRunsPolicy策略:只要线程池没关闭,这个策略直接在调用者线程中,运行被丢弃的任务。所以这个策略不会真的丢弃任务,但是任务提交线程的性能很可能急剧下降。

DiscardOldestPolicy策略:丢弃最老的一个请求,也就是即将被执行的一个任务,尝试再次提交当前任务。

DiscardPolicy策略:默默丢弃无法处理任务,不予任何处理。

都实现了RejectedExecutionHandler接口,自己也可以扩展策略去实现该接口。

 

ThreadPoolExecutor是一个可扩展的线程池,提供了三个接口对线程池进行控制。

提供了三个空实现beforeExecute(),afterExecute()和terminated()

在实际应用中可以对其扩展来实现对线程池运行状态的跟踪,输出有用的调试信息,帮助诊断系统故障

如下例子所示:

package geym.conc.ch3.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ExtThreadPool {
    public static class MyTask implements Runnable {
        public String name;

        public MyTask(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("正在执行" + ":Thread ID:" + Thread.currentThread().getId()
                    + ",Task Name=" + name);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>()) {
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("准备执行:" + ((MyTask) r).name);
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println("执行完成:" + ((MyTask) r).name);
            }

            @Override
            protected void terminated() {
                System.out.println("线程池退出");
            }

        };
        for (int i = 0; i < 5; i++) {
            MyTask task = new MyTask("TASK-GEYM-" + i);
            es.execute(task);
            Thread.sleep(10);
        }
        es.shutdown();
    }
}
输出如下:

准备执行:TASK-GEYM-0
正在执行:Thread ID:11,Task Name=TASK-GEYM-0
准备执行:TASK-GEYM-1
正在执行:Thread ID:12,Task Name=TASK-GEYM-1
准备执行:TASK-GEYM-2
正在执行:Thread ID:13,Task Name=TASK-GEYM-2
准备执行:TASK-GEYM-3
正在执行:Thread ID:14,Task Name=TASK-GEYM-3
准备执行:TASK-GEYM-4
正在执行:Thread ID:15,Task Name=TASK-GEYM-4
执行完成:TASK-GEYM-0
执行完成:TASK-GEYM-1
执行完成:TASK-GEYM-2
执行完成:TASK-GEYM-3
执行完成:TASK-GEYM-4
线程池退出
 

上述三个方法记录了一个任务开始结束和整个线程池的退出。shutdown()方法只是发送一个关闭信号,此方法执行后线程池不能再接受其他新任务了。

execute和submit的区别与联系:

execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。

execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出:

如下所示:

Future re=pools.submit(new DivTask(100,i));
re.get();

execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。
 

Runnable和Callable的区别和联系:

1)Runnable提供run方法,不会抛出异常,只能在run方法内部处理异常。Callable提供call方法,直接抛出Exception异常,也就是你不会因为call方法内部出现检查型异常而不知所措,完全可以抛出即可。

2)Runnable的run方法无返回值,Callable的call方法提供返回值用来表示任务运行的结果

3)Runnable可以作为Thread构造器的参数,通过开启新的线程来执行,也可以通过线程池来执行。而Callable只能通过线程池执行。

 

使用submit或者execute都可以得到部分信息:但不能知道任务哪里提交的下面方法彻底挖出堆栈信息

package geym.conc.ch3.trace;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TraceThreadPoolExecutor extends ThreadPoolExecutor {
   public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
         long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
      super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
   }

   @Override
   public void execute(Runnable task) {
      super.execute(wrap(task, clientTrace(), Thread.currentThread()
            .getName()));
   }

   @Override
   public Future<?> submit(Runnable task) {
      return super.submit(wrap(task, clientTrace(), Thread.currentThread()
            .getName()));
   }
   
   private Exception clientTrace() {
      return new Exception("Client stack trace1");
   }

   private Runnable wrap(final Runnable task, final Exception clientStack,
         String clientThreadName) {
      return new Runnable() {
         @Override
         public void run() {
            try {
               task.run();
            } catch (Exception e) {
               clientStack.printStackTrace();
               throw e;
            }
         }
      };
   }
}
再执行以下代码
public class DivTask implements Runnable {
    int a,b;
    public DivTask(int a,int b){
        this.a=a;
        this.b=b;
    }
    @Override
    public void run() {
        double re=a/b;
        System.out.println(re);
    }
}
public class TraceMain {

   public static void main(String[] args) {
      ThreadPoolExecutor pools=new TraceThreadPoolExecutor(0, Integer.MAX_VALUE,
                0L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
      for(int i=0;i<5;i++){
         pools.execute(new DivTask(100,i));
      }
   }

}

得到完整堆栈信息:

java.lang.Exception: Client stack trace1
    at geym.conc.ch3.trace.TraceThreadPoolExecutor.clientTrace(TraceThreadPoolExecutor.java:27)
    at geym.conc.ch3.trace.TraceThreadPoolExecutor.execute(TraceThreadPoolExecutor.java:16)
    at geym.conc.ch3.trace.TraceMain.main(TraceMain.java:15)
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
    at geym.conc.ch3.trace.DivTask.run(DivTask.java:11)
    at geym.conc.ch3.trace.TraceThreadPoolExecutor$1.run(TraceThreadPoolExecutor.java:36)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
100.0
25.0
33.0
50.0

否则只能拿到如下堆栈信息:

Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
    at geym.conc.ch3.trace.DivTask.run(DivTask.java:11)
    at geym.conc.ch3.trace.TraceThreadPoolExecutor$1.run(TraceThreadPoolExecutor.java:36)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
100.0
25.0
33.0
50.0

 

 

分而治之:Fork/Join框架

“分而治之” 一直是一个有效的处理大量数据的方法。著名的 MapReduce 也是采取了分而治之的思想。简单来说,就是如果你要处理1000个数据,但是你并不具备处理1000个数据的能力,那么你可以只处理其中的10个,然后,分阶段处理100次,将100次的结果进行合成,那就是最终想要的对原始的1000个数据的处理结果。

当一个线程试图帮助其他线程时,总是从任务队列底部开始获取数据,当线程试图执行自己任务时,则是从相反的顶部开始获取数据,这种行为十分有利于避免数据竞争。

为什么现在不用线程组:

虽然线程组看上去很有用处,实际上现在的程序开发中已经不推荐使用它了,主要有两个原因:

  • 1.线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。
  • 2.线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。

虽然线程组现在已经不被推荐使用了,但是它在线程的异常处理方面还是做出了一定的贡献。当线程运行过程中出现异常情况时,在某些情况下JVM会把线程的控制权交到线程关联的线程组对象上来进行处理。所以对线程组的了解还是有一定必要的。

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值