Java多线程之对象及变量的并发访问

本文探讨Java中解决多线程并发访问对象及变量的安全问题,包括synchronize同步方法、同步语句块以及volatile关键字的使用。通过实例分析它们在确保线程安全、数据可见性以及原子性方面的差异和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java对象及变量的并发访问

当多个线程同时对同一个对象中的实例变量进行并发访问时可能会产生线程安全问题。产生的后果就是”脏读”,即收到的数据其实是被更改过的。
如果访问的是方法中的变量,则不存在”非线程安全”问题
可以通过以下几种方式来解决,在对对象及变量并发访问过程中的安全问题
1. synchronize同步方法
2. 同步语句块
3. volatile关键字

synchronize同步方法

如果两个线程同时访问同一个对象的方法,不加控制,会出现意外的结果。通过用synchronize修饰方法,可以取得对象锁,那个线程先访问就先持有对象锁,其余的线程只能等待。
首先是没有用synchronize修饰的情况

public class HasSelfPrivateNum {
    private int num = 0;
    public void addI(String username){
        try{
            if (username.equals("a")){
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + "  num=" + num);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

public class SelfPrivateThreadA  extends Thread{
    private HasSelfPrivateNum num;
    public SelfPrivateThreadA(HasSelfPrivateNum num){
        this.num = num;
    }
    @Override
    public void run() {
    super.run();
    num.addI("a");
    }
}

public class SelfPrivateThreadB extends Thread{
    private HasSelfPrivateNum num;
    public SelfPrivateThreadB(HasSelfPrivateNum num){
    this.num = num;
    }
    @Override
    public void run() {
    super.run();
    num.addI("b");
    }
}

测试类

public class HasSelfPrivateNumTest extends TestCase {
    public void testAddI() throws Exception {
    HasSelfPrivateNum numA = new HasSelfPrivateNum();     
    //HasSelfPrivateNum numB = new HasSelfPrivateNum();
    SelfPrivateThreadA threadA = new SelfPrivateThreadA(numA);
    threadA.start();
    SelfPrivateThreadB threadB = new SelfPrivateThreadB(numA);
    threadB.start();
    Thread.sleep(1000 * 3);
    }

}

预期结果应该是a num=100 b num=200
但是实际结果如下:

a set over!
b set over!
b  num=200
a  num=200

用synchronize修饰方法addI()方法之后结果如下:

a set over!
a  num=100
b set over!
b  num=200

多个对象多个锁

取消测试类中注释的代码,因为2个线程访问的是2个不同的对象,2个线程仍然是异步执行。

synchronize修饰方法添加的是对象锁

当2个线程同时访问一个类中,2个不同的用synchronize修饰的方法时,有一个方法被访问,另一个仍旧不能访问。因为synchronize修饰方法添加的是对象锁

如果数据的设置和获取方法不是同步的,可以在任意时刻进行调用,可能会出现”脏读”情况,可以通过在设置和获取方法之前用synchronize修饰解决

synchronize拥有锁重入的功能
锁重入:即当一个线程获得一个对象锁之后,再次请求该对象可以再次得到该对象的锁。即synchronize方法/块的内部调用本类的其他synchronize方法/块时,可以永远得到锁的。
子类继承父类的时候,子类可以通过”可重入锁”调用父类的同步方法

出现异常,锁会自用释放

同步不具有继承性

同步语句块

对于上面的同步方法而言,其实是有些弊端的,如果同步方法是需要执行一个很长时间的任务,那么多线程在排队处理同步方法时就会等待很久,但是一个方法中,其实并不是所有的代码都需要同步处理的,只有可能会发生线程不安全的代码才需要同步。这时,可以采用synchronized来修饰语句块让关键的代码进行同步。用synchronized修饰同步块,其格式如下:

synchronized(对象){
//语句块
}
这里的对象,可以是当前类的对象this,也可以是任意的一个Object对象,或者间接继承自Object的对象,只要保证synchronized修饰的对象被多线程访问的是同一个,而不是每次调用方法的时候都是新生成就就可以。但是特别注意String对象,因为JVM有String常量池的原因,所以相同内容的字符串实际上就是同一个对象,在用同步语句块的时候尽可能不用String。
下面,看一个例子来说明同步语句块的用法和与同步方法的区别:

public class LongTimeTask {
    private String getData1;
    private String getData2;

    public void doLongTimeTask(){
        try{
            System.out.println("begin task");
            Thread.sleep(3000);
            String privateGetData1 = "长时间处理任务后从远程返回的值 1 threadName=" + Thread.currentThread().getName();
            String privateGetData2 = "长时间处理任务后从远程返回的值 2 threadName=" + Thread.currentThread().getName();

            synchronized (this){
                getData1 = privateGetData1;
                getData2 = privateGetData2;
            }

            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

public class LongTimeServiceThreadA extends Thread{

    private LongTimeTask task;
    public LongTimeServiceThreadA(LongTimeTask task){
        super();
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime1 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime1 = System.currentTimeMillis();
    }
}

public class LongTimeServiceThreadB extends Thread{

    private LongTimeTask task;
    public LongTimeServiceThreadB(LongTimeTask task){
        super();
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime2 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime2 = System.currentTimeMillis();
    }
}

测试类:

public class LongTimeServiceThreadATest extends TestCase {

    public void testRun() throws Exception {
        LongTimeTask task = new LongTimeTask();
        LongTimeServiceThreadA threadA = new LongTimeServiceThreadA(task);
        threadA.start();

        LongTimeServiceThreadB threadB = new LongTimeServiceThreadB(task);
        threadB.start();

        try{
            Thread.sleep(1000 * 10);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        long beginTime = CommonUtils.beginTime1;
        if (CommonUtils.beginTime2 < CommonUtils.beginTime1){
            beginTime = CommonUtils.beginTime2;
        }

        long endTime = CommonUtils.endTime1;
        if (CommonUtils.endTime2 < CommonUtils.endTime1){
            endTime = CommonUtils.endTime2;
        }
        System.out.println("耗时:" + ((endTime - beginTime) / 1000));

        Thread.sleep(1000 * 20);
    }

}

结果如下:

begin task
begin task
长时间处理任务后从远程返回的值 1 threadName=Thread-1
长时间处理任务后从远程返回的值 2 threadName=Thread-1
end task
长时间处理任务后从远程返回的值 1 threadName=Thread-1
长时间处理任务后从远程返回的值 2 threadName=Thread-1
end task
耗时:3

两个线程并发处理耗时任务只用了3s, 因为只在赋值的时候进行同步处理,同步语句块以外的部分都是多个线程异步处理的。
下面,说一下同步语句块的一些特性:

  1. 当多个线程同时执行synchronized(x){}同步代码块时呈同步效果。
  2. 当其他线程执行x对象中的synchronized同步方法时呈同步效果。
  3. 当其他线程执行x对象中的synchronized(this)代码块时也呈现同步效果。

细说一下每个特性,第一个特性上面的例子已经阐述了,就不多说了。第二个特性,因为同步语句块也是对象锁,所有当对x加锁的时候,x对象内的同步方法也呈现同步效果,当x为this的时候,该对象内的其他同步方法也要等待同步语句块执行完,才能执行。第三个特性和上面x为this是不一样的,第三个特性说的是,x对象中有一个方法,该方法中有一个synchronized(this)的语句块的时候,也呈现同步效果。即A线程调用了对x加锁的同步语句块的方法,B线程在调用该x对象的synchronized(this)代码块是有先后的同步关系。

上面说同步语句块比同步方法在某些方法中执行更有效率,同步语句块还有一个优点,就是如果两个方法都是同步方法,第一个方法无限在执行的时候,第二个方法就永远不会被执行。这时可以对两个方法做同步语句块的处理,设置不同的锁对象,则可以实现两个方法异步执行。

对类加锁的同步处理

和对象加锁的同步处理一致,对类加锁的方式也有两种,一种是synchronized修饰静态方法,另一种是使用synchronized(X.class)同步语句块。在执行上看,和对象锁一致都是同步执行的效果,但是和对象锁却有本质的不同,对对象加锁是访问同一个对象的时候成同步的状态,不同的对象就不会。但是对类加锁是用这个类的静态方法都是呈现同步状态。
下面,看这个例子:

public class StaticService {
    synchronized public static void printA(){
        try{
            System.out.println(" 线程名称为:" + Thread.currentThread().getName()
            + " 在 " + System.currentTimeMillis() + " 进入printA");
            Thread.sleep(1000 * 3);
            System.out.println(" 线程名称为:" + Thread.currentThread().getName()
                    + " 在 " + System.currentTimeMillis() + " 离开printA");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    synchronized public static void printB(){
        System.out.println(" 线程名称为:" + Thread.currentThread().getName()
        + " 在 " + System.currentTimeMillis() +  " 进入printB");
        System.out.println(" 线程名称为:" + Thread.currentThread().getName()
                + " 在 " + System.currentTimeMillis() +  " 离开printB");
    }

    synchronized public void printC(){
        System.out.println(" 线程名称为:" + Thread.currentThread().getName()
                + " 在 " + System.currentTimeMillis() +  " 进入printC");
        System.out.println(" 线程名称为:" + Thread.currentThread().getName()
                + " 在 " + System.currentTimeMillis() +  " 离开printC");
    }
}

测试类:

public class StaticServiceTest extends TestCase {

    public void testPrint() throws Exception{
        new Thread(new Runnable() {
            public void run() {
                StaticService.printA();
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                StaticService.printB();
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                new StaticService().printC();
            }
        }).start();

        Thread.sleep(1000 * 3);
    }

}

结果如下:

线程名称为:Thread-0 在 1487684345462 进入printA
线程名称为:Thread-2 在 1487684345462 进入printC
线程名称为:Thread-2 在 1487684345462 离开printC
线程名称为:Thread-0 在 1487684348465 离开printA
线程名称为:Thread-1 在 1487684348466 进入printB
线程名称为:Thread-1 在 1487684348466 离开printB

很明显的看出来,对类加锁和对对象加锁两者方法是异步执行的,而对类加锁的两个方法是呈现同步执行。
其特性也和同步对象锁一样。

锁对象锁的是该对象的内存地址,其存储的内容改变,并不会让多线程并发的时候认为这是不同的锁。所以改变锁对象的内容,并不会同步失效。

volatile关键字

主要作用是使变量在多个线程间可见

在多线程争抢对象的时候,处理该对象的变量的方式是在主内存中读取该变量的值到线程私有的内存中,然后对该变量做处理,处理后将值在写入到主内存中。上面举的例子,之所以出现结果与预期不一致都是因为线程自己将值复制到自己的私有栈后修改结果而不知道其他线程的修改结果。如果我们不用同步的话,我们就需要一个能保持可见的,知道其他线程修改结果的方法。JDK提供了volatile关键字,来保持可见性,关键字volatile的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量值。但是该关键字并不能保证原子性。

volatile不支持原子性。
synchronize与volatile的比较:
1. volatile是线程同步的轻量级实现,所以性能比synchronize好。但是volatile只能修饰变量,synchronize可以修饰方法。
2. volatile不会发生阻塞,synchronize会出现阻塞
3. volatile保证数据可见性,不能保证原子性;synchronize可以保证原子性,也可以间接保证可见性,因为他会将私有内存和公共内存中的数据做同步。
4. volatile解决变量在多个线程之间的可见性,synchronize解决多个线程之间访问资源的同步性。

原子操作:一个完整的操作,操作一旦开始就一直运行到结束

原子操作也不一定完全安全
因为有的情况下虽然方法虽然是原子的,但是方法和方法之间的调用却不是原子的。仍然需要同步去解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值