JavaEE-多线程(基础篇三)线程安全

码字不易,希望小伙伴们点赞收藏支持哦

文章目录

  • 线程安全是什么?
  • 为什么会有线程安全问题呢?
  • 造成线程安全的原因有哪些
    • 竞态条件 & 临界区
    • 共享资源
    • 局部变量
    • 局部的对象引用
    • 不可变的共享资源
    • 引用不是线程安全的
  • java中实现线程安全的方法
    • 同步代码块
    • 同步方法
    • Lock锁机制
    • 总结
      • 1、互斥同步
      • 2、非阻塞同步
      • 3、无需同步方案
        • 1)可重入代码
        • 2)线程本地存储
  • 知识点回顾
    • 线程的生命周期以及五种基本状态


线程安全是什么?

线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题大多是由全局变量静态变量引起的,局部变量逃逸也可能导致线程安全问题。
若每个线程中对全局变量静态变量只有操作,而无操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
类要成为线程安全的,首先必须在单线程环境中有正确的行为。如果一个类实现正确(这是说它符合规格说明的另一种方式),那么没有一种对这个类的对象的操作序列(读或者写公共字段以及调用公共方法)可以让对象处于无效状态,观察到对象处于无效状态、或者违反类的任何不可变量、前置条件或者后置条件的情况。
此外,一个类要成为线程安全的,在被多个线程访问时,不管运行时环境执行这些线程有什么样的时序安排或者交错,它必须仍然有如上所述的正确行为,并且在调用的代码中没有任何额外的同步。其效果就是,在所有线程看来,对于线程安全对象的操作是以固定的、全局一致的顺序发生的。
正确性与线程安全性之间的关系非常类似于在描述 ACID(原子性、一致性、独立性和持久性)事务时使用的一致性与独立性之间的关系:从特定线程的角度看,由不同线程所执行的对象操作是先后(虽然顺序不定)而不是并行执行的。

多线程环境中 , 存在数据共享 , 一个线程访问的共享 数据被其他线程修改了, 那么就发生了线程安全问题 , 整个访问过程中 , 无一共享的数据被其他线程修改了 就是线程安全的
程序中如果使用成员变量, 且对成员变量进行数据修改 , 就存在数据共享问题, 也就是线程安全问题

多线程编程,最重要,最困难的问题,就是线程安全问题,它是万恶之源,罪魁祸首,正式调度器随即调度/抢占式执行这个过程所导致的

为什么会有线程安全问题呢?

当多个线程同时共享一个全局变量,或者静态变量, 进行写的操作时, 可能会发生数据的冲突问题 ,也就是线程安全问题, 但是做读的操作不会引发线程安全问题

上面提到了一个重要的概念,就是修改共享数据
那么我们首先来创造一个线程不安全的代码案例

