线程是现代操作系统中一个很重要的概念,多线程功能很强大,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( "TestThread is here run" ); //会一直执行
}
}
}
|
运行后出现
TestThread is here run
TestThread is here run
TestThread is here run
TestThread is here run
TestThread 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().start(); //开启一个线程
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接口的方式可以达到资源共享的效果。(2017年修改,其实使用继承Thread也可以)
这个不举一个例子可能不太清楚,下面我就举一个买票的程序的例子
首先我们先写一个继承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