多线程--已废弃

一.进程和线程区别

     windows是多任务的,一般一个一个任务是一个线程

     1.进程是资源分配的最小单位,线程是程序执行的最小单位

     2.进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据

        表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,

        使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个

        线程的开销也比进程要小很多。

     3.线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之

        间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序

        的难点。

     4.但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个

        进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

     5.一个进程至少有一个线程,如果只有一个线程就是单线程的,否则为多线程。

二.Lock和Sychronized()

    1.一些需要了解基础知识

     (1)volatile

              volatile能保证线程的可见性、有序性但是不能保证原子性。

              下面只是笔记,详细版的原文在:

               http://www.importnew.com/18126.html

             (a)原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因

                                     素打断,要么就都不执行。举个最简单的例子,大家想一下假如为一

                                     个32位的变量赋值过程不具备原子性的话,会发生什么后果?例如:

                                      i = 9; 假若一个线程执行到这个语句时,我暂且假设为一个32位的变量

                                      赋值包括两个过程:为低16位赋值,为高16位赋值。那么就可能发生

                                      一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个
                                      线程去读取i的值,那么读取到的就是错误的数据。

             (b)可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量

                                     的值,其他线程能够立即看得到修改的。举个简单的例子,看下面这

                                     段代码:
 

 //线程1执行的代码
 int i = 0;
 i = 10;
 
 //线程2执行的代码
 j = i

             (c)有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下

                                    面这段代码:

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

                                           上面代码定义了一个int型变量,定义了一个boolean类型变量,然

                                    分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前

                                    面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句

                                    2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序

                                  (Instruction Reorder)。 

                                           下面解释一下什么是指令重排序,一般来说,处理器为了提高程

                                    序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句

                                    的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结

                                    果和代码顺序执行的结果是一致的。

                                           比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并

                                   没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。 

                                           但是要注意,虽然处理器会对指令进行重排序,但是它会保证程

                                   序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再

                                   看下面一个例子: 

int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4

     (2)CAS

              众所周知,Java是多线程的。但是,Java对多线程的支持其实是一把双刃剑。一旦

              涉及到多个线程操作共享资源的情况时,处理不好就可能产生线程安全问题。线程

              安全性可能是非常复杂的,在没有充足的同步的情况下,多个线程中的操作执行顺

              序是不可预测的。Java里面进行多线程通信的主要方式就是共享内存的方式,共享

              内存主要的关注点有两个:可见性和有序性。加上复合操作的原子性,我们可以认

              为Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性。Java内存

              模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。这里不再

              详细介绍JMM及锁的其他相关知识。但是我们要讨论一个问题,那就是锁到底是不

              是有利无弊的?

              悲观锁:Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用

                             一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共

                             享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲

                             观锁,所以可以说synchronized是悲观锁。

                             悲观锁的问题:

                                    * 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度

                                       延时,引起性能问题。

                                    * 一个线程持有锁会导致其它所有需要此锁的线程挂起。 

                                    * 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优

                                      先级倒置,引起性能风险。

                             而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假

                             设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

                             与锁相比,volatile变量是一个更轻量级的同步机制,因为在使用这些变量

                             时不会发生上下文切换和线程调度等操作,但是volatile不能解决原子性问

                             题,因此当一个变量依赖旧值时就不能使用volatile变量。因此对于同步最

                             终还是要回到锁机制上来。

              乐观锁:乐观锁( Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁

                             假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,

                             才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错

                             误的信息,让用户决定如何去做。上面提到的乐观锁的概念中其实已经阐

                             述了他的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实

                             现方式有一种比较典型的就是Compare and Swap(CAS)。

                             下面进入正题开始讲解CAS

                            (a)CAS介绍

                                     需要掌握:

                                            * CAS的算法

                                     CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,

                                     要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修

                                     改为B,否则什么都不做。

                                     执行过程:通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行

                                                       多步计算来获得新值 B,然后将 V 的值从 A改为 B。如

                                                       果 V 处的值尚未同时更改,则 CAS 操作成功。

                                    为了更清晰你理解,举例说明过程:

                                     

                              

                                    如图所示,假设t1和t2线程都同时去更改同一值为56的变量,所以他

                                    们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2

                                    线程的预期值都为56。假设t1在与t2线程竞争中线程t1能去更新变量

                                    的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知

                                    这次竞争中失败,并可以再次发起尝试)。t1线程去更新变量值改为

                                    57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值

                                    56不一致,就操作失败了。(以上文字摘自百度百科和

                                    https://blog.csdn.net/moakun/article/details/80144900

                            (b)CAS有个经典的ABA问题

                                     具体是啥,此处略,闲的蛋疼的时候在去了解     

                                     解决方案:在CAS操作时,带上版本号,没修改一次,版本号+1,不

                                     但比较对象是否相等,还要比较版本号是否一致。 

     (3)AQS

            (a)简介

                     需要掌握:

                     *  知道AQS 是volatile int state+CHL队列

                     *  CLH是一个双向队列

                     AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置

                    为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源

                    被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制

                    AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。CLH

                    (Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即

                    不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源

                    的线程封装成一个CLH锁队列的一个结点(Node)原理图如下:

                   

                    AQS,它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队

                    列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具

                    体volatile的语义,在此不述。state的访问方式有三种:

                    getState()

                    setState()

                    compareAndSetState()

                    AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如

                    ReentrantLock)和Share(共享,多个线程可同时执行,如

                    Semaphore/CountDownLatch)。

            (b)AQS方法介绍

                     不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时

                     只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列

                     的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

                     自定义同步器实现时主要实现以下几种方法:

                     * isHeldExclusively():该线程是否正在独占资源。只有用到condition才需

                                                         要去实现它。

                     * tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返

                                                 回false。

                     * tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返

                                                  回false。

                     * tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表

                                                             示成功,但没有剩余可用资源;正数表示成功,

                                                             且有剩余资源。

                     * tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,

                                                              失败则返回false。

            (c)Java中关于AQS的介绍

                     以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,

                    会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时

                    就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有

                    机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的

                  (state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多

                    么次,这样才能保证state是能回到零态的。再以CountDownLatch以例,任

                    务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。

                    这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state

                    会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),

                    会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余

                    动作。一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也

                    只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的

                    一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如

                    ReentrantReadWriteLock。  

     (4)可重入锁和不可重入锁

              synchronized 和   ReentrantLock 都是可重入锁

            (a)不可重入锁

                     所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方

                     法中尝试再次获取锁时,就会获取不到被阻塞。我们尝试设计一个不可重

                     入锁:  

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){    
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}

                     使用该锁:

