先来回顾一下,有关线程之间的协作:
• 粗粒度:子线程与子线程之间、和main线程之间缺乏同步
• 细粒度:线程之间有同步协作
–等待
因为业务需求,我们往往需要某一个线程等待另外的线程运行结束或者执行完某一个操作之后,才开始下一个线程
–通知/唤醒
后面的线程完成工作后,唤醒等待等待状态的其他线程,继续工作
–终止
执行完相应的操作之后,某一个特定功能的线程就会终止,释放系统资源
简单总结一下,线程状态 :
- –NEW 刚创建(new)
- –RUNNABLE 就绪态(start)
- –RUNNING 运行中(run)
- –BLOCK 阻塞(sleep)
- –TERMINATED 结束
使线程处于阻塞/和唤醒的方法:
sleep,时间一到,自己会醒来,在sleep时,线程处于BLOCK状态,当时间到达,自动进入RUNNABLE状态
wait/notify/notifyAll,等待,需要别人来唤醒 ,如果别的线程不唤醒这个等待的线程,他就一直处于该状态
join,等待另外一个线程结束 ,例如存在一个线程A,现在需要插入线程B,并要求线程B先执行完毕,然后再继续执行线程A,就需要join方法
interrupt,向另外一个线程发送中断信号,该线程收到信号,会触发InterruptedException(可解除阻塞),并进行下一步处理,代码如下:
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
TestThread1 t1 = new TestThread1();
TestThread2 t2 = new TestThread2();
t1.start();
t2.start();
// 让线程运行一会儿后中断
Thread.sleep(2000);
t1.interrupt(); //向t1线程发出信号,使之中断
t2.flag = false; //改变条件。使t2也终止
System.out.println("main thread is exiting");
}
}
class TestThread1 extends Thread {
public void run() {
// 判断标志,当本线程被别人interrupt后,JVM会被本线程设置interrupted标记
while (!interrupted()) {
System.out.println("test thread1 is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
System.out.println("test thread1 is exiting");
}
}
class TestThread2 extends Thread {
public volatile boolean flag = true;
public void run() {
// 判断标志,当本线程被别人interrupt后,JVM会被本线程设置interrupted标记
//而且interrupt可以认为是一种异常,需要处理异常抛出,有时候会使得资源来不及释放
while (flag) {
System.out.println("test thread2" + " gets resourcce ,is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("One round over,releasing system resource");
}
System.out.println("test thread2 is exiting");
}
}
运行结果看出,被中断之后的线程可能会来不及释放资源就被退出,不利于计算机正常的运行
下面简单介绍一个多线程经典的消费者和生产者的例子:
- 经典生产者与消费者问题
- 生产者不断的往仓库中存放产品,消费者从仓库中消费产品。
- 其中生产者和消费者都可以有若干个。
- 仓库规则:容量有限,库满时不能存放,库空时不能取产品 。
import java.util.Random;
public class ProduceAndComsumeTest {
public static void main(String[] args) throws InterruptedException {
Storage storage = new Storage(); //定义仓库
Thread producer1 = new Thread(new Producers(storage));
producer1.setName("生产者1");
Thread producer2 = new Thread(new Producers(storage));
producer2.setName("生产者2");
Thread consumer1 = new Thread(new Consumers(storage));
consumer1.setName("消费者1");
Thread consumer2 = new Thread(new Consumers(storage));
consumer2.setName("消费者2");
//分别启动生产者和消费者线程
producer1.start();
producer2.start();
Thread.sleep(1000);
consumer1.start();
consumer2.start();
}
}
class Products {
//定义产品类
private int id;// 产品id
private String name;// 产品名称
public Products(int id, String name) {
this.id = id;
this.name = name;
}
//对各种产品数据进行操作的函数:
public String toString() {
return "(产品ID:" + id + " 产品名称:" + name + ")";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Storage {
// 仓库容量为10
private Products[] products = new Products[10]; //定义一个容量为10的仓库
private int top = 0; //top变量指向当前存储的位置
// 生产者往仓库中放入产品,按照规定,我们每次只让一个生产者放入产品,因此用synchronized关键字修饰
public synchronized void putProducts(Products product) {
while (top == products.length) {
try {
System.out.println("producer wait");
wait();//仓库已满,等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//把产品放入仓库
products[top++] = product;
System.out.println(Thread.currentThread().getName() + " 生产了产品"+ product);
System.out.println("producer notifyAll");
notifyAll();//唤醒等待线程
}
// 消费者从仓库中取出产品
public synchronized Products getProducts() {
//同理,对于取出产品,也进行保护
while (top == 0) { //没有产品
try {
System.out.println("consumer wait");
wait();//仓库空,等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//从仓库中取产品
--top;
Products p = new Products(products[top].getId(), products[top].getName());
products[top] = null;
System.out.println(Thread.currentThread().getName() + " 消费了产品" + p);
System.out.println("comsumer notifyAll");
notifyAll();//唤醒等待线程
return p;
}
}
class Producers implements Runnable {
private Storage storage; //将仓库实例作为其类成员变量
public Producers(Storage storage) {
this.storage = storage; //定义构造函数,将仓库统一,即传入的仓库即为当前存放产品的仓库
}
@Override
public void run() {
int i = 0;
Random r = new Random();
while(i<10) //仓库未满
{
++i;
Products product = new Products(i, "编号" + r.nextInt(100));
storage.putProducts(product);
}
}
}
class Consumers implements Runnable {
private Storage storage;
public Consumers(Storage storage) {
this.storage = storage; //类似上述的生产者,消费者将传入的仓库作为消费物品的对象
}
public void run() {
int i = 0;
while(i<10)
{
i++;
storage.getProducts();
try {
Thread.sleep(100); //休眠一段时间,模拟消费者的状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
笔者电脑上运行部分结果如下,这个例子就很好的展现了各个线程在不同状态之间的切换:
因此在编程时,应该注意以下两种类型:
• 假如线程被动地暂停和终止 ,依靠别的线程来唤醒自己,线程自己无法从这种状态中恢复过来,没有及时释放资源,假如某个线程打开了某个文件,结果没有及时释放之,将变得很危险,亦或者当该线程持有某一段代码的锁时(在synchronized关键字修饰的代码区内wait,将会变得异常危险)
• 假如线程主动暂停和终止 ,可以定期监测共享变量 ,如果需要暂停或者终止,会先释放资源,再主动动作,非常优雅,需要暂停时,调用Thread.sleep(),休眠 ,最后run方法结束,线程终止。
• 多线程死锁 –每个线程互相持有别人需要的锁
经典的哲学家吃面问题:
假设有五位哲学家围坐在一张圆形餐桌旁。在餐桌上,每个人的面前有一碗意大利面,每两个哲学家之间有一只餐叉。(意大利面很滑,用一只餐叉很难吃到面,所以假设哲学家必须用两只餐叉吃东西,而且他们只能使用自己左、右边的那两只餐叉。)哲学家们只能做一件事:吃面,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。在这里,我们假设:哲学家们无限地想吃面;意大利面也是无限地在供给。
大家都同时想吃饭,结果同时拿起左手边的叉子,发现同时右边没有叉子;然后各怀私心,希望有人能放下他左手边的叉子,然后马上抢过来开吃。结果,没有一个人放手。
缺少叉子可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。
当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。
以下是一个演示的实例,先创建两个字符串,strA 、strB作为两把锁。让每个线程都用synchronized锁住字符串(线程A先锁strA ,再去锁strB;线程B先锁strA,再锁strB),如果线程A锁住strA ,线程B锁住strB,线程A就没办法锁住strB,线程B也没办法锁住strA,这个时候就陷入了死锁。
public class DeadLockTest {
public static String objA = "strA";
public static String objB = "strB";
public static void main(String[] args){
Thread a = new Thread(new LockTest1());
Thread b = new Thread(new LockTest2());
a.start();
b.start();
}
}
class LockTest1 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock1 running");
while(true){
synchronized(DeadLockTest.objA){
System.out.println("Lock1 lock strA");
Thread.sleep(1000);
//获取strA锁后先等一会儿,让Lock2有足够的时间锁住strB
synchronized(DeadLockTest.objB){
System.out.println("Lock1 lock strB");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
class LockTest2 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock2 running");
while(true){
synchronized(DeadLockTest.objB){
System.out.println("Lock2 lock strB");
Thread.sleep(3000);
synchronized(DeadLockTest.objA){
System.out.println("Lock2 lock strA");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
一般来说有以下两种解法:
1、服务生解法
一个简单的解法是引入一个餐厅服务生,哲学家必须经过他的允许才能拿起餐叉。因为服务生知道哪只餐叉正在使用,所以他能够作出判断避免死锁。
2、资源分级法
我们可以把桌上的资源(五把叉子)从一到五分级,并要求哲学家总是先拿起左右两边编号较低的叉子,再拿较高的,用完餐后总是先放下编号较高的叉子,再放下较低的。所以,当四位哲学家同时拿起他们手边编号较低的餐叉时,只有编号最高的餐叉留在桌上。
因此,为了预防死锁,我们可以引入第三方线程对资源进行调配,或者对资源进行等级排序
另外还需要注意的一点是,守护线程永远不要访问资源,如文件或数据库等
因为普通线程的结束,是run方法运行结束,而守护线程的结束,是run方法或者它守护的那个线程结束了,它就运行结束,亦或main函数结束 ,这个时候,守护线程根本来不及释放资源,这就很容易造成数据的错误甚至系统的崩溃。参考下面这个例子:
public class DaemonThreadTest
{
public static void main(String args[]) throws InterruptedException
{
TestThread4 t = new TestThread4();
t.setDaemon(true); //将t设置为main线程的守护线程
t.start();
Thread.sleep(2000);
System.out.println("main thread is exiting");
}
}
class TestThread4 extends Thread
{
public void run()
{
while(true)
{
System.out.println("TestThread4" + " gets resourcce ,is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("One round over,releasing system resource");
}
}
}
还没来得及释放资源,就被强行终止了,结果如下:
总结:
对于多线程管理,我们需要了解线程的多个状态,通过经典的生产者和消费者的例子,了解线程协作机制 。另外,在编写程序时,若想要线程协作简单化,可以采用粗粒度协作。同时,也应该注意synchronized关键字时,时刻提防死锁,以便程序能够正常运行,同时,了解后台线程的特性,使用时加以注意。