多线程复习笔记

1. 多线程是什么?    html

    线程就是程序中单独顺序的流控制。线程自己不能运行,它只能用于程序中。多线程则指的是在单个程序中能够同时运行多个不一样的线程执行不一样的任务。java

2. 为何使用多线程?c++

    多线程编程的目的,就是"最大限度地利用CPU资源",当某一线程的处理不须要占用CPU,而只和I/O等资源打交道时,让须要占用CPU资源的其它线程有机会得到CPU资源。编程

3. 线程与进程有什么不一样?缓存

    1). 资源是否独立:多个进程的内部数据和状态都是彻底独立的,而多线程是共享一块内存空间和一组系统资源,有可能互相影响。线程自己的数据一般只有寄存器数据,以及一个程序执行时使用 的堆栈,安全

          因此线程的切换比进程切换的负担要小。 多线程

    2). 切换成本:多线程程序比多进程程序须要更少的管理费用。进程是重量级的任务,须要分配给它们独立的地址空间。进程间通讯是昂贵和受限的。进程间的转换也是很须要花费的。另外一方面,线程是并发

          轻量级的选手。它们共享相同的地址空间而且共同分享同一个进程。线程间通讯是便宜的,线程间的转换也是低成本的。 app

4. 骨架框架

    Thread 类也实现了 Runnable 接口,实现了 Runnable 接口中的 run 方法

5. 多线程怎么用?

    1). 线程的实现有两种方式,第一种方式是继承 Thread 类,而后重写 run 方法;第二种是实现 Runnable 接口,而后实现其 run 方法。 

    2). 将咱们但愿线程执行的代码放到 run 方法中,而后经过 start 方法来启动线程,start方法首先为线程的执行准备好系统资源,而后再去调用run方法。当某个类继承了Thread 类以后,该类就叫作一个线程类。

6. 使用注意事项

    1). 多线程的数量应根据CPU核数以及IO操做的频繁程度而定。并非线程越多,效率越高。

    2). 中止线程的方式:不能使用 Thread 类的 stop 方法来终止线程的执行。 通常要设定一个变量,在 run 方法中是一个循环,循环每次检查该变量,若是知足条件则继续执行,不然跳出循环,线程结束。

    3). 多线程共享资源,执行同一任务,须要Runnable接口。

    4). 关于选择继承Thread仍是实现Runnable接口?

         a. Thread和Runnable的区别:若是一个类继承Thread,则不适合资源共享。可是若是实现了Runable接口的话,则很容易的实现资源共享。

         b. 实现Runnable接口比继承Thread类所具备的优点:

             1). 适合多个相同的程序代码的线程去处理同一个资源

             2). 能够避免java中的单继承的限制

             3). 增长程序的健壮性,代码能够被多个线程共享,代码和数据独立。

    5). 在java程序中,只要前台有一个线程在运行,整个java程序进程不会消失,因此此时能够设置一个后台线程,这样即便java进程消失了,此后台线程依然可以继续运行。

    6). getId() 用来获得线程ID

    7). 基本上全部的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称做同步互斥访问。

7. 优化

    1). 推荐自定义线程名称

    2). 使用线程池

8. 监控

    visualVM

 

多线程优化:

1. 给你的线程起个有意义的名字。 这样能够方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关

    的名字,全部的主要框架甚至JDK都遵循这个最佳实践。

2. 避免锁定和缩小同步的范围 锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。所以相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。

3. 多用同步类少用wait 和 notify  首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操做,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写

    和维护在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序能够不费吹灰之力得到优化。

4. 多用并发集合少用同步集合 这是另一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,因此在并发编程时使用并发集合效果更好。若是下一次你须要用到map,你应该首先想到用

    ConcurrentHashMap。个人文章Java并发集合有更详细的说明。

 

 

1. 同步锁是什么? 

    基本上全部的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称做同步互斥访问。一般来讲,是在访问临界资源的代码前面加上一个锁,

    当访问完临界资源后释放锁,让其余线程继续访问。在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。

2. 为何使用同步锁

    解决线程安全问题

3. synchronized和Lock有什么不一样?

    1). Lock不是Java语言内置的,synchronized是Java语言的关键字,所以是内置特性。Lock是一个类,经过这个类能够实现同步访问;

    2). 采用synchronized不须要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完以后,系统会自动让线程释放对锁的占用;而Lock则必需要用户去手动释放锁,若是没有

          主动释放锁,就有可能致使出现死锁现象。

