欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JAVA--并发的线程安全处理(一)--线程安全性

程序员文章站 2022-04-25 19:29:27
多线程并发与线程安全相关知识整理如下: 一、线程怎么保证安全性。 什么是线程安全性 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。 线程安全性的三大特征 原子性、有 ......

多线程并发与线程安全相关知识整理如下:

  1. 线程怎么保证安全性
  2. 如何安全发布对象
  3. 线程安全有哪些手段
  4. JUC组件的讲解
  5. 如何提高线程的调度

一、线程怎么保证安全性。

  • 什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

 

  • 线程安全性的三大特征

原子性、有序性、可见性

原子性:提供互斥访问,同一时刻只能有一个线程来对它进行操作。

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

可见性:一个线程对主内存的修改可以及时被其他线程观察到。

特性 操作
原子性  synchronized的代码块能保证串行执行
有序性  可以由volatile(禁止指令重排序)/synchronized(一个变量最多只能有一个线程对其lock)实现
可见性 可以由final(不会修改)、volatile(强制更新+读取主内存)以及synchronized(在unlock时会刷新所有已修改数据到主内存,lock时会从主内存重新加载数据)实现

 

  • 原子性-Atomic包

Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。

上代码

package Atomic;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

//线程安全
public class AtomicIntegerDemo {

    private final static int clientTotal=5000;//线程总数量
    private final static int threadTotal=200;//每次通过的线程数

    static AtomicInteger ai=new AtomicInteger(0);

    public static void main(String[] args) {
        //线程池
        ExecutorService executorService= Executors.newCachedThreadPool();
        //生成信号量
        final Semaphore semaphore=new Semaphore(threadTotal);//每次通过20个
        final CountDownLatch latch=new CountDownLatch(clientTotal);//计数器
        for(int i=0;i<clientTotal;i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();//申请/获取许可
                    //incrementAndGet()
                    //-->1.unsafe.getAndAddInt
                    //-->2.this.compareAndSwapInt(CAS)
                    //-->3.native 方法
                    //第2->3,实际是工作内存与主内存校验的过程(CAS特别重要)
                    ai.incrementAndGet();//数据加1
                    semaphore.release();//释放许可
                } catch (Exception e) {
                    System.out.println("execption:" + e);
                }
                latch.countDown();//计数器减1
            });
        }
        try {
           latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executorService.shutdown();//关闭线程
        System.out.println(ai.get());
    }
}

 

  • 原子性-锁(synchronized,lock)

  synchronized:依赖JVM,JVM会自动锁定和解除锁定

  Lock:依赖CPU指令,需要人工解锁,ReentrantLock

 

  Synchroized代码

  

package Sync;

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

public class SynchronizedDemo {

    //修饰整一个方法
    private synchronized void test1(){
        for(int i=0;i<10;i++){
            System.out.println("Test1 - "+i);
        }
    }
    //修饰某个代码块
    private void test2(){
        synchronized (this){
            for(int i=0;i<10;i++){
                System.out.println("Test2 - "+i);
            }
        }
    }

    public static void main(String[] args) {

        SynchronizedDemo demo1=new SynchronizedDemo();
        ExecutorService executorService= Executors.newCachedThreadPool();
            executorService.execute(()->{
                demo1.test1();
            });
            executorService.execute(()->{
                demo1.test2();
            });
        executorService.shutdown();
    }
}

Synchronized对应的作用域如下表

操作方法 作用域
修饰某段代码 大括号括起来的代码,作用于调用的对象
修饰方法 整个方法,作用于调用的对象
修饰静态方法 整个静态方法,作用于所有对象(从JVM原理考虑)
修饰类 括号括起来的部分,作用于所有对象(从JVM原理考虑)

 

 Lock代码

package Sync;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {

    private static int count=0;
    private final static int threadPoolCount=5000;
    private final static int threadCount=200;
    private static Lock lock=new ReentrantLock();
    public static void main(String[] args) {

        ExecutorService executorService= Executors.newCachedThreadPool();//线程池
        Semaphore semaphore=new Semaphore(threadCount);
        final CountDownLatch countDownLatch = new CountDownLatch(threadPoolCount);

        for(int i=0;i<threadPoolCount;i++){
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                     add();
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executorService.shutdown();
        System.out.println(count);
    }

    private static void add(){
        lock.lock();//锁定
        count++;//需要同步的方法
        lock.unlock();//解锁
    }
}

 

Atomic、synchronized、Lock对比

Atomic:在竞争激烈时能维持常态,比Lock性能好,但只能更新一个值

synchronized:不可中断锁,适合竞争不激烈,可读性好,JVM会自动释放资源。

Lock:可中断锁,在竞争激烈时能维持常态,需要人工加锁与解锁。

  

  • 可见性

导致共享变量在线程间不可见的原因(线程交叉执行、重排序结合线程交叉执行、共享变量更新后的值没有在工作内存和主存及时更新)

可见性-synchronized

JAVA内存模型里面synchronized的两条规定:

1.线程解锁前,必须把共享变量的最新刷新到主内存

2.线程加锁时,将清空工作内存*享变量的值,从而触发需要使用共享变量时,必须从主内存中重新读取最新的值

 

可见性-volatile

通过加入内存屏障禁止重排序优化来实现

volatile变量在写操作时,会在写操作后加入一个store屏障指令,将本地内存中的共享变量值刷新到主内存。

volatile变量在读操作时,会在读操作前加入一个load屏障指令,从主内存读取共享变量

package Sync;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class VolatileDemo {

    private static volatile int count=0;
    private final static int threadPoolCount=5000;
    private final static int threadCount=200;
    public static void main(String[] args) {

        ExecutorService executorService= Executors.newCachedThreadPool();//线程池
        Semaphore semaphore=new Semaphore(threadCount);
        final CountDownLatch countDownLatch = new CountDownLatch(threadPoolCount);

        for(int i=0;i<threadPoolCount;i++){
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        executorService.shutdown();
        System.out.println(count);
    }

    private static void add(){
        //1.第一步获取count
        //2.第二步+1
        //3.把count的值,返回到主内存中
        count++;
        //计算结果显示Volatile不具有原子性
        //Volatile比较适合使用 状态标记(boolean)
        //对变量的写操作不依赖于当前值
        //该变量没有包含在具有其他变量的变量中
    }
}

 

  • 有序性

JAVA内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到但单线程程序的执行,却会影响到多线程并发执行的正确性。

happens-before八大原则

程序次序规则:一个线程内,按照代码的顺序执行。

锁定规则:一个unLock的操作的发生于后面对一个锁的lock操作。

volatile变量规则:对变量的写操作先行发生于后面对这个变量的读操作。

传递规则:A操作B,B操作C,那么可以得出A操作C。

线程启动规则:Thread对象的start()方法先行发送与此线程的每一个动作。

线程中断规则:对线程interrupt()方法的调用先行发送与中断线程的代码检测到中断事件的发生。

线程终结规则:线程中所有的操作终止检测,可以通过Thread.join()方法来结束。

对象终结规则:一个对象的初始化完成先行发送于他的finalize()方法的开始。