多线程基础复习小笔记

1. 场景困惑:

    在主线程中开启一个线程t1 , 那么我如何能够获取这个线程t1的执行状态:是否开始执行?是否blocked?是否running?是否runnable?是否执行完毕Dead?执行结果是什么?

    我以为 Future模式的能力能够令我有办法知道线程t1处于blocked状态,可是我看完FutureTask的源码后发现不是。仅仅只有以下的几种:

private static final int NEW          = 0;

private static final int COMPLETING  = 1;

private static final int NORMAL      = 2;

private static final int EXCEPTIONAL  = 3;

private static final int CANCELLED    = 4;

private static final int INTERRUPTING = 5;

private static final int INTERRUPTED  = 6;

FutureTask 是Runnable和Future的实现类。其中Runnable的run()是启动线程以及用来变更线程t1的上述状态的,Future的实现方法则是用于管理操作线程的:如取消\中断线程、查询是否执行完毕、以及存放该线程的返回值(Callable对象返回值)和状态 。

FutureTask实现的Runnable的run()方法中记录线程t1对应状态的逻辑机理如下:

    个人感慨:这种模式的确是棒棒滴!有以下优秀点:

1.  将业务逻辑 抽离出run方法、封装到第三方的类Callable的call()中,Runnale的run方法则用来变更当前线程的执行状态。

        2.  Future的各方法用于操纵查询线程的执行状态。

3.  Runnable和Future的结合实现(即FutureTask类),则刚好实现了对 线程的执行的管理。由于将业务逻辑封装到第三方Callable中,那么实现了 线程管理与业务逻辑的解耦!!即FutureTask类可以作为通用的线程状态管理模版!

      不过,由于FutureTask源码中的状态记录字段stat 是使用valotile修饰的 而且 线程的输出结果outcome 也是 一个普通private变量而已,所以这令我们只能 new 一个FutureTask 对应一个 线程......否则还是会出现 线程安全问题.....

更高的领悟:对于一个原始朴素或者固定约定俗成的模式,Future模式启发了我如何将之改造成一个模版:将原来业务逻辑的嵌入点,改为用第三方类来封装,从而从原来的模式中抽离出来。

以上领悟自:http://blog.csdn.net/bboyfeiyu/article/details/24851847

2.  一张图理解线程的各状态以及转换

a. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入Runnable状态,才有机会转到Running状态。阻塞的情况分三种:

(一)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

(二)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

(三)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入Runnable状态。(注意,sleep是不会释放持有的锁)

b.  处于 同步阻塞 (锁池)中的线程,拿到对象的锁标识后并不是立马执行的,而是进入Runnable状态。你会发现,所有的阻塞结束后,都得进入 Runnable状态等待OS调度。而不是立马就执行。

c.  一个线程如果通过执行了 o.wait()来释放锁后进入等待阻塞状态(等待池中),等候接收到o.notify()的通知,此线程就会等待其他线程释放对象锁(cpu会将之从 等待池 转移到 锁池中)并且和其他同在锁池中的线程一起竞争对象锁。终于,一阵搏杀,它获取到锁了,于是就进入了Runnable状态等等CPU的调度。

d.  一个线程如果通过执行了 o.wait()来释放锁以等待,那么就会一共经历两次阻塞才能去到Runnable状态:等待阻塞(等待池中)和同步阻塞(锁池中)。如果是通过t0.join()或Thread.sleep()阻塞,则是经历一次阻塞就可以进入Runnable状态。

e.  o.wait() 和 o.notify()/o.notifyAll() 是只能在synchronized 修饰范围内使用。也就是说能执行o.wait() 和 o.notify()/o.notifyAll()的线程必定是当前唯一拥有着对象锁的线程。当此线程执行o.wait()时会释放锁并进入等待阻塞状态,此时正在synchronized块外面虎视眈眈的其他线程会有其中一个幸运地抢到锁。

