【编写高质量代码:改善Java程序的151个建议】第9章:多线程和并发___建议125~131

  • Post author:
  • Post category:java


建议125:优先选择线程池

建议126:适时选择不同的线程池来实现



Java线程池原理及实现


建议127:lock与synchronized是不一样的

直接上代码:

package OSChina.Multithread;

import java.util.Calendar;

public class Task {
    public void doSomething() {
        try {
            // 每个线程等待2秒钟,注意此时线程的状态转变为Warning状态
            Thread.sleep(2000);
        } catch (Exception e) {
            // 异常处理
        }
        StringBuffer sb = new StringBuffer();
        // 线程名称
        sb.append("线程名称:" + Thread.currentThread().getName());
        // 运行时间戳
        sb.append(",执行时间: " + Calendar.getInstance().get(Calendar.SECOND) + "s");
        System.out.println(sb);
    }
}
package OSChina.Multithread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TaskWithLock extends Task implements Runnable{
    // 声明显示锁
    private final Lock lock = new ReentrantLock();
    @Override
    public void run() {
        try {
            // 开始锁定
            lock.lock();
            doSomething();
        } finally {
            // 释放锁
           lock.unlock();
        }
    }
}
package OSChina.Multithread;

public class TaskWithSync extends Task implements Runnable{
    @Override
    public void run() {
        synchronized ("A"){
            doSomething();
        }
    }
}
package OSChina.Multithread;

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

public class Client {
    public static void runTasks(Class<? extends Runnable> clz) throws Exception{
        ExecutorService es = Executors.newCachedThreadPool();
        System.out.println("***开始执行 " + clz.getSimpleName() + " 任务***");
        // 启动3个线程
        for (int i = 0; i < 3; i++) {
            es.submit(clz.newInstance());
        }
        // 等待足够长的时间,然后关闭执行器
        TimeUnit.SECONDS.sleep(10);
        System.out.println("---" + clz.getSimpleName() + "  任务执行完毕---\n");
        // 关闭执行器
        es.shutdown();
    }

    public static void main(String[] args) throws Exception{
        // 运行显示任务
        runTasks(TaskWithLock.class);
        // 运行内部锁任务
        runTasks(TaskWithSync.class);
    }
}

1d631cd89e21564be48250c52c74250379b.jpg

显示锁是同时运行的,很显然pool-1-thread-1线程执行到sleep时,其它两个线程也会运行到这里,一起等待,然后一起输出,这还具有线程互斥的概念吗?

而内部锁的输出则是我们预期的结果,pool-2-thread-1线程在运行时其它线程处于等待状态,pool-2-threda-1执行完毕后,JVM从等待线程池中随机获的一个线程pool-2-thread-2执行,最后执行pool-2-thread-3,这正是我们希望的。

现在问题来了:Lock锁为什么不出现互斥情况呢?

这是因为对于同步资源来说显示锁是对象级别的锁,内部锁是类级别的锁,lock定义为多线程类的私有属性是起不到互斥作用的,除非把lock定义为所有线程的共享变量。

改一下代码,将lock定义在测试类中

// 声明显示锁
public static final Lock lock = new ReentrantLock();

3dc1bb8ae8d5e2367f3a3157dd6789614a5.jpg

除了这一点不同之外,显示锁和内部锁还有什么区别呢?还有以下4点不同:

1、Lock支持更细精度的锁控制:

假设读写锁分离,写操作时不允许有读写操作存在,而读操作时读写可以并发执行,这一点内部锁就很难实现。显示锁的示例代码如下:

