您的位置:首页 > 职场人生

黑马程序员----java线程

2015-06-10 22:55 661 查看
——- android培训java培训、期待与您交流! ———-

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程。比如在Windows系统中,一个运行的xx.exe就是一个进程。

线程是指进程中的一个执行任务(控制单元),一个进程中可以运行多个线程,多个线程可共享数据。

多进程:操作系统中同时运行的多个程序;

多线程:在同一个进程中同时运行的多个任务;

一个进程至少有一个线程,为了提高效率,可以在一个进程中开启多个控制单元。多线程存在一个特性:随机性,CPU在瞬间不断切换去处理各个线程,可以理解成多个线程在抢cpu资源。

进程与线程的区别:

1.进程有独立的进程空间,进程中的数据存放空间(堆空间和栈空间)是独立的。

2.线程的堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间可以影响的。

创建新线程的两种方法:

1.继承Thread类

子类覆写父类中的run方法,将线程运行的代码存放在run中。

建立子类对象的同时线程也被创建。

通过调用start方法开启线程。

2.实现Runnable接口

子类覆盖接口中的run方法。

通过Thread类创建线程,并将实现了Runnable接口的子类对象作为参数传递给Thread类的构造函数。

Thread类对象调用start方法开启线程。

两种方式比较

A extends Thread:

简单;不能再继承其他类了(Java单继承);同份资源不共享(将资源定义为静态字段也可共享)。

A implements Runnable:(推荐)

多个线程共享一个目标资源,适合多线程处理同一份资源;该类还可以继承其他类,也可以实现其他接口。

线程的生命周期:



新建:当程序使用new创建一个线程后,该线程处于新建状态,此时他和其他java对象一样,仅仅由Java虚拟机为其分配内存并初始化成员变量值。
Thread r = new Thread()


就绪:当线程对象调用start()方法后,该线程处于就绪状态,线程计入线程队列排队,此时该状态线程并未开始执行,它仅表示可以运行了。至于该线程何时运行,取决于JVM线程调度器的调度。
r.start()


运行:若处于就绪状态的线程获得了CPU,开始执行run()线程执行体,该线程处于执行状态。

阻塞:线程运行过程中需要被中断,目的是是其他的线程获得执行的机会。该状态就会进入阻塞状态。

注意:阻塞状态不能直接转成运行状态,阻塞状态只能重新进入就绪状态。

**死亡**run()执行完成,线程正常结束;

线程抛出未捕获的Exception或Error;

调用线程的stop()。(易导致死锁,不推荐)

注意:

主线程结束后,其他线程不受其影响,不会随之结束;

一旦子线程启动起来后,就拥有和主线程相等地位,不受主线程影响。

测试线程是否活着,可用线程对象的isAlive()方法。当线程处于就绪,运行,阻塞状态返回true。当线程处于新建和死亡状态,返回false。

已死亡的线程是不可能通过start()方法唤醒线程的。

否则引发IllegalThreadStateException异常;

示例代码1:继承方式创建新线程

public class ThreadDemo {
public static void main(String[] args) {
/**匿名内部内以继承的方式创建一个名称为‘新线程’的线程,并启动*/
new Thread("新线程"){
//覆写run方法,在新线程中循环打印1~10及线程名
@Override
public void run() {
for(int i=0; i<10;i++){
System.out.print(Thread.currentThread().getName()+"-----"+i);
}
}
}.start();

for(int i=0; i<10;i++){
//在主线程中循环打印1~10及线程名
System.out.print(Thread.currentThread().getName()+"-----"+i);
}
}
}
打印结果:
main-0
新线程-0
新线程-1
main-1
新线程-2
main-2
新线程-3
新线程-4
新线程-5
main-3
新线程-6
新线程-7
main-4
新线程-8
新线程-9
main-5
main-6
main-7
main-8
main-9


可以看出主线程和新线程随机切换

实例代码2:实现方式创建新线程

public class Demo1 {
public static void main(String[] args) {
//匿名内部内实现Runnable接口创建新Runnable对象
Runnable run = new Runnable() {
int num= 10;
public void run() {
//多次循环,保证10能被用完
for (int i = 0; i < 20; i++) {
if(num>0){
//降低线程速度,提高冲突出现几率
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//打印num并将其减去一,循环打印10~1
System.out.println(Thread.
4000
currentThread().getName()+num--);
}
}
}
};
//以Runnable为参数,创建自定义名称的线程,由于两个线程公用一个Runnable对象,所以Runnable对象中的num变量也被公用,从而达到共享资源。
new Thread(run,"线程1---").start();
new Thread(run,"线程2---").start();
}
}
打印结果:
线程1---10
线程2---10
线程1---9
线程2---8
线程1---7
线程2---6
线程1---5
线程2---4
线程1---3
线程2---2
线程1---1
线程2---0


