本篇博客将根据现有知识对Java多线程做以小结,以下博客仅作为个人学习过程的小结,如能对各位博友有所帮助不胜荣幸。
本篇博客将简单介绍线程安全的概念,以及实习线程安全的几种方法,重点叙述了几种常用的加锁操作,后期随学习深入还会补充修改。
之前介绍到,多线程的引入很大的提高了一个任务执行的效率,但另一方面在提高效率的同时也引入了一些风险
例如下面这个场景
//利用多线程,对同一个静态变量做++操作 public static int COUNT = 0; public static void main(String[] args) { Thread[] threads = new Thread[20];//创建一个线程数组同于存放接下来创建的线程 for(int i = 0; i < threads.length; i++){ threads[i] = new Thread(new Runnable() {//使用匿名内部类创建线程 @Override public void run() { for(int i = 0; i < 1000;i++) { COUNT++; } } }); } for(Thread t : threads){//依次启动这20个线程 t.start(); } while (Thread.activeCount() > 2){ Thread.yield();//保证Thread数组中的所有线程都跑完 } System.out.println(COUNT); }
上述代码是在多线程环境先实现对同一个静态变量进行++操作
一共创建20个线程,每个线程执行 COUNT++ 1000次,理论上最终当这20个线程全部执行完毕,COUNT的值会变为20000
实际上经过三次执行,COUNT的值都未达到20000,并且每次的值都不相同,为什么?
对于上述代码,其在启动后经历了如下流程:
启动 ——> 执行java.exe进程 ——> 初始化JVM参数 ——> 创建JVM虚拟机 ——> 启动后台线程 ——> 启动java级别的main线程(开始执行java中 main方法)
当thread线程开始执行run方法的COUNT++时,会拆解成三步执行
风险:此时因为会有20个Thread线程同时执行,有可能就会出现,两个线程的工作内存同时获取到COUNT(即获取到的COUNT值相同),那么此时两线程执行结束写回主内存后,COUNT的值只是+1并没有达到预期的+2,此时就出现了线程安全。
导致线程安全的根本原因:多个线程对同一段内存进行修改重新写入,导致修改的内容无法一定被真正修改
在Java中,synchronized关键字用来控制同步线程,多线程环境下,synchronized修饰的代码块、类、方法、变量不能被线程同时执行,
在 jdk1.6 以前 synchronized 的 java 内置锁不存在 偏向锁 -> 轻量级锁 -> 重量级锁 的锁膨胀机制,
锁膨胀机制是 1.6 之后为了优化 java 线程同步性能而实现的。而 1.6 之前都是基于 monitor 机制的重量级锁。
synchronized关键字加到静态方法和代码块上都是给该类加锁,加到实例方法和代码块上是给对象实例加锁
public class Singleton { private volatile static Singleton instance; private Singleton() { } public static Singleton getInstance(){ if(instance == null){ synchronized (Singleton.class){ instance = new Singleton(); } } return instance; } }
在单例模式中,instance采用volatile关键字修饰也非常关键,防止了在new Singleton(),这一步的初始化步骤和引入赋值步骤方法重排序
实现一个程序的多线程同步互斥(一段代码,在任意时间点,只能有一个线程执行)
synchronized修饰的代码块在反编译为字节码时,代码块前后加入了monitor字样,前面的是monitoreneter,后面要离开的是monitorexit,两标志分别标志获取到锁和释放锁
当执行monitorenter指令时,当前线程将试图获取对象锁所对应的monitor的持有权,当对象锁的monitor的计数器为0时,那么线程就可以取得monitor、并将计数器设置为1,并且成功获取到对象锁。如果当前线程已经拥有对象锁的monitor的持有权,那它就可以重入这个monitor,重入的时候计数器也会加1。如果其他线程已经拥有对象锁的monitor所有权,那么线程经会被阻塞,直到获取到对象锁的线程执行结束,即monitorexit指令被执行,执行后对象锁就会被释放并且会将计数器设置为0
如果只有一个获取对象锁的monitor标志,则在异常情况下退出程序后对象锁依旧无法正常释放,如此会导致死锁,所以设置后一个monitor标志,无论程序以何种方式退出,最终都会执行到monitorexit,在此处进行释放锁操作
重入锁是为了使同一个线程金可重复的获取一个对象锁,底层基于一个计数器,每获取到一次就+1,释放一次就-1
在实际场景中,很多被synchronized加锁的对象,其执行使用过程都很快,此时如果将其他线程全部设为阻塞态,会涉及到用户态和内核态的切换十分耗时。所以引入自旋的操作,让没有抢占到锁的线程一直循环,不断尝试获取锁,如此以来提高了效率。
在锁对象的对象头里有一个threadid字段,在第一次访问的时候threadid为空,JVM让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致,如果一致直接使用对象,如果不一致,则升级为轻量级锁;通过自旋一点次数来获取锁,如果执行一段次数后还没有获取到锁,此时就会把锁升级为重量级锁。
CAS——CompareAndSwap——比较并交换
CAS含三个操作数——内存位置(V)、预期指(A)、拟写入的新值(B)
第一步:比较内存位置(V)与预期值(A)是否相等
第二步:如果相等,就把拟写入的新值(B)写入内存位置(A)
第三部:返回boolean类型,表示操作是否成功
当多个线程同时对某个资源进行CAS操作时,只有一个线程会操作成功返回true,其他线程自旋等待。乐观锁就是CAS的典型实现
1.ABA问题
ABA问题就是V中的这个值从A变成了B,又从B变会了A,这个变化过程从CAS的角度看来,线程在这段时间并没有被占用
解决方案:引入版本号(如携带一个时间戳)
2.总是自旋开销过大
当抢占同一个锁的线程过多时,CAS自旋的概率就会比较大,从而浪费了很多CPU资源,此时的效率就会低于synchronized
3.只能保证一个共享变量的原子性操作
对于只对一个共享变量操作时,可以使用CAS的方式来保证原子性,但对多个共享变量操作时,CAS就不适用了,此时需要对其加锁。
Lock接口中提供的方法比synchronized加锁的同步方法和同步代码块更具扩展性,它允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象
其优势:
1、void lock();//获取锁
2、void lockInterruptibly;//获取锁的过程能够响应中断
3、boolean tryLock();//非阻塞式响应中断能立即返回,获取锁返回true反之为false
4、boolean tryLock(long time,TimeUnit unit);// 超时获取锁,在超时内或未中断的情况下能获取锁
5、Condition newCondition(); // 获取与lock绑定的等待通知组件,当前线程必须先获得了锁才能等待,等待会释放锁,再次获取到锁才能从等待中返回
通常使用显示使用lock的形式如下:
public class Test { public volatile static int COUNT = 0; public static void main(String[] args) { Lock lock = new ReentrantLock(); Thread[] threads = new Thread[20];//创建一个线程数组同于存放接下来创建的线程 for(int i = 0; i < threads.length; i++){ Runnable r = new Runnable() {//使用匿名内部类创建线程 @Override public void run() { for(int i = 0; i < 1000;i++) { lock.lock(); try { COUNT++; }finally { lock.unlock(); } } } }; threads[i] = new Thread(r); } for(Thread t : threads){//依次启动这20个线程 t.start(); } while (Thread.activeCount() > 2){ Thread.yield();//保证Thread数组中的所有线程都跑完 } System.out.println(COUNT); } }
AQS——抽象的队列式同步器,是一个用来构造锁和同步器的框架,适用AQS简单且高效的构造出应用广泛地大量的同步器,比如ReentrantLock、Semaphore,其他诸如RenntrantReadWriteLock、SynchronousQueue、FutureTask等都是基于AQS的
AQS实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等一系列底层的实现
AQS的核心思想是:如果请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那就需要一套线程阻塞等待以及被唤醒时锁的分配机制,这个机制时AQS使用CLH队列(虚拟双向队列)锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS底层定义了两种资源共享的方式:
ReentrantLock可重入锁,是实现了Lock接口的一个类,支持重入性,表示能够对共享重复加锁,即当前线程获取到该锁之后再次获取不会被阻塞
Java中synchronized关键字隐式的支持重入性,synchronized关键字是通过monitor的计数器来自增实现的。
实现原理:
ReadWriteLock是一个读写锁接口,读写锁是用来提升并发程序性能的分离技术,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁时资源是共享的,写锁时独占
读写锁具有三个重要特征:
ThreadLocal用于提供线程局部变量,在多线程环境中可以保证各个线程里的变量独立于其他线程的变量。可以理解为ThreadLocal为每个线程创建一个单独的共享变量副本互不影响
多个线程中使用ThreadLocal,是操作自己线程独立的变量,线程之间不相关
ThreadLocal是保证多线程环境下数据的独立性
public class Test { private static String commStr; private static ThreadLocal<String> threadStr = new ThreadLocal<String>(); public static void main(String[] args) { commStr = "main"; threadStr.set("main"); Thread thread = new Thread(new Runnable() { @Override public void run() { commStr = "thread"; threadStr.set("thread"); System.out.println("线程"+Thread.currentThread().getName()+":"); System.out.println("commStr:"+commStr); System.out.println("threadStr:"+threadStr.get()); } }); thread.start(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程"+Thread.currentThread().getName()+":"); System.out.println("commStr:"+commStr); System.out.println("threadStr:"+threadStr.get()); } }
执行结果
本质上ThreadLocal使每个线程的ThreadHashMap都维持着自己的共享变量副本,从而做到各个线程独立
线程池可以类比JMM中常量池的概念,其中事先存放好了一定数量的线程,当程序需要在某个线程下执行时,线程池就会分配出一个线程用于执行对应的程序。
线程池的引入解决了每次执行程序都需要消耗很大性能创建销毁线程的弊端,减少了每次启动、销毁线程的损耗。
在工具类Executors中提供了一些静态工厂方法用来生成一些常用的线程池:
ThreadPoolExecutor只有一种创建线程池的方式——通过自定义参数来创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor( 4,//corePoolSize:核心线程数 10,//maximumPoolSize:最大线程数(核心线程+临时线程) 60,//keepAliveTime:空闲时间数(临时线程可空闲的最长时间,超过该时间临时线程就会被销毁) TimeUnit.SECONDS,//unit:时间单位 new ArrayBlockingQueue<>(1000),//workQueue:阻塞队列(存放线程的容器) new ThreadFactory(){//threadFactory:匿名内部类 @Override public Thread newThread(Runnable r){ //线程的工厂类 return new Thread(r); } }, //handler:拒绝策略 //1. new ThreadPoolExecutor.AbortPolicy()//抛异常的方式 //2. new ThreadPoolExecutor.CallerRunsPolicy()// //3. new ThreadPoolExecutor.DiscardOldestPolicy()//把阻塞队列存放时间最久的任务丢弃 //4. new ThreadPoolExecutor.DiscardPolicy());//不处理该任务,直接丢弃
核心参数:
public class Test { public static int COUNT; public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor( 10, 30, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r); } }, new ThreadPoolExecutor.DiscardPolicy()); Lock lock = new ReentrantLock(); Runnable r = new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { lock.lock(); try { COUNT++; } finally { lock.unlock(); } } } }; for(int i = 0 ;i < 20; i++) { pool.execute(r); } while (pool.getActiveCount() > 0){ Thread.yield(); } pool.shutdown(); System.out.println(COUNT); } }
执行结果
线程死锁是指两个或两个以上线程在执行过程中,互相持有对方的资源并且不主动释放造成的死循环,这些永远在互相等待的线程/进程称为死锁
public class DeadLock { private static Integer A = 0; private static Integer B = 10; public static void main(String[] args) { deadLock(); } private static void deadLock() { Thread threadA = new Thread(new Runnable() { @Override public void run() { System.out.println("线程A:正在执行..."); System.out.println("线程A:开始获取A对象锁..."); synchronized (A){ System.out.println("线程A:获取A对象锁成功"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程A:线程A开始获取B对象..."); System.out.println("线程A:获取B对象锁成功"); synchronized (B){ Integer t = A; A = B; B = t; } } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { System.out.println("线程B:正在执行..."); System.out.println("线程B:开始获取B对象锁..."); synchronized (B){ System.out.println("线程B:获取B对象锁成功"); System.out.println("线程B:开始获取A对象锁..."); synchronized (A){ System.out.println("线程B:获取A对象锁成功"); System.out.println(A); System.out.println(B); } } } }); threadA.start(); threadB.start(); } }
只需要破坏上述的四个条件之一即可:
具体方法:
死锁:指两个或两个以上线程在执行过程中,互相持有对方的资源并不主动释放而造成的死循环
活锁:指线程没有阻塞,只是由于某些条件没有满足,而导致线程一直重复获取锁的过程
区别:处于活锁的线程状态在不断的改变,但处于死锁的线程状态一直没有改变处于等待状态,活锁有可能自行解开,而死锁不能
以上便是对多线程安全的知识点小结,随着后续学习的深入还会同步的对内容进行补充和修改,如能帮助到各位博友将不胜荣幸,敬请斧正