深入理解java:2. 多线程机制

引言

很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先理清一些概念,以免产生误会。

  • 多线程:指的是这个程序(一个进程)运行时,产生了不止一个线程。
  • 并行与并发:
    • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
    • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
  • 线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:
    void transferMoney(User from, User to, float amount){
      to.setMoney(to.getBalance() + amount);
      from.setMoney(from.getBalance() - amount);
    }
  • 同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能

线程的状态

线程状态

线程状态转换


各种状态一目了然,值得一提的是"blocked"这个状态:
线程在Running的过程中可能会遇到阻塞(Blocked)情况

  1. 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
  2. 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
  3. 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。

此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

线程的内存

在本节,我们需要将Thread类和Java中线程的概念分开来讲了:
  • 一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于JVM堆上。
  • Java中,每个线程都有一个调用栈,即使你不在Java程序中创建任何新的线程,线程也在后台运行着(如:main线程)。一个Java应用总是从main()方法开始运行,mian()方法运行在一个线程内,它被称为主线程。一旦创建一个新的线程,就产生一个新的调用栈。
 
Java的虚拟机内存分为几个部分,其中线程共享的是堆区和方法区,线程私有的是虚拟机栈,本地方法栈和程序计数器。在本文,我们只关注Java线程内存模型中最突出的两个模块:堆内存 和 栈内存
 
     在这里,堆内存,就是前面说的堆区,由各个线程共享,存储的是对象的实例;
     而栈内存,指的是虚拟机栈(在HotSpot虚拟机中,本地方法栈是与虚拟机栈放在一起实现的,所以这里不再专门区分),存储的是局部变量表、动态链接、方法出口。
 
 
把上面所说的堆内存中的对象和基本数据类型,称为主内存(main memory)
把上面所说的栈内存中用于存储变量的副本,称为本地内存(local memory)(或叫工作内存),这就组成了Java内存模型(JMM)
1,Java线程对于变量的所有操作(读取、赋值),都是在自己的工作内存中进行的,线程不直接读写主内存中的变量。
2,不同线程无法直接访问对方工作内存中的变量;
3,线程间变量值的传递,需要主内存来完成。
 
 

线程间通信

上节所讲到的Java内存模型(JMM)控制了线程间的通信,Java线程间通信,使用的是共享内存模型,而非消息传递模型。
也就是说,线程间是通过write-read内存中的公共状态来进行隐式通信的,多个线程之间不能直接传递数据交互,它们之间的交互只能通过共享变量来实现。
 
线程间通信的步骤:
  • 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  • 线程B到主内存中去读取线程A之前已更新过的共享变量。
 
 

每个对象都有的方法(机制)

synchronized, wait, notify 是任何对象都具有的同步工具。

讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。

在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。

wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。
 

synchronized, wait, notify结合:典型场景生产者消费者问题

复制代码
/**
   * 生产者生产出来的产品交给店员
   */
  public synchronized void produce()
  {
      if(this.product >= MAX_PRODUCT)
      {
          try
          {
              wait();  
              System.out.println("产品已满,请稍候再生产");
          }
          catch(InterruptedException e)
          {
              e.printStackTrace();
          }
          return;
      }

      this.product++;
      System.out.println("生产者生产第" + this.product + "个产品.");
      notifyAll();   //通知等待区的消费者可以取出产品了
  }

  /**
   * 消费者从店员取产品
   */
  public synchronized void consume()
  {
      if(this.product <= MIN_PRODUCT)
      {
          try 
          {
              wait(); 
              System.out.println("缺货,稍候再取");
          } 
          catch (InterruptedException e) 
          {
              e.printStackTrace();
          }
          return;
      }

      System.out.println("消费者取走了第" + this.product + "个产品.");
      this.product--;
      notifyAll();   //通知等待去的生产者可以生产产品了
  }
复制代码
 
volatile

多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去。

