您的位置:首页 > 编程语言 > Java开发

Java多线程完整版基础知识

2016-12-28 09:04 211 查看


ava多线程完整版基础知识


(翟开顺由厚到薄系列)

1.前言

线程是现代操作系统中一个很重要的概念,多线程功能很强大,Java语言对线程提供了很好的支持,我们可以使用java提供的thread类很容易的创建多个线程。线程很不难,我对之前学习过的基础,在这做了一个整理,本文主要参考的是Java研究组织出版的j2se进阶和张孝祥-java就业培训教材这两本书

2.概述

2.1线程是什么

主要是线程与进程的区别,这里不再阐述,自行网上搜索为什么使用线程:操作系统切换多个线程要比调度进程在速度上快很多,进程间无法共享,通讯麻烦。线程之间由于共享数据,所以交换数据很方便下面有个例子去解释多线程与单线程A单线程例子
1234567891011121314151617181920212223
package
firstTread;


/**

*@authorzhaikaishun

*

*/

public
class
TreadDemo1{

public
static
void
main(String[]args){

new
TestThread().run();
//会一直执行这段代码

while
(
true
){

System.out.println(
"mainthreadisrunning"
);

}


}


}

class
TestThread{
//这里没有继承Thread类

public
void
run(){

while
(
true
){

System.out.println(Thread.currentThread().getName()+
"ishererun"
);
//会一直执行

}

}

}
运行后分析:这里是单线程,会按照顺序,只会执行TestThread类的方法B多线程例子
1234567891011121314151617181920212223
package
firstTread;


/**

*@authorzhaikaishun

*

*/

public
class
TreadDemo1{

public
static
void
main(String[]args){

new
TestThread().run();
//会一直执行这段代码

while
(
true
){

System.out.println(
"mainthreadisrunning"
);

}


}


}

class
TestThread{
//这里没有继承Thread类

public
void
run(){

while
(
true
){

System.out.println(Thread.currentThread().getName()+
"ishererun"
);
//会一直执行

}

}

}


结果:
分析:这里使用了多线程,TreadDemo1中的run方法和main中的run方法会抢cpu执行,所以有时候输出有两种情况。注意,使用java多线程需要继承Thread类,还需调用其start()方法。

2.2java对线程的支持

Java吸收了一些多线程操作系统的技术特性,经过优化处理,在语言层次上实现了对线程的支持,它提供了Thread,Runnable,Thread,Group等一系列封装和类的接口,让程序员可以高效的开发java多线程程序,java还提供synchronized关键字和Object的wait(),notify()机制,用来实现进程的同步。

3.在java中使用线程

3.1Thread类和Runable方法

(a)继承Thread类Java用Thread类对线程进行封装,一旦创建了这个Thread实例,jvm就会为我们创建一个线程,当我们调用Thread类的strat方法时,线程就开始运行起来。创建线程的方法如下代码3.1,继承thread类创建线程的代码我们也可以使用匿名类的办法创建线程,这样代码比较简洁但是可读性较差代码3.2,匿名类继承Thread创建线程(b)实现Runble接口Runble是java提供的一个线程相关的接口,接口定义了一个方法publicvoidrun();某一个类一旦实现了该接口,那么这个类的实例就可以被一个java的thread对象调用。代码3.3,自定义一个类,实现Runnable接口代码3.4匿名类实现Runnable接口

3.2两种线程实现方法的比较

不论是那种方式,最后都需要通过Thread类的实例调用start()方法来开始线程的执行,start()方法通过java虚拟机调用线程中定义的run方法来执行该线程。通过查看java源程序中的start()方法的定义可以看到,它是通过调用操作系统的start0方法来实现多线程的操作的。但是一般在系统的开发中遇到多线程的情况的时候,以实现Runnable接口的方式为主要方式。这是因为实现接口的方式有很多的优点:1、就是通过继承Thread类的方式时,线程类就无法继承其他的类来实现其他一些功能,实现接口的方式就没有这中限制;2.也是最重要的一点就是,通过实现Runnable接口的方式可以达到资源共享的效果。这个不举一个例子可能不太清楚,下面我就举一个买票的程序的例子首先我们先写一个继承Thread类的程序,看看效果首先是一个线程类,继承了程序清单:ThreadTest类
12345678910111213141516
package
firstTread;


/**

*@authorzhaikaishun

*

*/

