您当前的位置: 首页 >  Java

Charge8

暂无认证

  • 1浏览

    0关注

    447博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Java 线程通信与线程的生命周期

Charge8 发布时间:2019-07-28 16:12:38 ,浏览量:1

一、并发同步

1、线程通信 -- 使用 synchronized 与 等待和唤醒机制

      线程通信:不同的线程执行不同的任务,如果这些任务有某种关系,线程之间必须能够通信、协调完成工作.

经典的生产者和消费者案例(Producer/Consumer):

public class ThreadDemo {
    public static void main(String[] args) {
        // 创建生产者和消费者共同的资源对象
        ShareResource shareResource = new ShareResource();

        // 启动生产者线程
        new Thread(new Producer(shareResource)).start();
        // 启动消费者线程
        new Thread(new Consumer(shareResource)).start();
    }
}
// 共享资源--水果
class ShareResource{
    private String name;
    private String colour;

    /**
     *  生产者向共享资源对象中存储数据
     * @param name  存储的名称
     * @param colour 存储的颜色
     */
    public void push(String name, String colour){
        this.name = name;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.colour = colour;
    }

    /**
     * 消费者从共享资源对象中取出数据
     */
    public void popup(){
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.name + "--" + this.colour);
    }
}

// 生产者
class Producer implements  Runnable{
    // 共享资源对象
    private ShareResource shareResource = null;

    public Producer(ShareResource shareResource){
        this.shareResource = shareResource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            if(i % 2 == 0){
                shareResource.push("苹果","红色");
            }else{
                shareResource.push("柚子","橘黄色");
            }
        }
    }
}
// 消费者
class Consumer implements Runnable{
    // 共享资源对象
    private ShareResource shareResource = null;

    public Consumer(ShareResource shareResource){
        this.shareResource = shareResource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            shareResource.popup();
        }
    }
}

                         

分析生产者和消费者案例存在的问题:

    建议在生产水果的名称和颜色之间以及在消费打印之前使用Thread.sleep(10); 使效果更明显.

    问题1:出现名称与颜色紊乱的情况.

                 解决方案:只要保证在生产水果的名称和颜色的过程保持同步,中间不能被消费者线程进来取走数据.

                 可以使用同步代码块/同步方法/Lock机制来保持同步性.

// 共享资源--水果
class ShareResource{
    private String name;
    private String colour;

    /**
     *  生产者向共享资源对象中存储数据
     * @param name  存储的名称
     * @param colour 存储的颜色
     */
    synchronized public void push(String name, String colour){
        this.name = name;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.colour = colour;
    }

    /**
     * 消费者从共享资源对象中取出数据
     */
    synchronized public void popup(){
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.name + "--" + this.colour);
    }
}

    问题2:应该出现生产一个数据,消费一个数据,交替出现

                 解决方案:得使用 等待和唤醒 机制.

// 共享资源--水果
class ShareResource{
    private String name;
    private String colour;
    private boolean isEmpty = true; // 表示共享资源对象是否为空的状态

    /**
     *  生产者向共享资源对象中存储数据
     * @param name  存储的名称
     * @param colour 存储的颜色
     */
    synchronized public void push(String name, String colour){
        try {
            while(!isEmpty){ // 当前对象为不空时等待消费者来获取
                // 使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池,只能被其他线程所唤醒
                this.wait();
            }
            this.name = name;
            Thread.sleep(10);
            this.colour = colour;
            isEmpty = false; // 设置共享资源中数据为空
            this.notify(); // 唤醒一个线程(消费者),多个使用notifyAll()
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 消费者从共享资源对象中取出数据
     */
    synchronized public void popup(){
        try {
            while(isEmpty){ // 当前对象为空时等待生产者来生产
                // 使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池,只能被其他线程所唤醒
                this.wait();
            }
            Thread.sleep(10);
            System.out.println(this.name + "--" + this.colour);
            isEmpty = true; // 设置共享资源中数据为空
            this.notify(); // 唤醒一个线程(生产者),多个使用notifyAll()
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

java.lang.Object 类提供了两类(等待和唤醒)用于线程通信的方法.

       wait():执行该方法的线程对象释放同步锁,JVM把该线程存放到等待池中,等待其他的线程唤醒该线程.

       notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待.

       notifyAll():执行该方法的线程唤醒在等待池中等待的所有的线程,把线程转到锁池中等待.

注意:上述方法只能被同步监听锁对象来调,否则报错 IllegalMonitorStateException异常.

同步锁池:

      同步锁一般都选择多个线程共同的资源对象.

      当前生产者在生产数据时(先拥有同步锁),其他线程就在锁池中等待获取锁.

      当线程执行完同步代码块时,就会释放同步锁,其他线程开始抢锁的使用权.

多个线程只有共享使用相同的一个对象时,多线程之间才有互斥效果,把这个用来做互斥的对象称之为 同步监听对象/同步锁.

      同步锁对象可以选择任意类型的对象都可以,只需要保证多个线程使用的是相同锁对象即可.

因为,只有同步监听锁对象才能调用 wait和 notify方法,所以,wait和 notify方法应该存在于Object类中,而不是Thread类中.

 

2、线程通信 -- 使用Lock机制和Condition接口

     wait和notify方法,只能被同步监听锁对象来调用,否则报错 IllegalMonitorStateException异常.

     而Lock机制根本就没有同步锁,也就没有自动获取锁和自动释放锁的概念。

     因为没有同步锁,所以Lock机制不能调用wait和notify方法.

解决方案:Java5中提供了Lock机制的同时也提供了处理Lock机制的通信控制的Condition接口.

从Java5开始,可以:

      1)使用Lock机制取代 synchronized代码块和 synchronized方法.

      2)使用Condition接口对象的await,signal,signalAll方法取代Object类中的wait,notify,notifyAll方法.

Condition接口提供一个使用实例

     

public class ThreadDemo {
    public static void main(String[] args) {
        // 创建生产者和消费者共同的资源对象
        ShareResource shareResource = new ShareResource();

        // 启动生产者线程
        new Thread(new Producer(shareResource)).start();
        // 启动消费者线程
        new Thread(new Consumer(shareResource)).start();
    }
}
// 共享资源--水果
class ShareResource{
    private String name;
    private String colour;
    private boolean isEmpty = true; // 表示共享资源对象是否为空的状态
    private final Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    /**
     *  生产者向共享资源对象中存储数据
     * @param name  存储的名称
     * @param colour 存储的颜色
     */
    public void push(String name, String colour){
        lock.lock(); // 获取锁
        try {
            while(!isEmpty){ // 当前对象为不空时等待消费者来获取
                condition.await();
            }
            this.name = name;
            Thread.sleep(10);
            this.colour = colour;
            isEmpty = false; // 设置共享资源中数据为空
            condition.signal(); // 唤醒一个线程(消费者),多个使用signalAll()
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock(); // 释放锁
        }
    }

    /**
     * 消费者从共享资源对象中取出数据
     */
    public void popup(){
        lock.lock(); // 获取锁
        try {
            while(isEmpty){ // 当前对象为空时等待生产者来生产
                condition.await();
            }
            Thread.sleep(10);
            System.out.println(this.name + "--" + this.colour);
            isEmpty = true; // 设置共享资源中数据为空
            condition.signal(); // 唤醒一个线程(消费者),多个使用signalAll()
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock(); // 释放锁
        }
    }
}

// 生产者
class Producer implements  Runnable{
    // 共享资源对象
    private ShareResource shareResource = null;

    public Producer(ShareResource shareResource){
        this.shareResource = shareResource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            if(i % 2 == 0){
                shareResource.push("苹果","红色");
            }else{
                shareResource.push("柚子","橘黄色");
            }
        }
    }
}
// 消费者
class Consumer implements Runnable{
    // 共享资源对象
    private ShareResource shareResource = null;

    public Consumer(ShareResource shareResource){
        this.shareResource = shareResource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            shareResource.popup();
        }
    }
}

 

二、线程的生命周期

      生命周期:一个事物从出生的那一刻开始到最终死亡中间的整个过程。线程也是有生命周期的,也存在不同的状态的,状态相互之间的可转换。如下图:

      

线程对象的状态存放在Thread类的内部类(State)中:

注意:Thread.State类其实是一个枚举类,因为线程对象的状态是固定的,只有6种,此时使用枚举来表示是最恰当的。

     

      有人又把阻塞状态、等待状态和计时等待状态合称为阻塞状态。如下图:

      

1、新建状态(new)

      使用new创建一个线程对象,仅仅在堆中分配内存空间,在调用start方法之前;新建状态下,线程压根就没有启动,仅仅只是存在一个线程对象而已。

      Thread t = new Thread(); //此时t就属于新建状态

      当新建状态下的线程对象调用了start方法,此时从新建状态进入可运行状态.

      线程对象的start方法只能调用一次,否则报错:IllegalThreadStateException异常。

2、可运行状态(runnable)

  分成两种状态:ready和running。分别表示就绪状态和运行状态。

       就绪状态:线程对象调用start方法之后,等待JVM的调度(此时该线程并没有运行)。

       运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。

3、阻塞状态(blocked)

     正在运行的线程因为某些原因放弃CPU,暂时停止运行,就会进入阻塞状态。此时JVM不会给线程分配CPU,直到线程重新进入就绪状态,才有机会转到运行状态.

     阻塞状态只能先进入就绪状态,而不能直接进入运行状态.

     阻塞状态的两种情况:

            1)当A线程处于运行过程时,试图获取同步锁时,却被B线程获取,此时JVM把当前A线程存到对象的锁池中,A线程进入阻塞状态.

            2)当线程处于运行过程时,发出了IO请求时,此时进入阻塞状态.

4、等待状态(waiting)(等待状态只能被其他线程唤醒):此时使用的无参数的wait方法,

       当线程处于运行过程时,调用了wait()方法,此时JVM把当前线程存在对象等待池中.

5、计时等待状态(timed waiting)(使用了带参数的wait方法或者sleep方法)

       1)当线程处于运行过程时,调用了wait(long time)方法,此时JVM把当前线程存在对象等待池中.

       2)当前线程执行了sleep(long time)方法.

6、终止状态(terminated):通常称为死亡状态,表示线程终止.

        1)正常执行完run方法而退出(正常死亡).

        2)遇到异常而退出(出现异常之后,程序就会中断)(意外死亡).

线程一旦终止,就不能再重启启动,否则报错:IllegalThreadStateException异常。

 

三、线程的控制操作

1、线程休眠:让执行的线程暂停一段时间,进入计时等待状态。

     方法:static void sleep(long millis)

     调用sleep后,当前线程放弃CPU,在指定时间段之内,sleep所在线程不会获得执行的机会。此状态下的线程不会释放同步锁/同步监听器。该方法更多的用于模拟网络延迟,让多线程并发访问同一个资源的错误效果更明显,在开发中也会故意使用该方法。

    线程的sleep方法应该写在线程的run()方法里,sleep()又是静态方法,所以最好的调用方法就是 Thread.sleep()。

    注意:sleep方法只能让当前线程睡眠。调用某一个线程类的对象t.sleep(),睡眠的不是t,而是当前线程。

    public static void main(String[] args) throws InterruptedException{
        for (int i = 10; i >0; i--) {
            System.out.println("剩余"+ i + "秒");
            Thread.sleep(1000);
        }
        System.out.println("Boom....");
    }

    java.util.concurrent.TimeUnit也可以控制线程睡眠: 

TimeUnit.SECONDS.sleep(1); TimeUnit.MINUTES.sleep(1); TimeUnit.HOURS.sleep(1); TimeUnit.DAYS.sleep(1);

2、联合线程:

     方法:void join()  :表示一个线程等待另一个线程(这个join的线程)完成后才执行。

     join方法被调用之后,线程对象处于阻塞状态。

    主要作用就是同步,它可以使得线程之间的并发执行变为串行执行。有人也把这种方式称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程。

    注意:join方法必须在线程start方法调用之后调用才有意义。如果一个线程都没有start,那它也就无法同步了。

    比如:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。

     join方法中可以传入参数,如果A线程中调用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并发执行。注意:jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。     

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException{
        System.out.println("begin....");
        JoinThread joinThread = new JoinThread();
        for (int i = 0; i < 15; i++) {
            System.out.println("main " + i);
            if (i ==5 ) {
                joinThread.start();//启动join线程
            }
            if (i == 10 ) {
                joinThread.join(); //强制运行该线程,直到结束后运行另一个线程
            }
        }
        System.out.println("end...");
    }
}
//联合线程
class JoinThread extends Thread {
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("Join" + i);
        }
    }
}

3、后台线程:

     在后台运行的线程,其目的是为其他线程提供服务,也称为“守护线程"。JVM的垃圾回收线程就是典型的后台线程。

     特点:若所有的前台线程都死亡,后台线程自动死亡。反过来,如果后台线程先执行完,前台线程会不停止。

方法:

     boolean isDaemon() ;//测试这个线程是否是守护线程。

     void setDaemon(boolean on) ;//将此线程标记为 daemon线程或用户线程。 

     前台线程创建的线程默认是前台线程,可以通过setDaemon方法设置为后台线程,并且当且仅当后台线程创建的新线程时,新线程是后台线程。

     设置后台线程:thread.setDaemon(true);该方法必须在start方法调用前,否则报错:IllegalThreadStateException异常。

public class ThreadDemo {
    public static void main(String[] args){
        //判断当前线程是否是守护线程
        System.out.println(Thread.currentThread().isDaemon()); // false
        for (int i = 0; i < 10; i++) {
            System.out.println("main " +i);
            if(i == 5){
                DaemomThread dt =new DaemomThread();
                dt.setDaemon(true);//设置为后台线程,并且在调用start之前设置
                dt.start();
            }
            //当前台线程结束之后.后台线程也会相应的自动结束
        }
    }
}
//后台线程
class DaemomThread extends Thread{
    public void run(){
        for (int i = 0; i             
关注
打赏
1664721914
查看更多评论
0.0371s