Java基础复习笔记系列之 多线程编程
参考地址:
http://blog.csdn.net/xuweilinjijis/article/details/8878649
今天的故事,让我们从上面这个图开始讲起。线程状态转换图。图很简单不要想得太复杂。了解了线程的基本的生命周期,那么我们要使用好它,就离不开了经常使用的几个方法:先来一段代码:
public class TestSleep { public static void main(String[] args){ MyThread t = new MyThread(); t.start(); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } t.interrupt(); } } class MyThread extends Thread{ public void run(){ while(true){ System.out.println(Math.random()); try { sleep(1000); } catch (InterruptedException e) { return; } } } }
我们发现调用从线程开始执行之后,主线程开始了睡觉。睡了十秒钟之后醒了,接着执行t.interrupt()方法,然后将从线程的while(true)条件破坏掉,然后我们看到的结果,就是再输出了十个随机double数之后,退出了(run方法一结束,线程就结束)。这其中我们用到了Thread.sleep()方法,显然,从上面的调用来看,它是一个静态的方法。它会抛出一个InterruptedException异常,而且我们必须且只能用try...catch...来处理它。且它的原则是谁调用它,谁睡眠。还有温故一下时间的概念,1秒=1000毫秒=1000,000纳秒。接着,再来一段程序:
public class TestJoin { public static void main(String[] args){ Thread t = new MyThread("subThread"); t.start(); try{ t.join(); }catch(InterruptedException e){ e.printStackTrace(); } for(int i = 0; i < 20; i++){ System.out.println("i am mainThread!"); } } } class MyThread extends Thread{ public MyThread(String s){ super(s); } public void run(){ for(int i = 0; i < 20; i++){ System.out.println("i am "+ getName()); try { sleep(1000); } catch (InterruptedException e) { return; } } } }
在这段程序中,我们使用了join()方法,用于合并线程。之所以用20作为测试数据,是防止时间片切换时的干扰。注意其中,用到了super(s)用于继承父类的构造方法,是为了给线程命名字。
知识点范围:
1、进程和线程区别?线程的创建和启动?(启动之后)线程的交互?
2、java中fork()函数。fork的含义是:分叉分歧。
3、java.lang.Object类下的三个方法:void notify()、void notifyAll()、void wait();以及void wait()的两个重载方法void wait(long timeout)、void wait(long timeout, int nanos).利用这三个方法,我们可以模拟出:生产者--仓库--消费者的模型。
4.线程的死锁、
5、线程的合并join()方法(是非静态方法)
6、线程的让步Thread.yield()方法(是静态方法) yield是屈从,放弃的含义。当前运行的线程让出CPU资源,该方法暂停当前正在执行的线程对象,转而去执行其他的线程。
7、守护线程,public final void setDaemon(boolean on),调用线程对象的setDaemon方法可以将其设置为守护线程。
守护线程的作用:守护线程的唯一用途就是告诉JVM不需要等待它退出,当JVM中所有的线程都是守护线程的时候就可以正常的退出了。普通线程不一样,JVM必须等待它的退出才可以正常的退出。
守护线程的理解,“比如你正在用Java写成的编辑器编写Word文档,你一边敲键盘,这是个非守护线程,后台还有一个拼写检查线程,它是个守护线程,他尽量不打扰你写稿子,你们可以同时进行,他发现有拼写错误时在状态条显示错误,但是你可以忽略!!就像城堡门前有个卫兵(守护线程),里面有诸侯(非守护线程),他们是可以同时干着各自的活儿,但是城堡里面的人都搬走了,那么卫兵也就没有存在的意了。
守护线程与普通线程的唯一区别是:当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则不会退出。(以上是针对正常退出,调用System.exit则必定会退出)所以setDeamon(true)的唯一意义就是告诉JVM不需要等待它退出,让JVM喜欢什么退出就退出吧,不用管它。
8、线程的休眠,Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)谁调用,谁休眠。无法精确地保证线程的执行顺序。
9、线程的同步,(先搞清楚什么是竞争资源?什么时候需要考虑同步?怎么同步?)
synchronized只能标记非抽象的方法,不能标识成员变量。
同步的示例,如:构建了一个信用卡账户,起初信用额为100w,然后模拟透支、存款等多个操作。显然银行账户User对象是个竞争资源,而多个并发操作的是账户方法operation(int x),当然应该在此方法上加上同步,并将账户的余额设为私有变量,禁止直接访问。如果不加synchronized,那么多个线程并发访问了竞争资源u,并对u的属性分别做了改动,造成错误。它的实现是分别对对象加锁,然后释放锁。
最佳实践:将竞争资源设置为private,使用synchronized关键字同步方法或代码。当然这不是唯一控制并发安全的途径。
10、java线程同步块,同步的根本的目的,是控制竞争资源的正确的访问,因此只要在访问竞争资源的时候保证同一时刻只能一个线程访问即可,因此Java引入了同步代码快的策略,以提高性能。
在使用synchronized关键字时候,应该尽可能避免在synchronized方法或synchronized块中使用sleep或者yield方法,因为synchronized程序块占有着对象锁,你休息那么其他的线程只能一边等着你醒来执行完了才能执行。不但严重影响效率,也不合逻辑。同样,在同步程序块内调用yeild方法让出CPU资源也没有意义,因为你占用着锁,其他互斥线程还是无法访问同步程序块。当然与同步程序块无关的线程可以获得更多的执行时间。
11、线程的同步与锁
同步的提出:线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
锁的原理:Java中每个对象都有一个内置锁。当程序运行到synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。当程序运行到synchronized同步方法或代码块时,该对象锁才起作用。一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
释放锁是指持锁线程退出了synchronized同步方法或代码块。
关于锁和同步,有一下几个要点:1)、只能同步方法,而不能同步变量和类;2)、每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。4)、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。6)、线程睡眠时,它所持的任何锁都不会释放。7)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。9)、在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。可以这样使用:synchronized (this){函数代码部分}。
静态方法同步,要同步静态方法,需要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。其实这个原则,就是同步时候对synchronized (this){函数代码部分}在静态方法下的特殊的使用为:synchronized (XXX.class){函数代码部分}。
如果线程不能获得锁会怎么样?如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上,线程进入该对象的一种池中,必须在哪里等待,直到其锁被释放,该线程再次变为可运行或运行为止。当考虑阻塞时,一定要注意哪个对象正被用于锁定:1、调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。2、调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。3、静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。4、对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。
什么时候需要同步?在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。对于非静态字段中可更改的数据,通常使用非静态方法访问。对于静态字段中可更改的数据,通常使用静态方法访问。如果需要在非静态方法中使用静态字段,或者在静态字段中调用非静态方法,问题将变得非常复杂。
线程安全类,当一个类已经很好的同步以保护它的数据时,这个类就称为“线程安全的”。即使是线程安全类,也应该特别小心,因为操作的线程间仍然不一定安全。举个形象的例子,比如一个集合是线程安全的,有两个线程在操作同一个集合对象,当第一个线程查询集合非空后,删除集合中所有元素的时候。第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出集合非空,但是当第一个执行清除后,第二个再执行删除显然是不对的,因为此时集合已经为空了。出现这种事件的原因是,上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作。解决上面问题的办法是,在操作集合对象上面做一个同步。这样,当一个线程访问其中一个同步方法时,其他线程只有等待。
同步线程小结:1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。4、对于同步,要时刻清醒在哪个对象上同步,这是关键。5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。
12、