最新公告
  • 欢迎您光临波比源码,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入我们
  • 牛刀小试 – 详解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. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!

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

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    波比源码
    一个高级程序员模板开发平台
    升级波友尊享更多特权立即升级