package thread;
public class Test14 {
    static class Counter{
        int count = 0;
        void increase(){
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i=0;i<5000;i++){
                counter.increase();
            }
        },"t1");
        Thread t2 = new Thread(() -> {
            for (int i=0;i<5000;i++){
                counter.increase();
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

JavaEE-多线程(基础篇三)线程安全_第1张图片
JavaEE-多线程(基础篇三)线程安全_第2张图片
可以看到,两次运行的结果都是不一样的,因为在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的。
我们站在底层的角度来看这个过程
那么count变量完成自增在CPU中是怎样实现的呢?
首先在CPU自身中,包含了一些寄存器,这个我在前面的<计算机基础>中有讲到,它的读取速度要比内存更快,但空间也比内存空间更小.
那么这个自增的过程就是在内存的存储器和CPU的寄存器之间实现的
首先,自增的过程分为三个指令

  1. LOAD:从内存读取数据到CPU
  2. ADD:在CPU寄存器中完成加法运算
  3. SAVE:把寄存器的数据再写回到内存中

我们在单线程情况下执行,是没有任何问题的,但如果是多线程并发执行,就不一定了
我现在来画一个图来帮助大家理解
首先是有线程1和线程2,他们的执行顺序刚好是错开的
JavaEE-多线程(基础篇三)线程安全_第3张图片
另外还有两个线程对应的CPU寄存器和内存
我们按照上图的顺序来执行,
先在内存中读取值为0,读进线程1寄存器中
线程1寄存器中的变量再进行ADD操作,加完了之后将寄存器中的值赋回内存中的变量,
然后紧接着,线程2再进行如上操作,这样操作下来,最后内存中的值是准确的想要得到的值
JavaEE-多线程(基础篇三)线程安全_第4张图片
但是如果是其他顺序的话,那赋值操作得出的结果就大相径庭了
比如
JavaEE-多线程(基础篇三)线程安全_第5张图片
如上图,首先线程1 从内存中读取数据0,赋值给寄存器1,然后寄存器2再读取内存上的值,这个时候寄存器1和2他们的值都是0,寄存器2的值上进行自增,0+1=1,所以寄存器2的值这时为1紧接着寄存器1的值自增,为1,然后把寄存器1 的值赋给内存,还是1.

我们在数据库中所提到的事务隔离性,其实就是并发导致的问题,此处的线程安全也是因为并发 随机调度执行所导致的问题.

造成线程安全的原因有哪些

竞态条件 & 临界区

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。上例中**add()**方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。

共享资源

允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程执行时共享了什么资源很重要。

局部变量

局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。下面是基础类型的局部变量的一个例子:

public void someMethod(){
  long threadSafeInt = 0;
  threadSafeInt++;
}

局部的对象引用

上面提到的局部变量是一个基本类型,如果局部变量是一个对象类型呢?对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内,所有的对象都存在共享堆中,所以对于局部对象的引用,有可能是线程安全的,也有可能是线程不安全的。
JavaEE-多线程(基础篇三)线程安全_第6张图片
那么怎样才是线程安全的呢?如果在某个方法中创建的对象不会被其他方法或全局变量获得,或者说方法中创建的对象没有逃出此方法的范围,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。否则就叫局部变量逃逸,下面是一个线程安全的局部引用样例:

public void someMethod(){
  LocalObject localObject = new LocalObject();
  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}

上面样例中LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象,始终在someMethod()方法内部。每个执行someMethod()的线程都会创建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了。

对象成员对象成员存储在上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。下面是一个样例:

public class NotThreadSafe{
    StringBuilder builder = new StringBuilder();

    public add(String text){
        this.builder.append(text);
    }  
}

如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题。例如:

NotThreadSafe sharedInstance = new NotThreadSafe();

new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

public class MyRunnable implements Runnable{
  NotThreadSafe instance = null;

  public MyRunnable(NotThreadSafe instance){
    this.instance = instance;
  }

  public void run(){
    this.instance.add("some text");
  }
}

注意两个MyRunnable共享了同一个NotThreadSafe对象。因此,当它们调用add()方法时会造成竞态条件
当然,如果这两个线程在不同的NotThreadSafe实例上调用call()方法,就不会导致竞态条件。下面是稍微修改后的例子:

new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();

现在两个线程都有自己单独的NotThreadSafe对象,访问的不是同一资源不满足竞态条件,是线程安全的。

如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

资源可以是对象数组文件数据库连接套接字等等。Java中我们无需主动销毁对象,GC替我们解决.
注意即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的。比如,2个线程执行如下行为:

检查记录A是否存在,如果不存在,插入A

如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录:

线程1检查记录A是否存在。检查结果:不存在
线程2检查记录A是否存在。检查结果:不存在
线程1插入记录A
线程2插入记录A

同样的问题也会发生在文件或其他共享资源上。因此,区分某个线程控制的对象是资源本身还是仅仅到某个资源的引用很重要。

不可变的共享资源

当多个 线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。
我们可以通过创建不可变的共享对象保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }
}

如果你需要对ImmutableValue类的实例进行操作,如添加一个类似于加法的操作,我们不能对这个实例直接进行操作,只能创建一个新的实例来实现,下面是一个对value变量进行加法操作的示例:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }

    public ImmutableValue add(int valueToAdd){
        return new ImmutableValue(this.value + valueToAdd);
    }
}

