飞道的博客

多线程之synchronized使用

387人阅读  评论(0)

之前几个文章中我已经说明了”线程安全“和”非线程安全“。
”非线程安全“其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是”脏读“,也就是取到的数据其实是被更改过得。而”线程安全“就是以获得的实例变量的值是进过同步处理的,不会出现藏独的现象。

1.方法内的变量为线程安全

”非线程安全“问题存在于”实例变量“中,如果是方法内部的私有变量,则不存在”非线程安全“问题,所得结果也就是”线程安全“的了。
下面我以一段代码说明,在实现方法内部声明一个变量时,是不存在”非线程安全“问题得。
示例代码:
方法代码:

package test;

/**
 * @Author LiBinquan
 */
public class HasSelfPrivateNum {
    public void addNum(String userName){
        try{
            int num = 0;
            if ("a".equals(userName)){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(1000);
            }else{
               num = 200;
                System.out.println("b set over");
            }
            System.out.println(userName +" num = "+num);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

线程1:

package test;

import java.util.Random;

/**
 * @Author LiBinquan
 */
public class ThreadTest1 extends Thread{
    private HasSelfPrivateNum name;
    public ThreadTest1(HasSelfPrivateNum name){
        super();
        this.name = name;
    }

    @Override
    public void run() {
        super.run();
        name.addNum("a");
    }
}

线程2:

package test;

import java.util.Random;

/**
 * @Author LiBinquan
 */
public class ThreadTest2 extends Thread{
    private HasSelfPrivateNum name;
    public ThreadTest2(HasSelfPrivateNum name){
        super();
        this.name = name;
    }

    @Override
    public void run() {
        super.run();
        name.addNum("b");
    }
}

运行类代码:

package test;

/**
 * @Author LiBinquan
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {
        HasSelfPrivateNum num = new HasSelfPrivateNum();
        ThreadTest1 threadTest1 = new ThreadTest1(num);
        threadTest1.start();
        ThreadTest2 threadTest2 = new ThreadTest2(num);
        threadTest2.start();
    }
}

输出:

由输出可见,方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变量时私有的特性造成的。

2.实例变量非线程安全

如果多个线程共同访问1个对象中的实例变量,则有可能出现”非线程安全“问题。
用线程访问的对象中如果有多个实例变量,则运行的结果有可能出现交叉情况。
我们将HasSelfPrivateNum类中的方法修改一下,
代码如下:

package test;

/**
 * @Author LiBinquan
 */
public class HasSelfPrivateNum {
    private int num =0;
    public void addNum(String userName){
        try{
            
            if ("a".equals(userName)){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(1000);
            }else{
               num = 200;
                System.out.println("b set over");
            }
            System.out.println(userName +" num = "+num);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

输出结果:

由输出的内容我们可以看出,如果两个线程同时操作业务对象中的实例变量,则有可能出现”非线程安全“问题。这边我们只需要将HasSelfPrivateNum类中的方法加上synchronized即可。
代码如下:

package test;

/**
 * @Author LiBinquan
 */
public class HasSelfPrivateNum {
    private int num =0;
    synchronized public void addNum(String userName){
        try{

            if ("a".equals(userName)){
                num = 100;
                System.out.println("a set over");
                Thread.sleep(1000);
            }else{
               num = 200;
                System.out.println("b set over");
            }
            System.out.println(userName +" num = "+num);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

输出:

结论:在两个线程范文同一个对象中的同步方法时一定是线程安全的。

3.多个对象多个锁

这个我们还是以上面的测试代码为例,我们只需要将输出类改为:
代码如下:

package test;

/**
 * @Author LiBinquan
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {
        HasSelfPrivateNum num1 = new HasSelfPrivateNum();
        HasSelfPrivateNum num2 = new HasSelfPrivateNum();
        ThreadTest1 threadTest1 = new ThreadTest1(num1);
        threadTest1.start();
        ThreadTest2 threadTest2 = new ThreadTest2(num2);
        threadTest2.start();
    }
}

输出:

由上输出可得出,两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是以一部的方式运行的。这个测试用例中创建了2个业务对象,在系统中产生出2个锁,所以运行结果是异步的,打印的效果就是先打印b,然后打印a。
从上面程序运行结果看,虽然在HasSelfPrivateNum中使用了synchronized关键字,但打印的顺序确实不同步的,是交叉的。
从这边可以看出synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当做锁,所以在上面的示例中,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
但如果多个线程访问多个对象,则JVM会创建多个锁。

4.synchronized方法与锁对象
下面我们来证明一下线程锁的是对象。
测试代码:
方法类:

package test;

/**
 * @Author LiBinquan
 */
public class MyObject {
    public void methodA(){
        try {
            System.out.println("开始 methodA 线程名称 = "+Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("结束");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

线程1 :

package test;


/**
 * @Author LiBinquan
 */
public class ThreadTest1 extends Thread{
    private MyObject object;
    public ThreadTest1(MyObject object){
        super();
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        object.methodA();
    }
}

线程2:

package test;


/**
 * @Author LiBinquan
 */
public class ThreadTest2 extends Thread{
    private MyObject object;
    public ThreadTest2(MyObject object){
        super();
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        object.methodA();
    }
}

运行类:

package test;

/**
 * @Author LiBinquan
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        ThreadTest1 threadTest1 = new ThreadTest1(object);
        threadTest1.setName("A");
        ThreadTest2 threadTest2 = new ThreadTest2(object);
        threadTest2.setName("B");
        threadTest1.start();
        threadTest2.start();
    }
}

输出:

我们在MyObject类中的方法加上synchronized,
代码如下:

package test;

/**
 * @Author LiBinquan
 */
public class MyObject {
    synchronized public void methodA(){
        try {
            System.out.println("开始 methodA 线程名称 = "+Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("结束");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

输出结果

通过上面的测试,可以得出结论,调用关键字synchronized声明的方法一定是排队运行的。另外需要记住”共享“这2个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。
那其他的方法在被调用是的效果,和如何查看Lock锁对象的效果。
测试代码:
方法类:

package test;

/**
 * @Author LiBinquan
 */
public class MyObject {
    synchronized public void methodA(){
        try {
            System.out.println("开始 methodA 线程名称 = "+Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("结束时间 = "+System.currentTimeMillis());
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public void methodB(){
        try{
            System.out.println("开始 methodB 线程名称 = "+Thread.currentThread().getName());
            System.out.println("开始时间 = "+System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("end");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

线程1 :

package test;


/**
 * @Author LiBinquan
 */
public class ThreadTest1 extends Thread{
    private MyObject object;
    public ThreadTest1(MyObject object){
        super();
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        object.methodA();
    }
}

线程2:

package test;


/**
 * @Author LiBinquan
 */
public class ThreadTest2 extends Thread{
    private MyObject object;
    public ThreadTest2(MyObject object){
        super();
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        object.methodB();
    }
}

运行类:

package test;

/**
 * @Author LiBinquan
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        ThreadTest1 threadTest1 = new ThreadTest1(object);
        threadTest1.setName("A");
        ThreadTest2 threadTest2 = new ThreadTest2(object);
        threadTest2.setName("B");
        threadTest1.start();
        threadTest2.start();
    }
}

输出:

通过输出我们可以得知,虽然线程A线程持有object对象锁,但是线程B完全可以异步调用非synchronized类型的方法。
接下来我们家methodB()方法前加上synchronized关键字,
代码如下:

package test;

/**
 * @Author LiBinquan
 */
public class MyObject {
    synchronized public void methodA(){
        try {
            System.out.println("开始 methodA 线程名称 = "+Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("结束时间 = "+System.currentTimeMillis());
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    synchronized public void methodB(){
        try{
            System.out.println("开始 methodB 线程名称 = "+Thread.currentThread().getName());
            System.out.println("开始时间 = "+System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("end");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

输出:

有输出可以得出2个结论:
1.A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
2.A线程先持有object对象Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需要等待,也就是同步。

5.脏读

在上面的我已经实现多个线程调用同一个方法时,为了避免数据出现交叉的情况,使用synchronized关键字来进行同步。
虽然在复制时进行了同步,但在取值时有可能出现一些意想不到的意外,这种情况就是脏读,发生脏读的情况是在读取实例变量时,此值已经被其它线程更改过了。
示例:

package test;

/**
 * @Author LiBinquan
 */
public class PublicVar {
    public String userName = "A";
    public String passWard = "AA";
    synchronized public void setValue(String userName,String passWard){
        try{
            this.userName = userName;
            Thread.sleep(5000);
            this.passWard = passWard;
            System.out.println("setValue threadName = "+Thread.currentThread().getName()+"  userName = "+userName + "  passWard = "+passWard);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public void getValue(){
        System.out.println("getValue threadName = "+Thread.currentThread().getName()+" userName = "+userName+"  passWard = "+passWard);
    }
}

线程类:

package test;


/**
 * @Author LiBinquan
 */
public class ThreadTest1 extends Thread{
    private PublicVar publicVar;
    public ThreadTest1(PublicVar publicVar){
        super();
        this.publicVar = publicVar;
    }

    @Override
    public void run() {
        super.run();
        publicVar.setValue("B","BB");
    }
}

运行类:

package test;

/**
 * @Author LiBinquan
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {
        PublicVar publicVar = new PublicVar();
        ThreadTest1 threadTest1 = new ThreadTest1(publicVar);
        threadTest1.start();
        Thread.sleep(200);//打印的结果受到此值大小影响
        publicVar.getValue();
    }
}

输出:

出现脏读是因为public void getValue()方法并不是同步的,所以可以在任意时候进行调用,解决办法是在getValue方法前加上synchronized关键字
修改后代码如下:

package test;

/**
 * @Author LiBinquan
 */
public class PublicVar {
    public String userName = "A";
    public String passWard = "AA";
    synchronized public void setValue(String userName,String passWard){
        try{
            this.userName = userName;
            Thread.sleep(5000);
            this.passWard = passWard;
            System.out.println("setValue threadName = "+Thread.currentThread().getName()+"  userName = "+userName + "  passWard = "+passWard);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    synchronized public void getValue(){
        System.out.println("getValue threadName = "+Thread.currentThread().getName()+" userName = "+userName+"  passWard = "+passWard);
    }
}

输出:

可见,方法setValue和getValue被依次执行。通过这个案例不仅要知道脏读是通过synchronized关键字解决的,还要知道如下内容:

当A线成调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,更准确的讲,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,但B线程可以随意调用其他非synchronized同步方法。
当A线成调用anyObject对象加入synchronized关键字的X方法时,A线成就获得了X方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,而B线程如果调用声明了synchronized关键字的非X方法时,必须等A线程将X方法执行完,也就是释放对象锁后才可以调用。这时A线程已经执行了一个完整的任务,也就是说userName和passWard这两个实例变量已经同时被赋值,不存在脏读的基本环境。

脏读一定会出现操作实例变量的情况下,这就是不同线程”争抢“实例变量的结果。

6.synchronized锁重入

关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象后,再次请求此对象锁时,是可以再次得到该对象的锁的。这也证明在一个synchronized方法/块的内部调用本类的其它synchronized方法/块时,是永远可以得到锁的。
方法类:

package test;

/**
 * @Author LiBinquan
 */
public class Service {
    synchronized public void service1(){
        System.out.println("service1");
        service2();
    }
    synchronized public void service2(){
        System.out.println("service2");
        service3();
    }
    synchronized public void service3(){
        System.out.println("service3");

    }
}

线程类:

package test;

/**
 * @Author LiBinquan
 */
public class ThreadTest extends Thread{
    @Override
    public void run() {
        Service service = new Service();
        service.service1();
    }
}

运行类:

package test;

/**
 * @Author LiBinquan
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
    }
}

输出:

”可重入锁“的概念是:自己可以再次获取自己的内部锁。比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
可重入锁也支持在父子类继承的环境中。
父类代码:

package test;

/**
 * @Author LiBinquan
 */
public class Main {
    public int i = 10;
    synchronized private void operateIMainMethod(){
        try{
            i--;
            System.out.println("main print i="+i);
            Thread.sleep(100);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

子类代码:

package test;

/**
 * @Author LiBinquan
 */
public class Sub extends Main{
    synchronized public void operateIMainMethod(){
        try{
            while (i > 0){
                i--;
                System.out.println("sub print i=" +i);
                Thread.sleep(100);
                this.operateIMainMethod();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

线程类:

package test;

/**
 * @Author LiBinquan
 */
public class ThreadTest extends Thread{
    @Override
    public void run() {
        Sub sub = new Sub();
        sub.operateIMainMethod();
    }
}

运行类:

package test;

/**
 * @Author LiBinquan
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
    }
}

运行结果:

这个示例说明,当存在父子类继承关系时,自雷是完全可以通过“可重入锁”调用父类的同步方法的。

7.出现异常,锁自动释放

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
示例代码:
方法类:

package test;

/**
 * @Author LiBinquan
 */
public class Service {
    synchronized public void testMethod() {
        if ("A".equals(Thread.currentThread().getName())) {
            System.out.println("ThreadName = " + Thread.currentThread().getName() + " run beginTime = " + System.currentTimeMillis());
            int i = 1;
            while (i == 1) {
                if (("" + Math.random()).substring(0, 8).equals("0.123456")) {
                    System.out.println("ThreadName = " + Thread.currentThread().getName() + " run exceptionTime = " + System.currentTimeMillis());
                    Integer.parseInt("a");//
                }
            }
        }else{
                System.out.println("Thread B run Time = " + System.currentTimeMillis());
            }
        }
    }

线程1:

package test;


/**
 * @Author LiBinquan
 */
public class ThreadTest1 extends Thread{
    private Service service;
    public ThreadTest1(Service service){
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.testMethod();
    }
}

线程2:

package test;


/**
 * @Author LiBinquan
 */
public class ThreadTest2 extends Thread{
    private Service service;
    public ThreadTest2(Service service){
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.testMethod();
    }
}

运行类:

package test;

/**
 * @Author LiBinquan
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        ThreadTest1 threadTest1 = new ThreadTest1(service);
        threadTest1.setName("A");
        threadTest1.start();
        Thread.sleep(500);
        ThreadTest2 threadTest2 = new ThreadTest2(service);
        threadTest2.setName("B");
        threadTest2.start();
    }
}

运行结果:

由输出我们可以看出,线程A出现异常并释放了锁,线程B进入方法正常打印,可以得出我们的结论就是,出现异常时锁会被自动释放。

8.同步不具有继承性

同步不可以继承。
测试代码:
父类代码:

package test;

/**
 * @Author LiBinquan
 */
public class Main {
    synchronized public void serviceMethod(){
        try{
            System.out.println("main 下一步 sleep begin threadName = "+Thread.currentThread().getName()+"  time ="+System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("main 下一步 sleep begin threadName = "+Thread.currentThread().getName()+"  time ="+System.currentTimeMillis());
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

子类代码:

package test;

/**
 * @Author LiBinquan
 */
public class Sub extends Main{
     public void serviceMethod(){
        try{
            System.out.println("main 下一步 sleep begin threadName = "+Thread.currentThread().getName()+"  time ="+System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("main 下一步 sleep begin threadName = "+Thread.currentThread().getName()+"  time ="+System.currentTimeMillis());
            super.serviceMethod();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

线程1:

package test;


/**
 * @Author LiBinquan
 */
public class ThreadTest1 extends Thread{
    private Sub sub;
    public ThreadTest1(Sub sub){
        super();
        this.sub = sub;
    }

    @Override
    public void run() {
        sub.serviceMethod();
    }
}

线程2:

package test;


/**
 * @Author LiBinquan
 */
public class ThreadTest2 extends Thread{
    private Sub sub;
    public ThreadTest2(Sub sub){
        super();
        this.sub = sub;
    }

    @Override
    public void run() {
        sub.serviceMethod();
    }
}

运行类:

package test;

/**
 * @Author LiBinquan
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {
        Sub sub = new Sub();
        ThreadTest1 threadTest1 = new ThreadTest1(sub);
        threadTest1.setName("A");
        threadTest1.start();
        Thread.sleep(500);
        ThreadTest2 threadTest2 = new ThreadTest2(sub);
        threadTest2.setName("B");
        threadTest2.start();
    }
}

输出:

从此处我们可以得出,同步不能继承,所以还得在子类中的方法加上synchronized关键字,
加上后的输出:

可以看到这样就同步了


转载:https://blog.csdn.net/qq_27581753/article/details/105900080
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场