什么是线程安全
线程安全问题在各种编程语言中都存在,需要首先申明的是:本文做所指的线程安全都是基于java语言展开讨论的。在定义“什么是线程安全”之前,首先来看下进程、线程和多线程。
进程:(百度百科的定义)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
这个定义理解起来比较抽象,在java中可以简单的理解为启动一个jvm实例就是启动一个进程,上述定义中的“某数据集合”可以理解为jvm对内存的分配,jvm内存结构分为:方法区、java堆区、栈区(vm栈和本地方法栈)、程序计数器(更多关于jvm的内存结构可以参考这里)。
线程:是程序执行流的最小单元。jvm内存结构中的vm栈、本地方法栈、程序计数器是属于线程私有数据,在开启一个线程后会为其开辟一个固定的大小的私有内存空间,线程结束时系统会自动回收这部分内存。本地方法栈和程序计数器,在我们平时开发中很少接触到,但我们平时开发的每个方法中定义的每个临时变量、方法参数都会被放到“vm栈”。
多线程:为了充分利用cpu,在一个进程中一般会开多个线程,cpu在多个线程之间切换执行,并行的执行多个任务。公共的数据会放到方法区和java堆区,所有线程都可以共享这些数据,可以称之为公共内存空间。多个线程之间如果需要协作,就需要多个线程访问同一份数据,一般就会把这些数据放到“堆区”(对象)或者方法区 常量池中(静态变量),通过在“vm栈”中定义局部变量指向这些公共数据的内存地址,实现多个线程之间的数据共享:
多个线程操作同一份数据,势必会导致数据的一致性问题,比如下面代码展示的场景:
public class Main{ public static int i=0;//计数器 public static void main(String[] args) throws Exception{ for (int j=0;j<10000;j++){//启动1000个线程执行 i++ Thread thread = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1);//增加并发 } catch (InterruptedException e) { e.printStackTrace(); } i++; } }); thread.start(); } Thread.sleep(2000);//延迟2秒打印,确保10000个线程已经执行完成 System.out.println(i); } }
本示例模拟一个计数器:启动10000个线程,对i并行执行++操作。你期望的结果是10000,但是每次执行的结果都有可能不一致,这其实就是多线程环境下“线程安全”问题。
讲了这么多,才引出什么是“线程安全”:如果多线程并发访问同一份数据,最终这份数据都会出现相同的结果,就是线程安全的。反之就是线程不安全。
核心:
1、多线程:多线程才会有线程安全问题,单线程环境下无所谓“线程安全”。
2、并发:是指多个线程以一定的顺序并发访问,才会出现线程不安全问题。
3、访问:这里的访问,指的是有修改和查询公共数据。如果只是查询不会出现“线程安全”问题。
3、同一份数据:一般是一个对象的成员变量,或者类变量(静态常量)。
通过第2点,可以看出只有在并发访问的情况下会引起“线程安全”问题,解决“线程安全”问题的根本办法就是把“并行”,改“串行”,用it术语来说就是“加锁”。在java中可以通过synchronized关键字和Lock(jdk1.5以后),最近阅读同事的代码发现大家对synchronized用法还是没分清,以为只要把synchronized加在方法上就可以了,本次只针对synchronized关键字进行讲解。
synchronized关键字用法
synchronized加锁本质上是锁对象,在java中的每个对象都可以作为锁,称为内置锁。当线程进入方法或者代码块时,首先获得该对象的锁,如果获取到就执行方法或者代码块(如果没有获取到锁,该线程会暂停执行,进入一个等待队列等待再次获得锁),执行结束后释放该对象锁,其他线程可以继续获得该锁继续执行。通过这种方式,可以保证方法或者代码块在多线程环境下“串行”执行。synchronized可以修饰静态方法、非静态方法、代码块,这三种情况的加锁控制粒度依次越来越细,下面分别介绍具体特性和使用场景。
synchronized修饰静态方法:这时加锁的方式是“类锁”,锁对象是该类的class对象,class对象在同一个类加载器下是唯一的,也就是说这锁是唯一。表现的特征为:同一个类中所有被synchronized修饰静态的方法,在多线程执行这些方法时获取的是同一把锁。
下面来看一个synchronized修饰静态方法的示例,本示例模拟统计任意时刻同时执行doBusiness业务方法的并发数(在实际项目方法监控中通常都会用到)。实现的逻辑很简单,在进入doBusiness方法前,对计数器+1,在doBusiness方法执行完成后,对计数器-1:
public class StaticMethod { public static int i=0;//计数器 public synchronized static void plus(){//获取StaticMethod.class 对象锁 i++; } public synchronized static void minus (){//获取StaticMethod.class 对象锁 i--; } public static void main(String[] args) throws Exception{ for(int j=0;j<1000;j++){ final int threadNum = j; Thread thread = new Thread(new Runnable() { @Override public void run() {//该方法可以用于模拟计算同时执行doBusiness()方法的并发数 plus();//进入业务方法前,先+1 doBusiness(threadNum); minus();//退出业务方法,再-1 } private void doBusiness(int threadNum){ try { Thread.sleep(100);//模拟业务方法执行 //System.out.println("线程"+threadNum+"执行完成"); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); } System.out.println("当前doBusiness方法并发数:" + i); Thread.sleep(2000);//等待所有的线程执行完成 System.out.println("当前doBusiness方法并发数(应该为0):" + i); } }
这里对plus()和minus()静态方法使用了synchronized修饰,当执行plus()方法时需要获取StaticMethod.class对象锁 然后释放,再执行minus()方法时又需要重新获取StaticMethod.class对象锁。可以理解成多个方法的执行都变成了串行。执行mian方法,打印结果为:
当前doBusiness方法并发数:282 当前doBusiness方法并发数(应该为0):0
执行过程示意图:
如果把plus()和minus()方法前的synchronized去掉,再次执行,就出现了线程安全问题:所有线程执行完成,最后计数器的打印值应该为0,但实际上很次都不同,还可能为负数:
当前doBusiness方法并发数:869 当前doBusiness方法并发数(应该为0):1
在静态方法方法上加锁使用起来很简单,但它会锁住整个类对象,该类的所有synchronized的静态方法都变为串行,消耗资源严重。需根据具体情况谨慎使用。
synchronized修饰非静态方法:这时锁住的是该类对应的具体的某个实例对象。也就是说同一个类的多个对象之间会有多把锁,可以并行执行互不干扰,具备一定的并发性。比如,下面的示例中,分别统计了在对象锁nonStaticMethod1、nonStaticMethod2情况下 doBusiness方法的并发数,在某一个时刻要统计两个doBusiness的总并发数,需要把两个对象的计数器i相加。这种控制粒度更细,在需要分别统计doBusiness多个调用方分别引起的并发数时 使用(同样运用于方法监控):
public class NonStaticMethod { public int i=0;//计数器 public synchronized void plus(){//获取StaticMethod.class 对象锁 i++; } public synchronized void minus(){//获取StaticMethod.class 对象锁 i--; } public static void main(String[] args) throws Exception{ NonStaticMethod nonStaticMethod1 = new NonStaticMethod(); for(int j=0;j<1000;j++){ Thread thread = new Thread(new Business(nonStaticMethod1,j)); thread.start(); } NonStaticMethod nonStaticMethod2 = new NonStaticMethod(); for(int j=0;j<1000;j++){ Thread thread = new Thread(new Business(nonStaticMethod2,j)); thread.start(); } System.out.println("nonStaticMethod1对象锁环境下 doBusiness方法并发数:" + nonStaticMethod1.i); System.out.println("nonStaticMethod2对象锁环境下 doBusiness方法并发数:" + nonStaticMethod2.i); Thread.sleep(2000);//等待所有的线程执行完成 System.out.println("nonStaticMethod1对象锁环境下 doBusiness方法并发数(应该为0):" + nonStaticMethod1.i); System.out.println("nonStaticMethod2对象锁环境下 doBusiness方法并发数(应该为0):" + nonStaticMethod2.i); } } class Business implements Runnable{ private NonStaticMethod nonStaticMethod; private int threadNum; public Business(NonStaticMethod nonStaticMethod, int threadNum) { this.nonStaticMethod = nonStaticMethod; this.threadNum = threadNum; } @Override public void run() {//该方法可以用于模拟计算同时执行doBusiness()方法的并发数 nonStaticMethod.plus();//进入业务方法前,先+1 doBusiness(threadNum); nonStaticMethod.minus();//退出业务方法,再-1 } //业务方法 private void doBusiness(int threadNum){ try { Thread.sleep(100);//模拟业务方法执行 //System.out.println("线程"+threadNum+"执行完成"); } catch (InterruptedException e) { e.printStackTrace(); } } }
需要再次强调的是,在类的非静态方法上加synchronized修饰(本示例中的plus()和minus方法),只能保证在这个类的某一个具体对象上串行执行这些方法,多个对象之间是并行的。这种方式的控制粒度比在静态方法上使用synchronized修饰更细,性能消耗相对较小。可以根据业务具体场景使用。运行 上述示例代码,执行结果如下:
nonStaticMethod1对象锁环境下 doBusiness方法并发数:350 nonStaticMethod2对象锁环境下 doBusiness方法并发数:875 nonStaticMethod1对象锁环境下 doBusiness方法并发数(应该为0):0 nonStaticMethod2对象锁环境下 doBusiness方法并发数(应该为0):0
其中前两个数字表示 此刻分别在nonStaticMethod1、nonStaticMethod2对象锁环境下的并发数。后面两个数字表示所有线程执行完成的情况下,当前的并发数,理论上是0(因为所有线程都执行完成了),如果这个数字为非0,说明程序有线程安全问题。
代码执行示意图:
synchronized修饰代码块:这种方式比修饰方法的粒度更细,在一个方法中一般会有多个操作,而会出现线程安全的地方往往只有少量代码,这时只需要对这块代码进行加锁,在多线程环境下执行这个方法只有这块代码串行执行,该方法的其他部分可以并行执行。这种方式可以最大限度的减少“串行”,性能相对前两种方式无疑是最高的。但难点就在于找到“线程安全”点(也称为“竞争条件”),一般都是查询或修改公共数据部分。如果使用不当,遗漏部分“竞争条件”,就会引起“线程安全”问题。
另外 synchronized修饰代码块 也分为“类锁”和“对象锁”,如果业务期望访问该代码块的所有线程都“串行”,可以使用该类的类锁,用法如下:
public void doBusiness(){ //并行执行区域 synchronized (StaticMethod.class){ //所有线程到这里都串行 } //并行执行区域 }
如果业务期望在同一个对象锁内“串行”,多个对象锁之间“串行”,可以创建多个对象锁,并作为方法的参数传入到代码块,用法如下:
public void doBusiness(Object lock){ //并行执行区域 synchronized (lock){ //锁对象下串行执行区域,多个锁之间串行执行 } //并行执行区域 }
回到文章开头,我们可以使用类锁,来消除线程安全问题,代码调整如下:
public class Main{ public static int i=0;//计数器 public static void main(String[] args) throws Exception{ for (int j=0;j<10000;j++){//启动1000个线程执行 i++ Thread thread = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1);//增加并发 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (Main.class){ i++; } } }); thread.start(); } Thread.sleep(2000);//延迟2秒打印,确保10000个线程已经执行完成 System.out.println(i); } }
多次执行main方法,打印结果信息是一致的,如下:
10000
总结
文章一开始说明了什么是“线程安全”(或者“线程安全问题”)。并详细讲解了如果使用synchronized来消除“线程安全问题”。
synchronized的作用就是通过把“并行”改“串行”来消除“线程安全”问题。但是我们都知道“串行”是性能最低的方式,也就是说要做到“线程安全”是需要代价的。我们可以根据具体场景不同,灵活使用 静态方法同步、非静态方法同步、代码块同步 尽量降低这个代价。
除了synchronized可以解决线程安全问题,当然还有其他方式 比如:
使用java api中的原子操作api(AtomicInteger等);
使用java 1.5以后的Lock(新锁);
消除多线程中的竞争条件:把读取或修改公共数据操作改为读取线程私有数据(ThreadLocal)、局部变量;
在某些情况下可以使用volatile代替加锁方式,性能比加锁更优越。
原子操作api 和 ThreadLocal 前面已经总结过了(分别可以点击这里 和这里),后面抽时间再聊下volatile和Lock(新锁)。
出处:
http://moon-walker.iteye.com/blog/2401457
相关推荐
在Java编程语言中,线程安全是多线程环境下编程时必须考虑的重要因素。线程安全的循环单链表是一种高效的数据结构,它允许在并发环境中进行插入、删除和遍历操作而不会出现数据不一致的情况。这篇博客文章将探讨如何...
Java线程安全是多线程编程中的一个关键概念,它涉及到多个线程访问共享资源时可能出现的问题。在Java中,线程安全问题通常与并发、内存模型和可见性有关。Java内存模型(JMM)定义了如何在多线程环境下共享数据的...
在Java编程中,多线程安全集合是程序员在并发环境下处理数据共享时必须考虑的关键概念。这些集合确保了在多个线程访问时的数据一致性、完整性和安全性,避免了竞态条件、死锁和其他并发问题。Java提供了一系列的线程...
Java多线程与线程安全实践Java多线程与线程安全实践Java多线程与线程安全实践Java多线程与线程安全实践Java多线程与线程安全实践Java多线程与线程安全实践Java多线程与线程安全实践Java多线程与线程安全实践Java多...
在实现线程安全的单例模式时,选择哪种方法取决于具体的应用场景。如果应用启动时间不是关键,并且希望尽可能减少内存消耗,可以选择懒汉式;如果应用对性能要求较高,可以选择饿汉式或静态内部类。双重校验锁则适用...
Java多线程与线程安全实践-基于Http协议的断点续传 Java多线程与线程安全实践-基于Http协议的断点续传 Java多线程与线程安全实践-基于Http协议的断点续传 Java多线程与线程安全实践-基于Http协议的断点续传 Java多...
### Java并发中的线程安全性 #### 1. 引言 随着Java技术的发展以及多核处理器的普及,Java并发编程成为软件开发中的一个重要领域。Java并发控制问题是国内外学者研究的热点之一,特别是在J2SE 1.5版本中引入了`...
Java线程安全是多线程编程中的一个关键概念,它涉及到在并发环境下如何正确地管理共享资源,确保程序的正确性和一致性。以下是对Java线程安全的深入总结: ### 一、线程安全的定义 线程安全是指当多个线程访问同一...
本示例将探讨如何在Java中实现一个线程安全的订票系统。 首先,我们要理解什么是线程安全。线程安全是指当多个线程访问同一代码块时,即使这些线程是并发执行的,程序也能保持其正确性,即不会出现数据混乱或丢失的...
Java提供了多种机制来保证线程安全,比如使用synchronized关键字来同步方法或代码块,实现线程之间的同步。当一个线程试图进入一个已经被另一个线程持有的同步代码块时,它将进入阻塞状态,直到同步代码块的执行线程...
JAVA多线程与线程安全实践-基于Http协议的断点续传 JAVA多线程与线程安全实践-基于Http协议的断点续传 JAVA多线程与线程安全实践-基于Http协议的断点续传 JAVA多线程与线程安全实践-基于Http协议的断点续传 JAVA多...
Java多线程与线程安全实践-基于Http协议的断点续传Java多线程与线程安全实践-基于Http协议的断点续传Java多线程与线程安全实践-基于Http协议的断点续传Java多线程与线程安全实践-基于Http协议的断点续传Java多线程与...
总之,理解并掌握Java中的线程安全问题及其解决方案是每个Java开发者必备的技能,这不仅可以确保程序的正确性,还能有效利用多核处理器,提升系统性能。在阅读源码时,也要注意观察作者如何处理线程安全,这对于提升...
此外,Java 5引入了BlockingQueue阻塞队列,它是一种线程安全的数据结构,线程可以等待队列中有数据可取或等待队列有空位可存,常用于生产者-消费者模型。 线程阻塞是指线程在运行过程中因为某些原因无法继续执行,...
这是Java实现线程安全的一种基本手段。 ##### 使用synchronized修饰方法 当`synchronized`用来修饰实例方法时,该方法称为同步方法。同一对象上的所有同步方法在同一时刻只能被一个线程访问。例如: ```java ...
Java线程是多线程编程的核心概念,是Java语言中实现并发执行的机制。线程安全和同步线程是确保在多线程环境下正确执行的关键因素。线程安全指的是一个方法或类在多线程环境下可以正确无误地运行,不会因为线程之间的...
Java多线程与线程安全编程实践-基于Http协议的断点续传.zip Java多线程与线程安全编程实践-基于Http协议的断点续传.zip Java多线程与线程安全编程实践-基于Http协议的断点续传.zip Java多线程与线程安全编程实践-...
这些方法必须在`synchronized`环境中使用,避免线程安全问题。 - `volatile`关键字:用于标记共享变量,确保多线程环境下的可见性和有序性,但不保证原子性。 - `join()`方法:让当前线程等待另一个线程完成其执行...
基于Http协议的断点续传-Java多线程与线程安全实践编程.zip 基于Http协议的断点续传-Java多线程与线程安全实践编程.zip 基于Http协议的断点续传-Java多线程与线程安全实践编程.zip 基于Http协议的断点续传-Java多...