3.  java线程是有优先级的,优先级高的线程会获取更高的优先级。

    static int MAX_PRIORITY 

              线程可以具有的最高优先级,取值为10。 

        static int MIN_PRIORITY 

              线程可以具有的最低优先级,取值为1。 

        static int NORM_PRIORITY 

              分配给线程的默认优先级,取值为5

    其中主线程的默认优先级为Thread.NORM_PRIORITY。

note:

1.“线程的优先级有继承关系,比如线程A中创建了线程B , 后者会拥有与前者等同的优先级。

2.  JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

3.  线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

    4.  join() :往往用于A线程需要B线程的处理结果 的场景。即:A线程中调用B.join(),那么A线程会等待线程B的run()执行完毕后才进入Runnable状态。不过要小心的是 假如线程B中又有join\sleeping 等令自己阻塞的方法时,那么 线程A 就得等待很久了。

从JDK源码看join():

public finalsynchronized void join(long millis) throws InterruptedException {

                long base = System.currentTimeMillis();

                long now = 0;

                if (millis < 0) {

                    throw new IllegalArgumentException("timeout value is negative");

                }

                if (millis == 0) {

                    while (isAlive()) {

                wait(0);

                    }

                } else {

                    while (isAlive()) {

                        long delay = millis - now;

                        if (delay <= 0) {

                            break;

                        }

            wait(delay);

                        now = System.currentTimeMillis() - base;

                    }

                }

            }

可以看到 调用t1.join() 实际上底层是调用 t1.wait()。这里有点绕:我们知道 o.wait() 会通知jvm将 当前线程放到 等待池 。同理,t1 也是一个对象啊!Main线程中调用t1.join() (获取t1对象的对象锁[join(**)是synchronized修饰的]),也就是说Main线程执行了 t1这个对象的wait(),  那么jvm也会将Main线程(释放t1对象的锁后[join(**)是synchronized修饰的])放到等待池中。此时Main线程得等待线程t1执行完毕才会 获取t1对象的锁才进入Runnable状态等待机会执行。

        由上述,也可以看到,其实Thread对象也跟普通对象一样,都是堆对象,没有其他区别。只不过Thread对象幸运地具备通知os创建新线程的能力(start())而已。

延伸猜测:Thread对象t1 对应的 线程t 在执行完run()后 是不是额外还"偷偷地"执行了t1.notifyAll()? 如果不是这样,那么为何 调用了t1.join() 的其他线程怎么可以被唤醒呢?

5.sleep()和yield()的区别

sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU  的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程

另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield()  方法执行时,当前线程仍处在Runnable状态,所以,既然优先级不同的线程都处于Runnable状态,那么不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

6.interrupt():不要以为它是中断某个线程!它只是給线程t1发送一个中断信号,如果线程t1正在阻塞状态(如正在sleep\wait或者准备sleep\wait 以及 死锁 时),就会立马中断并抛出异常,从而结束。但是如果你吃掉了这个异常,那么这个线程还是不会中断的!

public class ThreadTest {

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

System.out.println("Main线程开始...");

Apple apple = new Apple();

Thread t1 = new Thread(apple);

t1.start() ;

try {

Thread.sleep(3*1000);// 休眠主线程以确保线程t1进入休眠

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("main线程 准备中断 线程t1...");

t1.interrupt() ; //线程t1正在睡眠,我们中断它

System.out.println("Main线程结束!");

}

}

class Apple implements Runnable{

public void run() {

System.out.println("    线程"+Thread.currentThread().getName()+"开始");

try {

System.out.println("    线程"+Thread.currentThread().getName()+"睡眠.....");

Thread.sleep(15*1000); // 让当前线程睡眠久点!

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("    线程"+Thread.currentThread().getName()+"结束");

}

}

7. 我们在和多线程打交道,常见的就是保证数据共享时的线程安全问题,无非注意把握三方面:有序性,可见性,原子性。少见的则是活跃性问题,性能问题等。