可以看出两个线程公用num,然后随机交替使用num,但由于线程1尚未释放num(读取num,但还没来得对其重新赋值),线程2就切换进来,从而导致打印了两个10以及一个0;解决方法就是实现线程的同步。

线程同步:

当多个线程访问同一份数据的时候,很容易出现线程安全的问题。线程同步可以实现,多个线程在同一个时间段内只能有一个线程进行能对共享的数据进行操作,其它线程要等到该线程完成后才可以继续执行。

同步代码使用注意事项:
1.两个以上的线程同时访问一份资源时,为了避免线程安全问题使用同步代码。
2.同步线程必须使用同一个同步监听对象才能达到目的。
3.同步代码能解决线程安全问题,但会降低效率,当一个线程未释放资源时,其他线程必须等待。
4.同步代码块可自定义同步监听对象。非静态同步方法同步监听对象为this。静态同步方法同步监听对象为方法所在的字节码文件对象(this.getclass;类名.class )。
5.不能将run方法定义为同步方法,否则只能一个线程执行完run方法之后,其他线程才能进入run方法,从而使多线程没有意义。
6.注意同步代码的范围,因为同步代码范围的错误会导致同步失败。


线程同步的三种方法:

1.同步代码块:

格式:synchronized(同步监听对象){同步代码块}

示例代码

public class Demo1 {
public static void main(String[] args) {
//匿名内部内实现Runnable接口创建新Runnable对象
Runnable run = new Runnable() {
int num= 10;
public void run() {
for (int i = 0; i < 20; i++) {
//将判断num大小,打印num及对num的重新赋值放入同步代码块中
synchronized (this) {
if(num>0){
//降低线程速度,提高冲突出现几率
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+num--);
}
}
}
}
};
//以Runnable为参数,创建自定义名称的线程,由于两个线程公用一个Runnable对象,所以Runnable对象中的num变量也被公用,从而达到共享资源。
new Thread(run,"线程1---").start();
new Thread(run,"线程2---").start();
}
}
打印结果:
线程1---10
线程1---9
线程1---8
线程1---7
线程1---6
线程1---5
线程2---4
线程2---3
线程2---2
线程1---1


2.同步方法

同步方法等同于同步代码块的简写方式,监听对象只能是this,一般建议使用同步代码块。

格式:synchronized 返回值类型 方法名(参数列表){代码}

示例代码:

public class Demo1 {
public static void main(String[] args) {
//匿名内部内实现Runnable接口创建新Runnable对象
Runnable run = new Runnable() {
int num= 10;
public void run() {
for (int i = 0; i < 20; i++) {
//调用同步方法
function();
}
}

//将判断num大小,打印num及对num的重新赋值放入同步方法中
public synchronized void function(){
if(num>0){
//降低线程速度,提高冲突出现几率
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+num--);
}
}
};
//以Runnable为参数,创建自定义名称的线程,由于两个线程公用一个Runnable对象,所以Runnable对象中的num变量也被公用,从而达到共享资源。
new Thread(run,"线程1---").start();
new Thread(run,"线程2---").start();
}
}


3.可重入锁

java.util.concurrent.locks.ReentrantLock

一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

此类的构造方法接受一个可选的公平参数ReentrantLock(boolean fair) 。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。

示例代码

class X {
private final ReentrantLock LOCK = new ReentrantLock();
// ...

public void m() {
lock.lock();  // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}


实例代码

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {

public static void main(String[] args) {
//匿名内部内创建Runnable对象
Runnable run = new Runnable() {
int num =10;
//创建一个可重入锁
private final ReentrantLock LOCK = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 20; i++) {
//在需要同的代码之前锁定
LOCK.lock();
try{
//需要同步的代码块房子try中
if(num>0){
try {
Thread.currentThread().sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"--->"+num--);
}
}finally{
//将解锁代码放在finnaly里面,确保锁的释放
LOCK.unlock();
}
}
}
};

//启动两个新的线程
new Thread(run).start();
new Thread(run).start();
}

}
输出结果
Thread-0--->10
Thread-0--->9
Thread-0--->8
Thread-0--->7
Thread-0--->6
Thread-0--->5
Thread-0--->4
Thread-0--->3
Thread-0--->2
Thread-0--->1


注意事项:

