线程共享和协作(二):Synchronized、ThreadLocal、Volatile如何实现线程共享(4)

private volatile int value;

public final boolean compareAndSet(int expect, int update) {

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

}

可以看到是一个cas原子操作。

unsafe是java用来在CPU级别的操作CAS指令的类,对于程序员来说,此类是不可用。

由于是cpu级别的指令,其开销比需要操作系统参与的锁的开销小。

对于多个线程进入时,会先比较现在的value 是否与expect相等,如果不相等,则进入下一个循环。如果相等,则会更新成update值。

之后再进入的线程则会死循环。这样就保证了操作的原子性。

这样一个方法中 即包含了原子性,又包含了可见性

而关于Lock-Free算法,则是一种新的策略替代锁来保证资源在并发时的完整性的,Lock-Free的实现有三步:

  1. 循环(for(;😉、while)

  2. CAS(CompareAndSet)

  3. 回退(return、break)

用法

比如在多个线程操作一个count变量的情况下,则可以把count定义为AtomicInteger,如下:

public class Counter {

private AtomicInteger count = new AtomicInteger();

public int getCount() {

return count.get();

}

public void increment() {

count.incrementAndGet();

}

在每个线程中通过increment()来对count进行计数增加的操作,或者其它一些操作。这样每个线程访问到的将是安全、完整的count。

内部实现

采用Lock-Free算法替代锁+原子操作指令实现并发情况下资源的安全、完整、一致性

ABA问题(AtomicStampedReference的使用)

public class ABA {

// 普通的原子类,存在ABA问题

AtomicInteger a1 = new AtomicInteger(10);

// 带有时间戳的原子类,不存在ABA问题,第二个参数就是默认时间戳,这里指定为0

AtomicStampedReference a2 = new AtomicStampedReference(10, 0);

public static void main(String[] args) {

ABA a = new ABA();

a.test();

}

public void test() {

new Thread1().start();

new Thread2().start();

new Thread3().start();

new Thread4().start();

}

class Thread1 extends Thread {

@Override

public void run() {

a1.compareAndSet(10, 11);

a1.compareAndSet(11, 10);

}

}

class Thread2 extends Thread {

@Override

public void run() {

try {

Thread.sleep(200); // 睡0.2秒,给线程1时间做ABA操作

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“AtomicInteger原子操作:” + a1.compareAndSet(10, 11));

}

}

class Thread3 extends Thread {

@Override

public void run() {

try {

Thread.sleep(500); // 睡0.5秒,保证线程4先执行

} catch (InterruptedException e) {

e.printStackTrace();

}

int stamp = a2.getStamp();

a2.compareAndSet(10, 11, stamp, stamp + 1);

stamp = a2.getStamp();

a2.compareAndSet(11, 10, stamp, stamp + 1);

}

}

class Thread4 extends Thread {

@Override

public void run() {

int stamp = a2.getStamp();

try {

Thread.sleep(1000); // 睡一秒,给线程3时间做ABA操作

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“AtomicStampedReference原子操作:” + a2.compareAndSet(10, 11, stamp, stamp + 1));

}

}

}

Volatile

作用

Volatile可以看做是一个轻量级的synchronized,它可以在多线程并发的情况下保证变量的“可见性”,

什么是可见性

就是在一个线程的工作内存中修改了该变量的值,该变量的值立即能回显到主内存中,从而保证所有的线程看到这个变量的值是一致的,其二 volatile 禁止了指令重排,所以在处理同步问题上它大显作用,而且它的开销比synchronized小、使用成本更低。

虽然 volatile 变量具有可见性和禁止指令重排序,但是并不能说 volatile 变量能确保并发安全。

举个栗子:在写单例模式中,除了用静态内部类外,还有一种写法也非常受欢迎,就是Volatile+DCL:

public class Singleton {

private static volatile Singleton instance;

private Singleton() {

}

public static Singleton getInstance() {

if (instance == null) {

synchronized (Singleton.class) {

if (instance == null) {

instance = new Singleton();

}

}

}

return instance;

}

}

这样单例不管在哪个线程中创建的,所有线程都是共享这个单例的。

虽说这个Volatile关键字可以解决多线程环境下的同步问题,不过这也是相对的,因为它不具有操作的原子性,也就是它不适合在对该变量的写操作依赖于变量本身自己。举个最简单的栗子:在进行计数操作时count++,实际是count=count+1;,count最终的值依赖于它本身的值。所以使用volatile修饰的变量在进行这么一系列的操作的时候,就有并发的问题 .

volatile只能确保操作的是同一块内存,并不能保证操作的原子性。所以volatile一般用于声明简单类型变量,使得这些变量具有原子性,即一些简单的赋值与返回操作将被确保不中断。但是当该变量的值由自身的上一个决定时,volatile的作用就将失效,这是由volatile关键字的性质所决定的。

所以在volatile时一定要谨慎,千万不要以为用volatile修饰后该变量的所有操作都是原子操作,不再需要synchronized关键字了。

用法

因为volatile不具有操作的原子性,所以如果用volatile修饰的变量在进行依赖于它自身的操作时,就有并发问题,如:count,像下面这样写在并发环境中是达不到任何效果的:

public class Counter {

private volatile int count;

public int getCount(){

return count;

}

public void increment(){

count++;

}

}

而要想count能在并发环境中保持数据的一致性,则可以在increment()中加synchronized同步锁修饰,改进后的为:

public class Counter {

private volatile/无 int count;

public int getCount(){

return count;

}

public synchronized void increment(){

count++;

}

}

内部实现

汇编指令实现

Synchronized

作用

synchronized关键字是Java利用锁的机制自动实现的,一般有同步方法和同步代码块两种使用方式。Java中所有的对象都自动含有单一的锁(也称为监视器),当在对象上调用其任意的synchronized方法时,此对象被加锁(一个任务可以多次获得对象的锁,计数会递增),同时在线程从该方法返回之前,该对象内其他所有要调用类中被标记为synchronized的方法的线程都会被阻塞。当然针对每个类也有一个锁(作为类的Class对象的一部分),所以你懂的.。

正因为它基于这种阻塞的策略,所以它的性能不太好,但是由于操作上的优势,只需要简单的声明一下即可,而且被它声明的代码块也是具有操作的原子性。

最后需要注意的是synchronized是同步机制中最安全的一种方式,其他的任何方式都是有风险的,当然付出的代价也是最大的。

用法

public synchronized void increment(){

count++;

}

public void increment(){

synchronized (Counte.class){

count++;

}

}

内部实现

synchronized 关键字原理

ThreadLocal

作用

而ThreadLocal的设计,并不是解决资源共享的问题,而是用来提供线程内的局部变量,这样每个线程都自己管理自己的局部变量,别的线程操作的数据不会对我产生影响,互不影响,所以不存在解决资源共享这么一说,如果是解决资源共享,那么其它线程操作的结果必然我需要获取到,而ThreadLocal则是自己管理自己的,相当于封装在Thread内部了,供线程自己管理,这样做其实就是以空间换时间的方式(与synchronized相反),以耗费内存为代价,单大大减少了线程同步(如synchronized)所带来性能消耗以及减少了线程并发控制的复杂度。

用法

ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文

一般使用ThreadLocal,官方建议我们定义为private static ,至于为什么要定义成静态的,这和内存泄露有关,后面再讨论。

它有三个暴露的方法,set、get、remove。

public class TestThreadLocal {

private static final ThreadLocal value = new ThreadLocal() {

@Override

protected Integer initialValue() {

return 0;

}

};

public static void main(String[] args) {

for (int i = 0; i < 5; i++) {

new Thread(new MyThread(i)).start();

}

}

static class MyThread implements Runnable {

private int index;

public MyThread(int index) {

this.index = index;

}

public void run() {

System.out.println(“线程” + index + “的初始value:” + value.get());

for (int i = 0; i < 10; i++) {

value.set(value.get() + i);

}

System.out.println(“线程” + index + “的累加value:” + value.get());

}

}

}

运行结果如下,这些ThreadLocal变量属于线程内部管理的,互不影响:

线程0的初始value:0

线程3的初始value:0

线程2的初始value:0

线程2的累加value:45

线程1的初始value:0

线程3的累加value:45

线程0的累加value:45

线程1的累加value:45

线程4的初始value:0

线程4的累加value:45

对于get方法,在ThreadLocal没有set值得情况下,默认返回null,所有如果要有一个初始值我们可以重写initialValue()方法,在没有set值得情况下调用get则返回初始值。

Android开发除了flutter还有什么是必须掌握的吗?

相信大多数从事Android开发的朋友们越来越发现,找工作越来越难了,面试的要求越来越高了

除了基础扎实的java知识,数据结构算法,设计模式还要求会底层源码,NDK技术,性能调优,还有会些小程序和跨平台,比如说flutter,以思维脑图的方式展示在下图;

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

线程2的累加value:45

线程1的初始value:0

线程3的累加value:45

线程0的累加value:45

线程1的累加value:45

线程4的初始value:0

线程4的累加value:45

对于get方法,在ThreadLocal没有set值得情况下,默认返回null,所有如果要有一个初始值我们可以重写initialValue()方法,在没有set值得情况下调用get则返回初始值。

Android开发除了flutter还有什么是必须掌握的吗?

相信大多数从事Android开发的朋友们越来越发现,找工作越来越难了,面试的要求越来越高了

除了基础扎实的java知识,数据结构算法,设计模式还要求会底层源码,NDK技术,性能调优,还有会些小程序和跨平台,比如说flutter,以思维脑图的方式展示在下图;

[外链图片转存中…(img-yLNwxJll-1715354568935)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值