欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java中的线程安全

程序员文章站 2022-07-13 16:47:06
...

什么是线程安全

 

线程安全问题在各种编程语言中都存在,需要首先申明的是:本文做所指的线程安全都是基于java语言展开讨论的。在定义“什么是线程安全”之前,首先来看下进程、线程和多线程。

 

进程:(百度百科的定义)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

 

这个定义理解起来比较抽象,在java中可以简单的理解为启动一个jvm实例就是启动一个进程,上述定义中的“某数据集合”可以理解为jvm对内存的分配,jvm内存结构分为:方法区、java堆区、栈区(vm栈和本地方法栈)、程序计数器(更多关于jvm的内存结构可以参考这里)。

 

线程:是程序执行流的最小单元。jvm内存结构中的vm栈、本地方法栈、程序计数器是属于线程私有数据,在开启一个线程后会为其开辟一个固定的大小的私有内存空间,线程结束时系统会自动回收这部分内存。本地方法栈和程序计数器,在我们平时开发中很少接触到,但我们平时开发的每个方法中定义的每个临时变量、方法参数都会被放到“vm栈”。

 

多线程:为了充分利用cpu,在一个进程中一般会开多个线程,cpu在多个线程之间切换执行,并行的执行多个任务。公共的数据会放到方法区和java堆区,所有线程都可以共享这些数据,可以称之为公共内存空间。多个线程之间如果需要协作,就需要多个线程访问同一份数据,一般就会把这些数据放到“堆区”(对象)或者方法区 常量池中(静态变量),通过在“vm栈”中定义局部变量指向这些公共数据的内存地址,实现多个线程之间的数据共享:


java中的线程安全
            
    
    博客分类: 多线程 线程安全线程完全问题synchronized用法 
 

 

多个线程操作同一份数据,势必会导致数据的一致性问题,比如下面代码展示的场景:

 

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

 

 

执行过程示意图:


java中的线程安全
            
    
    博客分类: 多线程 线程安全线程完全问题synchronized用法 
 

 

如果把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,说明程序有线程安全问题。

 

代码执行示意图:


java中的线程安全
            
    
    博客分类: 多线程 线程安全线程完全问题synchronized用法 
 

 

 

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中的线程安全
            
    
    博客分类: 多线程 线程安全线程完全问题synchronized用法 
  • 大小: 42.6 KB
  • java中的线程安全
            
    
    博客分类: 多线程 线程安全线程完全问题synchronized用法 
  • 大小: 24.1 KB
  • java中的线程安全
            
    
    博客分类: 多线程 线程安全线程完全问题synchronized用法 
  • 大小: 39.3 KB