• 如何保证线程安全(转载于网络,仅用于个人学习)


    cpu在同一时刻执行多个任务。而Java并发则由多线程实现的。在jvm的世界里,线程就像不相干的平行空间,串行在虚拟机中,那么java如何保证线程安全?说到线程安全,首先简单了解一下多线程:

    多线程

    充分利用CPU资源,为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰。要了解多线程,首先要了解串行和并行的概念,这样才能更好地理解多线程。
    串行

    串行其实是相对于单条线程来执行多个任务来说的,就拿下载文件来举个例子:当下载多个文件时,在串行中它是按照一定的顺序去进行下载的,也就是说,必须等下载完A文件之后才能开始下载B文件,它们在时间上是不可能发生重叠的。

    并行

    下载多个文件,开启多条线程,多个文件同时进行下载,严格意义上的,在同一时刻发生的,并行在时间上是重叠的。(即同一时刻下载A、B文件。)

    简单了解了多线程,接 下来说说确保线程安全。

    什么是线程安全?

    既然是线程安全问题,那么肯定是在多个线程访问的情况下产生的,没有按照我们预期的行为执行,那么线程就不安全了。也就是说我们想要确保在多线程访问的时候,我们的程序还能按照我们预期的行为去执行,那么就是线程安全。

    首先看一段线程代码,看看是不是线程安全的,代码如下:

     1 /**
     2  * @Author 安仔夏天勤奋
     3  * Create Date is  2019/3/28
     4  * Des
     5  */
     6 public class TestThread {
     7     private static class XRunnable implements Runnable{
     8         private int count;
     9         @Override
    10         public void run() {
    11             for(int i=0;i<5;i++){
    12                 getCount();
    13             }
    14         }
    15         private void getCount(){
    16             count++;
    17             //打印 计数值
    18             System.out.println(""+count);
    19         }
    20     }
    21     public static void main(String []arg){
    22         XRunnable runnable = new XRunnable();
    23         Thread a_thread = new Thread(runnable);
    24         Thread b_thread = new Thread(runnable);
    25         Thread c_thread = new Thread(runnable);
    26         a_thread.start();
    27         b_thread.start();
    28         c_thread.start();
    29     }
    30 }

    打印出的结果是

    1
    2
    2
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Process finished with exit code 0
    

    从代码上看出,启动三个线程,每个线程都是循环5次得出顺序是1到15的结果。从结果可以看到,出现了两个2,出现这种情况显然表明这个方法根本就不是线程安全的,出现这种问题的原因有很多。

    如何确保线程安全?

    既然存在线程安全的问题,那么肯定得想办法解决这个问题,怎么解决?说说常见的几种方式。先上图,后分析。


    确保线程安全.png

    不可变(final)

    在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。final关键字修饰的类或数据不可修改,可靠性最高。如 String类,Integer类。

    线程封闭

    把对象封装到一个线程里,只有一个线程能看到这个对象,那么这个对象就算不是线程安全的,也不会出现任何线程安全方面的问题。线程封闭有三种:

    • Ad-hoc 线程封闭

      Ad-hoc线程封闭是指维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。事实上,对线程封闭对象(例如,GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中。

    • ThreadLocal线程封闭

      它是一个特别好的封闭方法,其实ThreadLocal内部维护了一个map,map的key是每个线程的名称,而map的value就是我们要封闭的对象。ThreadLocal提供了get、set、remove方法,每个操作都是基于当前线程的,所以它是线程安全的。

    • 堆栈封闭

      堆栈封闭其实就是方法中定义局部变量。不存在并发问题。多个线程访问一个方法的时候,方法中的局部变量都会被拷贝一份到线程的栈中(Java内存模型),所以局部变量是不会被多个线程所共享的。

    同步

    • 悲观锁
    • 非阻塞同步(乐观锁)
    • 锁优化(过度优化)

    悲观锁

    同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。

    • synchronized
      synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,。可重入,修饰(class、obj、代码块)。
      可重入:一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。
      注意:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。
    • Lock.lock()/Lock.unLock()
      ReentrantReadWriteLock是Lock的另一种实现方式,我们已经知道了ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。
      ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。
     1 class RRWTextLock<Data> {
     2       private final Map<String, Data> map = new TreeMap<>();
     3       private final ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
     4       private final Lock r = rrwl.readLock(); //读锁
     5       private final Lock w = rrwl.writeLock(); //写锁
     6 
     7       public Data get(String key) {
     8           r.lock();
     9           try { return map.get(key); }
    10           finally { r.unlock(); }
    11       }
    12       public String[] getAllKeys() {
    13           r.lock();
    14           try { return (String[]) map.keySet().toArray(); }
    15           finally { r.unlock(); }
    16       }
    17       public Data put(String key, Data value) {
    18           w.lock();
    19           try { return map.put(key, value); }
    20           finally { w.unlock(); }
    21       }
    22       public void clear() {
    23           w.lock();
    24           try { map.clear(); }
    25           finally { w.unlock(); }
    26       }
    27   }

    synchronize和锁都可以保证可见性。

    可见性

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    非阻塞同步(乐观锁)

    随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步

    非阻塞的实现CAS(Compare-and-Swap)
    CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。

    CAS的ABA问题
    因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。

    ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3C。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    volatile:
    volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。volatile也可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。关于volatile的知识还有很多的,这里就不展开了。先说说什么是有序性、原子性、可见性(上面也有说明):

    有序性

    有序性:即程序执行的顺序按照代码的先后顺序执行。在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。

    Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。

     1 public class Singleton {
     2       private volatile static Singleton instance;
     3       private Singleton(){}
     4       public static Singleton getInstance(){
     5           if(instance == null){
     6               synchronized (Singleton.class){
     7                   if(instance == null){
     8                       instance = new Singleton();
     9                   }
    10               }
    11           }
    12           return instance;
    13       }
    14   }

    原子性

    原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性就像数据库里面的事务一样,他们是一个团队,同生共死。

    在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)。要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。JDK里面提供了很多atomic类,AtomicInteger,AtomicLong,AtomicBoolean等等。
    看一下原子类的代码

     1 /**
     2  * @Author 安仔夏天勤奋
     3  * Create Date is  2019/3/28
     4  * Des
     5  */
     6 public class AtomicIntegerExample {
     7     // 请求总数
     8     public static int clientTotal = 8000;
     9     // 同时并发执行的线程数
    10     public static int threadTotal = 100;
    11     public static AtomicInteger count = new AtomicInteger(0);
    12     public static void main(String[] args) throws Exception {
    13         ExecutorService executorService = Executors.newCachedThreadPool();//获取线程池
    14         final Semaphore semaphore = new Semaphore(threadTotal);//定义信号量
    15         final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
    16         for (int i = 0; i < clientTotal ; i++) {
    17             executorService.execute(() -> {
    18                 try {
    19                     semaphore.acquire();
    20                     add();
    21                     semaphore.release();
    22                 } catch (Exception e) {
    23                    Log.e("lu","exception", e);
    24                 }
    25                 countDownLatch.countDown();
    26             });
    27         }
    28         countDownLatch.await();
    29         executorService.shutdown();
    30         Log.e("lu", "count="+count.get());
    31     }
    32     private static void add() {
    33         count.incrementAndGet();
    34     }
    35 }
    36 
    37 执行看到最后结果是8000是线程安全的。

    工具类

    • 同步容器(已过时)
      同步容器的工具有Vector、HashTable、Collections.synchroniedXXX()。

    • 并发容器(JUC)
      ConcurrentHashMap

      ConcurrentHashMap在jdk1.6中 segment分段锁,读写锁。缺点:弱一致性。

      CopyOnWriteArrayList

      Volative 保证可见性,读写锁。缺点:内存占用,弱一致性

    • JUC同步器 AQS(这个知识点到时候另行分析)

    总结

    1、总体来说线程安全在三个方面体现:

    • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized)。
    • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile)。
    • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
  • 相关阅读:
    Hacker(22)----解除系统中的密码
    Hacker(21)----密码攻防之加密与解密基础
    Hacker(20)----手动修复Windows系统漏洞
    Hacker(19)----检测Windows系统漏洞
    Hacker(18)----了解Windows系统漏洞
    Hacker(17)----认识Windows系统漏洞
    Linux通过nfs挂载根文件系统失败:VFS: Unable to mount root fs via NFS, trying floppy.
    恶补各种知识(编程基础篇)
    恶补各种知识(操作系统篇)
    恶补各种知识(查找排序篇)
  • 原文地址:https://www.cnblogs.com/lychee-wang/p/12495018.html
Copyright © 2020-2023  润新知