java 并发---Thread 线程

Posted float123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 并发---Thread 线程相关的知识,希望对你有一定的参考价值。

Thread 的状态

      线程共有五种状态.分别是: (1)新建 (2)就绪 (3)运行 (4)阻塞 (5)死亡 ,下面列列举的状态需要结合状态示意图更好理解.

 

  •  新建状态(New): 新创建了一个线程对象。
  • 就绪状态(Runnable): 线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得(包括我们所说的锁)。
  • 运行状态(Running): 就绪状态的线程获取了CPU,执行程序代码。
  • 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态
    • 等待阻塞 :  运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒.
    • 同步阻塞 :  运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中,言外之意就是锁被其他线程拿了,自己只能等待。
    • 其他阻塞 :  运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
    • 阻塞这个状态可以这样总结: 线程存在且没死亡,那么运行和就绪以外的状态就是阻塞,不管是否获得锁或是进入锁池. 
  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

 


     下面为线程的状态示意图: (出处见水印)

82ddc293417b1019ea5fbd878592c437

     

Thread 方法 和 Object两个方法

      后面两个Object 方法

  • sleep 方法 :  “sleep”—“睡觉”,意思就是休眠一段时间,时间过后继续执行,不释放锁和其他资源,进入阻塞状态
  • join   方法 :   源码实现在下方,可以看到只要thread存活的情况下就会某个时间内循环一个等待的方法(注意这个方法和下面的

                        wait 方法不是一回事,下面的wait方法会一直阻塞在那里),使用场景是例如某个操作执行前需要执行一个加载资源的

                        任务,那么执行的这个操作就要一直等待加载的操作完成以后才可以执行(join(0)).

  • yield 方法 :     让步于其他线程执行.
  • wait  方法 :     等待,释放锁和其他资源,进入等待队列,这个等待队列里边存放的对象都是等待获取锁的对象,另外一点, wait 是对象

                         object 中的方法,而不是线程中的方法,同时调用 XX.wait(); 时必须要在同步语句中,或是该对象已经被加锁的情况

                         下,试想一下,wait 方法本身就是某个对象加锁后释放锁,不可能没加锁的情况下可以释放锁.wait 不能自动唤醒,需要

                         notify / notifyAll 方法

  • notify/notifyAll 方法 :  唤醒

 

   join的源码实现

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() – base;
            }
        }
    }

       同步方法就是使用synchronized关键字修饰某个方法,这个方法就是同步方法,这个同步方法(非static方法)无须显示指定同步监视器,同步方法的同步监视器就是this,也就是调用该方法的对象。看一下下面的例子。

 

  1 public class Test {
  2 
  3     static class ThreadJoinTest extends Thread {
  4         public ThreadJoinTest(String name) {
  5             super(name);
  6         }
  7 
  8         @Override
  9         public void run() {
 10             for (int i = 0; i < 1000; i++) {
 11                 System.out.println(this.getName() + ":" + i);
 12             }
 13         }
 14     }
 15 
 16 
 17     public static void main(String[] args) throws InterruptedException {
 18         ThreadJoinTest t1 = new ThreadJoinTest("t1");
 19         ThreadJoinTest t2 = new ThreadJoinTest("t2");
 20         t1.start();
 21 
 22         t1.join();
 23         t2.start();
 24     }
 25 }

       上面的例子中在main方法中调用 t1.join 方法,我们走到 join 源码中就会发现 wait 方法的调用就是main方法的对象(即主线程),那么main方法的对象一直wait ,即是说 t2.start () 一直执行不到unless t1 finished.

