[JAVA多线程并发]Synchronization、Locks、Atomic Variables和CAS(三)

上一篇博客
// TODO ThreadPoolExecutor 有待回顾,在项目中踩了坑,需要之后对其源码,以及SynchronousQueue,LinkedBlockingQueue,ArrayBlockingQueue的源码做具体分析。

四、Thread Synchronization

1、Concurrency issues

多个线程尝试并发读写同一数据,常会发生一下两个问题:

  • 1、线程干扰错误
  • 2、内存一致性错误

1.1 线程干扰(竞争危害/竞态条件 Race Conditions)

改问题有点像mysql的脏读,由于未对资源上锁的原因,导致第一个线程修改还未提交时,第二个线程又来读取,即两个线程都读了相同的值,做了重复的事情。

package com;

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

/**
 * @author peng fei
 * @since 2020-07-25 23:51
 */

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

        Counter counter = new Counter();

        for (int i = 0; i < 1000; i++) {
            executorService.submit(counter::increment);
        }

        executorService.shutdown();
        executorService.awaitTermination(30, TimeUnit.SECONDS);

        System.out.println("final count is : " + counter.getCount());
    }

    static class Counter {
        int count = 0;

        public void increment() {
            count = count + 1;
        }

        public int getCount() {
            return count;
        }
    }
}

上述代码期待的结果为1000,实际执行出来可能小于1000。
在这里插入图片描述
造成问题的原因如下:

Thread-1:read count = 0
Thread-2:read count = 0
Thread-1: count = count + 1 (= 0 + 1)
Thread-2 :count = count + 1 (= 0 + 1)
导致重复赋值,两个线程操作后值为1。
如果是执行两次的情况下,那么就会导致值实际只增加了一次。

  • executorService.shutdown(); 关闭线程池,拒绝新提交的任务,将已提交的任务执行结束。
  • executorService.awaitTermination(30, TimeUnit.SECONDS); 等待30秒,如果已提交的任务还未全部执行完成,则引发InterruptedException异常,可以通过shutdownNow()来处理,返回出还未执行的任务。

1.2 内存一致性错误(Memory Consistency Errors)

  • 这种错误通常由于不同线程对同一个数据享有不同的观点导致。当一个线程在更新一些共享数据的时候,更改没有及时传播至其他线程,导致其他线程在对旧数据进行操作。
  • 发生这种错误的原因有很多,比如:编译器为了优化程序的表现,在对代码进行编译时会重新排列指令。而处理器为了优化事务,很可能会将值读入临时缓存、寄存器等来替代主内存中的值使用,最终导致了这种问题。
package com;

/**
 * @author peng fei
 * @since 2020-07-25 23:51
 */

public class SynchronizationApplication {
    private static String type = "cat";

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (type.equals("cat")) {
            }

            System.out.println("汪汪汪");

            while (type.equals("dog")) {
            }

            System.out.println("喵喵喵");
        });

        thread.start();
        Thread.sleep(1000);
        System.out.println("变狗");
        type = "dog";

        Thread.sleep(1000);
        System.out.println("变猫");
        type = "cat";
    }
}

上述代码预期的结果应该是:

变狗
汪汪汪
变猫
喵喵喵

但是实际的结果是这样的:
在这里插入图片描述
在将上述代码中的sleep去掉后结果又会不一致,其中还有一些原因是我没有搞清楚的。

2、Synchronized

2.1 Synchronized Methods

synchronized关键字,保证了防止多个线程同时访问该方法。

package com;

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

/**
 * @author peng fei
 * @since 2020-07-25 23:51
 */

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

        Counter counter = new Counter();

        for (int i = 0; i < 1000; i++) {
            executorService.submit(counter::increment);
        }

        executorService.shutdown();
        executorService.awaitTermination(30, TimeUnit.SECONDS);

        System.out.println("final count is : " + counter.getCount());
    }

    static class Counter {
        int count = 0;

        public synchronized void increment() {
            count = count + 1;
        }

        public int getCount() {
            return count;
        }
    }
}
  • synchronized关键字保证了一次只有一个线程能够进入increment()方法。
  • Synchronization概念是与单个对象有关系,主要针对多线程调用单个对象时,当多线程同时调用多个不同的对象时,那也不会存在竞态条件。

注意点:

  • synchronized关键字修饰非静态方法时,锁是加在该方法的对象的上的,多线程竞争的是同一个实例化对象的锁。
  • synchronized关键字修饰静态方法时,锁则是加在该方法的类上,所有的该类实例化的对象同时竞争一把锁。
  • 一个类可以同时拥有同步非同步方法,非同步方法可以被多个线程同时访问而不受限制。
  • 线程可以在synchronized方法中调用另外一个synchronized方法,这样线程便可以获得多把锁。但是要注意避免死锁问题的出现。

2.2 Synchronized Blocks

  • Java 内部使用一种**内部锁(监控锁)**来管理线程同步,每个对象都有一个内部锁与它关联。
  • 每当一个线程调用一个对象的Synchronized方法时,他将自动获取该对象的内部锁,并在退出方法时释放,即便方法是以抛出异常的方式退出的。
  • 在针对静态方法时,相乘将会获取Class 对象的内部锁。
  • synchronized关键字也可以被用在代码块上,但是必须提供该对象的内部锁给synchronized,如下例代码:
public void increment() {
	
	// Acquire Lock
	synchronized (this) {
		count = count + 1;
	}
	// Release Lock
}