  个人的观点是:能避免数据共享则尽量避免,否则则尽量采用低成本的同步技术。

8. 多线程问题,最常见的就是线程安全的问题。我们要保证线程安全,可以从下面三方面思考如何设计:

    1. 共享变量是无状态对象:共享对象的属性都是final修饰,或者不对外提供setter等。

2. 能否线程封闭!!

3.上述两种都无法采用时,那么无法避免使用同步来保证线程安全了。可是采用哪种同步技术呢?

    其中,线程封闭,就是没有共享对象, 即每个线程都new自己的对象。 提现在如下方面:

        1. 栈封闭,即使用局部变量。

        2. 使用ThreadLocal

3.程序控制线程封闭:容易发生线程安全的环节,只使用一个线程处理?????

9.我们通常将锁分为共享锁和排它锁,也叫做读锁和写锁。

10.保证原子性的最简单方式是操作系统指令,就是说如果一次操作对应一条操作系统指令,这样肯定可以能保证原子性。但是很多操作不能通过一条指令就完成。例如,对long类型的运算,很多系统就需要分成多条指令分别对高位和低位进行操作才能完成。

11. 可见性与jvm内存模型的关系:

从下图中我们可以看出,每个线程都有一个自己的工作内存(相当于CPU高级缓冲区【一、二、三级缓存】,这么做的目的还是在于进一步缩小存储系统与CPU之间速度的差异,提高性能),对于共享变量,线程每次读取的是工作内存中共享变量的副本,写入的时候也直接修改工作内存中副本的值,然后在某个时间点上再将工作内存与主内存中的值进行同步。这样导致的问题是,如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改。

12.volatile,synchronized(隐式锁), 显式锁,原子变量这些同步手段都可以保证可见性。

  可见性底层的实现是通过加内存屏障实现的:

1. 写变量之后加写屏障,保证CPU写缓冲区的值强制刷新回主内存

2. 读变量之前加读屏障,使缓存失效,从而强制从主内存读取变量最新值

写volatile变量 = 进入锁

读volatile变量 = 释放锁

    注意:虽说 volatile 修饰的变量 可以保证 内存可见性,可是 这个变量的私有变量引用的其他对象的私有变量引用的其他对象中的值改变了是不保证内存可见的。volatile修饰的变量仅仅保证了这个变量的成员的内存可见性。

13. 和多线程打交道久了,会发现其实我们一直都是和“原子性、可见性、有序性”三性打交道。初级的时候,我们保证“三性”是通过“volatile + synchronized”组合,其中volatile保证了内存可见性和有序性,synchronized保证了原子性。可是synchronized附带的高耗能是很不划算的,于是,人们就想着将同步下沉到更高性能的硬件层去。这样一来避开了jvm(OS)层面的高耗能线程同步,其次利用了高速的底层硬件,而这个创新就是CAS.于是volatile+CAS成了经典组合。juc并发包中的工具类都是基于volatile和CAS的。

14.  假设 volatile int i 是共享变量,i= 0:

        i = i+1; //会出现线程不安全问题

    于是 我们会这样解决线程安全问题:

        synchronized{

            i = i+1;

        }

    可是这样却非常耗性能,于是我们通过CAS降低耗能,同时也保证了原子性:

        volatile  AtomicInteger i = new AtomicInteger(0);//初始化共享变量

        ............

        ............

        i.getAndIncrement();//保证了原子性

15. 为何JUC中的原子数据类型可以保证“原子性”和“低耗能”?或者说,为何可以将原子性下沉到硬件层面?原因在于java中具体的CAS操作类sun.misc.Unsafe。Unsafe类提供了硬件级别的原子操作,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能。

16. 其实,借助CAS的思想,我们自己也是很容易构建一个非阻塞锁,如下:

public class LockUtil {

  static final AtomicInteger atomicInteger = new AtomicInteger(0);

  /**自旋CAS,只要不符合条件,线程则一直在循环,这样就可以模仿出同步效果*/

  public static void atomicLock() {

      for (; ; )

        if (atomicInteger.compareAndSet(0, 1)) break;

  }