Thread thread1 = new Thread(() -> {
            System.out.println("t1 开始执行" + new Date());
            synchronized (obj) {
                try {
                    Thread.currentThread().join(0);
                   //obj.wait();
                    System.out.println("线程1 继续执行,执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });


        Thread thread2 = new Thread(() -> {
            try {
                synchronized (obj) {

                    System.out.println("线程2 开始执行 " + new Date());
                    Thread.sleep(2 * 1000);
                    System.out.println("线程2 执行结束 " + new Date());
                  //  obj.notifyAll();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        thread1.start();

        Thread.sleep(4*1000);
        thread2.start();

       执行后会发现线程一调用join()方法后,线程2没能获取对象执行,而是等待线程1执行完成后,线程2才会执行.

 

       我们来看看在 Java 7 Concurrency Cookbook 中相关的描述(很清楚地说明了 join() 的作用):

Waiting for the finalization of a thread

In some situations, we will have to wait for the finalization of a thread. For example, we may have a program that will begin initializing the resources it needs before proceeding with the rest of the execution. We can run the initialization tasks as threads and wait for its finalization before continuing with the rest of the program. For this purpose, we can use the join() method of the Thread class. When we call this method using a thread object, it suspends the execution of the calling thread until the object called finishes its execution.

       当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。

 


 

方法使用 

wait 和 notify 方法

       可以看到 notify 方法的使用,是在同步方法内,并且同样获取同样的锁对象, wait 和 notify 方法的运用常常被用来做生产者-消费者的实现.

//以下代码来自参考文章,见参考资料
public class Test {
    public static Object object = new Object();
    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();
         
        thread1.start();
         
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
         
        thread2.start();
    }
     
    static class Thread1 extends Thread{
        @Override
        public void run() {
            synchronized (object) {
                try {
                    object.wait();
                } catch (InterruptedException e) {
                }
                System.out.println("线程"+Thread.currentThread().getName()+"获取到了锁");
            }
        }
    }
     
    static class Thread2 extends Thread{
        @Override
        public void run() {
            synchronized (object) {
                object.notify();
                System.out.println("线程"+Thread.currentThread().getName()+"调用了object.notify()");
            }
            System.out.println("线程"+Thread.currentThread().getName()+"释放了锁");
        }
    }
}

 

join 方法

       下面代码中parent线程会等待child线程执行完成后再继续执行.

// 父线程
public class Parent extends Thread {
    public void run() {
        Child child = new Child();
        child.start();
        child.join();
        // ...
    }
}
// 子线程
public class Child extends Thread {
    public void run() {
        // ...
    }
}

 

yield

public class YieldExcemple {

    public static void main(String[] args) {
        Thread threada = new ThreadA();
        Thread threadb = new ThreadB();
        // 设置优先级:MIN_PRIORITY最低优先级1;NORM_PRIORITY普通优先级5;MAX_PRIORITY最高优先级10
        threada.setPriority(Thread.MIN_PRIORITY);
        threadb.setPriority(Thread.MAX_PRIORITY);

        threada.start();
        threadb.start();
    }
}

class ThreadA extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("ThreadA--" + i);
            Thread.yield();
        }
    }
}

class ThreadB extends Thread {
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("ThreadB--" + i);
            Thread.yield();
        }
    }
}

          以下总结来自参考文章.

 

       Java线程中的Thread.yield( )方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。

        yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

      举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车,

有可能是其他人先上车了,也有可能是Yield先上车了。

     但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已,

     最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。

 

Thread 线程生成

         产生一个Thread的方式有两种  :

  • 继承 Thread
  • 创建一个类进程 Runnable  , 再把这个类传给 Thread

        第二种方式可以解决java 单继承的问题.同时接口和代码分离,比较清晰.同时同个线程可以复用由实现Runnable 的类.

public class DemoRunnable implements Runnable {
    public void run() {
        //Code
    }
}
//with a "new Thread(demoRunnable).start()" call

public class DemoThread extends Thread {
    public DemoThread() {
        super("DemoThread");
    }
    public void run() {
        //Code
    }
}

 

补充

中断

  让某个线程中断---Thread.interrupt()方法

      Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。

 

如何正确中断一个线程执行?

       可以参考这篇文章: 如何正确停止一个线程

 

处理InterruptionException

       可以参考这篇文章:Dealing with InterruptedException

       对于InterruptedException如何处理最重要的一个原则就是Don\'t swallow interrupts,一般两种方法:

  • 继续设置interrupted status
  • 抛出新的InterruptedException
  1 try {
  2     ………
  3 } catch (InterruptedException e) {
  4     // Restore the interrupted status
  5     Thread.currentThread().interrupt();
  6     // or thow a new
  7     //throw new InterruptedException();
  8 }

        文章也可提到了,既然线程有可能中断,那么我们也可以创建一个可取消的任务。

 

wait 方法

        这个方法在object里,这方法的文档中建议这样使用:

  1  synchronized (obj) {
  2                while (<condition does not hold>)
  3                    obj.wait(timeout);
  4                ... // Perform action appropriate to condition
  5            }

        这是为什么呢?因为要是多个线程在等待锁,当这个线程从wait 中醒来的时候,某个变量被另外的线程改变了就会发生异常,所以就加一个一个while判断。这可能有点难解释 , 看下面摘自Stackoverflow 的例子。

         链接地址: https://stackoverflow.com/questions/37026/java-notify-vs-notifyall-all-over-again

Clearly, notify wakes (any) one thread in the wait set, notifyAllwakes all threads in the waiting set. The following discussion should clear up any doubts. notifyAll should be used most of the time. If you are not sure which to use, then use notifyAll.Please see explanation that follows.

Read very carefully and understand. Please send me an email if you have any questions.

Look at producer/consumer (assumption is a ProducerConsumer class with two methods). IT IS BROKEN (because it uses notify) - yes it MAY work - even most of the time, but it may also cause deadlock - we will see why:

 

  1 public synchronized void put(Object o) {
  2     while (buf.size()==MAX_SIZE) {
  3         wait(); // called if the buffer is full (try/catch removed for brevity)
  4     }
  5     buf.add(o);
  6     notify(); // called in case there are any getters or putters waiting
  7 }
  8 
  9 public synchronized Object get() {
 10     // Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
 11     while (buf.size()==0) {
 12         wait(); // called if the buffer is empty (try/catch removed for brevity)
 13         // X: this is where C1 tries to re-acquire the lock (see below)
 14     }
 15     Object o = buf.remove(0);
 16     notify(); // called if there are any getters or putters waiting
 17     return o;
 18 }

 

FIRSTLY,

Why do we need a while loop surrounding the wait?

We need a while loop in case we get this situation:

Consumer 1 (C1) enter the synchronized block and the buffer is empty, so C1 is put in the wait set (via the wait call). Consumer 2 (C2) is about to enter the synchronized method (at point Y above), but Producer P1 puts an object in the buffer, and subsequently calls notify. The only waiting thread is C1, so it is woken and now attempts to re-acquire the object lock at point X (above).

Now C1 and C2 are attempting to acquire the synchronization lock. One of them (nondeterministically) is chosen and enters the method, the other is blocked (not waiting - but blocked, trying to acquire the lock on the method). Let\'s say C2 gets the lock first. C1 is still blocking (trying to acquire the lock at X). C2 completes the method and releases the lock. Now, C1 acquires the lock. Guess what, lucky we have a while loop, because, C1 performs the loop check (guard) and is prevented from removing a non-existent element from the buffer (C2 already got it!). If we didn\'t have a while, we would get an IndexArrayOutOfBoundsException as C1 tries to remove the first element from the buffer!

NOW,

Ok, now why do we need notifyAll?

In the producer/consumer example above it looks like we can get away with notify. It seems this way, because we can prove that the guards on the wait loops for producer and consumer are mutually exclusive. That is, it looks like we cannot have a thread waiting in the put method as well as the get method, because, for that to be true, then the following would have to be true:

buf.size() == 0 AND buf.size() == MAX_SIZE (assume MAX_SIZE is not 0)

HOWEVER, this is not good enough, we NEED to use notifyAll. Let\'s see why ...

Assume we have a buffer of size 1 (to make the example easy to follow). The following steps lead us to deadlock. Note that ANYTIME a thread is woken with notify, it can be non-deterministically selected by the JVM - that is any waiting thread can be woken. Also note that when multiple threads are blocking on entry to a method (i.e. trying to acquire a lock), the order of acquisition can be non-deterministic. Remember also that a thread can only be in one of the methods at any one time - the synchronized methods allow only one thread to be executing (i.e. holding the lock of) any (synchronized) methods in the class. If the following sequence of events occurs - deadlock results:

STEP 1:
- P1 puts 1 char into the buffer

STEP 2:
- P2 attempts put - checks wait loop - already a char - waits

STEP 3:
- P3 attempts put - checks wait loop - already a char - waits

STEP 4:
- C1 attempts to get 1 char
- C2 attempts to get 1 char - blocks on entry to the get method
- C3 attempts to get 1 char - blocks on entry to the get method

STEP 5:
- C1 is executing the get method - gets the char, calls notify, exits method
- The notify wakes up P2
- BUT, C2 enters method before P2 can (P2 must reacquire the lock), so P2 blocks on entry to the put method
- C2 checks wait loop, no more chars in buffer, so waits
- C3 enters method after C2, but before P2, checks wait loop, no more chars in buffer, so waits

STEP 6:
- NOW: there is P3, C2, and C3 waiting!
- Finally P2 acquires the lock, puts a char in the buffer, calls notify, exits method

STEP 7:
- P2\'s notification wakes P3 (remember any thread can be woken)
- P3 checks the wait loop condition, there is already a char in the buffer, so waits.
- NO MORE THREADS TO CALL NOTIFY and THREE THREADS PERMANENTLY SUSPENDED!

SOLUTION: Replace notify with notifyAll in the producer/consumer code (above).

 

        举个例子,五个人等待某个东西,唤醒时醒了五个人,东西被第一个人拿走了,那么后面的人就拿不到东西,那么再加一个判断while判断就不会出问题。这种机制还可以用来处理伪唤醒(spurious wakeup),所谓伪唤醒就是no reason wakeup,就是除了唤醒 interrupt之外的原因。

        同时notify()方法的随机性,有可能导致死锁,看上面回答。 

 

参考资料:

以上是关于java 并发---Thread 线程的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程之六:Runnable和Thread实现多线程的区别(含代码)

并发编程专题-线程的创建方式

并发编程专题-线程的创建方式

Java 多线程(并发)

Java并发编程-- 创建运行线程

Java多线程与并发库高级应用