《实战Java...》读书笔记

来源:互联网 发布:过程控制软件 编辑:程序博客网 时间:2024/05/21 19:33

1、如何终止一个线程?

    调用stop()方法不可取,因为stop会立即停止线程并释放锁,容易导致不一致,那怎么停止呢?一般是给线程设置一个标志位表示是否继续运行,然后设置一个方法,用来将标志位设置为false,并且在run方法中不断坚持这个标志位,如果为false则使用break跳出无限循环。

 

2、调用Thread.sleep时要处理InterruptedException,也就是要想好被中断了要怎么办。《实战Java高并发程序》中说sleep在捕获到InterruptedException时会清除中断标记,所以在处理异常的代码中需要再次中断自己,来设置标志位。

    如果希望一个线程在中断后退出,需要在run方法的开始不断检查当前线程是否被中断,利用Thread.currentThread().isInterrutped()来检查。

 

3、Thread.suspend方法在挂起时不释放锁。被挂起之后状态仍然是runnable。要是Thread.resume发生在Thread.suspend之前,那么线程将永远被阻塞,而LockSupport.park确不会这个样子,LockSupport.part同样是阻塞线程的。

    与Object.wait()相比,LockSupport不需要事先获得某个对象的锁,也不会抛出InterruptedException。类似的方法还有LockSupport.parkNanos(),LockSupport.parkUntil()。我感觉使用LockSupport.park的时候,是不会释放锁的。

 

4、thread0.join()的原理是一直在thread0上wait,while(isAlive){wait(0)},一定要先调用start再调用join,因为不调用start线程不是Alive的,如果一个线程被启动了但是还没终止,就是Alive的。

 

5、信号量可以允许多个线程(由构造函数指定)访问临界区,感觉跟锁一样,只不多是允许一定数量的线程。

 

6、不同线程池,比如newFixedThreadPool()方法,newSingleThreadExecutor()方法,newCachedThreadPool()方法,虽然看起来创建的线程有着完全不同的功能特点,但其内部实现均使用了ThreadPoolExecutor实现。在ThreadPoolExecutor中使用了一个AtomicInterger类型来保存线程池的状态和线程的数量,高三位用来保存状态,低29位用来保存线程数量。

    ThreadPoolExecutor构造方法中有一个参数BlockingQueue,被提交的任务将被放置在该队列中等待执行。线程池工厂Executors在产生不同的线程池时使用了不同的阻塞队列。可以参考下面几个:

newFixedThreadPool ( int )

new LinkedBlockingQueue<Runnable>()

newCachedThreadPool ()

new SynchronousQueue<Runnable>()

newScheduledThreadPool ()

new DelayedWorkQueue()

 

    除了使用Executors线程池静态工厂来产生线程池外,还可以直接使用(new) ThreadPoolExecutor来产生线程池,并重写其中的beforeExecutor/afterExecutor/terminated方法来扩展线程池。

    ThreadPoolExecutor的构造方法还有一个参数ThreadFactory,一般为Executors.defaultThreadFactory(),这个静态方法返回 newDefaultThreadFactory(),DefaultThreadFactory实现接口ThreadFactory,只有一个方法newThread(Runnable),返回一个Thread,我们可以在这个方法中设置自己的Thread属性(扩展ThreadFactory)。

 

7、Fork/Join框架是一种分而治之的思想,如果任务太大可以将这个任务划分为很多小任务。小任务与大任务是同一个类型,小任务调用fork执行,大任务等众多小任务join之后返回结果,join返回小任务的结果,大任务累加结果作为自己的结果。

    Fork/Join任务所执行的fork()和join()方法是在ForkJoinTask<V>中定义并实现的,所以任务类一定要继承自这个类。所谓ForkJoinTask就是支持fork分解以及join等待的任务。ForkJoinTask有两个子类,RecursiveActionRecursiveTask。它们分别表示没有返回值的任务和有返回值的任务。对于RecursiveAction和RecursiveTask来说,需要实现的抽象类是compute。

    Fork/Join任务需要提交到ForkJoinPool,即Fork/Join线程池,由下面这个样子:

    ForkJoinPoolpool = new ForkJoinPool();

    RecursiveTasktask = new RecursiveTask();

    pool.submit(task);

