牛刀小试 – 详解Java多线程


线程与多线程的概念

关于线程与多线程的较详细的理解可以参考:线程的解释 和多线程的解释。

而我们要做的是,对其进行“精炼"。我们每天都在和电脑、手机打交道,每天都在使用各种各样的利用软件。

打开上电脑的任务管理器,就能够看到有1项名为"进程"的栏目,点击到里面可能就会发现1系列熟习的名称:QQ,360等等。

所以首先知道了,QQ、360之类的利用软件在计算机上被称为1个进程。


而1个利用程序都会有自己的功能,用以履行这些进程当中的个别功能的程序履行流就是所谓的线程。

所以,线程有时候也被称为轻量级进程,是程序履行流当中的最小单元。

线程的划分尺度小于进程,其不能够独立履行,必须依存在利用程序中,由利用程序提供多个线程履行控制。

进程在履行进程中具有独立的内存单元,而多个线程同享内存,所以能极大地提高了程序的运行效力

所以简而言之的概括的话,就是:1个程序最少有1个进程,1个进程最少有1个线程。

以360杀毒来讲,里面的1项功能任务“电脑体检”就是该利用程序进程中的1个线程任务。

而除此任务以外,我们还可以同时进行多项操作。例如:“木马查杀”、“电脑清算”等。

那末,以上同时进行的多项任务就是所谓的存活在360利用程序进程中的多线程并发。

多线程的利与弊

多线程的有益的地方,不言而喻。在传统的程序设计语言中,同1时刻只能履行单任务操作,效力非常低。

假定在某个任务履行的进程中产生梗塞,那末下1个任务就只能1直等待,直至该任务履行完成后,才能接着履行。

而得益于多线程能够实现的并发操作,即便履行进程中某个线程因某种缘由产生阻塞,也不会影响到其它线程的履行。

也就是说,多线程并发技术带来的最大好处就是:很大程度上提高了程序的运行效力。


似乎百里而无1害的多线程并发技术,还有弊端吗?从某种程度上来讲,也是存在的:会致使任务履行效力的下降。

之所以这样讲,是由于所谓的“并发”其实不是真正意义上的并发,而是CPU在多个线程之间做着快速切换的操作。

但CPU的运算速度肯定是远远高于人类的思惟速度,所以就带来了1种“并发”的错觉。

那就不难想象了:假定某1进程中,线程A与线程B并发履行,CPU要做的工作就是:

不断快速且随机的在两个线程之间做着切换,分别处理对应线程上的线程任务,直到两个线程上的任务都被处理完成。


那末,也就能够斟酌这样的情况:CPU履行完本来线程A的线程任务只需要5秒;但如今由于另外一个线程B的并发加入。

CPU则不能不分出1部份时间切换到线程B上进行运算处理。因而可能CPU完成该线程任务A的时间反而延长到了7秒。

所以所谓的效力下降,就是指针对某单个任务的履行效力而言的。

也就是说,如果在多线程并发操作时,如果有某个线程的任务你认为优先级很高。那末则可以:

通过设置线程优先级或通过代码控制等手段,来保证该线程享有足够的“特权”。

注:Java中设置线程优先级,实际上也只是设置的优先级越大,该线程被CPU随机访问到的几率会相对高1些。


这个进程可以替换成1些实际生活中的情形来进行思考。快过年了,以家庭团圆为例。

假定你除准备炒1桌子美味的菜肴以外,过年自然还要有1顿热腾腾的饺子。那末:

传统单任务的操作进程可以被理解为:先把准备的菜肴都做好;菜都端上桌后便开始煮饺子。

这样做的坏处就是:如果在炒菜的中途产生1些意外情况,那末随着炒菜动作的暂停。煮饺子的动作也将被无穷期延后。

而对应于多线程并发的操作就是:1边炒菜,1边煮饺子。这时候你就是CPU,你要做的动作多是这样的:

炒菜的中途你能会抽空去看看锅里的饺子煮好没有;发现没有煮好,又回来继续炒菜。炒好1道菜后,再去看看饺子能出锅了没。

由此你发现,你做的工作与CPU处理多线程并发的工作是1样的:不断的在“煮饺子”与“炒菜”两个任务之间做着切换。

线程的周期及状态

