Java-多线程

一、基础

1、临界区:对资源的访问顺序敏感则存在竞态条件,竞态条件发生区叫临界区,写操作产生竞态条件,需要同步。

2、死锁:由于竞争资源或彼此通信导致阻塞,若无外力则无法推进,永远在互相等待。属于静态的问题,死锁发生进程被卡死,不会占用cpu,它会被调出去,比较好发现和分析。

嵌套管程死锁:线程1持有锁A,同时等待从线程2发来的信号,但是线程2需要获取锁A才能给线程1发信号。

3、活锁:两线程一直谦让,都无法使用资源,会比死锁更难发现,因为活锁是一个动态的过程。

4、饥饿:线程无法获得所需要的资源,导致一直无法执行。

阻塞:仅单线程使用;非阻塞:允许多线程同时进入临界区。

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()。

三、线程状态分析:

wKiom1h8OPihMfvrAABCO9cnMKs624.png

每对象都只有一个 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线程安全,了解两点:内存模型、线程同步机制。

 wKioL1h8OPrhm2qxAACG1Su18fo743.jpg


并发问题最终反映到java的内存模型上,要解决两个主要的问题:可见性和有序性。
1、可见性: 多线程之间的通信只能通过共享变量来进行,不能互相传递数据。
每个线程在自己的工作内存存储了主存对象的副本,当线程操作某个对象时,执行顺序如下:
 (1) 从主存复制变量到当前工作内存 (read and load)
 (2) 执行代码,改变共享变量值 (use and assign)
 (3) 用工作内存数据刷新主存相关内容 (store and write)
JVM规范定义了线程对主存的操作指令:readloaduseassignstorewrite
工作内存副本回写主内存,其它线程可见,这就是多线程的可见性问题。
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并不是一个原子操作,它的执行过程如下:
从主存中读取变量x副本到工作内存
x1
x1后的值写回主


synchronized解决执行有序性和内存可见性

如果调用obj.notify()则会通知阻塞队列的某个线程进入就绪队列。
每个锁对象有
就绪、阻塞两个队列,就绪队列存储了将要获得锁(notify通知)的线程,阻塞队列存储了被阻塞的线程,JVM检查锁对象的就绪队列有线程在等待,说明锁被占用。
一个线程执行临界区代码过程如下:
获得同步锁
清空工作内存
从主存拷贝变量副本到工作内存
对这些变量计算
将变量从工作内存写回到主存
释放锁


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()方法抛出的。


happen before原则:

一个监视器锁的unlock happen before 之后每一个对该监视器的lock

一个volatile字段 写操作 happen before 之后的每一个读

一个线程的start操作happen-before 线程内的任何操作

线程内的任何操作都happen-before任何从该线程的join()方法返回的


JVM与多线程:

虚拟机采用CAS配上失败重试的方式保证更新操作的原子性

如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部, 在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此对象处于轻量级锁定状态,如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向着线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。 轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。 如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。 偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。 当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态。


多线程的弊端:

在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。

像Java的线程栈,一般至少分配512K~1M的空间,超过1000线程,内存占用过高。

如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高。


sleep是Thread类的静态方法,这意味着只对当前线程有效,sleep多长时间是由当前线程决定,sleep将在接到时间到达事件事恢复线程执行,如果时间不到你只能调用interreput()来强行打断进行唤醒。

wait是Object的实例方法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起。

本质的区别是sleep:一个线程的运行状态控制,wait是线程之间的通讯的问题。

wait():如果当前线程不是对象所得持有者,该方法抛出一个java.lang.IllegalMonitorStateException 异常

notifyAll();相当于this.notifyAll();

同理wait();相当于this.wait();

注意java.lang.IllegalMonitorStateException 异常


若不拥有对象的锁标记,而试图用wait/notify协调共享对象资源,应用程序将抛出 IllegalMonitorStateException 


IllegalMonitorStateException 意味着一个或多个资源可能不再处于一致状态,表示程序出现了严重问题。由于IllegalMonitorStateException是RuntimeException类型,因此它可能中断产生异常的线程。


当且仅当创建线程是守护线程时,新线程才是守护程序


在Spring中,DAO和Service都以单实例的方式存在。Spring是通过ThreadLocal将有状态的变量(如Connection 等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring不遗余力地将有状态的对象无状态化,就是要达到单实例化Bean的目 的。

当DAO类作为一个单例类时,数据库链接(connection)被每一个线程独立的维护,互不影响。(基于线程的单例)

我们可以得出这样的结论:在相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中,则不同线程下的事务方法工作在独立的事务中。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章