请注意add()方法以加法操作的结果作为一个新的ImmutableValue类实例返回,而不是直接对它自己的value变量进行操作。

引用不是线程安全的

重要的是要记住,即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。看这个例子:

public void Calculator{
    private ImmutableValue currentValue = null;

    public ImmutableValue getValue(){
        return currentValue;
    }

    public void setValue(ImmutableValue newValue){
        this.currentValue = newValue;
    }

    public void add(int newValue){
        this.currentValue = this.currentValue.add(newValue);
    }
}

Calculator类持有一个指向ImmutableValue实例的引用。注意,通过setValue()方法和add()方法可能会改变这个引用,因此,即使Calculator类内部使用了一个不可变对象,但Calculator类本身还是可变的,多个线程访问Calculator实例时仍可通过setValue()和add()方法改变它的状态,因此Calculator类不是线程安全的。
换句话说:ImmutableValue类是线程安全的,但使用它的类不一定是。当尝试通过不可变性去获得线程安全时,这点是需要牢记的。
要使Calculator类实现线程安全,将getValue()、setValue()和add()方法都声明为同步方法即可。
接下来模拟一场面试:

面试官:上次问了你什么是线程安全

面试官:今天来说说怎么保证线程安全吧

Gremmie:嗯,先说说导致线程不安全的原因,主要有三点:

  • 原子性:一个或者多个操作在CPU执行的过程中被中断
  • 可见性:一个线程对共享变量的修改,另外一个线程不能立刻看到
  • 有序性:程序执行的顺序没有按照代码的先后顺序执行

Gremmie:为什么程序执行的顺序会和代码的执行顺序不一致

Gremmie:先来说说java平台的两种编译器:静态编译器(javac)和动态编译器(jit:just in time)。

面试官:这两个有啥区别?

Gremmie:嗯,区别在于:

  • 静态编译器是将.java文件编译成.class文件,之后便可以解释执行。

  • 动态编译器是将.class文件编译成机器码,之后再由jvm运行。

Gremmie:有时候,动态编译器为了程序的整体性能会对指令进行重排序

Gremmie:虽然重排序可以提升程序的性能,但是重排序之后会导致源代码中指定的内存访问顺序与实际的执行顺序不一样

Gremmie:就会出现线程不安全的问题。

Gremmie:下面简单谈谈针对以上的三个问题,java程序如何保证线程安全呢?

Gremmie:针对原子性问题:

Gremmie:JDK里面提供了很多atomic类

Gremmie:比如AtomicInteger、AtomicLong、AtomicBoolean等等

Gremmie:这些类本身可以通过CAS来保证操作的原子性

Gremmie:另外Java也提供了各种锁机制,来保证锁内的代码块在同一时刻只能有一个线程执行

Gremmie:比如使用synchronized加锁

Gremmie:这样,就能够保证一个线程在对资源进行读、改、写操作时,其他线程不可对此资源进行操作

Gremmie:从而保证了线程的安全性。

Gremmie:针对可见性问题:

Gremmie:同样可以通过synchronized关键字加锁来解决。

Gremmie:与此同时,java还提供了volatile关键字,要优于synchronized的性能,同样可以保证修改对其他线程的可见性。

Gremmie:volatile一般用于对变量的写操作不依赖于当前值的场景中,比如状态标记量等。

Gremmie:针对重排序问题:

Gremmie:可以通过synchronized关键字定义同步代码块或者同步方法保障有序性

Gremmie:另外也可以通过Lock接口来保障有序性。

Gremmie:以上就是保证线程安全的方案!

面试官:不错,今天面试就到这吧

java中实现线程安全的方法

在Java多线程编程当中,提供了多种实现Java线程安全的方式:

  1. 最简单的方式,使用Synchronization关键字:Java Synchronization介绍
  2. 使用java.util.concurrent.atomic 包中的原子类,例如 AtomicInteger
  3. 使用java.util.concurrent.locks 包中的锁
  4. 使用线程安全的集合ConcurrentHashMap
  5. 使用volatile关键字,保证变量可见性(直接从内存读,而不是从线程cache读)