public
class
ThreadTest
extends
Thread{

private
int
tickets=
100
;

public
void
run(){

while
(
true
){

//模拟买票程序,每次调用这个方法,ticket就会减一张

if
(tickets>
0
)

System.out.println(Thread.currentThread().getName()+
"issalingticket"
+tickets--);

}

}

}
程序清单:ThreadDemo4类
12345678910111213141516
package
firstTread;


/**

*@authorzhaikaishun

*

*/

public
class
ThreadDemo4{


public
static
void
main(String[]args){

ThreadTestt=
new
ThreadTest();

t.start();

t.start();

t.start();

t.start();

}

}
假如我们想用上述代码去模拟买票程序,run方法中每一次循环总票都减1,模拟卖出一张票,我们创建了一个线程,并且启动4次,希望能通过此种方式产生4个线程,结果怎么样呢结果:从运行结果来看,我们发现只有一个线程在运行,无论我们启动多少遍start()方法,结果只有一个线程接着我们修改ThreadDemo4类,在main方法中创建四个threadTest对象
123456789101112131415
package
firstTread;


/**

*@authorzhaikaishun

*

*/

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类
12345678910111213141516
package
firstTread;


/**

*@authorzhaikaishun

*

*/

public
class
ThreadDemo5{


public
static
void
main(String[]args){

ThreadTestt=
new
ThreadTest();
//这个类的实例就可以被一个java的thread对象调用。

new
Thread(t).start();
//thread对象调用。

new
Thread(t).start();

new
Thread(t).start();

new
Thread(t).start();

}

}
12345678910111213141516
package
firstTread;


/**

*@authorzhaikaishun

*

*/

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()+
"issalingticket"
+tickets--);

}

}

}
结果:如我们所想要的,四个线程同时处理一个资源3.有关这两种方法的性能差异,现在的pc速度如此的快,我们认为在上面的前提下比较性能差异没有多大意义我的建议:建议使用第二种方式,也就是实现Runable接口的方式

3.3线程的状态和属性

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()方法,用来判断

4.线程同步

4.1线程安全问题:

在上面的卖票得例子中,有可能出现一种我们不想要的情况,那就是有可能同一张票被打印两次多多次,打印的票号码为0甚至是负数等原因在这一段代码中
if
(tickets>0)
System.out.println(Thread.currentThread().getName()+
"issalingticket"
+tickets--);
假如当tickets=1的时候,线程1刚刚判断完
if
(tickets>0),正要处理下面的语句的时候,cpu被线程2给抢走,线程2开始执行,当线程2执行完一个run方法,这里的tickets会减少1,这时候tickets为0,然后跳转到线程1的中断的地方继续执行,因为之前线程1判断过if
(tickets>0),所以这里不再需要判断,直接执行System.out.println(Thread.currentThread().getName()+
"issalingticket"
+tickets--);,将会打印出为0的票,也就意味着最后一张票卖了2次


4.2同步代码块

使用synchronized方法:保证代码块的内容是“原子的”(物理中原子的也是可以分割的,所以我一般不说原子),保证里面的内容只能被一个线程在执行,必须等执行的线程离开后才能让其他线程执行,也就和独木桥差不多。
123
synchronized(object){

//这里写代码块

}
独木桥会让我们的过桥效率降低,同样,同步代码块也会降低代码的执行速度,所以,如果确定代码是安全的,就不要使用同步代码块了。
同步代码块实现同步的原理:任何类型的对象都有一个标志位,该标志位具有0,1两种状态,其开始为1,当执行到synchrozied方法之后,object对象标识位变为0,另外一个线程执行到synchrozed方法之后,将会先判断这个状态,如果发现是0,就暂时阻塞。可以把这个标志位理解成一个箱子的锁,该箱子只能放一个人的东西。
上述卖票程序的ThreadTest类可以如下:

12345678910111213141516171819202122232425262728
package
firstTread;


/**

*@authorzhaikaishun

*

*/

public
class
ThreadTest
implements
Runnable{
//现在是实现Runnable接口

private
int
tickets=
100
;

Stringstr=
new
String(
""
);
//这里设置一个对象,任意一个对象都可以

public
void
run(){

while
(
true
){

//模拟买票程序,每次调用这个方法,ticket就会减一张

synchronized
(str){

//这里写代码块

if
(tickets>
0
){

try
{

Thread.sleep(
10
);

}
catch
(Exceptione){

System.out.println(e.getMessage());

}


System.out.println(Thread.currentThread().getName()

+
"issalingticket"
+tickets--);

}

}

}

}

}
结果:
注意:
Stringstr=
new
String(
""
);
这个标志对象,相当于监听对象,必须放在run方法的外面,如果放在run方法里面,四个线程每次调用run方法,就会产生4个监听对象,这四个同步监视器是4个不同的对象,会导致彼此之间不能同步。