Java中线程的全部生命周期基本可以划分为以下4种状态:

  • new – 创建状态:顾明思议,Java通过new创建了1个线程对象过后,该线程就处于该状态。
  • runnable– 可履行状态:也就是指在线程对象调用start()方法落后入的状态。但需要注意的是该状态是“可履行状态”而不是“履行状态”。也就是说,当1个线程对象调用start方法后,只是意味着它获得到了CPU的履行资格,其实不代表马上就会被运行(CPU此时固然可能恰好切换在其它线程上做处理),只有具有了CPU当前履行权的线程才会被履行。
  • non Runnable– 不可履行/阻塞状态:也就是通过1些方法的控制,使该线程暂时释放掉了CPU的履行资格的状态。但此时该线程依然是存在于内存中的。
  • done退出状态:简单的说也就是当线程进入到退出状态,就意味着它灭亡了,不存在了。Java里通过stop方法可以强迫线程退出,但该方法由于可能引发死锁,所以是不建议使用的。另外1种进入该状态的方式,是线程的自然灭亡,也就当1个线程的任务被履行终了以后,就会自然的进入到退出状态。

以下是Java中1些用于改变线程状态的方法列表:

Java中创建线程的方式

Java里面创建的线程的方式主要分为:

  • 继承Thread类,并覆写run方法。
public class Demo extends Thread{

@Override
public void run() {
//…
}
}

  • 实现Runnable接口,并定义run方法:
public class Demo implements Runnable{

@Override
public void run() {
//…
}
}

  • 还有1种情况,如果你认为未将线程单独封装出来的时候,可以通过匿名内部类来实现。

开发中通常选择通过实现Runnbale接口的方式创建线程,好处在于:

1.Java中不支持多继承,所以使用Runnable接口可以免此问题。

2.实现Runnable接口的创建方式,等因而将线程要履行的任务单独分离了出来,更符合OO要求的封装性。

多线程的安全隐患

春运将至了,还是先通过1个老话题来看1个多线程并发的例子,来看看多线程可能存在的安全隐患。

package com.tsr.j2seoverstudy.thread;

public class TicketDemo {

public static void main(String[] args) {
Runnable sale = new TicketOffice();
Thread t1 = new Thread(sale, "1号售票窗口");
Thread t2 = new Thread(sale, "2号售票窗口");
t1.start();
t2.start();
}
}

class TicketOffice implements Runnable {
// 某车次的车票存量
private int ticket_num = 10;

@Override
public void run() {
while (true) {
if (ticket_num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
String output = Thread.currentThread().getName() + "售出了"
+ ticket_num– + "号票";
System.out.println(output);
} else {
break;
}
}
}
}

/*
可能出现以下的输出结果:
2号售票窗口售出了10号票
1号售票窗口售出了9号票
1号售票窗口售出了8号票
2号售票窗口售出了7号票
1号售票窗口售出了6号票
2号售票窗口售出了5号票
1号售票窗口售出了4号票
2号售票窗口售出了3号票
1号售票窗口售出了2号票
2号售票窗口售出了1号票
1号售票窗口售出了0号票
*/

按我们的理想的想法是:两个售票处共同完成某车次列车的10张车票:坐位号为1号到10号的车票的售票工作。

而根据程序的输出结果,我们发现的安全隐患是:有坐位号为0号的车票被售出了,买到这张车票的顾客该找谁说理去呢?


我们来分析1下为何会出现这样的毛病情况,其构成的缘由多是这样的:

当线程1履行完“1号售票窗口售出了2号票”以后,根据while循环的规则,再1次开始售票工作。

首先判断while为true,进入到while循环体;接着判断if语句,此时余票数为1张(也就是只剩下坐位号为1的车票了)。

1大于0,满足判断条件,进入到if语句块当中。此时履行到"Thread.sleep(10)"语句。

OK,当前线程进入到梗塞状态,暂时失去了Cpu的履行资格。因而Cpu重新切换,开始履行线程2。


因而线程2开始履行线程任务,又是老模样:while判断 – if判断,由于上次线程1判断后还没履行售票工作,就被阻塞了。

所以这次if判断依然为"1>0",满足判断条件,继续履行,又履行到线程休眠语句,因而线程2也进入阻塞状态。

此时两个线程暂时都不具有履行资格,但我们指定线程休眠的时间为10毫秒,因而10毫秒后,可能两个线程都苏醒了,恢复了Cpu的履行资格。

面对两个都处于可履行状态的线程,Cpu又只好随机选择1个先履行了。因而Cpu选择了线程2,线程2恢复履行。

线程2开始做自己上次没做完的事,因而履行表达式和输出语句,因而得到输出信息"2号售票窗口售出了1号票"。


线程2继续履行while判断,没问题。再履行if判断"0>0",不满足判断条件,因而履行到了break语句。

线程2到此退出循环,完成了所有线程任务,因而自然灭亡进入done状态。

因而现在Cpu的履行权自然就属于线程1了,线程1也犹如线程21样,从美梦中醒来,开始上次没做完的事。

问题就在这里出现了,虽然这个时候,堆内存中寄存的对象成员变量“ticket_num”的值实际上已是0了。

但是!由于上1次线程1已经过了if判断进入到了if语句块以内。所以它将直接开始履行表达式,并输出。

就构成了我们看到的毛病信息:“1号售票窗口售出了0号票”。并且这个时候实际上余票数的值已是“⑴”了。


所以,实际上之所以我们在处理卖票的代码之前加上让线程休眠10毫秒的代码,目的也就是为了摹拟线程安全隐患的问题。

而根据这个例子我们能够得到的信息就是:之所以多线程并发存在着安全隐患,正是CPU的实际处理方式是在不同线程之间做着随机的快速切换。

这意味着它其实不会保证当处理1个线程的任务时,1定会履行完该次线程的所有代码才做切换。而是可能做到1半就切换了。


所以,我们可以归纳线程安全隐患之所以会出现的缘由就是由于:

  • 多个并发线程操作同1个同享数据
  • 操作该同享数据的代码不止1行,存在多行

解决线程安全隐患的方法 – 同步锁

既然已了解了线程安全隐患之所以产生,就是由于线程在操作同享数据的途中,其它线程被参与了进来。

那末我们想要解决这1类的安全隐患,自然就是保证在某个线程在履行线程任务的时候,不能让其余线程来捣乱。

在样的做法,在Java当中被称为同步锁,也就是说给封装在同步当中的代码加上1把锁。

每次只能由1个线程能够获得到这把锁,只有当前持有锁的线程才能履行同步当中的代码,其它线程将被拒之门外。

Java中对同步的使用方式通常分为两种,即:同步代码块和同步函数。关键字synchronized用以声明同步。其格式分别为:

//同步代码块
synchronized (对象锁) {
//同步代码
}

//同步函数
synchronized void method(){
//同步代码
}

通过同步我们就能够解决上面所说的“春节卖票”问题的安全隐患:

package com.tsr.j2seoverstudy.thread;

public class TicketDemo {

public static void main(String[] args) {
Runnable sale = new TicketOffice();
Thread t1 = new Thread(sale, "1号售票窗口");
Thread t2 = new Thread(sale, "2号售票窗口");
t1.start();
t2.start();
}
}

class TicketOffice implements Runnable {
// 某车次的车票存量
private int ticket_num = 10;
Object objLock = new Object();

@Override
public void run() {
while (true) {
synchronized (objLock) {
if (ticket_num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
String output = Thread.currentThread().getName() + "售出了"
+ ticket_num– + "号票";
System.out.println(output);
} else {
break;
}
}
}
}
}

再次运行该代码,就不会再出现之前的安全隐患。

这正是由于我们通过同步代码块,将希望每次只有有1个线程履行的代码封装了起来,为它们加上了1把同步锁(对象)。


同步最需要注意的地方,就是要保证锁的1致性。这是由于我们说过了:

同步的原理就是锁,每次当有线程想要访问同步当中的代码的时候,只有获得到该锁才能履行。

所以如果锁不能保证是同1把的话,自然也就实现不了所谓的同步了。

可以试着将定义在TicketOffice的成员变量objLock移动定义到run方法当中,就会发现线程安全问题又出现了。

这正是由于,将对象类型变量objLock定义为成员变量,它会随着该类的对象存储在堆内存当中,该变量在内存中独此1份。

而移动到run方法内,则会存储在栈内存当中,而每个线程都会在栈内存中,单独开辟1条方法栈。

这样就等于每一个线程都有1把独自的锁,自然也就不是所谓的同步了。


而同步函数的原理实际上与同步代码块是相同的,不同的只是将本来包括在同步代码块当中的代码单独封装到1个函数中:

private synchronized void saleTicket() {
while (true) {
if (ticket_num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
String output = Thread.currentThread().getName() + "售出了"
+ ticket_num– + "号票";
System.out.println(output);
} else {
break;
}
}
}

而另外1点值得说明的是,就是关于不同方式使用的锁的差别:

同步代码块:可使用任1对象锁。

同步函数:使用this作为锁。

静态同步函数:使用该函数所在类的字节码文件对象作为锁

死锁现象

提到同步,就不能不提到与之相干的1个概念:死锁。

死锁是指两个或两个以上的进程在履行进程中,因争取资源而酿成的1种相互等待的现象,若无外力作用,它们都将没法推动下去。

此时称系统处于死锁状态或系统产生了死锁,这些永久在相互等待的进程称为死锁进程。同理,线程也会出现死锁现象。

<span style="font-family:SimSun;font-size:12px;">package com.tsr.j2seoverstudy.thread;

public class DeadLockDemo {
public static void main(String[] args) {
Queue q1 = new Queue(true);
Queue q2 = new Queue(false);
Thread t1 = new Thread(q1, "线程1");
Thread t2 = new Thread(q2, "线程2");
t1.start();
t2.start();
}
}

class MyLocks {
public static final Object LOCK_A = new Object();
public static final Object LOCK_B = new Object();
}

class Queue implements Runnable {

boolean flag;

Queue(boolean flag) {
this.flag = flag;
}

@Override
public void run() {
String threadName = Thread.currentThread().getName();
while (true) {
if (flag) {
synchronized (MyLocks.LOCK_A) {
System.out.println(threadName + "获得了锁A");
synchronized (MyLocks.LOCK_B) {
System.out.println(threadName + "获得了锁B");
}
}
} else {
synchronized (MyLocks.LOCK_B) {
System.out.println(threadName + "获得了锁B");
synchronized (MyLocks.LOCK_A) {
System.out.println(threadName + "获得了锁A");
}
}
}

}

}

}</span>

上面的程序就演示了1个死锁的现象:

线程1开启履行后,判断标记为true,因而先获得了锁A,并输出信息。

此时CPU做切换,线程2开启履行,判断标记为false,首先获得锁B,并输出相干信息。

但这时候候不管CPU再怎样样切换,程序都已没法继续推动了。

由于线程1想要继续推动必须获得的资源锁B现在被线程2持有,反之线程2需要的锁A被线程1持有。

这正是由于两个线程由于相互争取资源而酿成的死锁现象。

死锁还是很蛋疼的,1旦出现,程序的调试和查错修改工作都会变得很麻烦

线程通讯 – 生产者与消费者的例子

关于多线程编程,类似于车站卖票的例子是1种常见的使用处径。

这类利用途径通常为:多个线程操作同享数据,并且履行的是同1个动作(线程任务)。

车站售票:多个线程都是操作同1组车票,并且都是履行同1个动作:出售车票。

那末在多线程当中的另外一个经典例子:生产者与消费者,就描写的是另外一种常见的利用途径。

多个线程操作同享数据,但是不同的线程之间履行的是不同的动作(线程任务),这就是线程通讯的使用。


不同线程间的通讯应当怎样样来完成,其手段是通过Object类当中提供的几个相干方法:

  • wait():在其他线程调用此对象的notify()方法或notifyAll()方法前,致使当前线程等待。
  • notify():唤醒在此对象监视器上等待的单个(任逐一个)线程。
  • notifyAll():唤醒在此对象监视器上等待的所有线程。

首先,我们可能会思考的1点就是:既然是针对线程之间相互通讯的方法,为何没有被定义在线程类,反而被定义在了Object类当中。

由于这些方法事实上我们可以视作是线程监视器的方法,监视器其实就是锁。

我们知道同步中的锁,可以是任意的对象,那末既然是任1对象调用的方法,自然1定被定义在Object类中。

可以将所有使用同1个同步的线程视作被存储在同1个线程池当中,而该同步的锁就是该线程池的监视器。

由该监视器来调度对应线程池内的各个线程,从而到达线程通讯的目的。


接下来就来看生产者与消费者的例子:

1.生产者生产商品;

2.消费者购买商品。

3.可能会同时存在多个生产者与多个消费者。

4.多个生产者中某个生产者生产1件商品,就暂停生产,并在多个消费者中通知1个消费者进行消费;

  消费者消费掉商品后,停止消费,再通知任逐一个生产者进行新的生产工作。

package com.tsr.j2seoverstudy.thread;

public class ThreadCommunication {
public static void main(String[] args) {
Queue q = new Queue();
Customer c = new Customer(q);
Producer p = new Producer(q);
Thread t1 = new Thread(c, "消费者1-");
Thread t2 = new Thread(c, "消费者2-");
Thread t3 = new Thread(p, "生产者1-");
Thread t4 = new Thread(p, "生产者2-");

t1.start();
t2.start();
t3.start();
t4.start();
}
}

class Queue {
//当前商品数量是不是为0
private boolean isEmpty = true;

//生产
public synchronized void put() {
String threadName = Thread.currentThread().getName();
//如果生产者线程进入,而现在还有剩余商品
while (!isEmpty) {
try {
wait();//则该生产者暂时等待,不进行生产
} catch (InterruptedException e) {
}
}
//否则则生产1件商品
isEmpty = false;
System.out.println(threadName + "生产了1件商品");
//唤醒阻塞的线程,通知消费者消费
this.notifyAll();

}

//消费
public synchronized void take() {
String threadName = Thread.currentThread().getName();
//消费者前来消费,如果此时没有剩余商品
while (isEmpty) {
try {
wait();//则让消费者先行等待
} catch (InterruptedException e) {
}
}
//否则则消费掉商品
isEmpty = true;
System.out.println(threadName + "消费了1件商品");
//通知生产者没有商品了,起来继续生产
this.notifyAll();
}
}

class Customer implements Runnable {
Queue q;

Customer(Queue q) {
this.q = q;
}

@Override
public void run() {
for (int i = 0; i < 5; i++) {
q.take();
}

}
}

class Producer implements Runnable {
Queue q;

Producer(Queue q) {
this.q = q;
}

@Override
public void run() {
for (int i = 0; i < 5; i++) {
q.put();
}

}
}

这就是对线程通讯1个简单的利用。而需要记住的是:关于线程的停止与唤醒都必须定义在同步中。

由于我们说过了,关于所谓的线程通讯工作。实际上是通过监视器对象(也就是锁),来完成对线程的停止或唤醒的操作的。

既然使用的是锁,那末自然必须被定义在同步中。并且,必须确保相互通讯的线程使用的是同1个锁。

这是10分重要的,试想1下,如果试图用线程池A的监视器锁A去唤醒另外一个线程池B内的某1个线程,这自然是办不到的。

简单解释下,你可能已注意到在上面的例子中,我是直接采取"wait()"和"notifyAll()"的方式来唤醒和阻塞线程的。

那末你应当明白这其实对应于隐式的"this.wait()"与"this.notifyAll()",而同时我们已说过了:

在同步方法中,使用的锁正是this。也就是说,在线程通讯中,你可以将同步锁this看作是1个线程池的对象监视器。

当某个线程履行到this.wait(),就代表它在该线程池内阻塞了。而通过this.notify()则可以唤醒阻塞在这个线程池上的线程。


而到了这里,另外一值得1提的1点就是:

Thread类的sleep()方法和Object类的wait()方法都可使当前线程挂起,而它们的不同的地方在于:

1:sleep方法必须线程挂起的时间,超过指定时间,线程将自动从挂起中恢复。而wait方法可以指定时间,也能够不指定。

2:线程调用sleep方法会释放Cpu的履行资格(也就是进入到non Runnable状态),但不会释放锁;

   而通过调用wait方法,线程即会释放cpu的履行资格,同时也会释放掉锁。

线程通讯的安全隐患

与之前说过的卖票用例1样,对线程通讯的通讯也应当谨慎谨慎,否则也可能会引发相干的毛病。常见的问题例如:

1、使用notify而不是notifyAll唤醒线程可能会出现的问题

我在最初接触多线程的时候,容易这样斟酌,既然想要到达的目的是:

生产者线程生产1件商品,则唤醒1个消费者线程。消费者进行消费,则唤醒1个生产者线程。

既然notify()方法用于唤醒单个线程,而notifyAll()用于唤醒所有线程,那使用notifyAll不是浪费效力吗?

后来明白,很惋惜的是,我们要做的是唤醒单个对方线程。而notify没有这么强大。

它只是随机的唤醒1个处于阻塞状态下的线程,所以如果使用notify(),可能会看到以下的毛病情况:

没错,操蛋,又出现了坑爹的死锁。为何出现这样的情况呢?我们来分析1下:

  • 我们创建的4个线程经调用start方法以后,都进入了可履行状态,具有CPU履行资格。
  • CPU随机切换,首先赋予“生产者1”履行权,生产者1开始履行。
  • 生产者1判断isEmpty为true,履行1次生产任务。当履行notify方法时,当前还没有任何可以唤醒的阻塞线程。
  • 生产者1继续while循环,判断isEmpty为flase。履行wait,因而生产者1进入阻塞状态。

履行到此,当前处于可履行状态的线程为:生产者2、消费者1、消费者2

  • CPU在剩下的3个可履行状态中随机切换到了生产者2,因而生产者2开始履行。
  • 生产者2判断isEmpty为false,履行wait方法,因而生产者2也进入到临时阻塞状态。

因而,当前处于可履行状态的线程变成了:消费者1、消费者2

  • CPU继续随机切换,此次切换到消费者1开始履行。
  • 消费者1判断isEmpty为false,因而履行1次消费,修改isEmpty为true。
  • 履行到notify()方法,唤醒任1阻塞状态的线程,因而唤醒了生产者2。
  • 消费者1继续while循环,判断isEmpty为true,因而履行wait,进入阻塞。

到此,当前处于可履行状态的线程变成了:生产者2、消费者2

  • 一样的,CPU这次切换到消费者2履行。
  • 消费者2判断isEmpty为true,因而履行wait,进入阻塞。

好了,处于可履行状态的线程只剩下:生产者2。

  • 那末,自然现在只能是轮到生产者2履行了。
  • 判断isEmpty为true,履行1次生产。修改isEmpty为false。
  • 通过notify()方法随机唤醒了生产者1线程。
  • 再次履行while循环,判断isEmpty为false后,进入阻塞。

至此,唯1处于可履行状态的线程变成了:生产者1

  • 生产者1线程开始履行。
  • 判断isEmpty为false,履行wait进入阻塞。

这下好了,4个线程都进入了阻塞状态,而不是灭亡状态。自然的,死锁了。


2、使用if而不是使用while判断isEmpty可能出现的问题

如果使用if而不是while对isEmpty进行判断,可能会出现的毛病为:

1、不同的生产者连续生产了多件商品,但消费者只消费掉其中1件。

2、1个生产者生产了1件商品以后,有多个消费者进行连续消费。

出现这样的安全问题是由于if的判断机制酿成的:通过if来判断标记,只会履行1次判断。

所以可能会致使不该运行的线程运行了,从而出现数据毛病的情况。

这类问题的出现也就是与我们上面说的“售票处售出0号票”的毛病类似。


JDK1.5以后的新特性

我们前面已说到了,关于生产者与消费者的问题中。

我们的目的是,每当1个线程履行终了1次任务后,只唤醒单1的对方线程。

而在JDK1.5之前,为了不死锁的产生,我们不能不使用notifyAll()来唤醒线程。

而这样做有1个缺点就在于:每次都要唤醒所有处于阻塞的线程,自然就会致使效力下降。


在JDK1.5以后,,Java提供了新的工具用于解决此类问题,就是:Lock和Condition接口。

简答的说,就是对将本来的同步锁synchronized与对象监视器进行了封装,分别对应于于Lock及Condition。

并且,重要的是相对1.5之前,新的工具具有更灵活及更广泛的操作。


1、Lock的使用及注意事项

1、通过Lock lock =  new ReentrantLock();获得1个Lock对象。

2、通过成员方法lock(),用于对代码进行同步管理。

3、通过成员方法unlock(),用于同步代码履行终了后,释放锁对象。

4、由于不管在同步代码的履行进程中是不是出现异常,最后都必须释放该锁,否则可能会致使死锁现象的产生。所以通常在使用lock时,都会遵守以下格式:

     lock.lock();

     try{

     {

      // 同步代码….

     }finally{

    lock.unlock();

       }

     }

2、对象监视器Condition的使用及注意事项

1、可以通过Lock对象使用成员方法newCondition()来获得1个新的监视器对象。

2、Condition分别使用await();signal();signalAll()来替换本来Object类当中的wait();notify();及notifyAll()方法。

3、同1个Lock对象可以具有多个不同的Condition对象。


请注意1个很关键的特性:同1个Lock对象可以具有多个不同的Condition对象

也就是说:通过此特性,我们可以获得多个Condition对象,将操作不同线程任务的线程分别寄存在不同的Condition对象当中。

例如在前面所说的生产者消费者例子当中,我们就能够生成两组监视器,1组监视生产者线程,1组监视消费者线程。

从而到达我们想要的每次只唤醒对方线程而不唤醒本方线程的目的,修改后的例子代码以下:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadCommunication {
public static void main(String[] args) {
Queue q = new Queue();
Customer c = new Customer(q);
Producer p = new Producer(q);
Thread t1 = new Thread(c,"消费者1-");
Thread t2 = new Thread(c,"消费者2-");
Thread t3 = new Thread(p,"生产者1-");
Thread t4 = new Thread(p,"生产者2-");

t1.start();
t2.start();
t3.start();
t4.start();
}
}

class Queue {
private int goodsTotal;
private boolean isEmpty = true;

final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();

public void put() {
String threadName = Thread.currentThread().getName();
lock.lock();
try{
while (!isEmpty) {
try {
notFull.await();
} catch (InterruptedException e) {
}
}
goodsTotal ++;
System.out.println(threadName + "生产了1件商品");
isEmpty = false;
notEmpty.signal();
}finally{
lock.unlock();
}

}

public synchronized void take() {
String threadName = Thread.currentThread().getName();
lock.lock();
try{
while (isEmpty) {
try {
notEmpty.await();
} catch (InterruptedException e) {
}
}
goodsTotal –;
System.out.println(threadName + "消费了1件商品");
isEmpty = true;
notFull.signal();
}finally{
lock.unlock();
}
}
}

class Customer implements Runnable {
Queue q;

Customer(Queue q) {
this.q = q;
}

@Override
public void run() {
while (true) {
q.take();
}

}
}

class Producer implements Runnable {
Queue q;

Producer(Queue q) {
this.q = q;
}

@Override
public void run() {
while (true) {
q.put();
}

}
}

线程的经常使用方法

最后,看1下1些关于线程的经常使用方法。

1、线程的中断工作

1、通常使用自然中断的做法,也就是当某个线程的线程任务履行结束以后,该线程就会自然终结。

2、通过标记控制。如果线程任务中存在循环(通常都有),那末,可以在循环中使用标记,通过标记来控制线程的中断。


2、interrupt()方法:中断线程

我们知道sleep及wait等方法都可使线程进入阻塞状态。所以可能你在程序通过使用标记的方式来控制线程的中断,但由于进程中线程堕入了冻结(挂起/阻塞)状态,这时候通过标记将没法正常的控制线程中断。这时候,就能够通过interrupt方法来中断线程的冻结状态,强迫恢复到运行状态中来,让线程具有cpu的履行资格。但是由于此方法具有强迫性,所以会引发InterruptedException,所以要记得处理异常。

3、setDaemon()方法:将该线程标记为守护线程或用户线程。

所谓守护线程,可以理解为后台线程。对应的,我们在程序中开辟的线程都可以视为前台线程,在Java中,当所有的前台线程都履行结束以后,后台线程也将随之结束。

例如:你在某个程序中开辟两个线程,1个用于接收输入,1个用于控制输出。由于只有当有输入存在时,才会存在输出。这时候就能够通过setDaemon将输出线程设置为守护线程。这样当输入线程中断结束时,输出线程就会随之自动中断,而没必要再人为控制中断。


4、控制线程优先级

所谓控制线程优先级,是指我们可以通过设置线程的优先级来控制线程被CPU运行到的概率,线程的优先级越高,被CPU运行的几率越大。

通过setPriority()与getPriority()方法可以分别设置和获得某个线程的优先级。Java中线程的优先级取值范围为:1⑴0

Thread类中使用MAX_PRIORITY(10),NORM_PRIORITY(5),MIN_PRIORITY(1)3个常量代表最经常使用的线程优先级值。


5、join()方法

线程使用join方法,意味着该线程申请加入履行,所以通常如果要临时加入1个线程,可使用join()方法。并且,当履行到join方法以后,其余线程将等待使用该方法的线程履行完线程任务以后,再继续履行。


6、yiled()方法

暂停正在履行的线程对象,并履行其他线程。

波比源码 – 精品源码模版分享 | www.bobi11.com
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 本站源码并不保证全部能正常使用,仅供有技术基础的人学习研究,请谨慎下载
8. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!

波比源码 » 牛刀小试 – 详解Java多线程

发表评论

Hi, 如果你对这款模板有疑问,可以跟我联系哦!

联系站长
赞助VIP 享更多特权,建议使用 QQ 登录
喜欢我嘛?喜欢就按“ctrl+D”收藏我吧!♡