submit方法的参数是ForkJoinTask子类,返回类型也是ForkJoinTask类型。

 

8、关于Collections.synchronizedMap(new HashMap()),调用Collections.synchronizedMap时,其实是返回了一个SynchronizedMap类,这个类是Collections的内部类,这个类内部有两个变量,一个是Map<K,V>map,另一个是Object mutex,后面一个类执行Map实现,后面的mutex充当了锁的角色,每次执行具体的方法时,先获取所,以get方法为例:

        public V get(Objectkey) {

            synchronized (mutex) {returnm.get(key);}

        }

其他方法也都是这样的,Collections.synchronizedList也是这样的。这样可以实现线程安全,但是效果不一定好。

 

9、ConcurrentHashMap的size方法先尝试使用无锁的方式获取所有段(Segment)中的个体数目,如果两次累加的结果一致,就认为正确。尝试的次数为2次,第一次累加sum不为0,last等于0,所以是肯定不相等的,此次不相等再累加一次。再不想等就将所有的段都加锁。

    CopyOnWriteArrayList适用于读操作远大于写操作的场景,这个类实现了接口List<E>,因此操作跟普通的ArrayList差不多。这个类的读操作是完全不加锁的,只有写写操作才去竞争ReentrantLock锁。读操作如下:

    public E get(intindex) {

        return get(getArray(),index);

}

    final Object[] getArray() {

        returnarray;

}

    写操作是要加锁的,为了不影响读操作,写的时候(包括add方法和set方法),写方法会首先做一个原数组的拷贝,并在这个拷贝上执行写操作,最后将这个拷贝替换为对象的数组。写操作的整个过程都是需要加锁的。代码如下:

    public boolean add(E e) {

        final ReentrantLocklock = this.lock;

        lock.lock();

        try {

            Object[] elements = getArray();

            intlen = elements.length;

            Object[] newElements = Arrays.copyOf(elements,len + 1);

            newElements[len] =e;

            setArray(newElements);

            return true;

        } finally {

            lock.unlock();

        }

    }

    关于ArrayBlockingQueue,首先这个队列大小是固定的,不可扩展,三个构造函数里都必须指定大小,是一个循环队列。阻塞队列一般都有多个添加方法,add,put,offer,add方法是最简单的,其底层调用了offer方法,如果offer成功返回true,否则抛出异常,add方法如下:

    public boolean add(E e) {

        if (offer(e))

            return true;

        else

            throw newIllegalStateException("Queue full");

    }

offer方法成功返回true,失败返回false(失败是循环队列满了,添加不进去了),不抛出异常,这里需要注意的是offer方法不会阻塞,满了也不等待,直接返回,但是offer方法在其调用的方法insert里调用了notEmpty.signal(),虽然不等待,但是要唤醒阻塞在notEmpty锁上的其他线程,方法如下:

    public boolean offer(E e) {

        checkNotNull(e);

        final ReentrantLocklock = this.lock;

        lock.lock();

        try {

            if (count ==items.length)

                returnfalse;

            else {

                insert(e);

                returntrue;

            }

        } finally {

            lock.unlock();

        }

}

    private void insert(E x) {

        items[putIndex] =x;

        putIndex = inc(putIndex);

        ++count;

       notEmpty.signal();

}

这里重点说一下ArrayBlockingQueue的put方法(对应的阻塞获取方法是take),这个方法是纯正的实现阻塞队列的方法,put在队列满的时候不返回false,也不抛出异常,而是阻塞等待。put方法代码如下:

    public void put(E e) throwsInterruptedException {

        checkNotNull(e);

        final ReentrantLocklock = this.lock;

        lock.lockInterruptibly();

        try {

            while (count ==items.length)

                notFull.await();

            insert(e);

        } finally {

            lock.unlock();

        }

    }

看到没,如果满了(count==items.length),put方法就不停在一个while循环里等待(Effective里推荐的做法)。在成功插入之后,同样在insert方法里调用了notEmpty.signal();

    这里简单记录一下SynchronousQueue,这个类也实现了BlockingQueue接口,因此也是一个阻塞队列,但是这个队列没有缓冲,其容量为0,任何一个对SynchronousQueue的写需要等待一个对SynchronousQueue的读,反之亦然。因此SynchronousQueue与其说是一个队列,不如说是一个数据交换通道。

0 0