4. 骨架

    1). Lock接口

          a. lock():lock()方法是日常使用得最多的一个方法,就是用来获取锁。若是锁已被其余线程获取,则进行等待。         

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

      b. tryLock():tryLock()方法是有返回值的,它表示用来尝试获取锁,若是获取成功,则返回true,若是获取失败(即锁已被其余线程获取),则返回false,也就说这个方法不管如何都会当即返回。在拿不

              到锁时不会一直在那等待。

          c. tryLock(long time, TimeUnit unit):这个方法在拿不到锁时会等待必定的时间,在时间期限以内若是还拿不到锁,就返回false。若是若是一开始拿到锁或者在等待期间内拿到了锁,则返回true。              

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //若是不能获取锁,则直接作其余事情
}

       d. lockInterruptibly():lockInterruptibly()方法比较特殊,当经过这个方法去获取锁时,若是线程正在等待获取锁,则这个线程可以响应中断,即中断线程的等待状态。也就使说,当两个线程同时

              经过lock.lockInterruptibly()想获取某个锁时,倘若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法可以中断线程B的等待过程。因为lockInterruptibly()

              的声明中抛出了异常,因此lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

          e. unLock()方法是用来释放锁的。

          f. newCondition()这个方法

    2). ReentrantLock是惟一实现了Lock接口的类,而且ReentrantLock提供了更多的方法。

    3). ReadWriteLock也是一个接口,在它里面只定义了两个方法:readLock()和writeLock()。一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操做分开,分红2个锁来分配给线程,从而使得

         多个线程能够同时进行读操做。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

    4). ReentrantReadWriteLock里面提供了不少丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

5. 同步锁怎么用?

    1). synchronized方法 

    2). synchronized代码块

    3). Lock用法:         

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        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();
        try {
            System.out.println(thread.getName()+"获得了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}

  4). ReentrantReadWriteLock具体用法 

6. 使用注意事项

    1). 在Java中,每个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。

    2). 当一个线程正在访问一个对象的synchronized方法,那么其余线程不能访问该对象的其余synchronized方法。这个缘由很简单,由于一个对象只有一把锁,当一个线程获取了该对象的锁以后,其余线程

         没法获取该对象的锁,因此没法访问该对象的其余synchronized方法。

  3). 当一个线程正在访问一个对象的synchronized方法,那么其余线程能访问该对象的非synchronized方法。这个缘由很简单,访问非synchronized方法不须要得到该对象的锁,假如一个方法没用synchronized

         关键字修饰,说明它不会使用到临界资源,那么其余线程是能够访问这个方法的,

    4). 有一点要注意:对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,所以不会因为异常致使出现死锁现象。

    5). 若是采用Lock,必须主动去释放锁,而且在发生异常时,不会自动释放锁。所以通常来讲,使用Lock必须在try{}catch{}块中进行,而且将释放锁的操做放在finally块中进行,以保证锁必定被被释放,防止死锁的发生。

    6). 使用读锁,能够大大提高了读操做的效率。不过要注意的是,若是有一个线程已经占用了读锁,则此时其余线程若是要申请写锁,则申请写锁的线程会一直等待释放读锁。若是有一个线程已经占用了写锁,

         则此时其余线程若是申请写锁或者读锁,则申请的线程会一直等待释放写锁。

7. 优化

    1).  synchronized代码块

8. 监控

    visualVM

9. Lock和synchronized的选择

  总结来讲,Lock和synchronized有如下几点不一样:

  1). Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

  2). synchronized在发生异常时,会自动释放线程占有的锁,所以不会致使死锁现象发生;而Lock在发生异常时,若是没有主动经过unLock()去释放锁,则极可能形成死锁现象,所以使用Lock时须要在finally块中释放锁;

  3). Lock可让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不可以响应中断;

  4). 经过Lock能够知道有没有成功获取锁,而synchronized却没法办到。

  5). Lock能够提升多个线程进行读操做的效率。

  在性能上来讲,若是竞争资源不激烈,二者的性能是差很少的,而当竞争资源很是激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。因此说,在具体使用时要根据适当状况选择。

