标签:
线程是现代操作系统中一个很重要的概念,多线程功能很强大,java语言对线程提供了很好的支持,我们可以使用java提供的thread类很容易的创建多个线程。线程很不难,我对之前学习过的基础,在这做了一个整理,本文主要参考的是Java研究组织出版的j2se进阶和张孝祥-java就业培训教材这两本书
主要是线程与进程的区别,这里不再阐述,自行网上搜索
为什么使用线程:操作系统切换多个线程要比调度进程在速度上快很多,进程间无法共享,通讯麻烦。线程之间由于共享数据,所以交换数据很方便
下面有个例子去解释多线程与单线程
A单线程例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package firstTread; /** * @author zhaikaishun * */ public class TreadDemo1 { public static void main(String[] args) { new TestThread().run(); //会一直执行这段代码 while ( true ){ System.out.println( "main thread is running" ); } } } class TestThread{ //这里没有继承Thread类 public void run(){ while ( true ){ System.out.println(Thread.currentThread().getName()+ " is here run" ); //会一直执行 } } } |
运行后
分析:这里是单线程,会按照顺序,只会执行TestThread类的方法
B多线程例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package firstTread; /** * @author zhaikaishun * */ public class TreadDemo1 { public static void main(String[] args) { new TestThread().run(); //会一直执行这段代码 while ( true ){ System.out.println( "main thread is running" ); } } } class TestThread{ //这里没有继承Thread类 public void run(){ while ( true ){ System.out.println(Thread.currentThread().getName()+ " is here run" ); //会一直执行 } } } |
结果:
分析:这里使用了多线程,TreadDemo1中的run方法 和main中的run方法会抢cpu执行,所以有时候输出有两种情况。注意,使用java多线程需要继承Thread类,还需调用其start()方法。
Java吸收了一些多线程操作系统的技术特性,经过优化处理,在语言层次上实现了对线程的支持,它提供了Thread,Runnable,Thread,Group等一系列封装和类的接口,让程序员可以高效的开发java多线程程序,java还提供synchronized关键字和Object的wait(),notify()机制,用来实现进程的同步。
(a)继承Thread类
Java用Thread类对线程进行封装,一旦创建了这个Thread实例,jvm就会为我们创建一个线程,当我们调用Thread类的strat方法时,线程就开始运行起来。创建线程的方法如下
代码3.1,继承thread类创建线程的代码
我们也可以使用匿名类的办法创建线程,这样代码比较简洁但是可读性较差
代码3.2,匿名类继承Thread创建线程
(b)实现Runble接口
Runble是java提供的一个线程相关的接口,接口定义了一个方法
public void run();
某一个类一旦实现了该接口,那么这个类的实例就可以被一个java的thread对象调用。
代码3.3,自定义一个类,实现Runnable接口
代码3.4 匿名类实现Runnable接口
不论是那种方式,最后都需要通过Thread类的实例调用start()方法来开始线程的执行,start()方法通过java虚拟机调用线程中定义的run方法来执行该线程。通过查看java源程序中的start()方法的定义可以看到,它是通过调用操作系统的start0方法来实现多线程的操作的。
但是一般在系统的开发中遇到多线程的情况的时候,以实现Runnable接口的方式为主要方式。这是因为实现接口的方式有很多的优点:
1、就是通过继承Thread类的方式时,线程类就无法继承其他的类来实现其他一些功能,实现接口的方式就没有这中限制;
2.也是最重要的一点就是,通过实现Runnable接口的方式可以达到资源共享的效果。
这个不举一个例子可能不太清楚,下面我就举一个买票的程序的例子
首先我们先写一个继承Thread类的程序,看看效果
首先是一个线程类,继承了
程序清单: ThreadTest类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package firstTread; /** * @author zhaikaishun * */ public class ThreadTest extends Thread { private int tickets = 100 ; public void run(){ while ( true ){ //模拟买票程序,每次调用这个方法,ticket就会减一张 if (tickets> 0 ) System.out.println(Thread.currentThread().getName()+ " is saling ticket " +tickets--); } } } |
程序清单:ThreadDemo4类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package firstTread; /** * @author zhaikaishun * */ public class ThreadDemo4 { public static void main(String[] args) { ThreadTest t= new ThreadTest(); t.start(); t.start(); t.start(); t.start(); } } |
假如我们想用上述代码去模拟买票程序,run方法中每一次循环总票都减1,模拟卖出一张票,我们创建了一个线程,并且启动4次,希望能通过此种方式产生4个线程,结果怎么样呢
结果:从运行结果来看,我们发现只有一个线程在运行,无论我们启动多少遍start()方法,结果只有一个线程
接着我们修改ThreadDemo4类,在main方法中创建四个threadTest对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package firstTread; /** * @author zhaikaishun * */ public class ThreadDemo4 { public static void main(String[] args) { new ThreadTest().start(); new ThreadTest().start(); new ThreadTest().start(); new ThreadTest().start(); } } |
结果:确实是每个号被打了4遍,创建了4个线程,四个线程都在卖票,但是请注意,他们是各自在卖自己的100张票,并不能实现资源共享,不能去处理同一个资源
接着我们试着用实现Runable的方式,这才是正确的方式
程序清单:ThreadDemo5类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package firstTread; /** * @author zhaikaishun * */ public class ThreadDemo5 { public static void main(String[] args) { ThreadTest t = new ThreadTest(); //这个类的实例就可以被一个java的thread对象调用。 new Thread(t).start(); //thread对象调用。 new Thread(t).start(); new Thread(t).start(); new Thread(t).start(); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package firstTread; /** * @author zhaikaishun * */ public class ThreadTest implements Runnable { //现在是实现Runnable接口 private int tickets = 100 ; public void run(){ while ( true ){ //模拟买票程序,每次调用这个方法,ticket就会减一张 if (tickets> 0 ) System.out.println(Thread.currentThread().getName()+ " is saling ticket " +tickets--); } } } |
3.有关这两种方法的性能差异,现在的pc速度如此的快,我们认为在上面的前提下比较性能差异没有多大意义
我的建议:建议使用第二种方式,也就是实现Runable接口的方式
1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
注:Thread有个isAlive()方法,用来判断
在上面的卖票得例子中,有可能出现一种我们不想要的情况,那就是有可能同一张票被打印两次多多次,打印的票号码为0甚至是负数等
原因在这一段代码中
if
(tickets>0)
System.out.println(Thread.currentThread().getName()+
" is saling ticket "
+tickets--);
线程1刚刚判断完if
(tickets>0),正要处理下面的语句的时候,cpu被线程2给抢走,线程2开始执行,当线程2执行完一个run方法,这里的tickets会减少1,这时候tickets为0,然后跳转到线程1的中断的地方继续执行,因为之前线程1判断过
if
(tickets>0),所以这里不再需要判断,直接执行
System.out.println(Thread.currentThread().getName()+
" is saling ticket "
+tickets--);,将会打印出为0的票,也就意味着最后一张票卖了2次
1
2
3
|
synchronized (object) { //这里写代码块 } |
独木桥会让我们的过桥效率降低,同样,同步代码块也会降低代码的执行速度,所以,如果确定代码是安全的,就不要使用同步代码块了。
同步代码块实现同步的原理:任何类型的对象都有一个标志位,该标志位具有0,1两种状态,其开始为1,当执行到synchrozied方法之后,object对象标识位变为0,另外一个线程执行到synchrozed方法之后,将会先判断这个状态,如果发现是0,就暂时阻塞。可以把这个标志位理解成一个箱子的锁,该箱子只能放一个人的东西。
上述卖票程序的ThreadTest类可以如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package firstTread; /** * @author zhaikaishun * */ public class ThreadTest implements Runnable { // 现在是实现Runnable接口 private int tickets = 100 ; String str = new String( "" ); //这里设置一个对象,任意一个对象都可以 public void run() { while ( true ) { // 模拟买票程序,每次调用这个方法,ticket就会减一张 synchronized (str) { // 这里写代码块 if (tickets > 0 ) { try { Thread.sleep( 10 ); } catch (Exception e) { System.out.println(e.getMessage()); } System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--); } } } } } |
结果:
注意:
String str =
new
String(
""
);
这个标志对象,相当于监听对象,必须放在run方法的外面,如果放在run方法里面,四个线程每次调用run方法,就会产生4个监听对象,这四个同步监视器是4个不同的对象,会导致彼此之间不能同步。
4.3.同步函数
上述是对代码块进行的同步,同样,我们也能对某一个方法进行同步,只需要在同步的函数前加上关键字synchronized即可
例如上述代码可以写成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package firstTread; /** * @author zhaikaishun * */ public class ThreadTest implements Runnable { // 现在是实现Runnable接口 private int tickets = 100 ; String str = new String( "" ); //这里设置一个对象,任意一个对象都可以 public void run() { while ( true ) { // 模拟买票程序,每次调用这个方法,ticket就会减一张 sale(); } } public synchronized void sale(){ //同步方法 if (tickets> 0 ){ try { Thread.sleep( 10 ); } catch (Exception e){ System.out.println(e.getMessage()); } System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--); } } } |
当有一个线程进入了synchronized方法(获得监视器),其他线程就不能进入通一个对象所有使用了synchronized修饰的方法,直到第一个对象执行完他所在的synchronized方法(离开监视器)。
思考:既然synchronized方法需要有一个标志位,那么同步方法的标志位是什么呢,这里我先给出答案,同步方法的标志对象就是所在的对象,即this。
请看下面方法,通过一个str的取值,来判断是代码块还是函数间的同步
代码清单 ThreadDemo6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package firstTread; /** * @author zhaikaishun * */ public class ThreadDemo6 { public static void main(String[] args) { ThreadTest t = new ThreadTest(); new Thread(t).start(); //thread对象调用。 //让线程暂停一会儿才直观 try {Thread.sleep( 1 );} catch (Exception e){}; t.str= new String( "method" ); //如果str是method,调用同步函数 new Thread(t).start(); } } |
代码清单 ThreadTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package firstTread; /** * @author zhaikaishun * */ public class ThreadTest implements Runnable { // 现在是实现Runnable接口 private int tickets = 100 ; String str = new String( "" ); // 这里设置一个对象,任意一个对象都可以 public void run() { if ( "method" .equals(str)) { while ( true ) { sale(); } } else { synchronized (str) { while ( true ) { if (tickets > 0 ) { try { Thread.sleep( 10 ); } catch (Exception e) { System.out.println(e.getMessage()); } System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--); } } } } } public synchronized void sale() { // 同步方法 if (tickets > 0 ) { try { Thread.sleep( 10 ); } catch (Exception e) { System.out.println(e.getMessage()); } System.out.print( "函数方法在执行:" ); System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--); } } } |
运行结果:由于代码块和函数使用的监听器不一样,所以他们没有同步
如果想让他们同步,只需要设置相同的监听对象,将synchronized (str)改为synchronized (this)即可
死锁比较少见,而且难于调试:
所谓死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
其实很久之前学习数字电路,经常会遇到一些锁,这也是自动化的一些常见的问题,在计算机中,也有类似的东西,请看下图
R1 和R2,都只能被一个进程使用
T1在使用R1,同时没有使用完R1的情况下,想使用R2
T2在使用R2,同时在没有使用完R2的情况下,想使用R1
这时,T1等待T2放弃使用R2,同时T2等待T1放弃使用R1,他们都不会放弃自己所使用的,于是产生了等待,将会一直僵持下去。
下面这个例子就是
线程1进去对象obj1的监视器,而线程2进入了obj2的监视器,这时候进入了obj1的监视器的线程还试图进入使用obj2作为监视器的方法中,这显然会被阻塞隔离,是进不去的;同时,进入了obj2的监视器的线程也试图进入使用obj1作为监视器的方法中,这也显然会被阻塞隔离,是进不去的。 然后双方一致僵持着,程序停滞不前,这就是我们所谓的死锁,代码清单如下
代码清单:死锁例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
package deadlock; public class RunnableTest implements Runnable { private int flag = 1 ; private static Object obj1 = new Object(), obj2 = new Object(); public void run() { System.out.println( "flag=" + flag); if (flag == 1 ) { synchronized (obj1) { System.out.println( "我已经锁定obj1,休息0.5秒后锁定obj2去,但是估计进不去obj2,因为obj2也正在一个同步方法中" ); try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj2) { System.out.println( "进入了obj2 } } } if (flag == 0 ) { synchronized (obj2) { System.out.println( "我已经锁定obj2,休息0.5秒后锁定obj1去,但是估计进不了obj1,因为这obj1也在一个同步方法中 " );
try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj1) { System.out.println( "进入了obj1 } } } } public static void main(String[] args) { RunnableTest run01 = new RunnableTest(); RunnableTest run02 = new RunnableTest(); run01.flag = 1 ; run02.flag = 0 ; Thread thread01 = new Thread(run01); Thread thread02 = new Thread(run02); System.out.println( "线程开始喽!" ); thread01.start(); thread02.start(); } } |
结果:一直处于僵持状态
我们先从下面一个例子引出线程中的通信
下面例子讲的是一个生产和消费的关系,生产一样东西,取走这样东西。这个程序是每生产出一个PDD(人名),并且给这个人赋值为男
然后再取出来,然后生产一个“娇妹”(人名),并且赋值为女,然后再取出来。代码清单如下。
一个类Q,用来存储数据 Q:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package communication; public class Q { private String name= "PDD" ; private String sex= "男" ; public synchronized void put(String name,String sex) { this .name=name; try {Thread.sleep( 1 );} catch (Exception e){System.out.println(e.getMessage());} this .sex=sex; } public synchronized void get(){ System.out.println(name+ "----" +sex); } } |
生产者类Producer:生产数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package communication; public class Producer implements Runnable { Q q= null ; public Producer(Q q){ this .q=q; } int i= 0 ; public void run(){ while ( true ){ if (i== 0 ) q.put( "PDD" , "男" ); else q.put( "娇妹" , "女" ); i=(i+ 1 )% 2 ; } } } |
消费者类 Customer: 获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package communication; public class Customer implements Runnable { Q q= null ; public Customer(Q q){ this .q=q; } public void run(){ while ( true ){ q.get(); } } } |
主方法:
1
2
3
4
5
6
7
8
9
10
11
12
|
package communication; public class ThreadCommunication { public static void main(String[] args) { Q q = new Q(); new Thread( new Producer(q)).start(); try {Thread.sleep( 1 );} catch (Exception e){System.out.println(e.getMessage());} new Thread( new Customer(q)).start(); } } |
运行结果:
.....
PDD----男
娇妹----女
PDD----男
娇妹----女
PDD----男
娇妹----女
wait:告诉当前线程放弃监视器并且进入线程休眠状态,直到其他线程进入相同的监视器并且调用notify为止。
notify:唤醒同一对象监视器中调用wait的第一个线程。
notifyAll:唤醒同一对象监视器中调用wait的所有线程,具有优先级高的线程将会被先唤醒。
如果想让上面的程序满足我们的要求,我们可以在 类Q中定义一个新的成员变量bFull来标示数据存储空间的状态,当Customer取走数据后,bFull为false;当Producer存入数据后,bFull为true。只有bFull为true时,Customer才能取走数据,只有当bFull为False时Producer才能放入数据 Q的清单如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package communication; public class Q { private String name= "PDD" ; private String sex= "男" ; boolean bFull= false ; public synchronized void put(String name,String sex) { if (bFull) try { wait(); } catch (InterruptedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } this .name=name; try {Thread.sleep( 1 );} catch (Exception e){System.out.println(e.getMessage());} this .sex=sex; bFull= true ; notify(); } public synchronized void get(){ if (!bFull) try { wait(); } catch (InterruptedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } System.out.println(name+ "----" +sex); bFull= false ; notify(); } } |
结果:运行流程,自己看代码思考
参考文献:
【1】J2SE进阶(java研究组织 精品图书)
【2】张孝祥-Java就业教程
【3】java编程思想
【4】java核心技术卷1
标签:
原文地址:http://blog.csdn.net/t1dmzks/article/details/51893818