首页 > 代码库 > 多线程小结
多线程小结
一、基础
1、多任务在单个cpu不能并行,只能是并发,任务来回切。
2、临界区:对资源的访问顺序敏感则存在竞态条件。竞态条件发生区叫临界区,写操作产生竞态条件,需要同步。
阻塞:仅单线程使用;非阻塞:允许多线程同时进入临界区。
3、死锁:由于竞争资源或彼此通信导致阻塞,若无外力则无法推进,永远在互相等待。属于静态的问题,死锁发生进程被卡死,不会占用cpu,它会被调出去,比较好发现和分析。
嵌套管程死锁:线程1持有锁A,同时等待从线程2发来的信号,但是线程2需要获取锁A才能给线程1发信号。
4、活锁:两线程一直谦让,都无法使用资源,会比死锁更难发现,因为活锁是一个动态的过程。
5、饥饿:线程无法获得所需要的资源,导致一直无法执行。
阻塞:
1、无障碍:最弱的非阻塞,自由出入临界区,无竞争时限定步骤内完成操作,有竞争则回滚数据,所有线程相当于拿到系统快照,直至拿到快照有效为止。不断尝试导致线程相互干扰,卡死在临区,不保证线程一定能完成。
2、无锁:保证临区有进有出,每次竞争有一个线程可以胜出,解决了无障碍的问题,保证了所有线程都顺利执行下去,但是可能导致低优先级线程饥饿。
3、无等待:前提是无锁,它保证所有的线程都必须在有限步内完成,消除饥饿的。
案例:
1:如果只有读线程,没有写线程,那么这个则必然是无等待的;
2:如果既有读线程又有写线程,而每个写线程之前,都把数据拷贝一份副本,然后修改这个副本,而不是修改原始数据,因为修改副本,则没有冲突,那么这个修改的过程也是无等待的。最后需要做同步的只是将写完的数据覆盖原始数据。
总结:无障碍->竞争回滚;无锁->竞争一个线程胜出;无等待->限步,无饥饿。无锁使用得更加广泛一些。
二、Java的多线程
1、执行线程start方法后就会立即返回,不会等待到run方法执行完毕才返回。就好像run方法是在另外一个CPU上执行一样。
2、Thread的子类可以执行多个实现了Runnable接口的线程,典型的应用就是线程池。
3、synchronized:在同步构造器(synchronized)中用括号括起来的对象叫做监视器对象
类的synchronized:static synchronized method(){...}和static method(){synchronized(MyClass.class)}效果等同。
对象的synchronized:实例方法和方法内synchronized(this)效果相同,使用了“this”,即为调用同步方法的实例本身。
synchronized关键字并不是方法签名的一部分。子类覆写父类、接口的synchronized方法的时候,synchronized修饰符不会被自动继承的。实例方法的同步在子类、父类中使用同样的锁。内部类方法的同步独立于其外部类。非静态的内部类方法可以锁住其外部类。
4、线程已经启动但是未终止,即使处于阻塞(Blocked),isAlive总是返回true。线程在开始运行前、结束运行后、被取消(cancelled)isAlive都会返回false,所以无法得知处于false的线程具体的状态,即无法得知一个处于非活动状态的线程是否已经被启动过了。
5、线程无法得知自己是由哪个线程调用启动的。
6、线程优先级:具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。
7、stop方法会清除栈内信息,结束该线程,丢弃所有的锁,导致原子逻辑受损。
8、线程由native方法start0启动,申请栈内存、运行run方法、修改线程状态等职责,线程管理和栈内存管理都是由JVM负责的,如果覆盖了start方法,也就是撤消了线程管理和栈内存管理的能力,所以不能复写start()。
三、线程状态分析:
每对象都只有一个 monitor,只能被一个线程拥有,该线程就是 “Active Thread”。而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。
“Entry Set”等待中的线程状态: “Waiting for monitor entry”,
“Wait Set” 等待中的线程状态: “in Object.wait()”。
当线程申请进入临界区时,进入了 “Entry Set”队列等待获取monitor。
这时执行有以下可能性:
1、JVM检查Entry Set里面也没有其它等待线程,说明锁未被占用,获取monitor成功,执行临界区的代码,线程将处于 “Runnable”的状态。
只有一个- locked <0x00000007828050a0> (a java.io.BufferedInputStream)
2、获取monitor失败,进入Entry Set队列中等待,DUMP中表现为:“waiting for monitor entry” ,线程是阻塞状态。
"Thread-1" prio=6 tid=0x000000000c1a0800 nid=0x2c78 waiting for monitor entry [0x000000000c9cf000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000782804ce0> (a java.lang.Object)
at thread.ThreadStatus$2.run(ThreadStatus.java:40)
- locked <0x0000000782804ce0> (a java.lang.Object)
3、获取monitor成功,但又调用了对象的 wait、join 方法,放弃了 monitor ,进入 “Wait Set”队列。 DUMP中表现为: in Object.wait()。join()方法实现是通过wait,当线程A调用线程B的Thread的join(N)方法时,首先得获取到对象B的锁,然后执行B的Object的wait(N)方法,直到B唤醒A,可以理解为线程合并。核心逻辑 :while (isAlive()) {wait(N)}。
"Thread-1" prio=10 tid=0x08223250 nid=0xa in Object.wait() [0xef47a000..0xef47aa38]
java.lang.Thread.State: TIMED_WAITING (on object monitor) 对应wait(N)、join(N)
或者:java.lang.Thread.State: WAITING (on object monitor) 对应wait()、join()
at java.lang.Object.wait(Native Method)
- waiting on <0xef63beb8> (a java.util.ArrayList)
at java.lang.Object.wait(Object.java:474)
- locked <0xef63beb8> (a java.util.ArrayList)
at java.lang.Thread.run(Thread.java:595)
4、如果线程调用sleep(N),或者等待资源,状态为Wait on condition。
Wait on condition此时线程状态大致为以下几种:
java.lang.Thread.State: TIMED_WAITING (sleeping)定时的;
java.lang.Thread.State: WAITING (parking):一直等那个条件发生;
java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定时的,那个条件不到来,也将定时唤醒自己。
dump对应的 parking to wait for <0x00000000acd84de8>
(at java.util.concurrent.SynchronousQueue$TransferStack)”
首先,本线程肯定是在等待某个条件的发生,来把自己唤醒。其次,SynchronousQueue 并不是一个队列,只是线程之间移交信息的机制,当我们把一个元素放入到 SynchronousQueue 中时必须有另一个线程正在等待接受移交的任务,其调度队列用的是LinkedBlockingQueue, 执行take的时候会block住, 等待下一个任务进入队列中, 然后进入执行,因此这就是本线程在等待的条件。
来源: http://www.cnblogs.com/zhengyun_ustc/archive/2013/03/18/tda.html
在 JDK 5.0中,引入了 Lock机制,从而使开发者能更灵活的开发高性能的并发多线程程序,可以替代以往 JDK中的 synchronized和 Monitor的 机制。但是,要注意的是,因为 Lock类只是一个普通类, JVM无从得知 Lock对象的占用情况,所以在线程 DUMP中,也不会包含关于 Lock的信息, 关于死锁等问题,就不如用 synchronized的编程方式容易识别。
四、认识java线程安全,了解两点:内存模型、线程同步机制。
并发问题最终反映到java的内存模型上,要解决两个主要的问题:可见性和有序性。
1、可见性: 多线程之间的通信只能通过共享变量来进行,不能互相传递数据。
每个线程在自己的工作内存存储了主存对象的副本,当线程操作某个对象时,执行顺序如下:
(1) 从主存复制变量到当前工作内存 (read and load)
(2) 执行代码,改变共享变量值 (use and assign)
(3) 用工作内存数据刷新主存相关内容 (store and write)
JVM规范定义了线程对主存的操作指令:read,load,use,assign,store,write。
工作内存副本回写主内存,其它线程可见,这就是多线程的可见性问题。
2、有序性:先read-load,完成后线程会引用该副本。当同一线程再度引用该字段时,有可能重新从主存中获取变量副本(read-load-use),也有可能直接引用原来的副本(use),也就是说 read,load,use顺序可以由JVM实现系统决定。线程不能直接为主存中中字段赋值,它会将值指定给工作内存中的变量副本(assign),完成后这个变量副本会同步到主存储区(store-write),至于何时同步过去,根据JVM实现系统决定。
当同一线程多次重复对字段赋值时,比如:x=x+1
线程有可能只对工作内存中的副本进行赋值,只到最后一次赋值后才同步到主存储区,所以assign,store,weite顺序可以由JVM实现系统决定。线程执行x=x+1。从上面的描述中可以知道x=x+1并不是一个原子操作,它的执行过程如下:
1 从主存中读取变量x副本到工作内存
2 给x加1
3 将x加1后的值写回主
synchronized解决执行有序性和内存可见性
如果调用obj.notify()则会通知阻塞队列的某个线程进入就绪队列。
每个锁对象有就绪、阻塞两个队列,就绪队列存储了将要获得锁(notify通知)的线程,阻塞队列存储了被阻塞的线程,JVM检查锁对象的就绪队列有线程在等待,说明锁被占用。
一个线程执行临界区代码过程如下:
1 获得同步锁
2 清空工作内存
3 从主存拷贝变量副本到工作内存
4 对这些变量计算
5 将变量从工作内存写回到主存
6 释放锁
volatile只保证内存可见,不能保证有序性。防止多线程从下的指令重排序。
volatile和缓存一致性
1、 写volatile 变量,JVM 就会向CPU发送一条 Lock 前缀的指令,将当前CPU缓存行的数据回写到系统内存,此操作导致其他 CPU 里缓存了该内存地址的数据无效。
volatile它所修饰的域的原子操作都不需要经过线程的工作内存,而直接在主内存中进行修改,volatile主要被用于变量只有原子操作的场合,如赋值、移位等。
2、缓存一致性机制:
多CPU下,为了保证各个CPU的缓存是一致的,就会实现缓存一致性协议,每个CPU通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当CPU发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当CPU要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
阻止同时修改被多个CPU缓存的内存区域数据,一个CPU的缓存回写到内存会导致其他CPU的缓存无效。
3、不要将数组成员声明为volatile类型的。如果锁住了一个数组并不代表其数组成员都可以被原子的锁定。也没有能在一个原子操作中锁住多个对象的方法。
要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
1)对变量的写操作不依赖于当前值。
2)该变量没有包含在具有其他变量的不变式中。
线程的sleep()方法和yield()方法有什么区别? 异常抛出:sleep抛InterruptedException,yield无
优先级考虑:sleep无,yield相同或更高
线程状态:sleep进入阻塞,yield进入就绪,仅建议JVM对其它就绪状态的线程调度执行,而当前线程放弃时间不确定,有可能刚放弃,有马上获得CPU
可移植性:sleep>yield,与CPU调度相关。
一个还没有启动的线程上调用join方法是没有任何意义。
1、wait、join(内部实现wait)、sleep都涉及到了线程的中断,必须捕获InterruptedException,notify和notifyAll不需要捕获异常。
2、wait,notify和notifyAll是Object的实例方法,用于线程间通信,贡献对象,需要在同步机制,可用于不同线程间的调度,并且只能在其他线程调用本实例的notify()或者notifyAll()方法时被唤醒。
3、wait后进入等待锁定池,只有针对此对象发出notify或者notifyAll方法后,获得对象锁进入就绪状态,等到CPU调度
4、Thread.sleep是一个静态方法,如果线程A调用线程B的sleep()的时候,则线程A进入休眠状态,不会暂停线程B。
interrupt:
默认情况下,新建线程和创建它的线程属于同一个线程组,可以获取同一个线程组的其他线程的标识。当创建一个新的线程组,这个线程组成为当前线程组的子组,对不属于同一组的线程调用interrupt是不合法的。
interrupt方法不能终止一个线程状态,它只会改变中断标志位(如果在t1.interrupt()前后输出t1.isInterrupted()则会发现分别输出了false和true)。
ThreadGroup类uncaughtException,当线程组中的某个线程因抛出未检测的异常(比如空指针异常NullPointerException)而中断的时候,调用这个方法可以打印出线程的调用栈信息。
一个线程的中断状态是不允许被其他线程清除的。
2、如果Thread实例A、B,A线程调用B的interrupt方法。如果B正在wait/sleep/join(如果在执行普通的代码,不抛出中断异常),则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
但是InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。
本文出自 “浅谈技术” 博客,请务必保留此出处http://netpeak.blog.51cto.com/1135737/1892195
多线程小结