多线程并发与线程安全相关知识整理如下:
- 线程怎么保证安全性
- 如何安全发布对象
- 线程安全有哪些手段
- JUC组件的讲解
- 如何提高线程的调度
一、线程怎么保证安全性。
- 什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
- 线程安全性的三大特征
原子性、有序性、可见性
原子性:提供互斥访问,同一时刻只能有一个线程来对它进行操作。
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
可见性:一个线程对主内存的修改可以及时被其他线程观察到。
特性 | 操作 |
原子性 | 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()方法的开始。