4.3.同步函数

上述是对代码块进行的同步,同样,我们也能对某一个方法进行同步,只需要在同步的函数前加上关键字synchronized即可
例如上述代码可以写成:

1234567891011121314151617181920212223242526
package
firstTread;

/**

*@authorzhaikaishun

*

*/

public
class
ThreadTest
implements
Runnable{
//现在是实现Runnable接口

private
int
tickets=
100
;

Stringstr=
new
String(
""
);
//这里设置一个对象,任意一个对象都可以

public
void
run(){

while
(
true
){

//模拟买票程序,每次调用这个方法,ticket就会减一张

sale();

}

}

public
synchronized
void
sale(){
//同步方法

if
(tickets>
0
){

try
{

Thread.sleep(
10
);

}
catch
(Exceptione){

System.out.println(e.getMessage());

}

System.out.println(Thread.currentThread().getName()

+
"issalingticket"
+tickets--);

}

}

}
当有一个线程进入了synchronized方法(获得监视器),其他线程就不能进入通一个对象所有使用了synchronized修饰的方法,直到第一个对象执行完他所在的synchronized方法(离开监视器)。思考:既然synchronized方法需要有一个标志位,那么同步方法的标志位是什么呢,这里我先给出答案,同步方法的标志对象就是所在的对象,即this。

4.4.代码块与函数间的同步

请看下面方法,通过一个str的取值,来判断是代码块还是函数间的同步代码清单ThreadDemo6
1234567891011121314151617181920
package
firstTread;


/**

*@authorzhaikaishun

*

*/

public
class
ThreadDemo6{


public
static
void
main(String[]args){


ThreadTestt=
new
ThreadTest();

new
Thread(t).start();
//thread对象调用。

//让线程暂停一会儿才直观

try
{Thread.sleep(
1
);}
catch
(Exceptione){};


t.str=
new
String(
"method"
);
//如果str是method,调用同步函数

new
Thread(t).start();


}

}
代码清单ThreadTest
123456789101112131415161718192021222324252627282930313233343536373839404142434445
package
firstTread;


/**

*@authorzhaikaishun

*

*/

public
class
ThreadTest
implements
Runnable{
//现在是实现Runnable接口

private
int
tickets=
100
;

Stringstr=
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
(Exceptione){

System.out.println(e.getMessage());

}

System.out.println(Thread.currentThread().getName()

+
"issalingticket"
+tickets--);

}

}

}

}

}


public
synchronized
void
sale(){
//同步方法

if
(tickets>
0
){

try
{

Thread.sleep(
10
);

}
catch
(Exceptione){

System.out.println(e.getMessage());

}

System.out.print(
"函数方法在执行:"
);

System.out.println(Thread.currentThread().getName()

+
"issalingticket"
+tickets--);

}

}

}
运行结果:由于代码块和函数使用的监听器不一样,所以他们没有同步如果想让他们同步,只需要设置相同的监听对象,将synchronized(str)改为synchronized(this)即可

5.死锁:

死锁比较少见,而且难于调试:所谓死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。其实很久之前学习数字电路,经常会遇到一些锁,这也是自动化的一些常见的问题,在计算机中,也有类似的东西,请看下图R1和R2,都只能被一个进程使用T1在使用R1,同时没有使用完R1的情况下,想使用R2T2在使用R2,同时在没有使用完R2的情况下,想使用R1这时,T1等待T2放弃使用R2,同时T2等待T1放弃使用R1,他们都不会放弃自己所使用的,于是产生了等待,将会一直僵持下去。下面这个例子就是线程1进去对象obj1的监视器,而线程2进入了obj2的监视器,这时候进入了obj1的监视器的线程还试图进入使用obj2作为监视器的方法中,这显然会被阻塞隔离,是进不去的;同时,进入了obj2的监视器的线程也试图进入使用obj1作为监视器的方法中,这也显然会被阻塞隔离,是进不去的。然后双方一致僵持着,程序停滞不前,这就是我们所谓的死锁,代码清单如下代码清单:死锁例子
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
package
deadlock;
public
class
RunnableTest
implements
Runnable{

private
int
flag=
1
;

private
static
Objectobj1=
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
(InterruptedExceptione){

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
(InterruptedExceptione){

e.printStackTrace();

}

synchronized
(obj1){

System.out.println(
"进入了obj1"
);

}

}

}

}


