全面掌握Java多线程

全文目录

  • 一、线程的2种创建方法
    • 方法1:继承Thread类
    • 方法2:实现Runnable接口
  • 二、线程的状态
    • 1、新生状态
    • 2、就绪状态
    • 3、运行状态
    • 4、阻塞状态
    • 5、死亡状态
  • 三、杀死线程
  • 四、暂停线程的方法
    • 1、sleep(),线程主动由运行进入阻塞
    • 2、yield(),线程主动由运行进入就绪
  • 五、线程的联合join()
  • 六、守护线程
  • 七、获取线程信息
  • 八、线程优先级
  • 九、线程同步
    • 1、简介
    • 2、【重要】实现线程同步
    • 3、死锁产生的原因
    • 4、避免死锁的方法
      • (1)加锁顺序(线程按照一定的顺序加锁)
      • (2)加锁时限
      • (3)死锁检测
    • 5、总结:避免死锁的方式
  • 十、线程间通信
    • 1、notify()和notifyAll()的区别
    • 2、如何使用wait()
    • 3、永远在循环(loop)里调用 wait 和 notify而非 If
    • 4、总结
  • 十一、任务定时调度
    • 1、java.util.Timer
    • 2、java.util.TimerTask

一、线程的2种创建方法

方法1:继承Thread类

继承Thread类实现多线程的步骤:

  1. 在Java中负责实现线程功能的类是java.lang.Thread 类。
  2. 可以通过创建 Thread的实例来创建新的线程。
  3. 每个线程都是通过某个特定的Thread对象所对应的方法run( )来完成其操作的,方法run( )称为线程体。
  4. 通过调用Thread类的start()方法来启动一个线程。

代码块

public class TestThread extends Thread {
     //自定义类继承Thread类
    //run()方法里是线程体
    public void run() {
     
        for (int i = 0; i < 10; i++) {
     
            System.out.println(this.getName() + ":" + i);
            //getName()方法是返回线程名称
        }
    }
    public static void main(String[] args) {
     
    	//创建线程对象
        TestThread thread1 = new TestThread();
        //启动线程
        thread1.start();
        TestThread thread2 = new TestThread();
        thread2.start();
    }
}

运行结果:

Thread-1:0
Thread-0:0
Thread-1:1
Thread-0:1
Thread-1:2
Thread-0:2
Thread-0:3
Thread-0:4
Thread-0:5
Thread-0:6
Thread-0:7
Thread-0:8
Thread-1:3
Thread-0:9
Thread-1:4
Thread-1:5
Thread-1:6
Thread-1:7
Thread-1:8
Thread-1:9

Process finished with exit code 0

方法2:实现Runnable接口

在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了11.2.1节中实现线程类的缺点,即在实现Runnable接口的同时还可以继承某个类。所以实现Runnable接口的方式要通用一些。