同步代码块

public class ThreadSynchronizedSecurity {
 
    static int tickets = 15;
 
    class SellTickets implements Runnable {
        @Override
        public void run() {
            while (tickets > 0) {
                // 同步代码块
                synchronized (this) {
                    if (tickets <= 0) {
                        return;
                    }
                    System.out.println(Thread.currentThread().getName() + "--->售出第:  " + tickets + " 票");
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    tickets--;
                }
                if (tickets <= 0) {
                    System.out.println(Thread.currentThread().getName() + "--->售票结束!");
                }
            }
        }
    }
 
    public static void main(String[] args) {
        SellTickets sell = new ThreadSynchronizedSecurity().new SellTickets();
        Thread thread1 = new Thread(sell, "1号窗口");
        Thread thread2 = new Thread(sell, "2号窗口");
        Thread thread3 = new Thread(sell, "3号窗口");
        Thread thread4 = new Thread(sell, "4号窗口");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

JavaEE-多线程(基础篇三)线程安全_第7张图片

在使用synchronized 代码块时,可以与wait()、notify()、nitifyAll()一起使用,从而进一步实现线程的通信。其中wait()方法会释放占有的对象锁,当前线程进入等待池释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序;线程的sleep()方法则表示,当前线程会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁,也就是说,在休眠期间,其他线程依然无法进入被同步保护的代码内部,当前线程休眠结束时,会重新获得cpu执行权,从而执行被同步保护的代码。wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会释放对象锁

notify()方法会唤醒因为调用对象的wait()而处于等待状态的线程,从而使得该线程有机会获取对象锁。调用notify()后,当前线程并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM会在等待的线程中调度一个线程去获得对象锁,执行代码。

需要注意的是,wait()和notify()必须在synchronized代码块中调用。notifyAll()是唤醒所有等待的线程.

我们通过下一个程序,使得两个线程交替打印“A”和“B”各10次。请见下述实例代码:


 public class ThreadDemo {
 
    static final Object obj = new Object();
 
    //第一个子线程
    static class ThreadA implements Runnable {
        @Override
        public void run() {
            int count = 5;
            while (count > 0) {
                synchronized (ThreadDemo.obj) {
                    System.out.println("A-----" + count);
                    count--;
                    synchronized (ThreadDemo.obj) {
                        //notify()方法会唤醒因为调用对象的wait()而处于等待状态的线程,从而使得该线程有机会获取对象锁。
                        //调用notify()后,当前线程并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,
                        ThreadDemo.obj.notify();
                        try {
                            ThreadDemo.obj.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
 
                    }
                }
            }
        }
 
    }
 
    static class ThreadB implements Runnable {
        @Override
        public void run() {
            int count = 5;
            while (count > 0) {
                synchronized (ThreadDemo.obj) {
                    System.out.println("B-----" + count);
                    count--;
                    synchronized (ThreadDemo.obj) {
                        //notify()方法会唤醒因为调用对象的wait()而处于等待状态的线程,从而使得该线程有机会获取对象锁。
                        //调用notify()后,当前线程并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,
                        ThreadDemo.obj.notify();
                        try {
                            ThreadDemo.obj.wait();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
 
    public static void main(String[] args) {
        new Thread(new ThreadB()).start();
     new Thread(new ThreadA()).start();
 
    }
 
}

JavaEE-多线程(基础篇三)线程安全_第8张图片

同步方法

package com.my.annotate.thread;
 
public class ThreadSynchroniazedMethodSecurity {
    static int tickets = 15;
 
    class SellTickets implements Runnable {
        @Override
        public void run() {
            //同步方法
            while (tickets > 0) {
                synMethod();
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                if (tickets <= 0) {
                    System.out.println(Thread.currentThread().getName() + "--->售票结束");
                }
            }
        }
 
        synchronized void synMethod() {
            synchronized (this) {
                if (tickets <= 0) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + "---->售出第 " + tickets + " 票 ");
                tickets--;
            }
        }
    }
 
    public static void main(String[] args) {
        SellTickets sell = new ThreadSynchroniazedMethodSecurity().new SellTickets();
        Thread thread1 = new Thread(sell, "1号窗口");
        Thread thread2 = new Thread(sell, "2号窗口");
        Thread thread3 = new Thread(sell, "3号窗口");
        Thread thread4 = new Thread(sell, "4号窗口");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

JavaEE-多线程(基础篇三)线程安全_第9张图片

Lock锁机制

Lock锁机制, 通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class ThreadLockSecurity {
    static int tickets = 15;
 
    class SellTickets implements Runnable {
        Lock lock = new ReentrantLock();
        @Override
        public void run() {
            // Lock锁机制
            while (tickets > 0) {
                try {
                    lock.lock();
                    if (tickets <= 0) {
                        return;
                    }
                    System.out.println(Thread.currentThread().getName() + "--->售出第:  " + tickets + " 票");
                    tickets--;
                } catch (Exception e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                } finally {
                    lock.unlock();
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            if (tickets <= 0) {
                System.out.println(Thread.currentThread().getName() + "--->售票结束!");
            }
 
        }
    }
 
 
    public static void main(String[] args) {
        SellTickets sell = new ThreadLockSecurity().new SellTickets();
        Thread thread1 = new Thread(sell, "1号窗口");
        Thread thread2 = new Thread(sell, "2号窗口");
        Thread thread3 = new Thread(sell, "3号窗口");
        Thread thread4 = new Thread(sell, "4号窗口");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
 
}

JavaEE-多线程(基础篇三)线程安全_第10张图片
由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否;而ReentrantLock是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句显式释放锁lock.unlock()。另外,在并发量比较小的情况下,使用synchronized是个不错的选择;但是在并发量比较高的情况下,其性能下降会很严重,此时ReentrantLock是个不错的方案。

总结

保证线程安全以是否需要同步手段分类,分为同步方案和无需同步方案。
JavaEE-多线程(基础篇三)线程安全_第11张图片

1、互斥同步

互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区互斥量信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果互斥是方法,同步是目的

在java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorentermonitorexit这两个字节码质量,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象

此外,ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。

2、非阻塞同步

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

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

CAS缺点:
ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A变成了B又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了

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

3、无需同步方案

要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。

1)可重入代码

可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入不调用 非可重入的方法等。
(类比:synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁

2)线程本地存储

如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题

符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如**“生产者-消费者”模式**)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的**“一个请求对应一个服务器线程(Thread-per-Request)”**的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。

知识点回顾

线程的生命周期以及五种基本状态

JavaEE-多线程(基础篇三)线程安全_第12张图片
新建状态(New):当线程对象创建后就是进入到了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法,线程即进入到了就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是执行了start()此线程就会执行。
运行状态(Running):当CPU调度处于就绪状态的线程的时候,此时线程才会得以真正的执行,即进入运行状态。
注:就绪状态是进入运行状态的唯一入口,也就是说线程进入运行状态的前提是已经进入到了就绪状态。
阻塞状态(Blocked):处于运行状态的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,知道进入到就绪状态,才有机会再次被CPU调用以进入到运行状态,根据产生阻塞状态的三原因,阻塞状态可以分为三种:

  • 等待阻塞–》运行状态的线程执行wait()方法,使线程进入到阻塞状态
  • 同步阻塞–》线程获取同步锁失败,因为同步锁被其他线程所占用,这时线程就会进入同步阻塞状态;
  • 其他阻塞–》通过调用线程的sleep()或join()或发出了I/O请求的时候线程会进入阻塞状态,当sleep()状态超时,join()等待线程终止或者超时,或者I/O处理完毕,线程就会重新转入就绪状态。
    死亡状态(Dead):线程执行完了或者因一场退出了run()方法,该线程就结束了生命周期。

下一篇博客给大家详细介绍synchronized和volatile关键字
码字不易,希望多多支持

你可能感兴趣的:(JavaEE冲冲冲,java-ee,java,jvm)