Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1405259
  • 博文数量: 120
  • 博客积分: 182
  • 博客等级: 入伍新兵
  • 技术积分: 2278
  • 用 户 组: 普通用户
  • 注册时间: 2012-07-19 16:31
文章分类

全部博文(120)

文章存档

2015年(12)

2014年(13)

2013年(40)

2012年(55)

分类: Java

2015-03-30 21:17:03

java多线程基础学习之(1)——转载文章:http://blog.chinaunix.net/uid-27177626-id-4926148.html

1.多线程的问题

作为多线程程序,它具有“同时”处理多个任务,充分利用系统CPU资源的优点。但是多线程程序在运行时也会带来一定的问题,概括来说,主要有3方面的问题:

1.多个线程对象在“同时”执行时,顺序不可控

2.多个线程对象可能会对某个对象同时发起访问,即多个线程对象“并发”访问某个对象

3.多个线程对象之间出现死锁现象

(1)多个线程对象执行顺序不可控

线程对象启动后,其run()方法何时运行是由操作系统和JVM共同安排的,当应用程序中有多个线程对象被启动并申请执行时,各个线程对象的的执行顺序是随机的,应用程序是不能自己控制线程对象的执行顺序的。虽然通过优先级的设置可以决定让高优先级的线程对象先执行,但是同一个优先级中的线程对象的执行顺序还是随机的,这样其执行顺序还是不可控。

简单的解决办法:对线程对象执行wait操作,让某个线程对象等待一段时间再执行,或者是等待notify的通知后再进入申请资源执行的阶段。

(2)多个线程对象之间的并发操作

两个或者两个以上的线程对象“同时”对某个对象发起访问操作就是一种多个线程对象之间的并发现象。多线程对象“同时”对某个对象进行操作可能会出现错误或者失败。比如,两个线程对象“同时”对某个变量操作,当一个线程对象在对该变量还未操作完成时,另一个线程对象又开始运行了(CPU时间片轮转定时切换到另外的程序段执行),如果此时第一个线程对象还没完成对该变量应有的全部操作,那么久可能导致第二个线程对象执行得到错误的操作结果。而且,由于线程执行顺序的不可控性,也可能导致执行结果出错或者失败。

解决办法:java提供了多线程同步控制机制,保证了一个线程执行完之后再执行另一个线程,并且可以根据优先级等控制线程对象的执行顺序。

(3)多线程对象之间出现死锁现象    

为了控制多线程对象之间的执行顺序,有时多线程对象会调用wait()等待方法进行排序执行。而如果多个线程对处于等待状态时有可能会造成阻塞,因为它们在同时申请CPU时间片,多线程对象有可能出现封闭循环等待状态。比如,一个线程对象在等待下一个线程对象,而下一个线程对象又在等待下下个线程对象的执行等等诸如此类的现象,最后就造成了死锁。

关于死锁,在java中并没有提供检测和解决死锁问题的机制,因此,需要程序员自己在编写程序代码时做一些积极措施,防止多线程对象之间发生死锁现象。而且,最重要的是要尽量避免多线程对象之间交叉或者嵌套调用,如果无法避免,则要调整和安排好它们之间的调用顺序。

2.多线程的同步机制

和线程相关的关键字:thread(线程)、thread-safe(线程安全)intercurrent(并发的)、synchronized(同步的)asynchronized(异步的)volatile(易变的)、atomic(原子的)、share(共享) 

1)同步:

    Java运行系统中用一种称为线程监控器的控制机制来实现线程对象之间的同步执行。线程监视器可以认为是一个很小的、只能容纳一个线程的盒子,一旦一个线程进入监控器,其它的线程必须等待,直到那个线程退出监控为止。通过这种方式,一个监控器可以保证共享资源在同一时刻只可被一个线程使用。这种方式称之为同步。(一旦一个线程进入一个实例的任何同步方法,别的线程将不能进入该同一实例的其它同步方法,但是该实例的非同步方法仍然能够被调用)。

2)何时需要同步:

    线程间的通讯首要的方式就是对字段及其字段所引用的对象的共享访问。这种通信方式是及其高效的,但是也是导致了可能的错误:线程间相互干涉和内存一致性的问题。避免出现这两种错误的方法就是同步。

    ?线程间相互干扰描述了当多个线程访问共享数据时可能出现的错误。

    ?内存一致性错误描述的了共享内存可能导致的错误。

    ?同步方法(Synchronized method)描述了一种简单的可以有效防止线程间相互干扰及其内存一致性错误的方法。

    ?明锁及同步描述了一种更加通用的同步方法,以及同步是如何基于明锁而实现的。

    ?原子性描述了不能被其它线程干扰的操作。

    因此,当两个或者两个以上的线程对象通信或者共享一个数据结构对象时,需要一种机制来让它们相互牵制并能够正确地执行。而要跨线程维护正确的可见性,只要在几个线程之间共享非 final 变量,就必须使用 synchronized(或 volatile)以确保一个线程可以看见另一个线程做的更改。

    为了在线程之间进行可靠的通信,也为了互斥访问,同步是必须的。这归因于java语言规范的内存模型,它规定了:一个线程所做的变化何时以及如何变成对其它线程可见。 

多线程将异步行为引进程序,所以在需要同步时,必须有一种方法强制进行。例如:如果2个线程想要通信并且要共享一个复杂的数据结构,如链表,此时需要确保它们互不冲突,也就是必须阻止B线程在A线程读数据的过程中向链表里面写数据(A获得了锁,B必须等A释放了该锁)。