(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。

本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。
 
 
 
基本的线程类

基本的线程类指的是

Thread类,

Runnable接口,

Callable接口。

通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中

  1. class MyThread extends Thread{  
  2.       
  3.     private int ticket = 10;  
  4.     private String name;  
  5.     public MyThread(String name){  
  6.         this.name =name;  
  7.     }  
  8.       
  9.     public void run(){  
  10.         for(int i =0;i<500;i++){  
  11.             if(this.ticket>0){  
  12.                 System.out.println(this.name+"卖票---->"+(this.ticket--));  
  13.             }  
  14.         }  
  15.     }  
  16. }


Thread 类实现了Runnable接口,启动一个线程的方法:

 MyThread my = new MyThread();
  my.start();

Thread类相关方法:

复制代码
//当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)
public static Thread.yield() //让渡
//暂停一段时间
public static Thread.sleep()  //睡眠
//在一个线程中调用other.join(),将等待other执行完后才继续本线程。    
public join()//中断
//后两个函数皆可以被打断
public interrupte()//打断
复制代码

关于打断:它并不像stop方法那样会中断一个正在运行的线程。

线程会不时地检测 断标识位,以判断线程是否应该被断(断标识值是否为true)。

终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。
Thread.interrupted()检查当前线程是否发生中断,返回boolean
synchronized在获锁的过程中是不能被中断的。

中断是一个状态!interrupt()方法只是将这个状态置为true而已。

所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码。

Thread类最佳实践
写的时候最好要设置线程名称 Thread.name,并设置线程组 ThreadGroup,目的是方便管理。

在出现问题的时候,打印线程栈 (jstack -pid) 一眼就可以看出是哪个线程出的问题,这个线程是干什么的。

ThreadGroup tg = new ThreadGroup("My Group");
MyThread thrd = new MyThread(tg, "MyThreadName");
ThreadGroup比ExecutorService多以下几个优势  

1.ThreadGroup可以遍历线程,知道那些线程已经运行完毕,那些还在运行  

2.可以通过ThreadGroup.activeCount知道有多少线程从而可以控制插入的线程数

参考:http://www.cnblogs.com/yy2011/archive/2011/05/05/2037564.html


Runnable

  1. class MyThread1 implements Runnable{  
  2.     private int ticket =10;  
  3.     private String name;  
  4.     public void run(){  
  5.         for(int i =0;i<500;i++){  
  6.             if(this.ticket>0){  
  7.                 System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));  
  8.             }  
  9.         }  
  10.     }  

Callable

future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态

ExecutorService e = Executors.newFixedThreadPool(3);
 //submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 无阻塞
future.get() // return 返回值,阻塞直到该线程运行结束

 

高级的多线程控制类

Java1.5提供了一个非常高效实用的多线程包:java.util.concurrent, 提供了大量高级工具,可以帮助开发者编写高效、易维护、结构清晰的Java多线程程序。

1.ThreadLocal类

用处:保存线程的独立变量。对一个线程类(继承自Thread)
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

常用于用户登录控制,如记录session信息。

实现:每个Thread都持有一个TreadLocalMap类型的变量

(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身(ThreadLocal对象)为key,以目标为value。
主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,  get时将a返回。

ThreadLocal是一个特殊的容器。

 

2.原子类(AtomicInteger、AtomicBoolean……)

如果使用atomic wrapper class如atomicInteger,或者使用自己保证原子的操作,则等同于synchronized

//返回值为boolean
AtomicInteger.compareAndSet(int expect,int update)

该方法可用于实现乐观锁,CAS算法。

if(b.value.compareAndSet(old, value)){
   return ;
}else{
   //try again
   // if that fails, rollback and log
}

AtomicReference
对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,

即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。
这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号

3.Lock类 

lock: 在java.util.concurrent包内。共有三个实现:

ReentrantLock
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock

主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。

区别如下:

 
lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序)
提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。
本质上和监视器锁(即synchronized是一样的)
能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。
和Condition类的结合。

 

ReentrantLock    
可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。
使用方法是:

1.先new一个实例

static ReentrantLock r=new ReentrantLock();
2.加锁      
r.lock()或r.lockInterruptibly();

此处也是个不同,后者可被打断。当a线程lock后,b线程阻塞,此时如果是lockInterruptibly,那么在调用b.interrupt()之后,b线程退出阻塞,并放弃对资源的争抢,进入catch块。(如果使用后者,必须throw interruptable exception 或catch)    

3.释放锁   

r.unlock()

必须做!何为必须做呢,要放在finally里面。以防止异常跳出了正常流程,导致灾难。这里补充一个小知识点,finally是可以信任的:经过测试,哪怕是发生了OutofMemoryError,finally块中的语句执行也能够得到保证。

ReentrantReadWriteLock

可重入读写锁(读写锁的一个实现) 

 ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
  ReadLock r = lock.readLock();
  WriteLock w = lock.writeLock();

两者都有lock,unlock方法。写写,写读互斥;读读不互斥。可以实现并发读的高效线程安全代码

4.容器类

这里就讨论比较常用的两个:

BlockingQueue
ConcurrentHashMap

BlockingQueue
阻塞队列。该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,可以在队列头添加元素和在队尾删除或取出元素。类似于一个管  道,特别适用于先进先出策略的一些应用场景。普通的queue接口主要实现有PriorityQueue(优先队列),有兴趣可以研究