public class Count{
    Lock lock = new Lock();
    public void print(){
        lock.lock();
        doAdd();
        lock.unlock();
    }
    public void doAdd(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

                     当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执

                     行doAdd()中的逻辑,必须先释放锁。这时候产生了死锁。

            (b)可重入锁

                     指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获

                     取本对象上的锁,而其他的线程是不可以的。

                     我们设计一种可重入锁:

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
            throws InterruptedException{
        Thread thread = Thread.currentThread();
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

                      我们设计两个线程调用print()方法,第一个线程调用print()方法获取锁,

                      进入lock()方法,由于初始lockedBy是null,所以不会进入while而挂起当

                      前线程,而是是增量lockedCount并记录lockBy为第一个线程。接着第一

                      个线程进入doAdd()方法,由于同一线程,所以不会进入while而挂起,接

                      着增量lockedCount,当第二个线程尝试lock,由于isLocked=true,所以他

                      会获取该锁,直到第一个线程调用两次unlock()将lockCount递减为0,才

                      将标记为isLocked设置为false。

    2.sychronized()修饰静态方法和普通方法的区别?

       synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关

       系;修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。

    3.Lock和Sychronized()

     (1)Lock和Sychronized的区别

              简单的说区别是三点:等待可中断、可实现公平锁、锁可以绑定多个条件。

            (a)等待可中断

                     sychronized,是java内置的关键字,如果线程A获取了锁,线程B只能无限

                     的等待下去,自动释放线程锁;Lock,是一个接口,如果线程A获取了锁,

                     线程B可以选择放弃等待,需要手动调用unlock();方法释放线程锁。

            (b)可实现公平锁

                       sychronized锁是公平的;Lock的锁可以是公平的也可以是非公平的

                    (ReentrantLock默认情况下是非公平的)

            (c)可以绑定多个条件

                     面试时候尽量不要答。

     (2)Lock

            (a)接口方法介绍

                     其接口方法如下: 

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

                   (i)lock():用来获取锁。如果锁已被其他线程获取,则进行等待,需要手

                                       动调用unlock()方法释放锁。示例如下:

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

                        (ii)tryLock():尝试获取锁,如果获取成功,则返回true,如果获取失

                                                   败(即锁已被其他线程获取),则返回false。示例代码

                                                   如下:

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

                             (iii)tryLock(long time, TimeUnit unit)

                                                   该方法和tryLock()方法是类似的,区别在于这个方法在

                                                   拿不到锁时会等待一定时间,如果在时间期限之内拿不

                                                   到锁,就返回false。如果一开始拿到锁或在等待期间内

                                                   拿到了锁,则返回true。

                             (iv)lockInterruptibly()

                                             如果获取了锁定立即返回,如果没有获取锁定,当前线程处

                                             于休眠状态,直到或者锁定,或者当前线程被别的线程中断;

                                             由于lockInterruptibly()的声明中抛出了异常,所以

                                             lock.lockInterruptibly()必须放在try块中或者在调用

                                             lockInterruptibly()的方法外声明抛出InterruptedException。

                                             示例代码如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

                                 注意:当一个线程获取了锁之后,是不会被interrupt()方法中断的。因

                                            为interrupt()方法不能中断正在运行过程中的线程,只能中断阻

                                            塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁

                                            时,如果不能获取到,只有进行等待的情况下,是可以响应中断

                                            的。而用synchronized修饰的话,当一个线程处于等待某个锁的

                                            状态,是无法被中断的,只有一直等待下去。

            (b)ReenTrantLock

                     需要掌握:

                            ReenTrantLock的实现原理

                   (i)特性

                           ReenTrantLock是Lock接口的实现类,且是唯一的实现类。

                           * ReenTrantLock可以指定是公平锁还是非公平锁,默认是非公平锁。而

                             synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得

                             锁。

                             公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;

                             非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。

                           * ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要

                             唤醒的线们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部

                             线程

                           * ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过

                             lock.lockInterruptibly()来实现这个机制。

                   (ii)实现原理,参考

                            https://www.cnblogs.com/xrq730/p/4979021.html

                            简单的说,比如有线程1和线程2,线程1用CAS方法获取state,假设成功,

                            那么state由0改为1;这时,线程2在用CAS方法获取state,这时线程2被阻

                            塞。lock()方法执行的是如下逻辑:

                            ReentrantLock根据传入构造方法的布尔型参数实例化出Sync的实现类

                            FairSync和NonfairSync,分别表示公平的Sync和非公平的Sync。由于

                            ReentrantLock我们用的比较多的是非公平锁,所以看下非公平锁是如何实

                            现的。假设线程1调用了ReentrantLock的lock()方法,那么线程1将会独占锁,

                            整个调用链十分简单:

                           

                           其实lock()方法就是两步骤:

                                  * 设置AbstractQueuedSynchronizer的state为1

                                  * 设置AbstractOwnableSynchronizer的thread为当前线程

                           ReentrantLock的基本实现可以概括为:

                           先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入CLH队列

                           并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后

                           CAS再次尝试获取锁。在这个时候,如果:

                                  非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这

                                                   个线程抢先获取。

                                  公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在

                                                队首的话,就会排到队尾,由队首的线程获取到锁。

                           会先调用上面的方法。如果状态为0,则表明此时无人占有锁。此时尝试进行

                           set,一旦成功,则成功占有锁。如果状态不为0,再判断是否是当前线程获取

                           到锁。如果是的话,将状态+1,因为此时就是当前线程,所以不用CAS。这

                           也就是可重入锁的实现原理。

     (3)sychronized

              需要掌握:

                     *  JDK1.6对Sychronized做的优化

                     *  轻量级锁的实现细节 

                     *  轻量级锁如何膨胀为重量级锁?可逆吗? 

                             线程切换本身是比较消耗资源的,它需要操作系统帮忙,所以就需要重用户态

                     转换到内核态,状态的转换会消耗很多资源。JDK1.6对sychronized的优化思路就

                     是使其不进行状态转化,使用锁技术实现线程的同步。

            (a)原理

                   (i)修饰代码块

                           同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中

                           monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步

                           代码块的结束位置。这两个指令都需要一个reference类型的参数来指明要

                           锁定和解锁的对象,如果Java程序中的synchronized明确指定了对象参数,

                           那就是这个对象的reference;如果没有明确指定,那就根据sychronized

                           修饰的是实例方法还是类方法,去取对应的对象实例或是Class对象来作为

                           锁对象。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试

                           获取对象的锁,如果这个对象没有被锁定或者当前线程已经拥有了那个对

                           象的锁,把锁的计数器加1。相应的,在执行monitorexit指令时,会将锁计

                           数器减1,当计数器为0的时,锁就被释放。如果获取对象锁失败,那当前

                           线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

                   (ii)修饰方法

                            同步方法是通过方法常量池中的方法表关键字来控制, 如果用

                            synchronized修改方法,那么常量池里面会有ACC_SYNCHRONIZEDR

                            进行修饰。当线程访问加了synchronized的方法时, 会去判断是否有

                            ACC_SYNCHRONIZED标识。

                            * 如果有该标识, 当前线程会尝试获取该对象的monitor所有权获取所有

                              权的依据是monitor的计数器值是否为0, 为0则获取到了,同时将计数

                              器值 +1

                            * 如果已获取到monitor所有权, 则直接将计数器 +1并进入运行指令阶段

                            * 如果没有尝试获取该对象的monitor持有权,会进入等待队列进行阻塞

            (b)Jdk1.6对sychronized做的优化

                   (i)自旋锁

                           * 为了理解自旋锁,那么先了解非自旋锁的弊端:

                             非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获

                             取到锁的时候需要从内核态恢复,需要线程上下文切换。

                           * 什么是自旋锁?

                             是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该

                             线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到

                             锁才会退出循环。与传统的互斥锁相比,自旋锁中申请锁的线程一直处

                             于激活状态,而互斥锁申请锁的资源处于阻塞状态。自旋默认时10次,

                             JDK1.42中就已引入,需要手动开启,JDK1.6中默认开启。

                           * 自旋锁的优点:

                              自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是

                              active的;不会使线程进入阻塞状态,减少了不必要的上下文切换(

                              这里所谓的上下文切换即线程切换),执行速度快。

                           * 自旋锁的缺点

                              如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入

                              循环等待,白白消耗CPU;

                              自旋锁不是公平的,不公平的锁就会存在“线程饥饿”问题。

                   (ii)自适应自旋锁

                            该锁是为了解决自旋锁的缺点“如果某个线程持有锁的时间过长,就会导

                            致其它等待获取锁的线程进入循环等待,白白消耗CPU”而产生的。

                            自适应一位着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋

                            时间及锁的拥有者的状态来决定。

                                   如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程

                            正在运行中,那么虚拟机会认为这次自旋也很有可能再次成功,进而它将允许

                            自旋等待持续相对更长的时间,比如100个循环。另外对于某个锁,自旋很少

                            成功获得过,那在以后要获取这个锁时,将肯能省略掉自旋过程,以避免浪费

                            处理器的资源。

                   (iii)锁消除

                                    锁消除时指虚拟机即时编译器运行时,对一些代码上要求同步,但是被

                             检测到不可能存在共享数据竞争的锁进行消除。

                                    锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码

                             中,堆上的所有数据都不会逃逸出去重而被其它线程访问到,那就可以把它们

                             当做栈上的数据对待,认为它们是线程私有的,同步加锁就无需进行。

                                    变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员

                             自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?

                             答案是有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中

                             的普遍程度也许超过了大部分读者的想象。我们来看看代码清单13-6中的例

                             子,这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面

                             上还是程序语义上都没有同步。

                             代码清单13-6 一段看起来没有同步的代码:

public String concatString(String s1,String s2,String s3){
    return s1+s2+s3;
}

                             我们也知道,由于String是一个不可变的类,对字符串的连接操作总是通过生

                             成新的String对象来进行的,因此Javac编译器会对String连接做自动优化。

                             在JDK 1.5之前,会转化为StringBuffer对象的连续append()操作,在

                             JDK 1.5及以后的版本中,会转化为StringBuilder对象的连续append()操作,

                             即代码清单13-6中的代码可能会变成代码清单13-7的样子。

                             代码清单13-7 Javac转化后的字符串连接操作:

public String concatString(String s1,String s2,String s3){
    StringBuffer sb=new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}

                             现在大家还认为这段代码没有涉及同步吗?每个StringBuffer.append()方

                             法中都有一个同步块,锁就是sb对象。虚拟机观察变量sb,很快就会发现

                             它的动态作用域被限制在concatString()方法内部。也就是说,sb的所有

                             引用永远不会“逃逸”到concatString()方法之外,其他线程无法访问到它,

                             因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代

                             码就会忽略掉所有的同步而直接执行了。客观地说,既然谈到锁消除与逃逸

                             分析,那虚拟机就不可能是JDK 1.5之前的版本,实际上会转化为非线程安

                             全的StringBuilder来完成字符串拼接,并不会加锁,但这也不影响笔者用这

                             个例子证明Java对象中同步的普遍性。

                   (iv)锁粗化

                             原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制的尽

                             可能的小,这样是为了使得需要同步的操作数量尽可能的变小,如果存在

                             竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则是正

                             确的,但是如果一系列的连续操作都对同一对象反复的加锁和解锁,那即

                             使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的新能损耗。

                             例如下面代码中的连续append()就属于这种情况:

	public String concatString(String s1,String s2,String s3)
	{
		StringBuffer sb=new StringBuffer();
		sb.append(s1);
		sb.append(s2);
		sb.append(s3);
	}

                             如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加

                             锁同步的范围扩展(粗化)到整个操作序列的外部,按上例,会扩展到第

                             一个append()操作之前直至最后一个append()操作之后,这样只需要加锁

                             一次就可以了。

                   (v)轻量级锁

                             概念

                                    相对于重量级锁而言的。重量级锁,就是使用操作系统互斥量来实现

                             的锁。轻量级锁并不是用来代替重量级锁的,它的目标是减少无实际竞争

                             情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用

                             户态切换、线程阻塞造成的线程切换等。在有两个以上的线程争用同一把

                             锁时,轻量级锁不在有效,它将膨胀为重量级锁。

                             缺点

                                     如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量

                                     级锁的过程就成了浪费。

                             实现原理

                             轻量级锁的实现依靠对象头的Mark Word。那么现在先了解一下HotSpot

                             虚拟机的内存布局。

                                    HotSpot虚拟机的对象头(Object Header)分为两部分信息,第一部

                             分用于存储对象自身运行时的数据,如哈希码(HashCode)、GC分代年

                             龄(Generation GC Age)等,这部分数据的长度在32位和64位虚拟机中

                             分别位32bit和64bit,官方称它为“Mark Word”,它是实现轻量级锁和偏向

                             锁的关键;另外一部分用于存储指向方法区对象类型的数据指针,如果是

                             数组对象的话,如果是数组对象的话,还有一个

                             额外的部分用于存储数组的长度。

                                   HotSpot虚拟机对象头的Mark Word如下:

存储内容标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标记

偏向线程ID、偏向时间戳、

对象分代年龄

01可偏向

                                   各种锁对应的Mark Word存储内容如下:

                                   简单介绍了对象的内存布局之后,我们把话题转到轻量级锁的执行过程

                             上,在代码进入同步块的时候,如果此同步对象没有被锁定(锁标识位为01

                             状态),虚拟机会在当前线程的栈帧中建立一个名为“锁记录(Lock Record

                             )”的空间,用于存储锁对象的Mark Word的拷贝(官方把这一拷贝加了

                             Displaced前缀,即Displaced Mark Word),这时候线程堆中与对象头的状态

                             如下图所示:

                    

                             然后虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record

                             的指向,如果这个更新成功了,那么这个线程就拥有了对象的锁,并且对象

                             的Mark Word的锁标志位(Mark Word的最后两位)将变为 “00”,即表示此对

                             象处于轻量级锁定状态这时候线程堆栈与对象头的状态如下图所示:

                              

                             如果这个更新操作失败了,虚拟机首先会检查对对象的Mark Word是否指向

                             当前线程的栈帧,如果是说明当前线程已经拥有了这个对象的锁,那就可以

                             直接进入同步块继续执行。否则说明这个锁的对象已经被其它线程抢占了。

                             如果有两条以上的线程争用同一个锁,那么轻量级锁不在有效,要膨胀为重

                             量级锁。  

                             关于轻量级锁是如何膨胀为重量级锁的:

                                    现在来假设一个场景:当获取到锁的线程执行同步体之内的代码的时

                             候,另一个线程也完成了上面创建锁记录空间,将对象头中的MarkWord

                             复制到自己的锁记录中,尝试用CAS将对象头中的MarkWord修改为指向

                             自己的锁记录的指针,但是由于之前获取到锁的线程已经将MarkWord中

                             的记录修改过了(并且现在还在执行同步体中的代码),与这个现在试图

                             将MarkWord替换为自己的锁记录的线程自己的锁记录中的MarkWord的值

                             不符,CAS操作失败,因此这个线程就会不停地循环使用CAS操作试图将

                             MarkWord替换为自己的记录。这个循环是有次数限制的,如果在循环结

                             束之前CAS操作成功,那么该线程就可以成功获取到锁,如果循环结束之

                             后依然获取不到锁,则锁获取失败,MarkWord中的记录会被修改为指向

                             重量级锁的指针,然后这个获取锁失败的线程就会被挂起,阻塞了。当持

                             有锁的那个线程执行完同步体之后想用CAS操作将MarkWord中的记录改

                             回它自己的栈中最开始复制的记录的时候会发现MarkWord已被修改为指

                             向重量级锁的指针,因此CAS操作失败,该线程会释放锁并唤起阻塞等待

                             的线程,开始新一轮夺锁之争,而此时,轻量级锁已经膨胀为重量级锁,

                             所有竞争失败的线程都会阻塞,而不是自旋。轻量级锁一旦膨胀为重量级

                             锁,则不可逆转。因为轻量级锁状态下,自旋是会消耗cpu的,但是锁一

                             旦膨胀,说明竞争激烈,大量线程都做无谓的自旋对cpu是一个极大的浪

                             费。

                             解锁:

                                    轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替

                             换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前

                             锁存在竞争,锁就会膨胀成重量级锁。                           

                   (vi)偏向锁

                            概念

                            偏向锁的“偏”就是偏心、偏袒的“偏”,它的意思是这个锁会偏向于第一个获取

                            它的线程,如果在接下来的过程中,该锁没有被其它的线程获取,则持有锁

                            的线程则永远不需要在执行同步。

                            为什么要使用偏向锁

                            如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维

                            护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用

                            锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都

                            至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

                            实现原理

                            在Mark Word中CAS记录owner。

                            当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为

                            “01”,即偏向模式,同时使用CAS操作把获取到这个锁的线程ID记录在对象

                            的Mark Word中,如果CAS操作成功,持有偏向锁的线程以后进入这个锁相

                            关的同步块时,虚拟机都可以不在进行任何同步操作。

                            偏向锁的升级

                            何时升级?

                            当一个线程获取了偏向锁,当另外一个线程去尝试获取这恶锁时,偏向模式

                            就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未

                            锁定或轻量级锁定的状态。

                            下面举例说明偏向锁升级的过程:、

                            * 假设线程1当前拥有偏向锁对象,线程2是需要竞争到偏向锁

                            * 线程2来竞争锁对象;

                            * 判断当前对象头是否是偏向锁;

                            * 判断拥有偏向锁的线程1是否还存在;

                            * 线程1不存在,直接设置偏向锁标识为0(线程1执行完毕后,不会主动去释放偏

                              向锁);

                            * 使用CAS替换偏向锁线程ID为线程2,锁不升级,仍为偏向锁;

                            * 线程1仍然存在,暂停线程1;

                            * 设置锁标志位为00(变为轻量级锁),偏向锁为0;

                            * 从线程1的空闲monitor record中读取一条,放至线程1的当前

                              monitor record中;

                            * 更新mark word,将mark word指向线程1中monitor record的指针;

                            * 继续执行线程1的代码;

                            * 锁升级为轻量级锁;

                            * 线程2自旋来获取锁对象;

                            关于轻量级锁和偏向锁详细请看: 

                            https://blog.csdn.net/truong/article/details/74942155

    5.可重入锁和不可重入锁

       synchronized 和   ReentrantLock 都是可重入锁。

     (1)不可重入锁

              所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再

              次获取锁时,就会获取不到被阻塞。我们尝试设计一个不可重入锁:    

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){    
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}

             使用该锁:

public class Count{
    Lock lock = new Lock();
    public void print(){
        lock.lock();
        doAdd();
        lock.unlock();
    }
    public void doAdd(){
        lock.lock();
        //do something
        lock.unlock();
    }
}

             当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执行doAdd()

             中的逻辑,必须先释放锁。这时候产生了死锁。

     (2)可重入锁

              指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上

              的锁,而其他的线程是不可以的。

              我们设计一种可重入锁:

public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
            throws InterruptedException{
        Thread thread = Thread.currentThread();
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

              我们设计两个线程调用print()方法,第一个线程调用print()方法获取锁,进入lock()方

              法,由于初始lockedBy是null,所以不会进入while而挂起当前线程,而是是增量

              lockedCount并记录lockBy为第一个线程。接着第一个线程进入doAdd()方法,由于同

              一线程,所以不会进入while而挂起,接着增量lockedCount,当第二个线程尝试lock,

              由于isLocked=true,所以他不会获取该锁,直到第一个线程调用两次unlock()将

              lockCount递减为0,才将标记为isLocked设置为false。

三.线程中断

     1.thread.interrupt()概述

       interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际

      完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。

      更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时

      调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该

      线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,

      这时调用interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出

      InterruptedException。

     2.各种情况interrupt()

     (1)sleep() &interrupt()

              线程A正在使用sleep()暂停着: Thread.sleep(100000),如果要取消它的等待状态,

              可以在正在执行的线程里(比如这里是B)调用a.interrupt()[a是线程A对应到的

              Thread实例],令线程A放弃睡眠操作。即,在线程B中执行a.interrupt(),处于阻

              塞中的线程a将放弃睡眠操作。当在sleep中时线程被调用interrupt()时,就马上会

              放弃暂停的状态并抛出InterruptedException。抛出异常的,是A线程。

     (2)wait() &interrupt()

              程A调用了wait()进入了等待状态,也可以用interrupt()取消。不过这时候要注意锁

              定的问题。线程在进入等待区,会把锁定解除,当对等待中的线程调用interrupt()时,

              会先重新获取锁定,再抛出异常。在获取锁定之前,是无法抛出异常的。

     (3)join() &interrupt()

              当线程以join()等待其他线程结束时,当它被调用interrupt(),它与sleep()时一样,

              会马上跳到catch块里.。

四.run()、start()、wait()、join()

     1.run()和start()区别:

      (1)start():真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继

                            续执行下面的代码。

      (2)run():作为普通方法的方式调用,程序还是要顺序执行,并不能多线程执行还是

                          要等待run方法体执行完。

     2.sleep和wait区别

       (1)sleep(): 在调用sleep()方法的过程中,线程不会释放对象锁,就像当前线程仍

                                然在执行一样。

                                补充,关于sleep用法的说明:

class ThreadA extends Thread  
{  
    public void run(){  
        System.out.println("ThreadA is running");  
    }  
}  
  
public class TestNew {  
    public static void main(String[] args)throws InterruptedException {  
        // TODO Auto-generated method stub  
        ThreadA ta = new ThreadA();  
        ta.start();  
        ta.sleep(5000);  
        System.out.println("TestNew is running");  
    }  
} 

                                其实这段语句是主线程睡眠5秒,而不是ta线程,sleep

                                声明在哪个线程里哪个线程睡眠

       (2)wait(): 当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁

                              定池,只有针对此对象 调用notify()方法后本线程才进入对象锁定池准备。

    3.join()

       thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行

       的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续

       执行线程B。join方法必须在放在start()方法调用之后,否则不生效

五.java间线程通信的方式

    1.wait,notify,notifyAll

    2.管道通信。java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信

六.Callable和Future

   使线程具有返回值的功能。Callable用于拿到结果,Future用于返回结果。

   直接上例子:

   eg1:非线程池的方式返回一个int类型的数据

public class CallableAndFuture {

     public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                return 10;
            }
        };
        FutureTask<Integer> future = new FutureTask<Integer>(callable);
        new Thread(future).start();
        try {
            Thread.sleep(5000);// 可能做一些事情
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

   打印结果:10

   eg2:非线程池的方式返回一个String类型的数据。

public class CallableAndFuture {


    public static void main(String[] args) {
        Callable callable = new Callable() {
            public String call() throws Exception {
                return "aaa";
            }
        };
        FutureTask<String> future = new FutureTask<String>(callable);
        new Thread(future).start();
        try {
            Thread.sleep(5000);// 可能做一些事情
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

   打印结果:aaa

   eg3:线程池的方式

public class TaskCallable implements Callable<String>{


private int id;
    public TaskCallable(int id){
        this.id = id;
    }
    public String call() throws Exception {
        
        return "result of taskWithResult "+id;
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException
    {
    ExecutorService exec = Executors.newCachedThreadPool();//工头
    ArrayList<Future<String>> results = new ArrayList<Future<String>>();//
    for(int i = 0 ; i < 10 ;i++){
//     Future future=exec.submit(new TaskCallable(i));
//     future.get();
        results.add(exec.submit(new TaskCallable(i)));//submit返回一个Future,代表了即将要返回的结果
    }
    for(int i=0;i<10;i++)
    {
    System.out.println(results.get(i).get());
    }
    }
}

   打印结果:

   result of taskWithResult 0
   result of taskWithResult 1
   result of taskWithResult 2
   result of taskWithResult 3
   result of taskWithResult 4
   result of taskWithResult 5
   result of taskWithResult 6
   result of taskWithResult 7
   result of taskWithResult 8
   result of taskWithResult 9

七.线程池

    1.为什么要是使用线程池

        因为创建和销毁线程的开销过大,所以使用线程池来解决这个问题。

    2.java中的四种线程池

       需要掌握:

              *  线程池的参数

              *  线程池各参数的意义

              *  线程池的拒绝策略

              *  线程池的执行流程

     (1)newCachedThreadPool创建一个可缓存的线程池。如果线程池的大小超过了处理任

              务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增

              加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做

              限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

     (2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在

              队列中等待。

     (3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

              其设计思想是:每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并

              发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,

              ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询

              任务的状态。看个例子:

               

				 
 package com.ibm.scheduler;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorTest implements Runnable {
	private String jobName = "";

	public ScheduledExecutorTest(String jobName) {
		super();
		this.jobName = jobName;
	}

	@Override
	public void run() {
		System.out.println("execute " + jobName);
	}

	public static void main(String[] args) {
		ScheduledExecutorService service = Executors.newScheduledThreadPool(10);

		long initialDelay1 = 1;
		long period1 = 1;
        // 从现在开始1秒钟之后,每隔1秒钟执行一次job1
		service.scheduleAtFixedRate(
		        new ScheduledExecutorTest("job1"), initialDelay1,
				period1, TimeUnit.SECONDS);

		long initialDelay2 = 1;
		long delay2 = 1;
        // 从现在开始2秒钟之后,每隔2秒钟执行一次job2
		service.scheduleWithFixedDelay(
		        new ScheduledExecutorTest("job2"), initialDelay2,
				delay2, TimeUnit.SECONDS);
	}
}
Output:
execute job1
execute job1
execute job2
execute job1
execute job1
execute job2

                    上例中展示了展示了 ScheduledExecutorService 中两种最常用的调度方法

              ScheduleAtFixedRate和ScheduleWithFixedDelay。ScheduleAtFixedRate 每次执行时

              间为上一次任务开始起向后推一个时间间隔,即每次执行时间为 :initialDelay,

              initialDelay+period, initialDelay+2*period, …;ScheduleWithFixedDelay 每次执行时间

              为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialDelay,

              initialDelay+executeTime+delay, initialDelay+2*executeTime+2*delay。

              由此可见,ScheduleAtFixedRate 是基于固定时间间隔进行任务调度,

              ScheduleWithFixedDelay 取决于每次任务执行的时间长短,是基于不固定时间间隔进行

              任务调度。

     (4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执

               行任务, 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

     3.线程池参数

          corePoolSize:线程池的基本大小,即在没有任务需要执行的时候线程池的大小,并且只

                                   有在工作队列满了的情况下才会创建超出这个数量的线程。这里需要注意

                                   的是:在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而

                                   是要等到有任务提交时才会启动,除非调用了

                                   prestartCoreThread/prestartAllCoreThreads事先启动核心线程。再考虑到

                                   keepAliveTime和allowCoreThreadTimeOut超时参数的影响,所以没有任

                                   务需要执行的时候,线程池的大小不一定是corePoolSize。

          maximumPoolSize:最大线程数。表明线程中最多能够创建的线程数量。

          keepAliveTime:空闲的线程保留的时间。

          TimeUnit:空闲线程的保留时间单位。

          poolSize:线程池中当前线程的数量,当该值为0的时候,意味着没有任何线程,线程池

                           会终止;同一时刻,poolSize不会超过maximumPoolSize。这是一个我重未见

                           过的参数,本来没打算记录,但是因为在“线程池的执行流程”中涉及到该参数,

                           所以暂时把它记录在这。

          BlockingQueue:任务队列。

                                      分为:直接提交的、有界的、无界的、优先任务队列。

                                    (a)直接提交的任务队列(SynchronousQueue)

                                             SynchronousQueue没有容量。提交的任务不会被真实的保存在

                                             队列中,而总是将新任务提交给线程执行。如果没有空闲的线

                                             程,则尝试创建新的线程。如果线程数大于最大值

                                             maximumPoolSize,则执行拒绝策略。

                                    (b)有界的任务队列(ArrayBlockingQueue)

                                              创建队列时,指定队列的最大容量。若有新的任务要执行,如果

                                              线程池中的线程数小于corePoolSize,则会优先创建新的线程。

                                              若大于corePoolSize,则会将新任务加入到等待队列中。若等待

                                              队列已满,此时判断线程池中总线程数是否小于maximumPoolSize,

                                              如果总线程数不大于线程数最大值maximumPoolSize,则创建新

                                              的线程执行任务。若大于maximumPoolSize,则执行拒绝策略。

                                    (c)无界的任务队列(LinkedBlockingQueue)

                                             与有界队列相比,除非系统资源耗尽,否则不存在任务入队失败

                                             的情况。若有新的任务要执行,如果线程池中的线程数小于

                                             corePoolSize,线程池会创建新的线程。若大于corePoolSize,此

                                             时又没有空闲的线程资源,则任务直接进入等待队列。当线程池中

                                             的线程数达到corePoolSize后,线程池不会创建新的线程。若任务

                                             创建和处理的速度差异很大,无界队列将保持快速增长,直到耗尽

                                             系统内存。使用无界队列将导致在所有corePoolSize线程都忙时,

                                             新任务在队列中等待。这样,创建的线程就不会超过corePoolSize

                                          (因此,maximumPoolSize 的值也就无效了)。当每个任务完全独

                                            立于其他任务,即任务执行互不影响时,适合于使用无界队列;例

                                            如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当

                                            命令以超过队列所能处理的平均数连续到达时,此策略允许无界线

                                            程具有增长的可能性。

                                    (d)优先任务队列(PriorityBlockingQueue)

                                             带有执行优先级的队列。是一个特殊的无界队列。 

                                             ArrayBlockingQueue和LinkedBlockingQueue都是按照先进先出算

                                            法来处理任务。而PriorityBlockingQueue可根据任务自身的优先级

                                            顺序先后执行(总是确保高优先级的任务先执行)。

    4.线程池的执行流程

     (1)如果线程池的当前大小还没有达到基本大小(poolSize < corePoolSize),那么就新增

               加一个线程处理新提交的任务。

     (2)如果当前大小已经达到了基本大小,就将新提交的任务提交到阻塞队列排队,等候处

               理workQueue.offer(command);

     (3)如果队列容量已达上限,并且当前大小poolSize没有达到maximumPoolSize,那么就

              新增线程来处理任务;

     (4)如果队列已满,并且当前线程数目也已经达到上限,那么意味着线程池的处理能力已

               经达到了极限,此时需要拒绝新增加的任务。至于如何拒绝处理新增的任务,取决于

               线程池的饱和策略RejectedExecutionHandler。

   5.线程池的拒绝策略

   (1)直接丢弃(DiscardPolicy)

   (2)丢弃队列中最老的任务(DiscardOldestPolicy)。

   (3)默认,直接抛异常(AbortPolicy)

   (4)将任务分给调用线程来执行(CallerRunsPolicy)。

   6.线程池的面试题

   (1)阿里的面试官问了个问题,如果corepollSize=10,MaxPollSize=20,如果来了25个线程

            怎么办。

            答:慢慢的启动到10,然后把剩下的15个放到阻塞队列里面,并开始在线

                   程池里面创建线程,直到最大MaximumPoolSize。

八.多线程中的一些考点

     1.ConcurrentHashMap

      (1)ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后

               给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的

               数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们

               可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,

               又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在

               ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,

               但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的

               保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

      (2)ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment

               是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,

               HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment

               数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里

               包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守

               护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首

               先获得它对应的Segment锁。其结构如下图:

                

     2.CountDownLatch

        需要掌握:

               CountDownLatch的实现原理

      (1)作用:当一个线程需要等待其他线程完成各自的工作后再执行时,使用

                          CountDownLatch。例如,应用程序的主线程希望在负责启动框架服务的线

                          程已经启动所有的框架服务之后再执行。

      (2)实现原理

               CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每

               当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,表示

               所有的线程已经完成了任务,然后等待的线程就可以恢复执行任务。

      (3)主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程

               的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。其他N 个线程必须

               引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的

               任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用

               一次这个方法,在造函数中初始化的count值就减1。所以当N个线程都调 用了这个

               方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。

     3.ThreadLocal

      (1)是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问,

               通常是类中的 private static 字段,其内存模型如下图:

               

        

               从上面的结构图,我们已经窥见ThreadLocal的核心机制:

                 * 每个Thread线程内部都有一个Map。

                 * Map里面存储线程本地对象(key)和线程的变量副本(value)

               综上,每个Thread对象内部都维护了一个ThreadLocalMap这样一个

               ThreadLocal的Map,可以存放若干个ThreadLocal。当我们在调用

               get()方法的时候,先获取当前线程,然后获取到当前线程的

               ThreadLocalMap对象,如果非空,那么取出ThreadLocal的value,

               否则进行初始化,初始化就是将initialValue的值set到ThreadLocal

               中。

      (2)ThreadLocal是以空间换取时间,效率比sychronized高

                关于ThreadLocal在举一个例子:

class ConnectionManager {
     
    private static Connection connect = null;
     
    public static Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public static void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}

                     上面的例子,在单线程中使用是没有任何问题的,但是在多线程中

              使用会有两个问题:第一,这里面的2个方法都没有进行同步,很可能

              在openConnection方法中会多次创建connect;第二,由于connect是

              共享变量,那么必然在调用connect的地方需要使用到同步来保障线程

              安全,因为很可能一个线程在使用connect进行数据库操作,而另外一

              个线程调用closeConnection关闭链接。

                     所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处

              理,并且在调用connect的地方需要进行同步处理。这样将会大大影响

              程序执行效率,因为一个线程在使用connect进行数据库操作的时候,

              其他线程只有等待。

                     首先分析一下,这地方到底需不需要将connect变量进行共享(即

              使用static)?事实上是不需要的。假如每个线程中都有一个connect变

              量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即

              一个线程不需要关心其他线程是否对这个connect进行了修改的。那么

              我们把代码改成如下:

class ConnectionManager {
     
    private  Connection connect = null;
     
    public Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}
 
 
class Dao{
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();
         
        //使用connection进行操作
         
        connectionManager.closeConnection();
    }

                     这样处理由于每次都是在方法内部创建的连接,那么线程之间自然

              不存在线程安全问题,但是这样会有一个致命的影响:导致服务器压力

              非常大,并且严重影响程序执行性能。这是因为在方法中需要频繁地开

              启和关闭数据库连接,这样不尽严重影响程序执行效率,还可能导致服

              务器压力巨大。那么在这种情况下我们就可以使用ThreadLocal。

      (3)ThreadLocal原理               

               get()方法源码:   

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();

               set()方法源码:      

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);

     4.volatile

        volatile能保证线程的可见性、有序性但是不能保证原子性。

        下面只是笔记,详细版的原文在:

         http://www.importnew.com/18126.html

      (1)为了理解volatile首先引入并发编程的3个概念

             (a)原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因

                                     素打断,要么就都不执行。举个最简单的例子,大家想一下假如为一

                                     个32位的变量赋值过程不具备原子性的话,会发生什么后果?例如:

                                     i = 9; 假若一个线程执行到这个语句时,我暂且假设为一个32位的变量

                                     赋值包括两个过程:为低16位赋值,为高16位赋值。那么就可能发生

                                     一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个
                                     线程去读取i的值,那么读取到的就是错误的数据。

             (b)可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量

                                     的值,其他线程能够立即看得到修改的。举个简单的例子,看下面这

                                     段代码:

                             

 //线程1执行的代码
 int i = 0;
 i = 10;
 
 //线程2执行的代码
 j = i

 

             (b)有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下

                      面这段代码: 

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

                              上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别

                       对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么

                       JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不

                       一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。 

                              下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行

                       效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺

                       序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的

                       结果是一致的。

                              比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影

                       响,那么就有可能在执行过程中,语句2先执行而语句1后执行。 

                              但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终

                       结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例

                       子:                                  

int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4

                       这段代码有4个语句,那么是否可能按下面这个顺序执行顺序呢?

                        语句2   语句1    语句4   语句3

                       不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果

                       一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证

                       Instruction 1会在Instruction 2之前执行。虽然重排序不会影响单个线程内程

                       序执行的结果,但是多线程呢?下面看一个例子:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

                              上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。

                      假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为

                      初始化工作已经完成,那么就会跳出while循环,去执行

                      doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会

                      导致程序出错。从上面可以看出,指令重排序不会影响单个线程的执行,但

                      是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,

                      必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能

                      会导致程序运行不正确。

     (2)volatile解析

              一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那

              么就具备了两层语义:

            (a)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变

                     量的值,这新值对其他线程来说是立即可见的。

            (b)禁止进行指令重排序。

                     禁止指令重排又包含两层意思:

                   (i)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更

                           改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作

                          肯定还没有进行;

                   (ii)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执

                            行,也不能把volatile变量后面的语句放到其前面执行。

             所以,volatile可以保证指令的可见性和有序性。

                      我们在回到前面的例子:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

                      前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么就可

                      能导致context还没被初始化,而线程2中就使用未初始化的context去进行

                      操作,导致程序出错。这里如果用volatile关键字对inited变量进行修饰,

                      就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经

                      初始化完毕。

     (3)volatile原理

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

              “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加

               入volatile关键字时,会多出一个lock前缀指令”lock前缀指令实际上相当于一个

               内存屏障(也称内存栅栏),内存屏障会提供3个功能:

            (a)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不

                     会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,

                     在它前面的操作已经全部完成;

            (b)它会强制将对缓存的修改操作立即写入主存;

            (c)如果是写操作,它会导致其他CPU中对应的缓存行无效

     (4)volatile使用场景

              synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执

              行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意

              volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操

              作的原子性。通常来说,使用volatile必须具备以下2个条件:

            (a)对变量的写操作不依赖于当前值

            (b)该变量没有包含在具有其他变量的不变式中

              实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序

              的状态,包括变量的当前状态。

              例1,状态标记量:

volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}

              例2,双重检锁:

                       首先写一个错误的双重检索模式:

public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();   // error
                }
            }
        }
        return uniqueSingleton;
    }
}

                           如果这样写,运行顺序就成了:

                         (i)检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回。

                         (ii)获取锁。

                         (iii)再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象。

                           执行双重检查是因为,如果多个线程同时通过了第一次检查,并且其中

                           一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次

                           检查的线程就不会再去实例化对象。这样,除了初始化的时候会出现加锁

                           的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问

                           题。

                           隐患:

                          上述写法看似解决了问题,但是有个很大的隐患。实例化对象的那行代码

                        (标记为error的那行),实际上可以分解成以下三个步骤:

                        (i)分配内存空间

                        (ii)初始化对象

                        (iii)将对象指向刚分配的内存空间

                          但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,

                          顺序就成了:

                        (i)分配内存空间

                        (ii)将对象指向刚分配的内存空间

                        (iii)初始化对象

                          现在考虑重排序后,两个线程发生了以下调用:

TimeThread AThread B
T1检查到uniqueSingleton为空 
T2获取锁 
T3再次检查到uniqueSingleton为空 
T4uniqueSingleton分配内存空间 
T5uniqueSingleton指向内存空间 
T6 检查到uniqueSingleton不为空
T7 访问uniqueSingleton(此时对象还未完成初始化)
T8初始化uniqueSingleton 

 

                       那么正确的双重检索模式如下:

public class Singleton {
    private volatile static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();
                }
            }
        }
        return uniqueSingleton;
    }
}

     10.线程的状态

          线程从创建到结束总共经过如下几个状态:

          新建、就绪、运行、阻塞及死亡。

     11.对象锁、方法锁、类锁

         https://blog.csdn.net/zhujiangtaotaise/article/details/55509939

         翻明白这个例子,它里面的所谓的对象锁其实就是方法锁,叫

         的名字比较郁闷

         强调一点,类锁对所有实例都起作用,比如双重检索模式的单例。

九.多线程自己学习的

     1.守护线程和非守护线程(有空了解一下守护线程的使用场景)

        非守护线程也叫用户线程

        非守护线程:非守护线程包括常规的用户线程,比如我们实现的一个线程(没有调

                              用thread.setDaemon(true)),就是非守护线程。

                              守护线程,Java虚拟机在它所有非守护线程已经离开后自动离开。

        守护线程:守护线程则是用来服务用户线程的,比如说GC线程。如果没有其他用

                          户线程在运行,那么就没有可服务对象,也就没有理由继续下去。

                          main()函数创建的线程也是守护线程

        如何创建守护线程:使用thread.setDaemon(true)方法,该方法必须在thread.start()

                                        之前设置,你不能把正在运行的常规线程设置为守护线程,否则

                                        会跑出一个IllegalThreadStateException异常,如果线程是守护

                                        线程,则isDaemon方法返回true。

十.线程数设置多少合适?

    和cpu核数之间的关系

    单核的cpu设置多线程也是合适的

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值