来自一位架构师的分享,学习Java多线程的一些总结

Java多线程的应用复杂性之如jvm有限的几个内存方面的操作和规范,就像无数纷繁复杂的应用逻辑建立在有限的指令集上。


如何写出线程安全的程序,有各种各样需要遵循的规则,如果硬是去记忆这些写法或者规则,就事倍功半了,最好是先学习原理,抓住问题的主干,再拓展细节,这也是大家公认的学习某种技术的方式。对于多线程的问题,java使用java内存模型 JMM来保证多个线程可以有效地,正确地工作。

学习的步骤可以分为:

关注大师的言行,跟随大师的举动——JUC包已经足够丰富,按照API规范正确使用。

和大师一起修行——理解多线程问题的由来,以及jvm给出的解决方案,需要理解java的内存模型JMM,以及JMM给出的线程工作内存与主内存交互的规则如何形成JMM的happens-before原则等。参考书有知名的《深入理解Java虚拟机》第12章,最重要的《JSR-133 Java内存模型与线程规范》以及《并发编程实战》第16章。

领悟大师的意境——JUC包的实现原理,volatile和CAS构筑了JUC包的基础类,AQS,非阻塞数据结构,原子变量,这些基础类又构建了JUC包的高层类,Lock,同步器,阻塞队列,并发容器,Executor等。理解了高层类的原理,能够心里有底地使用这些类,构建健壮的应用。

成为真正的大师——学习JUC包的实现,说不定哪天也能写出一样优秀的类。

一些背景知识:

1. 操作系统中线程的实现

现代操作系统的线程主要有三种实现:内核线程实现,用户线程实现,混合实现

内核线程(KLT):线程表由内核维护,由内核完成线程的切换,内核通过调度器对线程进行调度,并将线程的任务映射到处理器上,每个内核线程可以视为内核的一个分身。程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(LWP)(广义上来说,轻量级进程也是在用户空间的进程中的,所以也是一种用户线程)。LWP和KLT是一一对应的,是1:1的关系,因此也叫作一对一线程模型(1:1)。内核线程最大的特点就是,如果有轻量级进程发生了阻塞,不会影响整个进程的工作,内核会运行其他可运行的线程。缺点也是明显的:各种线程操作都需要系统调用,需要在用户态和内核态进行来回切换,代价高昂,而且因为占用内核空间,所以内核能支持的数量是有限的。

用户级线程(UT):狭义上的用户线程是指,完全建立在用户空间的线程库上,由所在进程实现管理的线程。最大的亮点在于可以在不支持多线程的操作系统之上实现多线程,如DOS,同时因为不需要切换到内核态,所以快速且低消耗,也能支持更大规模的线程数量。这种模型中,一个轻量级进程对应多个线程,因此叫做一对多线程模型(1:N),用户进程的优势在于不需要内核的支援,而因为没有内核的支援,所有的线程操作都需要用户自己处理,导致复杂性是其劣势。线程的创建,切换,调度都是需要考虑的问题,现在使用用户线程的程序已经越来越少了。java线程在JDK1.2之前,使用用户线程。

用户线程+轻量级进程的混合实现:使用轻量级进程作为用户线程和内核线程的桥梁的一种实现,用户线程和轻量级进程的比例不定,因此也叫多对多线程模型(N:M),UNIX家族中的Solaris提供了N:M的线程模型实现。

更详细的说明可以查看介绍操作系统的书籍。

2. java虚拟机中线程的实现

jdk1.2 之前,java使用的是称为“绿色线程”的用户线程,而在1.2中,线程模型替换为基于操作系统原生线程模型来实现

操作系统支持什么样的线程模型,很大程度上影响java的线程模型,windows和linux系统提供的线程模型是1:1的,所有这两个平台上的JDK使用的是1:1的线程模型,一个java线程映射到一个轻量级进程中,Solaris系统同时支持1:1和N:M,该平台中的JDK可以指定参数选择线程模型。

操作系统的线程特性会对线程的并发规模和操作成本产生影响,但是对java程序的编写和运行来说是透明的。

3. 多线程环境下的一些问题:

安全性问题:在没有正确同步的情况下,多线程环境下程序可能得出错误的结果。

活跃性问题:在多线程环境下,当某个操作应该继续执行却无法继续执行下去,就造成了活跃性问题,如:死锁,饥饿,活锁。

性能问题:线程的频繁切换将带来极大的开销,如:

保存和恢复执行上下文,丢失局部性

使用同步机制的时候,这些机制会抑制某些编译器优化以保证执行顺序,

如使用volatile保证可见性的情况下,使内存缓冲区中的数据无效,其他线程需要重新从主内存中加载

所有这些因素会带来额外的性能开销。

4. 一些相关概念

竞争条件:多线程的环境下,程序执行的结果取决于线程交替执行的方式。而线程的交替操作顺序是不可预测的,如此程序执行的结果也是不可预测的。

状态:状态在多线程编程中是一个很核心的概念,因为线程安全性的核心就在于:对可变的共享状态的访问操作进行正确的管理。

非正式的定义:状态可以简单理解为存储在对象的域中的数据,下面的Counter类中的count就是一个Counter对象的状态。如果不能正确地访问和修改count,那么count的值就不具备正确性。状态也包括一个对象依赖的对象的域。

多线程的环境下,主要是可变的,共享的状态会导致安全性问题,可变意味着状态可以被修改,共享意味着可以被多个线程改变,自然而然的,有三种方法来解决这个问题:1.让对象不可改变 2. 让状态不可共享 3. 必须共享和改变的,使用某种机制来保证顺序——同步。

或者更加直接可靠的,不要给对象状态,一个没有状态的对象一定是线程安全的。

线程安全性:当多个线程访问某个类的时候,这个类始终能表现正确的行为,那么这个类是线程安全的。

public class Counter {
	private long count = 0;
	public long getCount(){
		return count;
	}
}

针对Java架构,我这边给大家准备了一些关于Kafka、Mysql、Tomcat、Docker、Spring、MyBatis、Nginx、Netty、Dubbo、Redis、Netty、Spring cloud、分布式、高并发、性能调优、微服务等架构技术的资料,希望能帮助一些技术遇到了瓶颈但是你又拒绝平庸,期待蜕变,想进入一线互联网公司或者给自己涨薪的程序大咖!

欢迎大家加入java技术交流群:1007476384,免费分享Spring框架、Mybatis框架SpringBoot框架、SpringMVC框架、SpringCloud微服务、Dubbo框架、Redis缓存、RabbitMq消息、JVM调优、Tomcat容器、MySQL数据库教学视频及架构学习思维导图

猜你喜欢

转载自blog.csdn.net/weixin_43640104/article/details/89710209