前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java并发编程系列-(7) Java线程安全

Java并发编程系列-(7) Java线程安全

作者头像
码老思
发布于 2023-10-19 12:07:40
发布于 2023-10-19 12:07:40
27000
代码可运行
举报
文章被收录于专栏:后端精进之路后端精进之路
运行总次数:0
代码可运行

7. 线程安全

7.1 线程安全的定义

如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的。

类的线程安全表现为:

  • 操作的原子性
  • 内存的可见性

不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。

7.2 如何保证线程安全

栈封闭

所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态。

比如下面的例子,a和b都是在方法内部定义的,无法被外部线程所访问,当方法结束后,栈内存被回收,所以是线程安全的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void fun(){
    int a = 1;
    int b= 2// do something
}
无状态

没有任何成员变量的类,就叫无状态的类,这种类不存在共享的资源,显然是安全的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class StatelessClass {
	
	public int service(int a,int b) {
		return a*b;
	}
}
不可变的类

让状态不可变,两种方式:

  1. 加final关键字。对于一个类,所有的成员变量应该是私有的,并且可能的情况下,所有的成员变量应该加上final关键字。需要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
  2. 根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值。

下面例子中的,成员变量都是final并且也没有提供给外部修改变量的地方,因此是线程安全的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ImmutableFinal {
	
	private final int a;
	private final int b;
	
	public ImmutableFinal(int a, int b) {
		super();
		this.a = a;
		this.b = b;
	}

	public int getA() {
		return a;
	}

	public int getB() {
		return b;
	}
}

下面的例子中,虽然User成员变量是final的,无法修改引用。但是外部依然可以通过getUser获取到User的引用之后,修改User对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ImmutableFinalRef {
	
	private final int a;
	private final int b;
	private final User user;//这里就不能保证线程安全了
	
	public ImmutableFinalRef(int a, int b) {
		super();
		this.a = a;
		this.b = b;
		this.user = new User();
	}

	public int getA() {
		return a;
	}

	public int getB() {
		return b;
	}
	
	public User getUser() {
		return user;
	}

	public static class User{
		private int age;

		public User(int age) {
			super();
			this.age = age;
		}

		public int getAge() {
			return age;
		}

		public void setAge(int age) {
			this.age = age;
		}
		
	}
	
	public static void main(String[] args) {
		ImmutableFinalRef ref = new ImmutableFinalRef(12,23);
		User u = ref.getUser();
		//u.setAge(35);
	}
}
volatile

volitile在ConcurrentHashMap等并发容器中都有使用,用于保证变量的可见性。最适合一个线程写,多个线程读的情景。

加锁和CAS

加锁可以显示地控制线程对类的访问,使用正确可以保证线程安全。

CAS操作通过不断的循环对比,试图对目标对象进行修改,也能保证线程安全。广泛用于JDK并发容器的实现中。

安全的发布

类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。

ThreadLocal

这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口与方法,这些方法为使用该变量的每个线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。从概念上讲,你可以将ThreadLocal视为包含了Map<Thread, T>对象,其中保存了特定于该线程的值,但ThreadLocal的实现并非如此,这些特定的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收。

7.3 死锁

定义

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

死锁的根本成因:获取锁的顺序不一致导致。

可以利用下面的示意图帮助理解:

死锁范例