java5开始出现,同步锁一定要放在线程公用的代码里面

线程死锁

当两个线程相互等着对方释放同步监听器;

等着要对方的结果后才能继续执行就会发生死锁。

常见死锁原因为同步代码嵌套,两个线程使用了两个同步监听对象,而且使用的顺序不同,线程1监听对象a,再监听对象b,二线程2先监听对象b,再监听对象a,这是可能出现线程1锁定对象a,尚未锁定对象b,此时线程2进来锁定了对象b,然后判断对象a被占用,只能等待,而此时线程1也在等待b被释放,由此造成线程死锁

示例代码:

public class DeadlockDemo {

public static void main(String[] args) {
//定义两个同步监听对象
final Object lock1 = new Object();
final Object lock2 = new Object();

Runnable run1 = new Runnable() {
public
edbd
void run() {
for (int i = 0; i < 100; i++) {
//线程1先锁定监听对象1后锁定监听对象2
synchronized (lock1) {
//锁定1后听一下,给线程2锁定监听对象2创造机会
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("1----->"+i);
}
}
}

}
};

Runnable run2 = new Runnable() {
//线程2先锁定监听对象2后锁定监听对象1
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (lock2) {
synchronized (lock1) {
System.out.println("2----->"+i);
}
}
}

}
};

new Thread(run1).start();
//先让线程1跑一下,然后线程2进来抢监听对象2,抢到了就锁死
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(run2).start();
}
}


线程通信:

wait():让当前线程放弃监视器进入等待,直到其他线程调用同一个监视器并调用notify()或notifyAll()为止。

notify():唤醒在同一对象监听器中调用wait方法的第一个线程。

notifyAll():唤醒在同一对象监听器中调用wait方法的所有线程

wait()、notify()、notifyAll(),这三个方法属于Object 不属于 Thread,这三个方法必须由同步监视对象来调用,两种情况:

1.synchronized修饰的方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中调用这三个方法;

2.synchronized修饰的同步代码块,同步监视器是括号里的对象,所以必须使用该对象调用这三个方法;

3.要是使用的是Lock对象来保证同步的,系统中不存在隐式的同步监视器对象,此时,Lock代替了同步方法或同步代码块,Condition代替了同步监视器的功能;Condition对象通过Lock对象的newCondition()方法创建;

里面方法包括:

await(): 等价于同步监听器的wait()方法;

signal(): 等价于同步监听器的notify()方法;

signalAll(): 等价于同步监听器的notifyAll()方法;

示例:

需要解决的问题,一个对象有两个属性,一个线程不停的更改此对象的属性,另一个对象不停的读取,当写线程只更改完一个属性,读线程就切换进来,就会导致属性之间反生错位,或者写线程只写过一次,而读线程连续读了两次也会导致错误,此时可以如下解决:

class Person{
//定义一个人,具有姓名及年龄两个字段并复习toString方法
private String name;
private String gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return (name+"---->"+gender);
}
}

class Creater extends Thread{
//写线程,不停的更改person的属性值
Person p;
public Creater(Person p){
this.p=p;
}
@Override
public void run() {
for(int i=0;i<100;i++){
//当i为偶数是,p是男,并在名字后加上男女以示区分
if(i%2==0){
p.setName(i+"男");
//让线程等待一会,不然一个线程跑完了,另一个还没进来
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
p.setGender("男");
}else {
p.setName(i+"女");
p.setGender("女");
}
}
}
}