BlockingQueue在队列的基础上添加了多线程协作的功能:


BlockingQueue


除了传统的queue功能(表格左边的两列)之外,还提供了阻塞接口put和take,带超时功能的阻塞接口offer和poll。put会在队列满的时候阻塞,直到有空间时被唤醒;take在队 列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用,堪称神器。

常见的阻塞队列有:

ArrayListBlockingQueue
LinkedListBlockingQueue
DelayQueue
SynchronousQueue

ConcurrentHashMap
高效的线程安全哈希map。请对比hashTable , concurrentHashMap, HashMap

5.管理类

管理类的概念比较泛,用于管理线程,本身不是多线程的,但提供了一些机制来利用上述的工具做一些封装。
了解到的值得一提的管理类:ThreadPoolExecutor和 JMX框架下的系统级管理类 ThreadMXBean
ThreadPoolExecutor
如果不了解这个类,应该了解前面提到的ExecutorService,开一个自己的线程池非常方便:

复制代码
ExecutorService e = Executors.newCachedThreadPool();
    ExecutorService e = Executors.newSingleThreadExecutor();
    ExecutorService e = Executors.newFixedThreadPool(3);
    // 第一种是可变大小线程池,按照任务数来分配线程,
    // 第二种是单线程池,相当于FixedThreadPool(1)
    // 第三种是固定大小线程池。
    // 然后运行
    e.execute(new MyRunnableImpl());
复制代码

该类内部是通过ThreadPoolExecutor实现的,掌握该类有助于理解线程池的管理,本质上,他们都是ThreadPoolExecutor类的各种实现版本。请参见javadoc:

corePoolSize:池内线程初始值与最小值,就算是空闲状态,也会保持该数量线程。
maximumPoolSize:线程最大值,线程的增长始终不会超过该值。
keepAliveTime:当池内线程数高于corePoolSize时,经过多少时间多余的空闲线程才会被回收。回收前处于wait状态
unit:
时间单位,可以使用TimeUnit的实例,如TimeUnit.MILLISECONDS 
workQueue:待入任务(Runnable)的等待场所,该参数主要影响调度策略,如公平与否,是否产生饿死(starving)
threadFactory:线程工厂类,有默认实现,如果有自定义的需要则需要自己实现ThreadFactory接口并作为参数传入。
 
 
 
 
 
参考资料

《Java编程思想》
线程执行者:http://ifeve.com/thread-executors-1/
线程原理:http://www.open-open.com/bbs/view/1318332540655
Java线程详解:http://www.cnblogs.com/riskyer/p/3263032.html
计算机操作系统之进程和线程:http://www.cnblogs.com/loveyakamoz/archive/2012/11/14/2770811.html
用户级线程和内核级线程的区别:http://blog.csdn.net/debugingstudy/article/details/12668389
Java多线程编程总结:http://lavasoft.blog.51cto.com/62575/27069
Java多线程小结,及解决应用挂死的问题:http://sis*huo*k.com/forum/posts/list/1280.html(这个网页居然是违禁词。。无语了)
Java编程体验:线程的7种状态及相互转换(图) http://blog.csdn.net/jiafu1115/article/details/6804386
Java内存模型总结:http://blog.csdn.net/vking_wang/article/details/8574376
java内存模型与多线程:http://www.cnblogs.com/fguozhu/articles/2657904.html
java虚拟机内存模式。栈和堆:http://rainyear.iteye.com/blog/1735121
Java内存模型:http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
Java 多线程编程之八:多线程的调度 :http://blog.csdn.net/defonds/article/details/8664948
Java 线程的Interrupt:http://blog.csdn.net/hudashi/article/details/6958550
用Interrupt中断Java线程:http://hapinwater.iteye.com/blog/310558
Runnable、Callable、Executor、Future、FutureTask关系解读 : http://blog.csdn.net/zhangzhaokun/article/details/6615454
Java并发编程:Callable、Future和FutureTask : http://www.cnblogs.com/dolphin0520/p/3949310.html
Java并发编程:线程池的使用:http://www.cnblogs.com/dolphin0520/p/3932921.html
 java中Executor、ExecutorService、ThreadPoolExecutor介绍 :http://blog.csdn.net/linghu_java/article/details/17123057
ScheduledThreadPoolExecutor  :http://hubingforever.blog.163.com/blog/static/17104057920109643632988/
ThreadPoolExecutor  : http://hubingforever.blog.163.com/blog/static/1710405792010964339151/
 
原文地址:https://www.cnblogs.com/my376908915/p/6756895.html