Java多线程并发编程学习

Java多线程并发编程学习笔记
关键字:java.util.concurrent;Executors;Executor;ExecutorService;ScheduledExecutorService;
ThreadFactory;Callable;AtomicLong;CountDownLatch;Lock;

1 线程与任务
1.1 理解
  线程是比进程更小的实体,可以独立运行,能共享访问整个进程的所有资源。任务是线
程实际工作的内容,一个线程需要指定他的任务,才会工作,所以Thread必须指定Runnable。
  通过线程的start()启动的任务才被认为是一个线程,如果直接调用runnable.run(),这只
能是看做一次函数调用,不具备任何线程的特性,比如生命周期管理等。
  一旦一个thread调用run方法后,就会在gc中注册,在run放回前,不可能被gc回收。
1.2 线程池
  线程池,顾名思义,就是存放线程的池子,里面有正在运行的线程和闲置的线程,线程
池会根据当前池子里的状态,决定是否需要创建线程。Java.util.concurrent包中提供了线程池
相关的类。
  线程池分为4种,分别是newCachedThreadPool、newFixedThreadPool、
newSingleThreadExecutor和newScheduledThreadPool,他们有一个共同点,就是能够复用现
有的空闲线程,这也是任务XX池设计的初衷,比如数据库连接池。
1. newCachedThreadPool
容量自动扩展的线程池,示例代码:
ExecutorService exec = Executors.newCachedThreadPool();
for(int j = 0; j < i; j++)
{
 Thread t = new MultiThread();
 exec.submit(t);
}

2. newFixedThreadPool
能指定容量的线程池,示例代码:
ExecutorService exec = Executors.newFixedThreadPool(10);
for(int j = 0; j < i; j++)
{
 Thread t = new MultiThread();
 exec.submit(t);
}
3. newSingleThreadExecutor
容量=1的线程池,其实就是单个线程。实例代码略
4. newScheduledThreadPool
能指定容量,且池中的线程具备定时执行的能力。实例代码:
ScheduledExecutorService exec = Executors.newScheduledThreadPool(10);
for(int j = 0; j < i; j++)
{
Thread t = new MultiThread();
exec.scheduleAtFixedRate(t, 1, 3, TimeUnit.SECONDS);
exec.scheduleWithFixedDelay(t, 1, 3, TimeUnit.SECONDS);
}//从现在起的下一秒开始执行t,每隔3秒执行一次。
ScheduleAtFixedRate 每次执行时间为上一次任务开始起向后推一个时间间隔,即
每次执行时间为 :initialDelay, initialDelay+period, initialDelay+2*period, ...;
ScheduleWithFixedDelay 每次执行时间为上一次任务结束起向后推一个时间间隔,
即每次执行时间为:initialDelay, initialDelay+executeTime+delay, 
initialDelay+2*executeTime+2*delay。由此可见,ScheduleAtFixedRate 是基于固定
时间间隔进行任务调度,ScheduleWithFixedDelay 取决于每次任务执行的时间长短,
是基于不固定时间间隔进行任务调度

1.3 从线程中获取返回值
  普通的线程都是实现Runnable接口,并且允许通过start()启动,但这种方式下,
子线程无法返回结果,如果需要获取子线程执行的结果,那么需要实现Callable接
口(call方法),并且指明泛型为返回值类型,通过submit提交,示例代码:
static class MultiThreadWithReturn extends Thread implements Callable<String>{
  @Override
  public String call() throws Exception {
   // TODO Auto-generated method stub
   return ""+Thread.currentThread().getId();
  }
 }

 public static void main(String[] args) {
  List<Future<String>> fslist = new ArrayList<Future<String>>();
  ExecutorService execc = Executors.newCachedThreadPool();
  for(int j = 0; j < 5; j++)
  {
   Thread t = new MultiThreadWithReturn();
   Future<String> fs = (Future<String>) execc.submit(t);
   fslist.add(fs);
  }
  for(Iterator<Future<String>> it = fslist.iterator(); it.hasNext();)
  {
   Future<String> fs = it.next();
   String result = "";
   try {
    result = fs.get();
    //此处是阻塞的,fs只是类似子线程的句柄,并不是执行结果。fs.isDone()可以
判断当前是否已经执行结束,避免阻塞
   } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } catch (ExecutionException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } finally {
    System.out.println(result);
   }
  }//当for循环执行完成后,表示所有的子线程都执行完成了


1.4 线程状态及阻塞场景
1.4.1 线程的状态
线程有4个状态,新建NEW、就绪Runnable、阻塞Blocked、死亡Dead。

1.4.2 何时进入阻塞
a: 被sleep:这种阻塞,可以被中断
 b:被wait:不需要考虑是否可以被中断
 c:I/O输入阻塞,等待输入:不能被中断
 d:同步锁阻塞,等待获得锁:不能被中断
