美文网首页Android开发Android开发
Java 并发(2) -- Java中的原子类&互斥 & 线程中

Java 并发(2) -- Java中的原子类&互斥 & 线程中

作者: kolibreath | 来源:发表于2020-03-02 21:56 被阅读0次
守望先锋黑百合

相关文章链接

因为上一篇文章由于篇幅原因,很多概念塞进去的话会过于臃肿,这篇文章主要分成两个小节,对于上篇文章进行补充:

  1. 对上篇文章的补充
    1.1 ThreadLocal类
    1.2 线程中的异常处理
  2. 互斥的概念
    2.1 锁和监视器
    2.2 使用可重入锁和AtomInteger重写上篇文章的例子

上篇文章的补充

上篇文章说明了volatilesynchronized关键字的用法,其中volatile关键字可以保证除了longdouble类型的的可见性,之所以如此,是因为JVM可以保证的原子操作一般是32位的,而64位longdouble需要两次操作,这样就会产生字撕裂问题。上次内容补充“字撕裂”问题。关于JMM内存模型,可以详细参考这篇文章

ThreadLocal的使用

既然volatile是为了让属性在线程间访问能够一致,然而线程之间的工作内存却只是主内存的copy,之后还要再刷新回去,线程之间能否拥有一个自己的真正的“私人空间”呢?
我们可以使用ThreadLocal

ThreadLocal 例子

public class ThreadLocalVariableHolder {

    //虽然是一个static 但是还是每一个线程不相同
    private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
        private Random random = new Random(47);
        @Override
        protected synchronized Integer initialValue() {
            return random.nextInt(10000);
        }
    };

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

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


    private static class Accessor implements Runnable{

        private final int id;
        private Accessor(int id){
            this.id = id;
        }

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

        public String toString(){
            return "#" + id + ": " + ThreadLocalVariableHolder.get();
        }
    }
    public static void main(String args[]) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5 ; i++) {
            executorService.execute(new Accessor(i));
        }
        try {
            TimeUnit.MILLISECONDS.sleep(300);
            executorService.shutdownNow();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ThreadLocal可以使用get()set()方法获取和设置值,初始值需要重写 initialValue()方法获取。main()中初始化了5个线程,放入线程池开始运行。随着类而初始化的变量,在Accessorrun()方法中被修改。因为ThreadLocal是每个线程都有的。虽然ThreadLocalVariableHolder是一个静态变量,但是每个线程中的随机的开始值是不相同的,运行结果如下:

#1: 556
#3: 1862
#2: 6694
#0: 9259
#2: 6695
#4: 962
#3: 1863
#1: 557
#3: 1864
#4: 963
#2: 6696
#0: 9260
#2: 6697
#4: 964
#3: 1865
#1: 558
#3: 1866
#4: 965
#2: 6698
#0: 9261
#2: 6699
.....

线程中的异常处理

如果run()方法中抛出了没有捕获的异常会怎么样呢?

 public static class CaughtThread implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread() + "--->"+ Thread.currentThread().getUncaughtExceptionHandler());
            throw new RuntimeException();

        }
    }

 public static  void main(String args[]){
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new CaughtThread());
        executorService.shutdown();
    }

控制台会直接返回错误然后结束掉

Exception in thread "pool-1-thread-1" java.lang.RuntimeException
    at ExceptionHandlerFactory$CaughtThread.run(ExceptionHandlerFactory.java:20)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

线程使用了这样的一个理念: 线程是一个独立的代码片段,自己的问题不应该交给外部处理。所以我们可以通过这样的方法完成线程的异常处理:

public class ExceptionHandlerFactory implements ThreadFactory {

    public static class MyExceptionHandler implements Thread.UncaughtExceptionHandler{
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("靠谱的异常捕捉方式 " + e);
        }
    }

    public static class CaughtThread implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread() + "--->"+ Thread.currentThread().getUncaughtExceptionHandler());
            throw new RuntimeException();

        }
    }
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setUncaughtExceptionHandler(new MyExceptionHandler());
        return thread;
    }

    public static  void main(String args[]){
        ExecutorService executorService = Executors.newCachedThreadPool(new ExceptionHandlerFactory());
//        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new CaughtThread());
        executorService.shutdown();
    }
}
  1. 自定义一个异常处理类
  2. 通过实现ThreadFactory类中的newThread方法,给需要处理的线程手动设置好处理类
  3. 使用工厂方法,需要生成什么样的线程就调用什么样的方法