public class TestThread2 implements Runnable {
     //自定义类实现Runnable接口;
    //run()方法里是线程体;
    public void run() {
     
        for (int i = 0; i < 10; i++) {
     
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
    public static void main(String[] args) {
     
        //创建线程对象,把实现了Runnable接口的对象作为参数传入
        Thread thread1 = new Thread(new TestThread2());
        //启动线程
        thread1.start();
        Thread thread2 = new Thread(new TestThread2());
        thread2.start();
    }
}

二、线程的状态

线程的状态有:新生状态,就绪状态,运行状态,阻塞状态,死亡状态。

1、新生状态

用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

2、就绪状态

处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

	有4中原因会导致线程进入就绪状态:
      1. 新建线程:调用start()方法,进入就绪状态;
      2. 阻塞线程:阻塞解除,进入就绪状态;
      3. 运行线程:调用yield()方法,直接进入就绪状态;
      4. 运行线程:JVM将CPU资源从本线程切换到其他线程。

3、运行状态

在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

4、阻塞状态

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:

  1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
  2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
  3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。当引起该操作阻塞的原因消失后,线程进入就绪状态。
  4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

5、死亡状态

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个:一个是正常运行的线程完成了它run()方法内的全部工作; 另一个是线程被强制终止,如通过执行stop()或destroy()方法来终止一个线程(注:stop()/destroy()方法已经被JDK废弃,不推荐使用)。
当一个线程进入死亡状态以后,就不能再回到其它状态了。

三、杀死线程

终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。
通常的做法是提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。

public class TestThreadCiycle implements Runnable {
     
    boolean live = true;// 标记变量,表示线程是否可中止;
    public TestThreadCiycle() {
     
        super();
    }
    public void run() {
     
        int i = 0;
        //当live的值是true时,继续线程体;false则结束循环,继而终止线程体;
        while (live) {
     
            System.out.println(i++);
        }
    }
    public void terminate() {
     
        live = false;
    }

四、暂停线程的方法

暂停线程执行常用的方法有sleep()和yield()方法,区别如下:

1、sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
2、yield()方法:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。

1、sleep(),线程主动由运行进入阻塞

直接在run方法里使用sleep方法就可以了。

try {
     
	Thread.sleep(2000);//调用线程的sleep()方法;
} catch (InterruptedException e) {
     
	e.printStackTrace();
}

2、yield(),线程主动由运行进入就绪

代码:

public class TestThreadState {
     
    public static void main(String[] args) {
     
        StateThread thread1 = new StateThread();
        thread1.start();
        StateThread thread2 = new StateThread();
        thread2.start();
    }
}
//使用继承方式实现多线程
class StateThread extends Thread {
     
    public void run() {
     
        for (int i = 0; i < 100; i++) {
     
            System.out.println(this.getName() + ":" + i);
            Thread.yield();//调用线程的yield()方法;
        }
    }
}

运行结果:

Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
Thread-0:2
Thread-1:2
Thread-0:3
Thread-0:4
Thread-1:3
Thread-1:4

Process finished with exit code 0

五、线程的联合join()

线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。如下面示例中,“爸爸线程”要抽烟,于是联合了“儿子线程”去买烟,必须等待“儿子线程”买烟完毕,“爸爸线程”才能继续抽烟。
示例代码:

public class TestThreadState {
     
    public static void main(String[] args) {
     
        System.out.println("爸爸和儿子买烟故事");
        Thread father = new Thread(new FatherThread());
        father.start();
    }
}
class FatherThread implements Runnable {
     
    public void run() {
     
        System.out.println("爸爸想抽烟,发现烟抽完了");
        System.out.println("爸爸让儿子去买包红塔山");
        Thread son = new Thread(new SonThread());
        son.start();
        System.out.println("爸爸等儿子买烟回来");
        try {
     
            son.join();
        } catch (InterruptedException e) {
     
            e.printStackTrace();
            System.out.println("爸爸出门去找儿子跑哪去了");
            // 结束JVM。如果是0则表示正常结束;如果是非0则表示非正常结束
            System.exit(1);
        }
        System.out.println("爸爸高兴的接过烟开始抽");
    }
}
class SonThread implements Runnable {
     
    public void run() {
     
        System.out.println("儿子出门去买烟");
        System.out.println("儿子买烟需要3分钟");
        try {
     
            for (int i = 1; i <= 3; i++) {
     
                System.out.println("第" + i + "分钟");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println("儿子买烟回来了");
    }
}

运行结果

爸爸和儿子买烟故事
爸爸想抽烟,发现烟抽完了
爸爸让儿子去买包红塔山
爸爸等儿子买烟回来
儿子出门去买烟
儿子买烟需要3分钟
第1分钟
第2分钟
第3分钟
儿子买烟回来了
爸爸高兴的接过烟开始抽

Process finished with exit code 0

六、守护线程

Java分为两种线程:用户线程和守护线程

所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

守护线程和用户线程的没啥本质的区别:唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。

在使用守护线程时需要注意一下几点:

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
  • 在Daemon线程中产生的新线程也是Daemon的。
  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

示例代码

public class Test {
     
    public static void main(String[] args) {
     
        守护者 a = new 守护者();
        用户线程 b = new 用户线程();
        
        a.setDaemon(true);//将线程设置成为守护线程
        a.start();
        
        b.start();
    }
}

class 守护者 extends Thread {
     
    @Override
    public void run() {
     
        while (true) {
     
            System.out.println("守护中...");
            yield();
        }
    }
}

class 用户线程 extends Thread {
     
    @Override
    public void run() {
     
        for (int i = 0; i < 2; i++) {
     
            System.out.println("正常操作");
        }
    }
}

运行结果

正常操作
正常操作
守护中...
Process finished with exit code 0

七、获取线程信息

方法 功能
isAlive() 判断线程是否还活着
getPriority() 获取线程的优先级
setPriority() 设置线程的优先级
setName() 给线程指定一个名字
getName() 获取线程的名字
currentThread() 取得当前正在运行的线程对象,也即是取得自己本身

示例代码

public class Test {
     
    public static void main(String[] args) {
     
        Info info = new Info("不知");
        Thread thread = new Thread(info);
        System.out.println(thread.isAlive());        //isAlive用法
        thread.setName("不识");   //setName用法
        System.out.println("getPriority用法 默认优先级:" + thread.getPriority());
        thread.setPriority(8);  //setPriority用法
        thread.start();
        System.out.println(thread.isAlive());
        try {
     
            Thread.sleep(1000);//等待一秒
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println(thread.isAlive());
    }
}

class Info implements Runnable {
     
    String name;

    public Info(String name) {
     
        this.name = name;
    }

    @Override
    public void run() {
     
        //currentThread()的用法
        //getName用法
        System.out.println(Thread.currentThread().getName() + "--->" + name);
    }
}

运行结果

false
getPriority用法 默认优先级:5
true
不识--->不知
false

Process finished with exit code 0

八、线程优先级

  1. 处于就绪状态的线程,会进入“就绪队列”等待JVM来挑选。
  2. 线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。
  3. 使用下列方法获得或设置线程对象的优先级。
int getPriority();
void setPriority(int newPriority);

注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高的线程后调用优先级低的线程。

示例代码

public class TestThread {
     
    public static void main(String[] args) {
     
        Thread t1 = new Thread(new MyThread(), "t1");
        Thread t2 = new Thread(new MyThread(), "t2");
        //优先级越高越容易被执行
        t1.setPriority(10);
        t2.setPriority(1);
        t1.start();
        t2.start();
    }
}
class MyThread extends Thread {
     
    public void run() {
     
        for (int i = 0; i < 3; i++) {
     
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

运行结果:

t2: 0
t1: 0
t1: 1
t1: 2
t2: 1
t2: 2

Process finished with exit code 0

九、线程同步

1、简介

同步问题的提出
现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。 比如:教室里,只有一台电脑,多个人都想使用。天然的解决办法就是,在电脑旁边,大家排队。前一人使用完后,后一人再使用。
线程同步的概念
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
不使用同步的风险
当没有线程同步机制,两个线程同时操作同一个银行账户对象,会使得数据不安全,显然银行不会答应的。

2、【重要】实现线程同步

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。

由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。

public class TestSync {
     
   public static void main(String[] args) {
     
       Account a1 = new Account(100, "高");
       Drawing draw1 = new Drawing(80, a1);
       Drawing draw2 = new Drawing(80, a1);
       draw1.start(); // 你取钱
       draw2.start(); // 你老婆取钱
   }
}

/*
* 简单表示银行账户
*/
class Account {
     
   int money;
   String aname;

   public Account(int money, String aname) {
     
       super();
       this.money = money;
       this.aname = aname;
   }
}

/**
* 模拟提款操作
*
* @author Administrator
*/
class Drawing extends Thread {
     
   int drawingNum; // 取多少钱
   Account account; // 要取钱的账户
   int expenseTotal; // 总共取的钱数

   public Drawing(int drawingNum, Account account) {
     
       super();
       this.drawingNum = drawingNum;
       this.account = account;
   }

   @Override
   public void run() {
     
       draw();
   }

   void draw() {
     
       synchronized (account) {
     
           if (account.money - drawingNum < 0) {
     
               System.out.println(this.getName() + "取款,余额不足!");
               return;
           }
           try {
     
               Thread.sleep(1000); // 判断完后阻塞。其他线程开始运行。
           } catch (InterruptedException e) {
     
               e.printStackTrace();
           }
           account.money -= drawingNum;
           expenseTotal += drawingNum;
       }
       System.out.println(this.getName() + "--账户余额:" + account.money);
       System.out.println(this.getName() + "--总共取了:" + expenseTotal);
   }
}

运行结果:

Thread-1取款,余额不足!
Thread-0--账户余额:20
Thread-0--总共取了:80

Process finished with exit code 0

synchronized (account)意味着线程需要获得account对象的“锁”才有资格运行同步块中的代码。

Account对象的“锁”也称为“互斥锁”,在同一时刻只能被一个线程使用。A线程拥有锁,则可以调用“同步块”中的代码;B线程没有锁,则进入account对象的“锁池队列”等待,直到A线程使用完毕释放了account对象的锁,B线程得到锁才可以开始调用“同步块”中的代码。

3、死锁产生的原因

死锁”指的是:多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,死锁产生的原因:

  • 系统资源的竞争:通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
  • 进程推进顺序非法:进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。
  • Java中一个最简单的死锁情况:一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。
  • 死锁环路:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。

从以上例子,我们可以得出结论,产生死锁可能性的两个最根本原因:
1、在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2
2、默认的锁申请操作是阻塞的。


【总结】下面再来通俗的解释一下死锁发生时的条件:

  1. 互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件: 进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系,即A等B,B等C,C等A。

【简单死锁案例】
下面案例中,“化妆线程”需要同时拥有“镜子对象”、“口红对象”才能运行同步块。那么,实际运行时,“小丫的化妆线程”拥有了“镜子对象”,“大丫的化妆线程”拥有了“口红对象”,都在互相等待对方释放资源,才能化妆。这样,两个线程就形成了互相等待,无法继续运行的“死锁状态”。

class Lipstick {
     //口红类

}
class Mirror {
     //镜子类

}
class Makeup extends Thread {
     //化妆类继承了Thread类
    int flag;
    String girl;
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    @Override
    public void run() {
     
        // TODO Auto-generated method stub
        doMakeup();
    }

    void doMakeup() {
     
        if (flag == 0) {
     
            synchronized (lipstick) {
     //需要得到口红的“锁”;
                System.out.println(girl + "拿着口红!");
                try {
     
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }

                synchronized (mirror) {
     //需要得到镜子的“锁”;
                    System.out.println(girl + "拿着镜子!");
                }

            }
        } else {
     
            synchronized (mirror) {
     
                System.out.println(girl + "拿着镜子!");
                try {
     
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                synchronized (lipstick) {
     
                    System.out.println(girl + "拿着口红!");
                }
            }
        }
    }

}

public class TestDeadLock {
     
    public static void main(String[] args) {
     
        Makeup m1 = new Makeup();//大丫的化妆线程;
        m1.girl = "大丫";
        m1.flag = 0;
        Makeup m2 = new Makeup();//小丫的化妆线程;
        m2.girl = "小丫";
        m2.flag = 1;
        m1.start();
        m2.start();
    }
}

运行结果

大丫拿着口红!
小丫拿着镜子!

可见,产生了死锁,两个线程均无法完成自己的操作。

4、避免死锁的方法

在有些情况下死锁是可以避免的。下面介绍三种用于避免死锁的技术。

(1)加锁顺序(线程按照一定的顺序加锁)

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。【简单死锁案例】修改后的代码如下:

class Lipstick {
     //口红类

}

class Mirror {
     //镜子类

}

class Makeup extends Thread {
     //化妆类继承了Thread类
    int flag;
    String girl;
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    @Override
    public void run() {
     
        // TODO Auto-generated method stub
        doMakeup();
    }

    void doMakeup() {
     
        synchronized (lipstick) {
     
            System.out.println(girl + "拿着口红!");
            try {
     
                Thread.sleep(1000);
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
            synchronized (mirror) {
     
                System.out.println(girl + "拿着镜子!");
            }
        }
        System.out.println(girl + "化妆完毕");
    }
}

public class TestDeadLock {
     
    public static void main(String[] args) {
     
        Makeup m1 = new Makeup();// 大丫的化妆线程;
        m1.girl = "大丫";
        Makeup m2 = new Makeup();// 小丫的化妆线程;
        m2.girl = "小丫";
        m1.start();
        m2.start();
    }
}

运行结果

大丫拿着口红!
大丫拿着镜子!
大丫化妆完毕
小丫拿着口红!
小丫拿着镜子!
小丫化妆完毕

Process finished with exit code 0

(2)加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试,在这段时间可以做点别的事儿。
这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。

(3)死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
利用数据结构检测死锁
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。
例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。
当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。
那么当检测出死锁时,这些线程该做些什么呢?
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(原因同超时类似,不能从根本上减轻竞争)。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。
那么当检测出死锁时,这些线程该做些什么呢?
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(原因同超时类似,不能从根本上减轻竞争)。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

5、总结:避免死锁的方式

1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实。
2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量。
3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。

我们可以使用ReentrantLock.tryLock()方法,在一个循环中,如果tryLock()返回失败,那么就释放以及获得的锁,并睡眠一小段时间。这样就打破了死锁的闭环。比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1。此时如果T3申请锁L1失败,那么T3释放锁L3,并进行睡眠,那么T2就可以获得L3了,然后T2执行完之后释放L2, L3,所以T1也可以获得L2了执行完然后释放锁L1, L2,然后T3睡眠醒来,也可以获得L1, L3了。打破了死锁的闭环。
通过Timer和Timetask,我们可以实现定时启动某个线程。

十、线程间通信

Java中进程间通信的方式很简单,实际上只是synchronized关键字和三个函数的使用。

  • wait()
  • notify()
  • notifyAll()

1、notify()和notifyAll()的区别

不同之处在于,notify 仅仅通知一个线程,并且我们不知道哪个线程会收到通知,然而 notifyAll 会通知所有等待中的线程。换言之,如果只有一个线程在等待一个信号灯,notify和notifyAll都会通知到这个线程。但如果多个线程在等待这个信号灯,那么notify只会通知到其中一个,而其它线程并不会收到任何通知,而notifyAll会唤醒所有等待中的线程。

2、如何使用wait()

尽管关于wait和notify的概念很基础,它们也都是Object类的函数,但用它们来写代码却并不简单。如果你在面试中让应聘者来手写代码,用wait和notify解决生产者消费者问题,我几乎可以肯定他们中的大多数都会无所适从或者犯下一些错误,例如在错误的地方使用 synchronized 关键词,没有对正确的对象使用wait,或者没有遵循规范的代码方法。说实话,这个问题对于不常使用它们的程序员来说确实令人感觉比较头疼。
怎么在代码里使用wait()
正确的方法:在被synchronized的情况下,对在多线程间共享的那个Object来使用wait。
在生产者消费者问题中,这个共享的Object就是那个缓冲区队列。
哪个对象应该被synchronized呢
答案是,那个你希望上锁的对象就应该被synchronized,即那个在多个线程间被共享的对象。在生产者消费者问题中,应该被synchronized的就是那个缓冲区队列。

3、永远在循环(loop)里调用 wait 和 notify而非 If

现在你知道wait应该永远在被synchronized的背景下和那个被多线程共享的对象上调用,下一个一定要记住的问题就是,你应该永远在while循环,而不是if语句中调用wait。
因为线程是在某些条件下等待的——在我们的例子里,即“如果缓冲区队列是满的话,那么生产者线程应该等待”,你可能直觉就会写一个if语句。但if语句存在一些微妙的小问题,导致即使条件没被满足,你的线程你也有可能被错误地唤醒。所以如果你不在线程被唤醒后再次使用while循环检查唤醒条件是否被满足,你的程序就有可能会出错——例如在缓冲区为满的时候生产者继续生成数据,或者缓冲区为空的时候消费者开始小号数据。所以记住,永远在while循环而不是if语句中使用wait!

基于以上认知,下面这个是使用wait和notify函数的规范代码模板:

// The standard idiom for calling the wait method in Java 
synchronized (sharedObject) {
      
    while (condition) {
      
    sharedObject.wait(); 
        // (Releases lock, and reacquires on wakeup) 
    } 
    // do action based upon condition e.g. take or put into queue 
}

在while循环里使用wait的目的,是在线程被唤醒的前后都持续检查条件是否被满足。

4、总结

多线程通信重点:

  1. 你可以使用wait和notify函数来实现线程间通信。你可以用它们来实现多线程(>3)之间的通信。
  2. 永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。
  3. 永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
  4. 永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。
  5. 基于前文提及的理由,更倾向用 notifyAll(),而不是 notify()。

生产者-消费者示例代码

public class TestProduce {
     
    public static void main(String[] args) {
     
        SyncStack sStack = new SyncStack();// 定义缓冲区对象;
        Shengchan sc = new Shengchan(sStack);// 定义生产线程;
        sc.setName("生产者");
        Xiaofei xf = new Xiaofei(sStack);// 定义消费线程;
        xf.setName("消费者");

        try {
     
            Thread.sleep(5000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }

        xf.start();
        sc.start();
    }
}

class Mantou {
     // 馒头
    int id;

    Mantou(int id) {
     
        this.id = id;
    }
}

class SyncStack {
     // 缓冲区(相当于:馒头筐)
    int index = 0;
    Mantou[] ms = new Mantou[10];

    public synchronized void push(Mantou m) {
     
        while (index == ms.length) {
     //说明馒头筐满了
            try {
     
                //wait后,线程会将持有的锁释放,进入阻塞状态;
                //这样其它需要锁的线程就可以获得锁;
                this.wait();
                //这里的含义是执行此方法的线程暂停,进入阻塞状态,
                //等消费者消费了馒头后再生产。
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }
        // 唤醒在当前对象等待池中等待的第一个线程。
        //notifyAll叫醒所有在当前对象等待池中等待的所有线程。
        this.notify();
        // 如果不唤醒的话。以后这两个线程都会进入等待线程,没有人唤醒。
        ms[index] = m;
        index++;
    }

    public synchronized Mantou pop() {
     
        while (index == 0) {
     //如果馒头筐是空的;
            try {
     
                //如果馒头筐是空的,就暂停此消费线程(因为没什么可消费的嘛)。
                this.wait();                //等生产线程生产完再来消费;
            } catch (InterruptedException e) {
     
                e.printStackTrace();
            }
        }
        this.notify();
        index--;
        return ms[index];
    }
}

class Shengchan extends Thread {
     // 生产者线程
    SyncStack ss = null;

    public Shengchan(SyncStack ss) {
     
        this.ss = ss;
    }

    @Override
    public void run() {
     
        for (int i = 0; i < 10; i++) {
     
            System.out.println("生产馒头:" + i);
            Mantou m = new Mantou(i);
            ss.push(m);
        }
    }
}

class Xiaofei extends Thread {
     // 消费者线程;
    SyncStack ss = null;

    public Xiaofei(SyncStack ss) {
     
        this.ss = ss;
    }

    @Override
    public void run() {
     
        for (int i = 0; i < 10; i++) {
     
            Mantou m = ss.pop();
            System.out.println("消费馒头:" + i);
        }
    }
}

运行结果

生产馒头:0
生产馒头:1
生产馒头:2
消费馒头:0
生产馒头:3
消费馒头:1
生产馒头:4
消费馒头:2
生产馒头:5
消费馒头:3
生产馒头:6
消费馒头:4
生产馒头:7
消费馒头:5
生产馒头:8
消费馒头:6
生产馒头:9
消费馒头:7
消费馒头:8
消费馒头:9

Process finished with exit code 0

十一、任务定时调度

通过Timer和Timetask,我们可以实现定时启动某个线程。

1、java.util.Timer

在这种实现方式中,Timer类作用是类似闹钟的功能,也就是
定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。

2、java.util.TimerTask

TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。

【示例11-14】java.util.Timer的使用

import java.util.Timer;
import java.util.TimerTask;

public class TestTimer {
     
    public static void main(String[] args) {
     
        Timer t1 = new Timer();//定义计时器;
        MyTask task1 = new MyTask();//定义任务;
        t1.schedule(task1, 3000);  //3秒后执行;
//        t1.schedule(task1,5000,1000);//5秒以后每隔1秒执行一次!
        //GregorianCalendar calendar1 = new GregorianCalendar(2010,0,5,14,36,57);
        //t1.schedule(task1,calendar1.getTime()); //指定时间定时执行;
    }
}

class MyTask extends TimerTask {
     //自定义线程类继承TimerTask类;

    public void run() {
     
        for (int i = 0; i < 10; i++) {
     
            System.out.println("任务1:" + i);
        }
    }
}

运行结果

任务1:0
任务1:1
任务1:2
任务1:3
任务1:4
任务1:5
任务1:6
任务1:7
任务1:8
任务1:9

运行以上程序时,可以感觉到在输出之前有明显的延迟(就是3秒!)。还有几个方法,我注释掉了,大家自己试试吧!
在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask实现。
【建议】
实际开发中,我们可以使用开源框架quanz,更加方便的实现任务定时调度。实际上,quanz底层原理就是我们这里介绍的内容。

你可能感兴趣的:(Java学习笔记)