3)原子操作

Java原子操作是指不会被打断的操作。但是原子操作在有些情况下不一定是线程安全的。java原子操作的不安全因素可能的原因是:java线程允许线程在自己的内存区中保存变量的副本;为了提高性能,允许线程使用本地的私有拷贝进行工作而非每次都使用主存的变量(有待考证??)虽然原子操作是线程安全的,但各个线程在读取到变量副本之后,会各自对其做自己的操作,因为没有将改变写入主存,因此对其它线程是不可见的。因此针对这样的情况也是需要通过同步机制来解决的。

注意:

A.java多线程中同步也就是串行使用一些资源(比如数据结构/共享数据),对共享、可变的数据进行同步.对于函数中的局部变量没必要进行同步.对于不可变数据,也没必要进行同步.多线程中访问共享可变数据才有必要.

B.如果数据将在线程间共享.例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取.当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率.

4)java多线程的同步机制

ThreadLocalsynchronized( )/synchronized修饰符 ③wait()notify()

④ volatile.这些方法都是为了解决多线程访问中对同一变量的访问冲突。

A.ThreadLocal

ThreadLocal 保证不同线程拥有不同实例,相同线程一定拥有相同的实例,即为每一个使用该变量的线程提供一个该变量的副本,每一个线程都可以独立改变自己的副本,而不是与其它线程的副本冲突。

ThreadLocal的优点是提供了线程安全的共享对象。

与其它同步机制的区别:同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信;而ThreadLocal 是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源,这样就不需要多个线程进行同步了。

B.volatile

volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化回写到共享内存。 

这样做的优点是在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 

缘由:Java 语言规范中指出,为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只有当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时得到共享成员变量的变化。而 volatile 关键字就是提示JVM :对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。

使用技巧:在两个或者更多的线程访问的成员变量上使用 volatile 。当要访问的变量已在 synchronized 代码块中,或者为常量时,不必使用。

线程为了提高效率,将某成员变量(A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行AB的同步,因此存在AB不一致的情况。volatile就是用来避免这种情况的。 volatile告诉jvm,它所修饰的变量不保留拷贝,直接访问主内存中的(读操作多时使用较好;线程间需要通信,本条做不到)

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后的值之间没有约束。只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件: 

①对变量的写操作不依赖于当前值;

②该变量没有包含在具有其他变量的不变式中。

(如果变量被声明为volatile,在每次访问时都会和主存一致;如果变量在同步方法或者同步块中被访问,当在方法或者块的入口处获得锁以及方法或者块退出时释放锁时变量被同步。)

C.sleep()&wait()

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。

waitObject类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态

D.synchronized( )/synchronized修饰符

同步对象通过锁机制来实现。在这里最重要的关键字是synchronized。在java多线程程序中,可以通过使用synchronized关键字来修饰某个类中需要同步的方法,其使用格式是这样的:

Public synchronized void methodName() {……}

除了可以作为方法的修饰符之外,它还可以作为方法体内的语句使用。Synchronized语法的使用格式是这样的:

Synchronized(syncObject){    /*syncObject指出需要“同步”机制约束的类                  

                          对象*/

}

Synchronized()语句中,syncObject指示一个对象,大括号“{}”中的代码是要受到同步控制约束的。它们需要进入线程监视器中运行。因此Synchronized()语句也称作同步控制代码块。当Synchronized()语句中的代码在线程监视器中运行时,包含Synchronized()语句的对象也同时进入到线程监视器中受到保护,而该对象中的Synchronized()语句和Synchronized修饰的方法在被调用时都是需要等待的。

Synchronized()语句和Synchronized修饰的方法在线程监视器中是没有区别的。如果Synchronized修饰的方法消耗的CPU时间太长,而又只有一两条语句需要在线程监视器中受到保护地运行时,建议将需要在线程监视器中运行的语句放到Synchronized()语句的大括号中比较好。因为这样做就不需要将整个方法都修饰为Synchronized,从而有助于提高线程对象的工作效率

事实上,java线程同步机制的原理简单而言就是把Synchronized修饰的方法或者Synchronized()语句锁定在线程监视器中运行,它相当于给该方法上了一把线程锁,同时也锁定了包含该方法的对象。例如,如果在一个对象中存在几个使用Synchronized修饰的方法或者Synchronized()语句,那么一次只能有一个Synchronized修饰的方法或者Synchronized()语句独占线程监视器。当一段代码在监视器中被执行时,包含该代码的对象就会被锁定,称之为对象获得了锁,在该对象中所有的其他被调用的Synchronized修饰的方法或者Synchronized()语句都需要等待。在线程监视器中被执行的方法结束运行时,意味着它退出了线程监控器,解除了对象的锁定状态,此时对象中的Synchronized方法或者Synchronized()语句才可以被调用执行。

注意:

1.并不是线程对象中的所有方法都可以用Synchronized修饰,线程对象的run()方法就不需要被Synchronized修饰。

2.尽量避免多线程对象的Synchronized方法或者含有Synchronized()语句的方法之间的相互交叉调用,防止线程间死锁的产生。而且,因为java多线程的同步机制需要占用较大的计算机系统资源开销,所以应该尽量避免使用同步控制。

学习参考:
1.《Java程序设计案例教程》
2.http://blog.csdn.net/you_off3/article/details/7572704
3.http://blog.csdn.net/zhangxingping/article/details/8604947

阅读(2387) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~