ExecutorService executorService = Executors.newCachedThreadPool(new ExceptionHandlerFactory());

互斥的概念

临界资源的概念

两个线程同时竞争一个资源,这个资源就称为临界资源。临界资源需要处理得当,否则有可能就会造成死锁,比如哲学家问题中,所有哲学家都拿着左手边的筷子,看着右边的人希望他放下筷子,这就锁住了。

锁 & 对象监视器

  1. 监视器
    监视器是一种实现同步的模型,Java在对象的层面上就对监视器模式进行了支持,详细可以参考Java 监视器模型的实现

下面是我对与锁和监视器的理解:
Java中的每一个对象都有一个对象监视器,线程如果需要获取互斥数据的话,需要从监视器手上获取这样的“许可”,这样的许可 我们就称作锁。


上篇文章最后的例子中currentEvenValue 就是一个临界资源。我们常常使用同步的方法可以解决临界资源的访问问题,现在使用Java中的可重入锁之一的ReentrantLock重写上篇中的问题:

什么叫做可重入?

上面说的哲学家问题就是一个可能会造成死锁的著名问题,请看下面的代码


    private static synchronized void testA(){
        System.out.println("this is testA");
        testB();
    }

    private static synchronized void testB(){
        System.out.println("this is testB");
        testA();
    }

testA() 调用了 testB(), testB() 调用了 testA()这两个方法套娃了,因为synchronized是一个可重入锁,所以testA()获取对象上的锁调用testB()的时候可以成功。可重入锁概括起来就是:如果这个线程已经获取了这个对象上的锁,之后不需要再重复获取。所以Java中的显示锁ReentrantLock也是一个可重入锁,和synchronized关键字在可重入上意思相同。

使用可重入锁重写上面的例子

使用同步方法重写的方法:

public class SynchronizedEvenChecker extends IntGenerator{

    private int currentEvenValue = 0;
    //父类中的方法是同步方法,子类中的方法不一定会继承
    @Override
    public synchronized int next() {
        currentEvenValue++;
        Thread.yield(),
        currentEvenValue++;
        return currentEvenValue;
    }
}

使用可重入锁:

 private Lock lock = new ReentrantLock();
  @Override
    public int next() {
        lock.lock();
        try{
            ++currentEvenValue;
            Thread.yield();
            ++currentEvenValue;
            return currentEvenValue;
        }finally {
            lock.unlock();
        }
    }

可以看出来ReentrantLock的使用很简单,实际上使用ReentrantLock有这两点好处:

  1. 可以使用finally语句在最后进行解锁操作
  2. ReentrantLock相比synchronized有更细粒度的控制能力,不需要和对象或者类联系起来

Java中的原子类

CAS操作

CAS操作是Java原子类的核心,CAS操作。CAS 操作是一个原子操作,即CompareAndSwap操作需要接收三个操作数,目标内存地址V,旧值A,待修改的值B。当且仅当V中的值等于A时,将V中的值修改成B。
同样的,这个类我们也可以使用AtomicInteger重写

 private AtomicInteger currentEvenValueAtomic = new AtomicInteger(0);

    //使用Java原子类机制引用机制
    @Override
    public int next() {
        return currentEvenValueAtomic.addAndGet(2);
    }

addAndGet()是一个原子操作,这样就不需要加上同步锁了。


这篇文章的内容比较杂,因为有些概念需要理清楚,下篇文章将会涉及到定义在Object中的几个final 方法: wait() notify() notifyAll() 以及 哲学家问题的解决。


参考内容:
监视器–Java同步基本概念
锁和监视器的区别
notify 和 notifyAll区别
线程挂起,睡眠,阻塞区别
Thread join 相关用法

读《Thinking in Java有感》,遂记之

相关文章

网友评论

    本文标题:Java 并发(2) -- Java中的原子类&互斥 & 线程中

    本文链接:https://www.haomeiwen.com/subject/huepextx.html