public
static
void
main(String[]args){

RunnableTestrun01=
new
RunnableTest();

RunnableTestrun02=
new
RunnableTest();

run01.flag=
1
;

run02.flag=
0
;

Threadthread01=
new
Thread(run01);

Threadthread02=
new
Thread(run02);

System.out.println(
"线程开始喽!"
);

thread01.start();

thread02.start();

}

}
结果:一直处于僵持状态

6.线程间的通信

我们先从下面一个例子引出线程中的通信下面例子讲的是一个生产和消费的关系,生产一样东西,取走这样东西。这个程序是每生产出一个PDD(人名),并且给这个人赋值为男然后再取出来,然后生产一个“娇妹”(人名),并且赋值为女,然后再取出来。代码清单如下。一个类Q,用来存储数据Q:
123456789101112131415
package
communication;
public
class
Q{

private
Stringname=
"PDD"
;

private
Stringsex=
"男"
;

public
synchronized
void
put(Stringname,Stringsex){

this
.name=name;

try
{Thread.sleep(
1
);}
catch
(Exceptione){System.out.println(e.getMessage());}

this
.sex=sex;

}

public
synchronized
void
get(){


System.out.println(name+
"----"
+sex);

}

}
生产者类Producer:生产数据
123456789101112131415161718
package
communication;
public
class
Producer
implements
Runnable{

Qq=
null
;

public
Producer(Qq){

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:获取数据
1234567891011121314
package
communication;
public
class
Customer
implements
Runnable{

Qq=
null
;

public
Customer(Qq){

this
.q=q;

}

public
void
run(){

while
(
true
){

q.get();

}

}


}
主方法:
123456789101112
package
communication;
public
class
ThreadCommunication{
public
static
void
main(String[]args){

Qq=
new
Q();

new
Thread(
new
Producer(q)).start();

try
{Thread.sleep(
1
);}
catch
(Exceptione){System.out.println(e.getMessage());}

new
Thread(
new
Customer(q)).start();
}

}
运行结果:.....
PDD----男
PDD----男
PDD----男
PDD----男
娇妹----女
娇妹----女
娇妹----女
娇妹----女
娇妹----女
娇妹----女
娇妹----女
.......分析:这并不是我们想要的,我们想要的是下面这种类型的,producer每存放一次数据,customer取一次数据,反之,producer必须等customer取完数据之后才能开始存数据。这就是要将的线程间的通信问题,Java通过Object的wait,notify,notifyAll这几个方法实现线程间的通信。PDD----男娇妹----女PDD----男娇妹----女PDD----男娇妹----女wait:告诉当前线程放弃监视器并且进入线程休眠状态,直到其他线程进入相同的监视器并且调用notify为止。notify:唤醒同一对象监视器中调用wait的第一个线程。notifyAll:唤醒同一对象监视器中调用wait的所有线程,具有优先级高的线程将会被先唤醒。如果想让上面的程序满足我们的要求,我们可以在类Q中定义一个新的成员变量bFull来标示数据存储空间的状态,当Customer取走数据后,bFull为false;当Producer存入数据后,bFull为true。只有bFull为true时,Customer才能取走数据,只有当bFull为False时Producer才能放入数据Q的清单如下:
123456789101112131415161718192021222324252627282930313233
package
communication;
public
class
Q{

private
Stringname=
"PDD"
;

private
Stringsex=
"男"
;

boolean
bFull=
false
;

public
synchronized
void
put(Stringname,Stringsex){

if
(bFull)

try
{

wait();

}
catch
(InterruptedExceptione1){

//TODOAuto-generatedcatchblock

e1.printStackTrace();

}

this
.name=name;

try
{Thread.sleep(
1
);}
catch
(Exceptione){System.out.println(e.getMessage());}

this
.sex=sex;

bFull=
true
;

notify();

}

public
synchronized
void
get(){

if
(!bFull)

try
{

wait();

}
catch
(InterruptedExceptione1){

//TODOAuto-generatedcatchblock

e1.printStackTrace();

}

System.out.println(name+
"----"
+sex);

bFull=
false
;

notify();

}

}
结果:运行流程,自己看代码思考娇妹----女
PDD----男
娇妹----女
PDD----男
娇妹----女
PDD----男
娇妹----女参考文献:【1】J2SE进阶(java研究组织精品图书)【2】张孝祥-Java就业教程【3】java编程思想【4】java核心技术卷1
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: