请选择 进入手机版 | 继续访问电脑版
MSIPO技术圈 首页 IT技术 查看内容

中断-阻塞

2023-07-13

在其他对象上同步
synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this),这正是PairManager2所使用的方式。在这种方式中,如果获得了synchronized块上的锁,那么该对象其他的synchronized方法和临界区就不能被调用量。因此,如果在this上同步,临界区的效果就会直接缩小在同步的范围内。
有时必须在另一个对象上同步,但是如果你要这么做,就必须确保所有相关的任务都是在同一个对象上同步。

class DualSynch {
    private Object syncObject = new Object();

    public synchronized  void f() {
        for (int i = 0; i < 5; i++) {
            System.out.println("f()");
            Thread.yield();
        }
    }

    public  void g() {
        synchronized (syncObject) {
            for (int i = 0; i < 5; i++) {
                System.out.println("g()");
                Thread.yield();
            }
        }
    }
}

public class SyncObject {
    public static void main(String[] args) {
        final DualSynch dualSynch = new DualSynch();
        new Thread() {
            public void run() {
                dualSynch.f();
            }
        }.start();
        dualSynch.g();
    }
}

DaylSync.f()(通过同步整个方法)在this同步,而g()有一个在syncObject上同步的synchronized块。因此,这两个同步是互相独立的。通过在main中创建调用f的Thread对这一点进行演示,因为main线程是被用来调用g的。从输出中可以看到,这两个方式在同时运行,因此任何一个方法都没有因为对另一个方法的同步而阻塞。
线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同对的存储。因此,如果你有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。主要是,它们使得你可以将状态与线程关联起来。
创建和管理线程本地存储可以由java.long.ThreadLocal类来实现。

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Accessor implements Runnable {

    private final int id;

    public Accessor(int idn) {
        id = idn;
    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) { // 是否中断
            ThreadLocalVariableHolder.increment();
            System.out.println(this);
            Thread.yield();
        }
    }

    @Override
    public String toString() {
        return "#" +id + ": "+ ThreadLocalVariableHolder.get();
    }
}


public class ThreadLocalVariableHolder {
    /**
     * // ThreadLocal对象通常当作静态域存储。
     * 在创建ThreadLocal时,你只能通过get和set方法访问该对象的内容,
     * 其中,get方法将返回与其线程相关联的对象的副本,而set会将参数插入到
     * 为其线程存储的对象中,并返回存储中原有的对象。
     * increment和get方法在ThreadLocalVariableHolder中演示了这一点。
     * 注意:increment和get方法都不是synchronized的,因为ThreadLocal保证了不会出现不限竞争条件。
     * 当运行这个程序时,你可以看到每个单独的线程都分配了自己的存储,因为它们每个都需要跟踪自己的计数值,
     * 即便只有一个ThreadLocalVariableHolder对象
     */
    private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){ 
        //线程变量,意思是ThreadLocal中填充的变量属于当前线程。
        // 该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
        // ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
        private Random random = new Random();

        protected synchronized  Integer initialValue() {
            return random.nextInt(10000);
        }
    };

    public static void increment() {
        value.set(value.get()+1);
    }

    public static int get() {
        return value.get();
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i < 5; i++) {
            executorService.execute(new Accessor(i));
        }

        TimeUnit.SECONDS.sleep(3);

        executorService.shutdownNow(); //所有的Accessors 杀死
    }
}

终结任务
在前面的某些示例中,cancel和isCanceled方法被放到了一个所有任务都可以看到的类中。这些任务通过检查isCanceled来确定何时终止它们自己,对于这个问题来说,这是一种合理的方式。但是,在某些情况下,任务必须更加突然地终止。
装饰性花园
在这个仿真程序中,花园委员会希望了解每天通过多个大门进入公园的总人数。每个大门都有一个十字转门或某种其他形式的计数器,并且任何一个十字转门的计数值递增时,就表示公园中的总人数的共享计数值也会递增。

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


class Count {
    private int count = 0;

    private Random rand = new Random(47);

    //删除synchronized关键字以查看计数失败
    // 如果移除synchronized ,就会有差异,只需要用互斥来同步对Count的访问。
    public synchronized int increment() {
        int temp = count;

        // 使用了Randon对象,目的是在从把count读取到temp中,到递增temp并将其存储到count的这段时间里,有大约一半的一半时间产生让步。
        // 如果synchronized关键字注释掉,那么这个程序就会崩溃,因为多个任务将同时访问并修改count

        if (rand.nextBoolean()) {
            Thread.yield();
        }

        return (count = ++temp);
    }

    public synchronized int value() {
        return count;
    }
}

/**
 * 单个Count对象来跟踪花园参观者的主计数值,并且将其Entrance类中的一个静态域进行存储。
 * Count.increment和Count.value都是synchronized的,用来控制对count域的访问。
 *
 * 每个Entrance任务都维护着一个本地值number,它包含通过某个特定入口进入的参观者的数量。
 * 这提供了对count对象的双重检查,以确保其记录的参观者数量是正确的。Entrance.run只是递增number和count对象,然后休眠100毫秒。
 *
 * 因为Entrance.canceled是一个volatile布尔标志,而它只会读取和赋值,所以不需要同步对其的访问,就可以安全地操作它,
 * 如果你对诸如此类的情况有任何疑虑,那么最好总是使用synchronized
 *
 */
class Entrance implements Runnable {

    private static Count count = new Count();

    private static List<Entrance> entries = new ArrayList<Entrance>();

    private int number = 0;

    //不需要同步来启动
    private final int id;

    private static volatile boolean canceled = false;

    public Entrance(int id) {
        this.id = id;
        //将此任务列在列表中.也可以防止死任务的垃圾收集
        entries.add(this);
    }

    public static void cancel() {
        canceled = true;
    }

    @Override
    public void run() {
        while (!canceled) {
            synchronized (this) {
                ++number;
            }
            System.out.println(this+" Total: " + count.increment());

            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("sleep interrupted");
            }
        }
        System.out.println("Stopping "+ this);
    }

    public synchronized int getValue() {
        return number;
    }

    public String toString() {
        return "Entrance " + id + ": " + getValue();
    }

    public static int getTotalCount() {
        return count.value();
    }

    public static int sumEntrances() {
        int sum = 0;
        for (Entrance entry : entries) {
            sum += entry.getValue();
        }
        return sum;
    }
}

public class OrnamentalGarden {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i < 5; i++) {
            executorService.execute(new Entrance(i));
        }
        //跑一会儿。然后停下来收集数据
        TimeUnit.SECONDS.sleep(3);
        Entrance.cancel();
        executorService.shutdown();
        if (!executorService.awaitTermination(250, TimeUnit.MILLISECONDS)) { //等待任务结束,如果所有的任务在超时时间达到之前全部结束,则返回true 否则返回false。
                //表示不是所有所有的任务都已经结束了。尽管这会导致每个任务都退出其run方法,并因此作为任务而终止。
                // 但是Entrance对象仍旧是有效的,因为在构造器中,每个Entrance对象都存储在称为entrances的静态List<Entrance>中
                // 因此,sumEntrances仍旧可以作用于这些有效的Entrance对象。
            System.out.println("Some tasks were not terminated!");
        }

        System.out.println("Total: " + Entrance.getTotalCount());
        System.out.println("Sum of Entances: " + Entrance.sumEntrances());

    }
}

在阻塞时终结
前面示例中的Entrance.run在其循环中包含对sleep的调用。我们知道,sleep最终将唤醒,而任务也将返回循环的开始部分,去检查canceled标志,从而决定是否跳出循环。但是sleep一种情况,它使任务从执行状态变为阻塞状态,而有时你必须终止被阻塞的任务。
线程状态:
1:新建:当线程被创建时,它只会短暂的处于这种状态。此时它已经分配了必须的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将这个线程转变为可运行状态或阻塞状态。
2:就绪:在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,它就可以运行;这不同于死亡和阻塞状态。
3:阻塞:线程能够运行,但是有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,它才有可能执行操作。
4:死亡:处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run方法返回,但是任务的线程还可以被中断,你将要看到这一点。
进入阻塞状态:
一个任务进入阻塞状态,可能有如下原因:
1:通过调用sleep使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。
2:你通过调用wait使线程挂起。直到线程得到了notify或notifyAll消息或者signal或signalAll消息,线程才会进入就绪状态,我们将在稍后的小姐中验证这一点。
3:任务在等待某个输入输出完成。
4:任务视图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务以及获取了这个锁。
在较早的代码中,也可能会看到用suspend和resume来阻塞和唤醒线程,但是在现代java中这些方法被废止了(可能导致死锁)。stop方法也已经被废止了,因为它不释放线程获得的锁,并且如果线程处于不一致的状态,其他任务可以在这种状态下浏览并修改他们。
有时你希望能够终止处于阻塞状态的任务。如果对于处于阻塞状态的任务,你不能等待其到达代码中可以检查器状态值的某一点,因而决定让它主动地终止,那么你就必须强制这个任务跳出阻塞状态。
中断:
在Runnable.run方法的中间打断它,与等待该方法到达对cancel标志的测试等。当你打断被阻塞的任务时,可能需要清理资源。正因为这一点,在任务的run方法中间打断,更像是抛出的异常,因此在java线程中的这种类型的异常中断中用到了异常。为了在以这种方式终止任务时,返回众所周知的良好状态,你必须执行考虑代码的执行路径,并仔细编写catch子句以正确清除所有事物。
Thread类包含interrupt方法,因此你可以终止被阻塞的任务,这种方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当抛出该异常或者该任务调用Thread.interrupted时,中断将被复位。正如你将看到的,Thread.interrupted提供了离开run循环而不抛出异常的第二种方式。
为了调用interrupt,你必须持有Thread对象,你可能已经注意到了,新的concurrent类似乎在避免对Thread对象的直接操作,转而尽量通过Executor来执行所有操作,如果你在Executor上调用shutdowmNow,那么它将发送一个Interrupt调用给它启动的所有线程。这么做是有意义的,因为当你完成工程中的某个部分或者整个程序时,通常会希望同时关闭某个特定Executor的所有任务。然后,你有时也会希望只中断某个单一任务,如果使用Executor,那么通过调用submit而不是executor来启动任务,就可以持有该任务的上下文,submit将返回一个Future<?>,其中有一个未修饰的参数,因为你永远都不会在骑上调用get----持有这个Future的关键在于你可以在其上调用cancel,并因此可以使用它来中断某个特定任务。如果你将true传递给cancel,那么它就会拥有在该线程上调用interrupt以停止这个线程的权限。因此,cancel是一种中断由Executor启动的单个线程的方式。

相关阅读

手机版|MSIPO技术圈 皖ICP备19022944号-2

Copyright © 2023, msipo.com

返回顶部