10. 锁的相关概念介绍

      1). 可重入锁:若是锁具有可重入性,则称做为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上代表了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单

           的例子,当一个线程执行到某个synchronized方法时,好比说method1,而在method1中会调用另一个synchronized方法method2,此时线程没必要从新去申请锁,而是能够直接执行方法method2。

      2). 可中断锁:可中断锁:顾名思义,就是能够相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。若是某一线程A正在执行锁中的代码,另外一线程B正在等待获取该锁,可能因为等待时间

           过长,线程B不想等待了,想先处理其余事情,咱们可让它中断本身或者在别的线程中中断它,这种就是可中断锁。在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。

      3). 公平锁:公平锁即尽可能以请求锁的顺序来获取锁。好比同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最早请求的线程)会得到该所,这种就是公平锁。非公平锁即没法保证锁的获取是

           按照请求锁的顺序进行的。这样就可能致使某个或者一些线程永远获取不到锁。在Java中,synchronized就是非公平锁,它没法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,

           它默认状况下是非公平锁,可是能够设置为公平锁。咱们能够在建立ReentrantLock对象时,经过如下方式来设置锁的公平性:

           ReentrantLock lock = new ReentrantLock(true);

           若是参数为true表示为公平锁,为fasle为非公平锁。默认状况下,若是使用无参构造器,则是非公平锁。

      4). 读写锁:读写锁将对一个资源(好比文件)的访问分红了2个锁,一个读锁和一个写锁。正由于有了读写锁,才使得多个线程之间的读操做不会发生冲突。ReadWriteLock就是读写锁,它是一个接口,

           ReentrantReadWriteLock实现了这个接口。能够经过readLock()获取读锁,经过writeLock()获取写锁。

 

 

深刻剖析volatile关键字

1.volatile关键字的两层语义

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:

  1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。

  2)禁止进行指令重排序。

  先看一段代码,假如线程1先执行,线程2后执行:

1
2
3
4
5
6
7
8
//线程1
boolean  stop =  false ;
while (!stop){
     doSomething();
}
 
//线程2
stop =  true ;

   这段代码是很典型的一段代码,不少人在中断线程时可能都会采用这种标记办法。可是事实上,这段代码会彻底运行正确么?即必定会将线程中断么?不必定,也许在大多数时候,这个代码可以把线程中断,

      可是也有可能会致使没法中断线程(虽然这个可能性很小,可是只要一旦发生这种状况就会形成死循环了)。

  下面解释一下这段代码为什么有可能致使没法中断线程。在前面已经解释过,每一个线程在运行过程当中都有本身的工做内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在本身的工做内存当中。

  那么当线程2更改了stop变量的值以后,可是还没来得及写入主存当中,线程2转去作其余事情了,那么线程1因为不知道线程2对stop变量的更改,所以还会一直循环下去。

  可是用volatile修饰以后就变得不同了:

  第一:使用volatile关键字会强制将修改的值当即写入主存

  第二:使用volatile关键字的话,当线程2进行修改时,会致使线程1的工做内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三:因为线程1的工做内存中缓存变量stop的缓存行无效,因此线程1再次读取变量stop的值时会去主存读取。

  那么在线程2修改stop值时(固然这里包括2个操做,修改线程2工做内存中的值,而后将修改后的值写入内存),会使得线程1的工做内存中缓存变量stop的缓存行无效,而后线程1读取时,发现本身的缓存行无效,

      它会等待缓存行对应的主存地址被更新以后,而后去对应的主存读取最新的值。

  那么线程1读取到的就是最新的正确的值。