下面的程序中,两个线程分别获取到了first和second,然后相互等待,产生了死锁。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DeadLockSample extends Thread {
    private String first;
    private String second;
    public DeadLockSample(String name, String first, String second) {
        super(name);
        this.first = first;
        this.second = second;
    }
    public void run() {
        synchronized (first) {
            System.out.println(this.getName() + " obtained: " + first);
            try {
                Thread.sleep(1000L);
                synchronized(second) {
                    System.out.println(this.getName() + " obtained: " + second);
                }
            } catch (InterruptedException e) {
                // Do nothing
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        String lockA = "lockA";
        String lockB = "lockB";
        DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);
        DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}
定位和解决死锁

Debug时可以使用 jps 或者系统的 ps 命令、任务管理器等工具,确定进程 ID。其次,调用 jstack 获取线程栈,jstack your_pid. jstack 本身也会把类似的简单死锁抽取出来,直接打印出来。

如果是开发自己的管理工具,需要用更加程序化的方式扫描服务进程、定位死锁,可以考虑使用 Java 提供的标准管理 API,ThreadMXBean,其直接就提供 findDeadlockedThreads() 方法用于定位,上面的例子中用到了这个方法。

怎么预防死锁?
  1. 如果可能的话,尽量避免使用多个锁,并且只有需要时才持有锁。
  2. 如果必须使用多个锁,尽量设计好锁的获取顺序。如果对于两个线程的情况,可以参考如下的实现:

在实现转账的类时,为了防止由于相互转账导致的死锁,下面的实现中,通过对比账户的hash值来确定获取锁的顺序。当两者的hash值相等时,虽然这种情况非常少见,使用了单独的锁,来控制两个线程的访问顺序。

注意System.identityHashCode()是JDK自带的hash实现,在绝大部分情况下,保证了对象hash值的唯一性。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SafeOperate implements ITransfer {
	private static Object tieLock = new Object();//加时赛锁

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
    	
    	int fromHash = System.identityHashCode(from);
    	int toHash = System.identityHashCode(to);
    	//先锁hash小的那个
    	if(fromHash<toHash) {
            synchronized (from){
                synchronized (to){
                    System.out.println(Thread.currentThread().getName()
                    		+" get"+to.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }    		
    	}else if(toHash<fromHash) {
            synchronized (to){
                Thread.sleep(100);
                synchronized (from){
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }    		
    	}else {//解决hash冲突的方法
    		synchronized (tieLock) {
				synchronized (from) {
					synchronized (to) {
	                    from.flyMoney(amount);
	                    to.addMoney(amount);						
					}
				}
			}
    	}
    	
    }
}
  1. 使用带超时的方法,为程序带来更多可控性。

类似 Object.wait(…) 或者 CountDownLatch.await(…),都支持所谓的 timed_wait,我们完全可以就不假定该锁一定会获得,指定超时时间,并为无法得到锁时准备退出逻辑。

  1. 使用Lock实现(推荐)

并发 Lock 实现,如 ReentrantLock 还支持非阻塞式的获取锁操作 tryLock(),这是一个插队行为(barging),并不在乎等待的公平性,如果执行时对象恰好没有被独占,则直接获取锁。

标准的使用流程如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
while(true) {
   if(A.getLock().tryLock()) {
    try {
    	if(B.getLock().tryLock()) {
    		try {
    		  //两把锁都拿到了,开始执行业务代码
    	           break;
    		}finally {
    		  B.getLock().unlock();
    	    }
       }
    }finally {
    	A.getLock().unlock();
    }
  }
  // 非常重要,sleep随机的时间,以防两个线程谦让,产生长时间的等待,也就是活锁
  SleepTools.ms(r.nextInt(10));
}

7.4 活锁/线程饥饿/无锁

活锁

活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。

在上面解决死锁的第四个方案中,为了避免活锁,采用了随机休眠的机制。

线程饥饿

线程执行中有线程优先级,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。

无锁

对于并发控制而言,锁是一种悲观的策略,它总是假设每一次的临界区操作会产生冲突,由此,如果有多个线程同时需要访问临界区资源,则宁可牺牲资源让线程进行等待。

无锁是一种乐观的策略,它假设对资源的访问是没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿地状态下持续执行。当遇到冲突,则使用CAS来检测线程冲突,如果发现冲突,则重试直到没有冲突为止。

CAS算法的过程是,它包含三个参数CAS(V,E,N),V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才将V的值设置为N,如果V值和E值不同,说明已经有其他线程做了更新,则当前线程什么都不做。使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。

7.5 影响性能的因素

  • 上下文切换:一般花费5000-10000个时钟周期,几微秒
  • 内存同步:加锁等操作,增加额外的指令执行时间
  • 阻塞:挂起线程,包括额外的上下文切换

7.6 锁性能优化

减少锁的持有时间

减少锁的持有时间有助于降低锁冲突的可能性,进而提升系统的并发能力。

减小锁粒度

这种技术的典型使用场景就是ConcurrentHashMap。

对于HashMap来说,最重要的两个方法就是get() 和put(),一种最自然的想法就是对整个HashMap加锁,必然可以得到一个线程安全的对象.但是这样做,我们就认为加锁粒度太大.对于ConcurrentHashMap,它内部进一步细分了若干个小的hashMap,称之为段(SEGMENT).默认的情况下,一个ConcurrentHashMap被进一步细分为16个段

如果需要在ConcurrentHashMap中增加一个新的表项,并不是整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁,并完成put()操作.在多线程环境中,如果多个线程同时进行put()操作,只要被加入的表项不存放在同一个段中,则线程间便可以做到真正的并行。

读写分离锁来替换独占锁

在读多写少的场合,使用读写锁可以有效提升系统的并发能力

锁分离

如果将读写锁的思想进一步的延伸,就是锁分离.读写锁根据读写锁操作功能上的不同,进行了有效的锁分离.使用类似的思想,也可以对独占锁进行分离.

以LinkedBlockingQueue为例,take函数和put函数分别实现了冲队列取和往队列加数据,虽然两个方法都对队列进项了修改,但是LinkedBlockingQueue是基于链表的所以一个操作的是头,一个是队列尾端,从理论情况下将并不冲突

如果使用独占锁则take和put就不能完成真正的并发,所以jdk并没有才用这种方式取而代之的是两把不同的锁分离了put和take的操作

锁粗化

凡事都有一个度,如果对同一个锁不停地进行请求,同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。

为此,虚拟机在遇到一连串连续地对同一锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步次数,这个操作叫做锁的粗化.

7.7 实现线程安全的单例模式

懒汉式
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。

双重检验锁
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {
    private static volatile Singleton singleton = null;
    private Singleton() {
    }
    public static Singleton getSingleton() {
        if (singleton == null) { // 尽量避免重复进入同步块
            synchronized (Singleton.class) { // 同步.class,意味着对同步类方法调用
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
  • volatile 能够提供可见性,以及保证 getInstance 返回的是初始化完全的对象。
  • 在同步之前进行 null 检查,以尽量避免进入相对昂贵的同步块。
  • 直接在 class 级别进行同步,保证线程安全的类方法调用。

在这段代码中,争论较多的是 volatile 修饰静态变量,当 Singleton 类本身有多个成员变量时,需要保证初始化过程完成后,才能被 get 到。 在现代 Java 中,内存排序模型(JMM)已经非常完善,通过 volatile 的 write 或者 read,能保证所谓的 happen-before,也就是避免常被提到的指令重排。换句话说,构造对象的 store 指令能够被保证一定在 volatile read 之前。

饿汉式

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton{
    //类加载时就初始化
    private static final Singleton instance = new Singleton();
    
    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}
静态内部类(推荐)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

静态内部类是在被调用时才会被加载,因此它是懒汉式的。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java 多线程与并发编程详解:原理、实现与实战
随着 CPU 核心数的提升,单线程程序已无法充分发挥硬件性能。Java 提供了强大的并发编程支持,允许我们使用多线程来:
用户11690575
2025/06/06
1160
【Java并发编程】线程安全与性能
类的线程安全表现为: 操作的原子性,类似数据库事务。 内存的可见性,当前线程修改后其他线程立马可看到。 不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。
Java深度编程
2020/06/10
6080
【Java并发编程】线程安全与性能
Java并发编程实战系列10之避免活跃性危险
10.1 死锁 哲学家问题 有环 A等B,B等A 数据库往往可以检测和解决死锁//TODO JVM不行,一旦死锁只有停止重启。 下面分别介绍了几种典型的死锁情况: 10.1.1 Lock ordering Deadlocks 下面是一个经典的锁顺序死锁:两个线程用不同的顺序来获得相同的锁,如果按照锁的请求顺序来请求锁,就不会发生这种循环依赖的情况。 public class LeftRightDeadlock { private final Object left = new Object();
JavaEdge
2018/04/28
7180
聊聊并发编程的10个坑
说实话,在java中并发编程是一大难点,至少我是这么认为的。不光理解起来比较费劲,使用起来更容易踩坑。
苏三说技术
2022/08/25
4710
聊聊并发编程的10个坑
《java并发编程实战》总结
①发挥多处理器的强大优势 ②建模的简单性 ③异步事件的简化处理④相应更灵敏的用户界面
CBeann
2023/12/25
2630
《java并发编程实战》总结
Java并发编程的艺术(一)——并发编程需要注意的问题
并发是为了提升程序的执行速度,但并不是多线程一定比单线程高效,而且并发编程容易出错。若要实现正确且高效的并发,就要在开发过程中时刻注意以下三个问题: 上下文切换 死锁 资源限制 接下来会逐一分析这三个问题,并给出相应的解决方案。 问题一:上下文切换会带来额外的开销 线程的运行机制 一个CPU每个时刻只能执行一条线程; 操作系统给每条线程分配不同长度的时间片; 操作系统会从一堆线程中随机选取一条来执行; 每条线程用完自己的时间片后,即使任务还没完成,操作系统也会剥夺它的执行权,让另一条线程执行 什么是“上下文
大闲人柴毛毛
2018/03/09
7900
Java并发编程的总结和思考
编写优质的并发代码是一件难度极高的事情。Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更好的选择。本文是对并发编程的一点总结和思考,同时也分享了Java 5以后的版本中如何编写并发代码的一点点经验。
大龄老码农-昊然
2021/04/03
3150
Java并发编程的总结和思考
Java并发编程之线程安全
线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了
Java微观世界
2025/01/21
1030
Java并发编程之线程安全
多线程之死锁就是这么简单
前言 只有光头才能变强 回顾前面: ThreadLocal就是这么简单 多线程三分钟就可以入个门了! 多线程基础必要知识点!看了学习多线程事半功倍 Java锁机制了解一下 AQS简简单单过一遍 Lock锁子类了解一下 线程池你真不来了解一下吗? 本篇主要是讲解死锁,这是我在多线程的最后一篇了。主要将多线程的基础过一遍,以后有机会再继续深入! 死锁是在多线程中也是比较重要的知识点了! 那么接下来就开始吧,如果文章有错误的地方请大家多多包涵,不吝在评论区指正哦~ 声明:本文使用JDK1.8 一、死锁讲解 在Ja
Java3y
2018/06/11
7303
JAVA并发之多线程引发的问题剖析及如何保证线程安全
为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为:
Java宝典
2020/12/04
2.1K0
多线程的同步和死锁
李家酒馆酒保
2017/12/28
1.3K0
多线程的同步和死锁
Java并发编程实战 04死锁了怎么办?
在第三篇文章最后的例子当中,需要获取到两个账户的锁后进行转账操作,这种情况有可能会发生死锁,我把上一章的代码片段放到下面:
Johnson木木
2020/05/13
4430
Java并发编程实战 04死锁了怎么办?
彻底理解Java并发:ReentrantLock锁
synchronized 线程等待时间过长,获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,这将极大的影响程序执行效率。
栗筝i
2022/12/01
6890
彻底理解Java并发:ReentrantLock锁
【JUC】010-深入单例模式、CAS、ABA问题、可重入锁、自旋锁、死锁排查
https://blog.csdn.net/qq_29689343/article/details/105046493
訾博ZiBo
2025/01/06
1090
【JUC】010-深入单例模式、CAS、ABA问题、可重入锁、自旋锁、死锁排查
Java并发编程之cas理论(无锁并发)
前面看到的AtomicInteger的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
Java微观世界
2025/01/21
920
Java并发编程之cas理论(无锁并发)
Java并发简介(什么是并发)
并发编程中有很多术语概念相近,容易让人混淆。本节内容通过对比分析,力求让读者清晰理解其概念以及差异。
鱼找水需要时间
2023/03/09
8590
Java并发简介(什么是并发)
Java并发编程的艺术(十二)——线程安全
1. 什么是『线程安全』? 如果一个对象构造完成后,调用者无需额外的操作,就可以在多线程环境下随意地使用,并且不发生错误,那么这个对象就是线程安全的。 2. 线程安全的几种程度 线程安全性的前提:对『线程安全性』的讨论必须建立在对象内部存在共享变量这一前提,若对象在多条线程间没有共享数据,那这个对象一定是线程安全的! 2.1. 绝对的线程安全 上述线程安全性的定义即为绝对线程安全的情况,即:一个对象在构造完之后,调用者无需任何额外的操作,就可以在多线程环境下随意使用。 绝对的线程安全是一种理想的状态,
大闲人柴毛毛
2018/03/09
8640
java并发编程(四)
volatile 解决的是多核CPU带来的缓存与CPU之间数据的可见性,实现禁止指令重排
疯狂的KK
2020/03/11
3000
Java并发编程:线程安全和锁机制的实现
Java是一种面向对象的编程语言,具有良好的并发编程能力。在多线程并发编程中,线程安全和锁机制是极其重要的两个概念。下面将介绍什么是线程安全和锁机制,以及如何实现。
用户1289394
2023/09/11
2950
Java并发编程:线程安全和锁机制的实现
Java并发编程-各种锁
来 源:https://www.cnblogs.com/huangjuncong/p/8542008.html
JavaFish
2019/10/16
9090
相关推荐
Java 多线程与并发编程详解:原理、实现与实战
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验