对象及变量的并发访问

阿福聊编程

我是阿福,公众号「JavaClub」作者,一个在后端技术路上摸盘滚打的程序员,在进阶的路上,共勉!

文章已收录在 JavaSharing 中,包含Java技术文章,面试指南,资源分享。

对象及变量的并发访问

2.1 synchronized同步方法

首先在这里说明两个概念:线程安全和非线程安全

线程安全:就是获取的实例变量的值经过同步处理不会出现脏读的现象。

非线程安全:就是多个线程对同一个对象中的实例变量进行并发访问时发生脏读的,即取到的数据其实是被更改过的。

2.1.1 实例变量非线程安全

下面我们用代码来说明一下非线程安全,HasSelfPrivateNum.java代码如下:

public class HasSelfPrivateNum {
    private int num = 0;

    public void addI(String username) {
        try {
            if (("a").equals(username)) {
                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 (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

自定义线程类 MyThread1.java, MyThread2.java 代码如下:

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

public class MyThread2 extends Thread{
    private HasSelfPrivateNum hasSelfPrivateNum;
    public MyThread2 (HasSelfPrivateNum hasSelfPrivateNum){
        this.hasSelfPrivateNum=hasSelfPrivateNum;
    }

    @Override
    public void run() {
        hasSelfPrivateNum.addI("b");
    }
}

创建运行类 Test.java,代码如下:

public class Test {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum=new HasSelfPrivateNum();
        MyThread1 myThread1=new MyThread1(hasSelfPrivateNum);
        myThread1.start();
        MyThread2 myThread2=new MyThread2(hasSelfPrivateNum);
        myThread2.start();
    }
}

代码运行运行结果:

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

两个线程同时访问一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则会出现 “非线程安全”的问题,那我们如何解决这个问题,只需要在 public void addI(String username)方法前面加上关键字synchronized即可 ,更改后方法的代码如下:

public synchronized void addI(String username)

再次运行程序结果如下:

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

所以得出结论,两个线程访问同一个对象中的同步方法时一定是线程安全的。

2.1.2 多个对象多个锁

其他类的代码不变,修改运行类 Test.java,代码如下:

public class Test {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum1=new HasSelfPrivateNum();
        HasSelfPrivateNum hasSelfPrivateNum2=new HasSelfPrivateNum();
        MyThread1 myThread1=new MyThread1(hasSelfPrivateNum1);
        myThread1.start();
        MyThread2 myThread2=new MyThread2(hasSelfPrivateNum2);
        myThread2.start();
    }
}

运行程序结果如下:

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

示例是两个线程同时访问同一个类的两个不同的实例的同一个方法,效果却是以异步的方式运行的。本示例创建两个HasSelfPrivateNum(业务对象),在系统中产生2个锁,所以运行结果是异步的。打印的效果是先打印 b,然后再打印a.

那么从上面的运行结果来看,虽然在HasSelfPrivateNum.java中使用synchronized关键字,但是打印的顺序却不是同步的,是交叉的。那为什么出现这样的结果呢?

因为关键字synchronized取得的锁是都是对象锁,而不是把一段代码或者是方法当作锁。所以上面的事了中,哪个线程执行带有synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,其他线程只能呈现等待状态,前提是多个线程访问的是同一个对象。

但如果多个线程访问多个对象,则JVM会创建多个锁。

那我们在不修改运行类 Test.java的情况下,如何实现线程安全呢?

修改HasSelfPrivateNum.java代码如下:

public class HasSelfPrivateNum {
    private static int num = 0;
    public static synchronized void addI(String username) {
        try {
            if (("a").equals(username)) {
                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 (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

运行程序结果如下:

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

从运行的结果来看,实现类同步的效果,上面的实例中将synchronized关键字加到非静态(static)方法上运行的效果是不一样的, 其实它们的本质是不同的,synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非静态(static)方法上是给对象上锁。

结论:

  • synchronized加在静态方法上给对象上锁。
  • synchronized加在非静态方法上给Class类上锁。

2.2 synchronized同步代码块

2.2.1 synchronized(this)同步代码快的使用

当两个并发线程访问同一个对象objectsynchronized(this)同步代码块时,一段时间内只能一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

自定义线程类 MyThread1.java, MyThread2.java ,运行类 Test.java 代码不变,修改HasSelfPrivateNum.java,代码如下:

public class ObjectService {
    public void serviceMethod() {
        try {
            synchronized (this){
                System.out.println("begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("end time= " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

程序运行结果

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

上述实验执行synchronized (this)同步代码块,实现线程安全。但使用synchronized (this)需要注意的是,当一个线程访问object的一个synchronized (this)同步代码块时,其他的线程对同一个object中所有其他synchronized (this)同步代码块的访问将被阻塞,这样说明synchronized (this)属于使用“对象监听器”,和synchronized关键字加到非静态(static)方法上是给对象上锁是一样的。

举一个示例验证synchronized (this)属于使用“对象监听器

HasSelfPrivateNum.java新增一个方法,那么在HasSelfPrivateNum.java 类中有两个同步的方法,分别是: addI()update() ,新增代码如下:

public void update(){
    synchronized (this) {
        try {
            System.out.println("同步代码块update");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

修改MyThread2.java,更改run()方法中调用类的方法,代码如下:

@Override
public void run() {
    hasSelfPrivateNum.update();
}

运行类的代码不变,程序运行的结果如下:

a set over
a num= 100
同步代码块update

2.2.2 synchronized(非this)同步代码快的使用

修改HasSelfPrivateNum.java类,使用Object 类作为对象监听器 ,代码如下:

public class HasSelfPrivateNum {
    private int num = 0;    
    private Object object = new Object();
    public void addI(String username) {
        //这个对象可以是任何类 
        synchronized (object) {
            try {
                if (("a").equals(username)) {
                    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 (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

其他类代码不做修改,程序运行结果如下:

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

锁非this对象有一定的优点:如果一个类紫红有很多synchronized的方法,这时虽然能实现同步,但会出现阻塞,所以会影响效率,但弱国使用同步代码块锁时非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,这样大大提高效率。

2.2.3 synchronized(class)代码块

其实静态同步synchronizedsynchronized(class)代码块实现的效果是一样。

下面我们来验证synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非静态(static)方法上是给对象上锁。

创建类HasSelfPrivateNum.java,代码如下:`

public class HasSelfPrivateNum {

    public synchronized static void addA() {
        try {
            System.out.println("线程的名称: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入 addA() ");
            Thread.sleep(2000);
            System.out.println("线程的名称: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开 addA() ");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public synchronized static void addB() {
        try {
            System.out.println("线程的名称: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入 addB() ");
            Thread.sleep(2000);
            System.out.println("线程的名称: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开 addB() ");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void addC() {
        try {
            System.out.println("线程的名称: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入 addC() ");
            Thread.sleep(2000);
            System.out.println("线程的名称: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开 addC() ");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

自定义线程类 MyThread1.java, MyThread2.javaMyThread3.java代码如下:

public class MyThread1 extends Thread{

    @Override
    public void run() {
        HasSelfPrivateNum.addA();
    }
}

public class MyThread2 extends Thread{

    @Override
    public void run() {
        HasSelfPrivateNum.addB();
    }
}

public class MyThread3 extends Thread{
    private HasSelfPrivateNum hasSelfPrivateNum;
    public MyThread3(HasSelfPrivateNum hasSelfPrivateNum){
        this.hasSelfPrivateNum=hasSelfPrivateNum;
    }
    @Override
    public void run() {
        hasSelfPrivateNum.addC();
    }
}

创建运行类 Test.java,代码如下:

public class Test {
    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum=new HasSelfPrivateNum();
        MyThread1 myThread1=new MyThread1();
        myThread1.start();
        MyThread2 myThread2=new MyThread2();
        myThread2.start();
        MyThread3 myThread3=new MyThread3(hasSelfPrivateNum);
        myThread3.start();
    }
}

程序运行结果:

线程的名称: Thread-0 在 1618653226240 进入 addA()
线程的名称: Thread-2 在 1618653226241 进入 addC()
线程的名称: Thread-2 在 1618653228245 离开 addC()
线程的名称: Thread-0 在 1618653228245 离开 addA()
线程的名称: Thread-1 在 1618653228246 进入 addB()
线程的名称: Thread-1 在 1618653230247 离开 addB()

出现异步的原因是静态方法 addA()addB() 持有一个Class锁,非静态addC()持有一个对象锁,而Class锁可以对类的所有实例起作用。

下面我们验证一下静态同步synchronizedsynchronized(class)代码块实现的效果是一样

修改上述HasSelfPrivateNum.java,addC()方法的代码,其他类不做修改:`

public  void addC() {
        synchronized (HasSelfPrivateNum.class){
            try {
                System.out.println("线程的名称: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 进入 addC() ");
                Thread.sleep(2000);
                System.out.println("线程的名称: " + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + " 离开 addC() ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

程序运行结果:

线程的名称: Thread-0 在 1618653359853 进入 addA()
线程的名称: Thread-0 在 1618653361857 离开 addA()
线程的名称: Thread-2 在 1618653361857 进入 addC()
线程的名称: Thread-2 在 1618653363859 离开 addC()
线程的名称: Thread-1 在 1618653363859 进入 addB()
线程的名称: Thread-1 在 1618653365863 离开 addB()

从程序的运行结果来看,同步synchronized(class)代码块的作用其实和synchronized**的静态方法是一样。

总结:

synchronized同步方法: 静态同步方法非静态同步方法

synchronized同步代码块 :synchronized(this),synchronized(非this),synchronized(class)


看到这里今天的分享就结束了,如果觉得这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~

欢迎关注个人公众号 「JavaClub」,定期为你分享一些技术干货。

阅读 977
57 声望
40 粉丝
0 条评论
57 声望
40 粉丝
文章目录
宣传栏