3、Volatile

  • volatile关键字被用于在多线程问题中避免线程一致性错误,它会告诉编译器避免对变量的优化工作。
  • 如果你用volatile标记一个变量,编译器将不会优化或重排该变量前后的指令。同时变量的值将总会从主内存中读取来取代临时寄存器。
    例子:
package com;

/**
 * @author peng fei
 * @since 2020-07-25 23:51
 */

public class VolatileApplication {
    private static volatile String type = "cat";

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (type.equals("cat")) {
            }

            System.out.println("汪汪汪");

            while (type.equals("dog")) {
            }

            System.out.println("喵喵喵");
        });

        thread.start();
        Thread.sleep(1000);
        System.out.println("变狗");
        type = "dog";

        Thread.sleep(1000);
        System.out.println("变猫");
        type = "cat";
    }
}

在这里插入图片描述

五、Locks

1. ReentrantLock

  • ReentrantLock(重入锁)是一个行为与 ”通过synchronized关键字访问内部锁/隐式锁“ 行为相同的的互斥锁。
  • 一个目前拥有ReentrantLock锁的线程可以不止一次的获取它,而不会有任何问题出现。
package com;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author peng fei
 * @since 2020-08-03 14:22
 */

class ReentrantLockMethodsCounter {
    private final ReentrantLock lock = new ReentrantLock();

    private int count = 0;

    public int incrementAndGet() {
        System.out.println("IsLocked : " + lock.isLocked());

        System.out.println("IsHeldByCurrentThread:" + lock.isHeldByCurrentThread());

        boolean isAcquired = lock.tryLock();
        System.out.println("Lock Acquired : " + isAcquired + "\n");

        if (isAcquired) {
            try {
                Thread.sleep(2000);
                count = count + 1;
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            } finally {
                lock.unlock();
            }
        }

        return count;
    }

}

public class ReentrantLockMethodClass {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        ReentrantLockMethodsCounter counter = new ReentrantLockMethodsCounter();

        executorService.submit(() -> {
            System.out.println("IncrementCount (First Thread)" +
                    counter.incrementAndGet());
        });

        executorService.submit(() -> {
            System.out.println("IncrementCount (Second Thread)" +
                    counter.incrementAndGet());
        });

        executorService.shutdown();
    }
}
  • tryLock() 方法尝试不暂停线程去获得锁,如果线程由于其他线程持有该锁而没能获得锁,那么tryLock() 方法将会立刻return而不去等待锁的释放。
  • 你也可以为tryLock() 方法指定一个延迟时间去等待锁的释放-

lock.tryLock(1, TimeUnit.SECONDS);

2. ReadWriteLock

  • ReadWriteLock读写锁包括了一对锁,即读锁和写锁。读锁可能被线程持有的同时,写锁可能没有被任何线程持有。
  • 读写锁可以提高并发等级,它相对于其他锁,在面对写少于读时能有更好的并发表现。
class ReadWriteCounter {
    ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count = 0;

    public int incrementAndGetCount() {
        lock.writeLock().lock();
        try {
            count = count + 1;
            return count;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public int getCount() {
        lock.readLock().lock();
        try {
            return count;
        } finally {
            lock.writeLock().unlock();
        }
    }
}
  • 上述例子中,当没有线程调用incrementAndGetCount()方法时,多个线程可以同时执行getCount()方法。
  • 如果有任何一个线程调用了incrementAndGetCount()方法并获得了写锁,那么所有的读线程都将会暂停执行等待写线程返回。

六、Atomic

1. Atomic Variables

  • Java在并发包java.util.concurrent.atomic包中定义了一系列的Class来支持在单个变量上支持Atomic(原子)操作。
  • 原子类内部使用了被现代CPU支持的CAS(compare-and-swap)指令来实现同步,这些指令通常要比锁更快。
package com;

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

/**
 * @author peng fei
 * @since 2020-08-04 09:43
 */
class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public int incrementAndGet() {
        return count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}


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

        AtomicCounter counter = new AtomicCounter();

        for (int i = 0; i < 1000; i ++) {
            executorService.submit(counter::incrementAndGet);
        }

        executorService.shutdown();
        executorService.awaitTermination(60, TimeUnit.SECONDS);

        System.out.println("Final Count is :" + counter.getCount());

    }
}

在这里插入图片描述
上述代码中使用了原子操作,底层使用了CAS:

2. CAS

所谓cas就是compare and swap技术,其由cpu指令实现。
在这里插入图片描述

  • 由上图可知CAS其实是一种无锁技术,这也是其性能要比synchronized和lock高的原因。CAS也可以认为是一种乐观锁,其乐观认为其他线程在运行过程中不会修改原值。然后通过检测原值变动的方式解决同步问题。
  • CAS两个步骤:冲突检测数据更新
  • CAS的缺点:
    • 1、ABA问题:

在线程未做CAS判断时,原值被其他线程修改为B后又改为了A。
解决方法:

  1. 使用版本号机制,如手动增加版本号字段。
  2. JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题,除了比较预期值外,还增加了一个时间戳,同时去比对时间戳是否一致。
  • 2、循环时间长开销大

自旋CAS长时间不成功导致。

  • 3、只能保证一个共享变量的原子操作

Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

// TODO关于JAVA并发暂时告一段落,主要是目前缺少应用经验,理解不够透彻。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值