class Reader extends Thread{
//读线程,不停的读取p的属性值,并打印
Person p;
public Reader(Person p){
this.p=p;
}

public void run(){
for(int i=0;i<100;i++){
try {
//让线程等待一会,避免其独占资源
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(p);
}
}
}

开始写和读
public class Demo1 {
public static void main(String[] args) {
Person p = new Person();
//将同一个p传递给两个线程,让他们共同操作同一个对象
new Creater(p).start();
new Reader(p).start();
}
}

以此运行的打印结果
2男---->女
2男---->男
5女---->女
8男---->女
8男---->女
9女---->女
9女---->女
9女---->女
9女---->女
9女---->女


可以看出不加同步时,会出现姓名和性别不匹配

现将写和读的代码同步,监听对象为他们共有的对象p“

class Reader extends Thread{
//读线程,不停的读取p的属性值,并打印
Person p;
public Reader(Person p){
this.p=p;
}

public void run(){
for(int i=0;i<10;i++){
try {
//让线程等待一会,避免其独占资源
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (p) {
System.out.println(p);
}
}
}
}
public Creater(Person p){
this.p=p;
}
@Override
public void run() {
for(int i=0;i<10;i++){
//当i为偶数是,p是男,并在名字后加上男女以示区分
synchronized(p){
if(i%2==0){
p.setName(i+"男");
//让线程等待一会,不然一个线程跑完了,拎一个还没进来
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
p.setGender("男");
}else {
p.setName(i+"女");
p.setGender("女");
}
}
}
}
}
此时运行一次的结果为
0男---->男
7女---->女
8男---->男
9女---->女
9女---->女
9女---->女
9女---->女
9女---->女
9女---->女
9女---->女


第一步解决了姓名和性别不匹配的问题,但是还是有的属性对读了多次,有的一次都没读,要保证每一个都能被读到就可以考虑notify和wait了;

首先给Person添加以个字段isNull,当Person尚未被定义或被读取过一次后,其值就为true,然后reader及Creater监测此值决定要不要工作。

代码

class Creater extends Thread{
//写线程,不停的更改person的属性值
Person p;
public Creater(Person p){
this.p=p;
}

@Override
public void run() {
for(int i=0;i<10;i++){
synchronized(p){
//当p为空,向p中写入数据,然后将状态改为非空,提醒读线程去读取,然后检查是否被读取,没有则等待
while(p.isNull){
//当i为偶数是,p是男,并在名字后加上男女以示区分
if(i%2==0){
p.setName(i+"男");
//让线程等待一会,不然一个线程跑完了,另一个还没进来
try {Thread.currentThread().sleep(100);} catch (InterruptedException e) {e.printStackTrace();}
p.setGender("男");
}else{
p.setName(i+"女");
try {Thread.currentThread().sleep(100);} catch (InterruptedException e) {e.printStackTrace();}
p.setGender("女");
}
//
p.isNull=false;
p.notify();
}
try {p.wait();} catch (InterruptedException e) {e.printStackTrace();}
}
}
}
}

class Reader extends Thread{
//读线程,不停的读取p的属性值,并打印
Person p;
public Reader(Person p){
this.p=p;
}

public void run(){
for(int i=0;i<10;i++){
//让线程等待一会,避免其独占资源
try {Thread.currentThread().sleep(100);} catch (InterruptedException e) {e.printStackTrace();}
synchronized (p) {
//判断p是否为空,为空的则等待,不为空则读取并将其状态改为空,然后提醒写线程工作
while(p.isNull)try {p.wait();} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(p);
p.isNull=true;
p.notify();

}
}
}
}

运行结果:
0男---->男
1女---->女
2男---->男
3女---->女
4男---->男
5女---->女
6男---->男
7女---->女
8男---->男
9女---->女


线程常用方法:

join方法:调用join方法的线程对象强制运行,该线程强制运行期间,其他线程无法运行,必须等到该线程结束后其他线程才可以运行。有人也把这种方式成为联合线程

setDaemon方法:后台线程:处于后台运行,任务是为其他线程提供服务。也称为“守护线程”或“精灵线程”。JVM的垃圾回收就是典型的后台线程。

特点:若所有的前台线程都死亡,后台线程自动死亡。

设置后台线程:Thread对象setDaemon(true);

setDaemon(true)必须在start()调用前。否则出现IllegalThreadStateException异常;

前台线程创建的线程默认是前台线程;创建线程是后台线程时,新线程也是后台线程。

判断是否是后台线程:使用Thread对象的isDaemon()方法;

sleep:调用sleep()后,在指定时间段之内,该线程不会获得执行的机会。

线程优先级:每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。默认情况下main线程具有普通的优先级(5),而它创建的线程也具有普通优先级。

Thread对象的setPriority(int x)和getPriority()来设置和获得优先级。

yield:Thread的静态方法,可以是当前线程暂停,但是不会阻塞该线程,而是进入就绪状态。暂停当前正在执行的线程对象,并执行其他线程;所以完全有可能:某个线程调用了yield()之后,线程调度器又把他调度出来重新执行。

过时方法,易导致死锁:

stop:终止线程

马上让线程停止运行,并释放该线程所持有的锁,该操作无法保证对象的内部状态正确;最好通过一个参数来控制线程在什么时候结束,不要直接调用此方法。

suspend:挂起线程

使线程进入“阻塞”状态,该状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行,在被resume方法调用前,不可用.

如果要suspend的目标线程对一个重要的系统资源持有锁,那么没任何线程可以使用这个资源直到要suspend的目标线程被resumed。

resume:恢复线程

恢复被suspend方法挂起的线程
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java