此处讨论是否可以被中断,并不是关心能否被中断本身,而是关心意外中断是否会带来
死锁,比如sleep时会占用cpu,如果它会被中断,需要考虑是否能释放锁。
I/O和同步锁是不能在代码层面被中断的,只能被外部中断,比如I/O操作的文件被删除
了,socket断开了之类的场景,此时无法捕获到中断,故只能捕获到关闭异常并做处理
2 并发
2.1 并发的实际意义和使用场景
  在多处理器上,并发可以让每个处理器处理各自的任务,从而实现多个处理器并发工作。
那么,在单处理器上,并发有什么意义呢?如果任务可以顺利的执行,那么单处理器就没有
使用并发的必要,因为这个处理器一直在忙于正常的工作,且没有其他处理器了。但是,如
果任务不能顺利执行呢?比如阻塞?当一个任务被阻塞时,处理器就闲置了,此时如果有多
个任务,那是不是可以让处理器去处理另一个任务呢?这就是单处理器下实现并发的意义,
常见的场景是事件机制。
  总而言之,使用并发有2种情况,第一种,利用多处理器的特性,提高整个系统的计算
能力,第二种,利用任务可能挂起的特性,提高单个处理器的利用率,不让其空等。


2.2.2 父线程join()所有子线程,只要代码跑完所有join就表示子都运行结束
示例代码:
List<Thread> ts = new ArrayList<Thread>();
for(int j = 0; j < i; j++)
{
 Thread t = new MultiThread();
 ts.add(t);
 exec.submit(t);
}
for(Iterator<Thread> it = ts.iterator(); it.hasNext();)
{
 Thread t = it.next();
 try {
  t.join();//此处会阻塞,等待t执行完成后继续
 } catch (InterruptedException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
 }
}

2.2.3 通过倒计时锁CountDownLatch,一旦await结束,表示子都运行结束
示例代码:
static class MultiThreadCountDownLatch extends Thread implements Runnable{
  private CountDownLatch cd;
  MultiThreadCountDownLatch(CountDownLatch cd)
  {
   this.cd = cd;
  }
  @Override
  public void run() {
   cd.countDown();
  }
 }
 public static void main(String[] args) {
  ExecutorService execd = Executors.newCachedThreadPool();
  CountDownLatch cd = new CountDownLatch(10);
  for(int j = 0; j < 10; j++)
  {
   Thread t = new MultiThreadCountDownLatch(cd);
   execd.submit(t);
  }
  try {
   cd.await();//此处阻塞,当子线程执行cd.countDown()时会使计数--,当到达0
时,此处唤醒
  } catch (InterruptedException e1) {
   // TODO Auto-generated catch block
   e1.printStackTrace();
  } finally {
   System.out.println("10个子都执行结束");
  }

2.2.4 通过栅栏动作CyclicBarrier,一旦匿名runnable执行run,就表示子都运行
结束
示例代码:
static class MultiThreadCyclicBarrier extends Thread implements Runnable{
  private CyclicBarrier cb;
  MultiThreadCyclicBarrier(CyclicBarrier cb)
  {
   this.cb = cb;
  }
  
  @Override
  public void run() {
   try {
    cb.await();
   } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } catch (BrokenBarrierException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } finally {
    System.out.println("Thread "+this.getId()+" over !!");
   }
  }
 }
 public static void main(String[] args) {
  ExecutorService execb = Executors.newCachedThreadPool();
  CyclicBarrier cb = new CyclicBarrier(10, new Runnable(){
   @Override
   public void run() {//cb被调用10次await后,会触发run的执行
    // TODO Auto-generated method stub
    System.out.println("main over!! ");
   }
   
  });//创建一个值为10的栅栏
  for(int j = 0; j < 10; j++)
  {
   Thread t = new MultiThreadCyclicBarrier(cb);
   execb.submit(t);
  }

2.2.5 最原始的方法,通过wait和notify配合完成
示例代码如下,仔细揣摩synchronized加锁的位置,提示:无法确保wait一次只接收一个
notify,也就无法通过wait执行次数烂判断,故只能通过notify的次数来判断;必须确保子线
程发起notify时,父线程已经在wait了,这样才能保证i从10变为0,从而证明所有子都跑完了:
static class MultiThread extends Thread implements Runnable{
  @Override
  public void run() {
   synchronized (MultiThreadTest.list) 
   {
    System.out.println("Thread "+this.getId()+": get list lock and 
start notify list");
    i--;
    list.notify();
    //通知发出后,未必能立即被唤醒,此时可能会同时有多个通知发给同一个挂起对象
    System.out.println("Thread "+this.getId()+": notify list over, i 
= "+i);
   }
  }
 }
 
 public static void main(String[] args) {
// while(i > 0)//如果while放到外层,则先判断i的值,再获取锁。有可能导致notify先抢到锁,
//并在挂起前就发生了所有的notify,此时再执行list.wait就会导致挂死,不会再有notify信号。//
参见think in java p1203
// {
  synchronized (MultiThreadTest.list) 
  {
   ExecutorService exec = Executors.newCachedThreadPool();
   for(int j = 0; j < i; j++)
   {
    Thread t = new MultiThread();
    exec.submit(t);
   }
   while(i > 0)//while在内层,表示应该先抢到锁再判断i的值,此时由于锁被抢到了,
则表示notify不会执行,i--也不会执行,此时判断i值才是有意义的
   {
    try {
     System.out.println("Main thread start waiting");
     list.wait();//挂起,释放锁,只有一个子会抢到锁
    } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
   }
  }
// }

这里有一个弊端,i是一个全局变量,在锁范围内维护,而wait其实只有最后一次是有
效的,但是如果父子线程不再一个类中,在2个不同的地方执行,那i就无法被共享。
也就无法实现:
static class MultiThread extends Thread implements Runnable{

  public void run() {
   synchronized (MultiThreadTest.list) 
   {
    try {
     sleep(10);
    } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
    System.out.println("Thread "+this.getId()+": get list lock and 
start notify list");
    i--;
    list.notify();
    //通知发出后,未必能立即被唤醒,此时可能会同时有多个通知发给同一个挂起对象
    System.out.println("Thread "+this.getId()+": notify list over, i 
= "+i);
   }
  }
 }

public static void main(String[] args) {
// while(i > 0)//如果while放到外层,则先判断i的值,在获取锁。
//有可能导致notify先抢到锁,并在挂起前就发生了所有的notify,此时再执行list.wait就会导致挂
死,不会再有notify信号。参见think in java p1203
// {
  synchronized (MultiThreadTest.list) 
  {
   //主线程,负责启动所有子线程,并等所有子线程返回后,在继续执行,创建10个子线程,
并逐个启动
   ExecutorService exec = Executors.newCachedThreadPool();
   for(int j = 0; j < i; j++)
   {
    Thread t = new MultiThread();
    exec.submit(t);
   }
   int k = i;
   int maxK = k;
   while(k > 0)//while在内层,表示应该先抢到锁再判断i的值,此时由于锁被抢到了,
则表示notify不会执行,i--也不会执行,此时判断i值才是有意义的
   {
    try {
     list.wait();
     k--;
     System.out.println("主线程唤醒,第"+(maxK-k)+"次");
     TimeUnit.MILLISECONDS.sleep(50);
    } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
   }
  }
// }

执行结果:
Thread 204: get list lock and start notify list
Thread 204: notify list over, i = 99
Thread 202: get list lock and start notify list
Thread 202: notify list over, i = 98
...省略,都是一样的
Thread 10: get list lock and start notify list
Thread 10: notify list over, i = 2
Thread 8: get list lock and start notify list
Thread 8: notify list over, i = 1
主线程唤醒,第1次              ---此处,是第一次被唤醒,这里至少有98次notify消息
Thread 206: get list lock and start notify list
Thread 206: notify list over, i = 0
主线程唤醒,第2次              ---此处,是第二次被唤醒,这里至少有2次notify消息

由此可见,当wait收到多个notify时,只会响应一次,一旦唤醒,锁就占用了,子线程也就无法
再notify了,等再次挂起时,会清楚所有前一次的notify。所以,这里wait唤醒的此处远远小于
100次。
3 线程间协作
3.1 Sleep、wait、notify、notifyAll的关系
3.1.1 Sleep和wait的比较
a Sleep:睡眠,不释放任何资源(包括CPU、锁),类似死等,待睡眠时间超时后,立
即唤醒继续向下执行,由于不释放CPU,所以唤醒后不需要等待操作系统调度,本身就
有CPU资源
b Wait:挂起,释放所有资源(包括CPU、锁),直接进入等待区,待挂起超时后,不立
即向下执行,而进入就绪队列,等待调度获取CPU。等待期间可以被notify唤醒。通常
配合同步锁一起使用。挂起是针对某个对象而言(如a.wait()),执行挂起的代码会被
阻塞在这一句。
3.1.2 Notify和notifyAll的比较
a Notify:通知获得该对象的单个线程
b NotifyAll:通知获得该对象的所有线程,竞争后,只有一个线程获得notify消息


4 性能比较分析
4.1 Synchronized、Lock、Atomic性能比较
> Synchronized:在使用频率较低时,相对高效,且代码易读
> Lock:开销小,性能好,且稳定,代码可读性差,要配合try-finally
> Atomic:性能好,但多个atomic配合使用时,性能明显下降,故使用场景单一


4.2 Collections.synchronizedHashMap和ConcurrentHashMap性能比较
> Collections.synchronizedHashMap:性能差
> ConcurrentHashMap:性能优异,做过特殊处理,优化措施包括分段锁+JMM,具体参见
http://guibin.iteye.com/blog/1172231



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值