  /**解锁    */

  public static void atomicUnlock() {

      atomicInteger.compareAndSet(1, 0);

  }

}

可是,其实这样是极其简单的,而且也是耗性能、应用面狭窄的:

1)atomicLock() : 在并发场景下,你会发现所有的线程都在这个方法内一直循环,直到break才停止。这其实对于CPU是一个浪费。那么,我们为何不尝试让它“停下休息”一会?

2)atomicLock() : 假如我们实现了上述的“停下休息一会”,那个“幸运儿”break,然后顺利完成任务再atomicUnlock()解锁。然而,“幸运儿”解锁完了 ,可是我们如何找到原来的正在“休息”的线程并唤醒其中之一呢?

    带着上述问题,我们就可以很容易理解:“在 java.util.concurrent.locks包中有很多Lock的实现类,如ReentrantLock、 ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖 java.util.concurrent.AbstractQueuedSynchronizer类” 。AbstractQueuedSynchronizer类对象正是负责存放“正在休息的”线程们 的集合容器。当runtime线程执行完原子任务后,通过AbstractQueuedSynchronizer类对象将其中一个线程唤醒。

    至于“让线程们停下休息会”,用什么方法会比较好?AbstractQueuedSynchronizer类的源码是通过LockSupport.park() 和 LockSupport.unPark()。其本质是:Unsafe类,即直接操纵底层来实现。

17. synchronized和lock性能区别

synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。但是到了Java1.6,发生了变化。synchronize在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

18. 中断机制

    1) Thread对象A.interrupt():一般被Thread对象B调用。

    2) Thread.currentThread.isInterrupted():

3) Thread.interrupted(): 判断当前线程的标识位并返回,并且清除true!!!即true会改为false,非中断。这一定要谨慎使用。

旧版本的java提供的是抢占式中断,如stop()、suspend()、resume(),可是强制性中断线程是很大风险的。于是后面的版本就推行协作式中断:tA线程调用tB.interrupt()时,底层native方法仅仅是修改了tB对象的一个标识字段为true而已。这就表示传达了tB中断信号,至于tB是否中断,开看tB自己所处的状态以及自己的意向。就犹如父母叮嘱在外游子要呵护好身体,至于游子是否呵护就看他自己了。

    注意:

A.  对于处于阻塞中的线程,被中断,会“尽可能快”抛出InterruptedException并且将中断标识位置false。即被中断线程刚调用完:Thread.sleep()、threadx.join()、object.wait()。因此,我们就可以用下面的代码框架来处理线程阻塞中断:

    try {

        //wait、sleep或join

    } catch(InterruptedException e) {//抛异常同时会将中断标识位置false

        //某些中断处理工作:如建议将中断状态设置为true。

    }

    1.所谓“尽可能快”,我猜测JVM就是在线程调度调度的间隙检查中断变量,速度取决于JVM的实现和硬件的性能。   

  2.处于阻塞状态的线程 被中断后抛出exception并置回中断状态为false, 有时候是不利的, 因为这个中断状态可能会作为别的线程的判断条件, 所以稳妥的办法是在处理exception的地方把状态复位:

不过如果当前线程处于IO阻塞状态中的话,如异步socket I/O 和 利用Selector实现的异步I/O,是不会抛异常的,而是等IO操作结束后会有对应的反应行为:详见:Java线程中断的本质和编程原则 - Dlite的技术笔记 - 博客频道 - CSDN

    B. 如果你正在使用的是 任务与线程分离的框架,如Executer等 ,那么切莫轻易调用框架中线程的.interrupt(),因为这些框架很有可能会利用到中断机制,如果你自己贸然直接中断框架管理的线程,那么很有可能会破坏掉框架的管理一致性。一般框架在设计时也会考虑到这点,所以框架也会给出对应于原生jdk操作的api接口,我们调用它会安全。

19. 当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。 界区实现方法有两种,一种是用synchronized,一种是用Lock显式锁实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342