package OSChina.Multithread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Foo {
    // 可重入的读写锁
    private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    // 读锁
    private static final Lock r = rwl.readLock();
    // 写锁
    private static final Lock w = rwl.writeLock();

    // 多操作,可并发执行
    public static void read() {
        try {
            r.lock();
            Thread.sleep(1000);
            System.out.println("read......");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            r.unlock();
        }
    }

    // 写操作,同时只允许一个写操作
    public static void write() {
        try {
            w.lock();
            Thread.sleep(1000);
            System.out.println("write.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            w.unlock();
        }
    }
}

可以编写一个Runnable实现类,把Foo类作为资源进行调用(注意多线程是共享这个资源的),然后就会发现这样的现象:读写锁允许同时有多个读操作但只允许一个写操作,也就是当有一个写线程在执行时,所有的读线程都会阻塞,直到写线程释放锁资源为止,而读锁则可以有多个线程同时执行。

2、Lock锁是无阻塞的,synchronized是阻塞的

3、Lock可实现公平锁,synchronized只能是非公平锁

4、Lock是代码级的,synchronized是JVM级的

Lock是通过编码实现的,synchronized是在运行期由JVM释放的,相对来说synchronized的优化可能性高,毕竟是在最核心的部分支持的,Lock的优化需要用户自行考虑。

显示锁和内部锁的功能各不相同,在性能上也稍有差别,但随着JDK的不断推进,相对来说,显示锁使用起来更加便利和强大,在实际开发中选择哪种类型的锁就需要根据实际情况考虑了:灵活、强大选择lock,快捷、安全选择synchronized。


建议128:预防线程死锁

1、死锁的概念

死锁是指多个进程在运行过程中因争夺资源而造成的一种僵局。当进程处于僵持状态时,若无外力作用,它们都将无法再向前推进。

2、产生死锁的原因

① 竞争资源

可剥夺资源和非剥夺性资源:

进程在获得这类资源后,该资源可以再被其它线程剥夺,CPU和主存均属于可剥夺性资源。另一类资源是不可剥夺性资源,当系统把这类资源分配给某进程后,再不能强行回收,只能在进程用完后自行释放,如磁带机、打印机等。

竞争非剥夺性资源:

在系统中所配置的非剥夺性资源,由于它们的数量不能满足诸进程运行的需要,会使进程在运行过程中,因争夺这些资源而陷入僵局。

竞争临时资源:

临时资源是指由一个进程产生,被另一个进程使用短暂时间后变无用的资源,它也可能产生死锁。

② 进程间推进顺序非法

进程在运行过程中,请求和释放资源的顺序不当,同样会产生死锁。

3、死锁的一些常用概念

① 互斥条件:

指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。

② 请求和保持条件:

指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其它进程占有,此时请求进程阻塞,但又对自己获得的其它资源保持不放。

③ 不剥夺资源:

指进程已获得资源,在使用完之前,不能被剥夺,只能在使用完时由自己释放。

④ 环路等待条件:

指在发生死锁时,必然存在一个进程—资源的环形链,即进程集合(P0,P1,P2,…,Pn)中的P0正在等待一个P1占用的资源;P1正在等待一个P2占用的资源,……,Pn正在等待已被P0占用的资源。

建议129:适当设置阻塞队列的长度

ArrayBlockingQueue类最常用的add方法

如果直接调用offer方法插入元素, 在超出容量的情况下, 它除了返回false外, 不会提供任何其他信息, 如果代码不做插入判断, 那就会造成数据的“默默”丢失, 这就是它与非阻塞队列的不同之处。

如果应用期望无论等待多长时间都要运行该任务, 不希望返回异常就需要用BlockingQueue接口定义的put方法了, 它的作用也是把元素加入到队列中, 但它与add、offer方法不同, 它会等待队列空出元素, 再让自己加入进去, 通俗地讲, put方法提供的是一种“无赖”式的插入, 无论等待多长时间都要把该元素插入到队列中。

与插入元素相对应, 取出元素也有不同的实现, 例如remove、poll、take等方法, 对于此类方法的理解要建立在阻塞队列的长度固定的基础上, 然后根据是否阻塞、阻塞是否超时等实际情况选用不同的插入和提取方法。

建议130:使用CountDownLatch协调子线程

CountDownLatch是一个非常实用的多线程控制工具类。常用的就下面几个方法:

CountDownLatch(int count) //实例化一个倒计数器,count指定计数个数
countDown() // 计数减一
await() //等待,当计数减到0时,所有线程并行执行

对于倒计数器,一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检测。只有等到所有的检查完毕后,引擎才能点火。那么在检测环节当然是多个检测项可以同时进行的。代码实现:

public class CountDownLatchDemo implements Runnable{

    static final CountDownLatch latch = new CountDownLatch(10);
    static final CountDownLatchDemo demo = new CountDownLatchDemo();

    @Override
    public void run() {
        // 模拟检查任务
        try {
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("check complete");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //计数减一
            //放在finally避免任务执行过程出现异常,导致countDown()不能被执行
            latch.countDown();
        }
    }


    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i=0; i<10; i++){
            exec.submit(demo);
        }

        // 等待检查
        latch.await();

        // 发射火箭
        System.out.println("Fire!");
        // 关闭线程池
        exec.shutdown();
    }
}

上述代码中我们先生成了一个CountDownLatch实例。计数数量为10,这表示需要有10个线程来完成任务,等待在CountDownLatch上的线程才能继续执行。latch.countDown();方法作用是通知CountDownLatch有一个线程已经准备完毕,倒计数器可以减一了。latch.await()方法要求主线程等待所有10个检查任务全部准备好才一起并行执行。

建议131:CyclicBarrier 让多线程齐步走

CyclicBarrier中文意思是“循环栅栏”

1、构造函数:

public CyclicBarrier(int parties)//parties 是参与线程的个数
public CyclicBarrier(int parties, Runnable barrierAction)//barrierAction是最后一个到达线程要做的任务

2、重要方法:

public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
  • 线程调用 await() 表示自己已经到达栅栏
  • BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时

一个线程组的线程需要等待所有线程完成任务后再继续执行下一次任务,代码实例:

package OSChina.Multithread;

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    static class TaskThread extends Thread {

        CyclicBarrier barrier;

        public TaskThread(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(getName() + " 到达栅栏 A");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 A");

                Thread.sleep(2000);
                System.out.println(getName() + " 到达栅栏 B");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 B");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        int threadNum = 5;
        CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 完成最后任务");
            }
        });

        for(int i = 0; i < threadNum; i++) {
            new TaskThread(barrier).start();
        }
    }
}

2a13e804b34d8b894f7f809835591e52805.jpg

从打印结果可以看出,所有线程会等待全部线程到达栅栏之后才会继续执行,并且最后到达的线程会完成 Runnable 的任务。

3、CyclicBarrier 使用场景

可以用于多线程计算数据,最后合并计算结果的场景。

4、CyclicBarrier 与 CountDownLatch 区别

① CyclicBarrier 是可循环利用的,CountDownLatch 是一次性的

② CyclicBarrier 参与的线程职责是一样的,CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。



版权声明:本文为guorui_java原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。