2. volatile保证原子性吗?

  从上面知道volatile关键字保证了操做的可见性,可是volatile能保证对变量的操做是原子性吗?

  下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public  class  Test {
     public  volatile  int  inc =  0 ;
     
     public  void  increase() {
         inc++;
     }
     
     public  static  void  main(String[] args) {
         final  Test test =  new  Test();
         for ( int  i= 0 ;i< 10 ;i++){
             new  Thread(){
                 public  void  run() {
                     for ( int  j= 0 ;j< 1000 ;j++)
                         test.increase();
                 };
             }.start();
         }
         
         while (Thread.activeCount()> 1 )   //保证前面的线程都执行完
             Thread.yield();
         System.out.println(test.inc);
     }
}

    你们想一下这段程序的输出结果是多少?也许有些朋友认为是10000。可是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

  可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操做,因为volatile保证了可见性,那么在每一个线程中对inc自增完以后,在其余线程中都能看到修改后的值啊,因此有10个线程分别进行了1000次操做,那么最终inc的值应该是1000*10=10000。

  这里面就有一个误区了,volatile关键字能保证可见性没有错,可是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,可是volatile没办法保证对变量的操做的原子性

  在前面已经提到过,自增操做是不具有原子性的,它包括读取变量的原始值、进行加1操做、写入工做内存。那么就是说自增操做的三个子操做可能会分割开执行,就有可能致使下面这种状况出现:

  假如某个时刻变量inc的值为10,

  线程1对变量进行自增操做,线程1先读取了变量inc的原始值,而后线程1被阻塞了;

  而后线程2对变量进行自增操做,线程2也去读取变量inc的原始值,因为线程1只是对变量inc进行读取操做,而没有对变量进行修改操做,因此不会致使线程2的工做内存中缓存变量inc的缓存行无效,因此线程2会直接去主存读取inc的值,发现inc的值时10,而后进行加1操做,并把11写入工做内存,最后写入主存。

  而后线程1接着进行加1操做,因为已经读取了inc的值,注意此时在线程1的工做内存中inc的值仍然为10,因此线程1对inc进行加1操做后inc的值为11,而后将11写入工做内存,最后写入主存。

  那么两个线程分别进行了一次自增操做后,inc只增长了1。

  解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?而后其余线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,可是要注意,

      线程1对变量进行读取操做以后,被阻塞了的话,并无对inc值进行修改。而后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,可是线程1没有进行修改,因此线程2根本就不会看到修改的值。

  根源就在这里,自增操做不是原子性操做,并且volatile也没法保证对变量的任何操做都是原子性的。

  把上面的代码改为如下任何一种均可以达到效果:

  采用synchronized:

      采用Lock:

      采用AtomicInteger:      

      在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做类,即对基本数据类型的 自增(加1操做),自减(减1操做)、以及加法操做(加一个数),减法操做(减一个数)进行了封装,保证这些操做是

      原子性操做。atomic是利用CAS来实现原子性操做的(Compare And Swap),CAS其实是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操做。

3.volatile能保证有序性吗?

  在前面提到volatile关键字能禁止指令重排序,因此volatile能在必定程度上保证有序性。

  volatile关键字禁止指令重排序有两层意思:

  1)当程序执行到volatile变量的读操做或者写操做时,在其前面的操做的更改确定所有已经进行,且结果已经对后面的操做可见;在其后面的操做确定尚未进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

  可能上面说的比较绕,举个简单的例子:

1
2
3
4
5
6
7
8
//x、y为非volatile变量
//flag为volatile变量
 
x =  2 ;         //语句1
y =  0 ;         //语句2
flag =  true ;   //语句3
x =  4 ;          //语句4
y = - 1 ;        //语句5

   因为flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句一、语句2前面,也不会讲语句3放到语句四、语句5后面。可是要注意语句1和语句2的顺序、语句4和语句5的顺序是不做任何保证的。

  而且volatile关键字能保证,执行到语句3时,语句1和语句2一定是执行完毕了的,且语句1和语句2的执行结果对语句三、语句四、语句5是可见的。

  那么咱们回到前面举的一个例子:

1
2
3
4
5
6
7
8
9
//线程1:
context = loadContext();    //语句1
inited =  true ;              //语句2
 
//线程2:
while (!inited ){
   sleep()
}
doSomethingwithconfig(context);

   前面举这个例子的时候,提到有可能语句2会在语句1以前执行,那么久可能致使context还没被初始化,而线程2中就使用未初始化的context去进行操做,致使程序出错。

  这里若是用volatile关键字对inited变量进行修饰,就不会出现这种问题了,由于当执行到语句2时,一定能保证context已经初始化完毕。

4.volatile的原理和实现机制

  前面讲述了源于volatile关键字的一些使用,下面咱们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

  下面这段话摘自《深刻理解Java虚拟机》:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;

  2)它会强制将对缓存的修改操做当即写入主存;

  3)若是是写操做,它会致使其余CPU中对应的缓存行无效。

 

使用volatile关键字的场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些状况下性能要优于synchronized,可是要注意volatile关键字是没法替代synchronized关键字的,

由于volatile关键字没法保证操做的原子性。一般来讲,使用volatile必须具有如下2个条件:

1)对变量的写操做不依赖于当前值

2)该变量没有包含在具备其余变量的不变式中

实际上,这些条件代表,能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,个人理解就是上面的2个条件须要保证操做是原子性操做,才能保证使用volatile关键字的程序在并发时可以正确执行。

 

Java中使用volatile的几个场景。

1. 状态标记量

2. double check

具体参见:http://www.cnblogs.com/dolphin0520/p/3920373.html

 

 

 

CAS (Central Authentication Service)  单点登陆

CAS (Compare And Swap)