高并发--卷2--对象并发访问

脏读

我们经常会遇到要同时在数据库中修改几个数据的场景。下面列举了一种场景,一个线程正在修改账号密码,另一个线程在读取账号密码,这个时候,会出现并发引起的脏读的问题,有可能,第一个线程刚把账号改完,密码还没改过了,而另一个线程就在读取账号和密码了,这个时候数据就是不统一的。我们来看下面这段有问题的代码。

public class Person{
    private String username = "1";
    private String password = "1";
    synchronized public void setValue(String username,String password) throws InterruptedException{
        this.username = username;
        Thread.sleep(5000);
        this.password = password;
    }

    public String getValue(){
        return "username=" + this.username + "   password=" + this.password;
    }
}

//测试
public class Test{
    public static void main(String[] args) {
        final Person p = new Person();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    p.setValue("a", "a");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                System.out.println(p.getValue());
            };
        };

        t1.start();
        t2.start();
    }
}

我们看到,如果是多个线程修改密码,这是线程安全的,因为,它前面加了synchronized关键字,但是由于getValue()是不同步的,这个在setValue()还没执行完,就获取数据,这个时候,线程就不安全了,我们做如下更改。

public class Person{
    private String username = "1";
    private String password = "1";
    synchronized public void setValue(String username,String password) throws InterruptedException{
        this.username = username;
        Thread.sleep(5000);
        this.password = password;
    }

    synchronized public String getValue(){
        return "username=" + this.username + "   password=" + this.password;
    }
}

做如上修改之后,a线程先进行setValue(),由于这个方法前面加了synchronized关键字,所以,当t1线程执行的时候,它哪到了对象锁,如果其他同步方法想调用这个对象,要等t1线程先释然锁,然后再执行,笔者观察到,输出结果在执行后5秒才打印的,也就证明了这一点。

锁重入

两个加了synchronized的方法能不能相互调用呢?我们来看一个例子。

class User{
    private String username;
    private String password;
    private String mail;
    synchronized public void setUser(String username,String password){
        this.username = username;
        this.password = password;
    }

    synchronized public void setUser(String username,String password,String mail){
        this.setUser(username, password);   //这里直接调用synchronized关键字修饰的方法
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.mail = mail;
    }


    synchronized public String getUser(){
        return "username="+ this.username + "\npassword" + this.password + "\nmail=" + this.mail;
    }
}

public class Test{
    public static void main(String[] args) throws InterruptedException {
        final User p = new User();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                p.setUser("a", "a","123@163.com");
            };
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                p.setUser("b", "b","234@163.com");
            };
        };

        t1.start();
        t2.start();
        Thread.sleep(500);
        System.out.println(p.getUser());
    }
}

输出结果:
username=a
password=a
mail=123@163.com
输出结果并没有发生冲突,也就是说,是username和password是对应的。同时也证明了能在synchronized中修饰的方法中调用其他synchronized修饰的方法。

遇到异常,锁会释放

public class Test{
    public static void main(String[] args) throws InterruptedException {
        final User2 p = new User2();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    p.setUser("a", "a");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            };
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                try {
                    p.setUser("b", "b");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            };
        };

        t1.start();
        t2.start();
        Thread.sleep(500);
        System.out.println(p.getUser());
    }
}


class User{
    private String username;
    private String password;
    synchronized public void setUser(String username,String password) throws Exception{
        this.username = username;
        if(username.equals("a")) throw new Exception();
        Thread.sleep(5000);
        this.password = password;
    }

    synchronized public String getUser(){
        return "username=" + this.username + "\npassword=" + this.password;
    }
}

class User2 extends User{

}

输出结果:
java.lang.Exception
at User.setUser(Test.java:39)
at Test$1.run(Test.java:8)
username=b
password=b

这个输出结果一方面证明了,synchronized修饰的方法,会继承到子类,但是如果子类中重写了该方法,并且没有使用synchronized修饰,那么这个方法就是不同步的。以上结果只等待了5秒,也证明了,如果遇到异常,锁会释放。

一半异步一半同步来提高执行效率

可以通过一半异步和一半同步的方法来提高执行效率,仅仅把小部分需要同步的地方进行同步。
我们先来以前直接在函数上添加synchronized关键字的方法来同步执行。

public class Test{
    public static void main(String[] args) {
        final Service service = new Service();
        Thread t1 = new Thread("A"){
            @Override
            public void run() {
                try {
                    service.a();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread("B"){
            @Override
            public void run() {
                try {
                    service.a();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        t2.start();
    }
}


class Service{
    String start;
    String end;
    synchronized public void a() throws InterruptedException{
        long str1 = System.currentTimeMillis();
        Thread.sleep(3000); //这里可能还有很长一段的其他操作,这里抽象成一个线程睡眠
        long str2 = System.currentTimeMillis();
        String name = Thread.currentThread().getName();
        start=name + " - start - " + str1;
        end= name + "- end - "+str2;
        System.out.println(start);
        System.out.println(end);
    }
}

我们可以发现输出结果为:
A - start - 1527776484547
A- end - 1527776487547
B - start - 1527776487547
B- end - 1527776490547
也就是说,一定要等A执行完了才能执行B函数。事实上,有可能在str1和str2之间的那段代码,有可能是不要求同步的,那么这也就让B白白等待了3秒,降低了执行效率。

下面来看一半异步一半同步的解决方案。

public class Test{
    public static void main(String[] args) {
        final Service service = new Service();
        Thread t1 = new Thread("A"){
            @Override
            public void run() {
                try {
                    service.a();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread("B"){
            @Override
            public void run() {
                try {
                    service.a();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        t2.start();
    }
}


class Service{
    String threadName;
    String start;
    String end;
    public void a() throws InterruptedException{
        long str1 = System.currentTimeMillis();
        Thread.sleep(3000); //这里可能还有很长一段的其他操作,这里抽象成一个线程睡眠
        long str2 = System.currentTimeMillis();
        synchronized (this) {
            threadName = Thread.currentThread().getName();      
            start=threadName + " - start - " + str1;
            end= threadName + "- end - "+str2;
            System.out.println(start);
            System.out.println(end);            
        }
    }
}


输出结果:
B - start - 1527777631172
B- end - 1527777634173
A - start - 1527777631172
A- end - 1527777634173

我们可以发现,两个是线程几乎是同时完成的。这在一定程度上提高了执行效率。

证明synchronized是锁定的当前对象

如果有多个方法,都加了锁synchronized(this)锁。那么调用顺序是怎样的呢?我们来看如下例子。

public class Test{
    public static void main(String[] args) {
        final Service service = new Service();
        Thread t1 = new Thread("A"){
            @Override
            public void run() {
                try {
                    service.a();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread("B"){
            @Override
            public void run() {
                service.b();
            }
        };
        t1.start();
        t2.start();
    }
}


class Service{
    public void a() throws InterruptedException {
        synchronized (this) {
            System.out.println(System.currentTimeMillis());
            Thread.sleep(5000);
        }
    }

    public void b(){
        synchronized (this) {
            System.out.println(System.currentTimeMillis());
        }       
    }
}

输出结果:
1527779448736
1527779453736
发现,两个线程并不是异步执行的,而是等一个线程执行完之后再执行的另外一个线程,这就证明了synchronized(this)是锁定的当前对象。

synchronized(非this对象)

使用synchronized(非this对象)同样也能达到同步访问效果,如下这种脏读现象就能用这种方式解决。

import java.util.ArrayList;
import java.util.List;


public class Test{
    public static void main(String[] args) {
        final MyList<String> list = new MyList<String>();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    list.a("1");
                }
                super.run();
            }
        };
        Thread t2 = new Thread(){
            public void run() {
                System.out.println(list.getSize());
            };
        };  
        t1.start();
        t2.start();
    }
}

class MyList<T>{
    List<T> list = new ArrayList<T>();
    public void a(T username){
        synchronized (this) {
            list.add(username);
        }
    }

    public int getSize(){
        synchronized (this) {
            return list.size();
        }
    }
}

输出结果:361
如果希望等待所有add方法执行完成之后,再执行getSize方法,那么使用synchronized(非this)锁

import java.util.ArrayList;
import java.util.List;


public class Test {
    public static void main(String[] args) {
        final MyList<String> myList = new MyList<String>(); 
        Thread t1 = new Thread(){
            @Override
            public void run() {
                synchronized (myList) {
                    for (int i = 0; i < 1000; i++) {
                        myList.add("1");
                    }
                }
            }
        };

        Thread t2 = new Thread(){
            public void run() {
                System.out.println(myList.getSize());
            };
        };

        t1.start();
        t2.start();
    }
}


class MyList<T>{
    List<T> list = new ArrayList<T>();
    synchronized public void add(T t){
        list.add(t);
    }

    synchronized public int getSize(){
        return list.size();
    }
}

对象锁的结论

当一个对象中含有synchronized(this)、synchronized 修饰成员函数、synchronized(非this锁)三种锁的时候,同时被几个

synchronized锁定class

这种锁锁定的是整个class,它可以让各个静态函数之间同步,但是它与非静态函数之间任然是异步,也就是说,synchronized在锁定静态资源的情况下,它只能让访问的线程静态资源产生同步的效果。


public class Test {
    public static void main(String[] args) {
        final Service s = new Service();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    Service.a();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                try {
                    s.c();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread t3 = new Thread(){
            @Override
            public void run() {
                try {
                    Service.b();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        t2.start();
        t3.start();
    }
}


class Service{
    synchronized public static void a() throws InterruptedException{
        System.out.println("aaaaa start" + System.currentTimeMillis());
        Thread.sleep(3000);
        System.out.println("aaaaa end"  + System.currentTimeMillis());
    }

    synchronized public static void b() throws InterruptedException{
        synchronized(Service.class){
            System.out.println("bbbbb start"  + System.currentTimeMillis());
            Thread.sleep(3000);
            System.out.println("bbbbb end"  + System.currentTimeMillis());
        }
    }

    synchronized public void c() throws InterruptedException{
        System.out.println("ccccc start"  + System.currentTimeMillis());
        Thread.sleep(3000);
        System.out.println("ccccc end"  + System.currentTimeMillis());
    }
}

我们可以看出输出结果为:
ccccc start1528079860918
bbbbb start1528079860918
bbbbb end1528079863919
ccccc end1528079863919
aaaaa start1528079863919
aaaaa end1528079866919


根据如上输出结果,很明显,当一个函数是静态函数并且被synchronized修饰的时候,该类产生的所有对象,对静态函数的访问都具有同步效果。很明显c()函数没有同步的效果,原因是该函数不是静态资源。

死锁–无限等待

当一个线程一直占用着当前对象的锁,那么另一个线程只能一直等着,永远得不到执行,这也是一种死锁的情况。
那么如何解决这种死锁的情况呢?我们使用两把锁就可以解决这个问题。如下例。

public class Test{
    public static void main(String[] args) {
        final Service service = new Service();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                service.methodA();
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                service.methodB();
            }
        };
        t1.start();
        t2.start();
    }
}

class Service{
    Object o1 = new Object();
    public  void methodA(){
        synchronized (o1) { //锁1
            System.out.println("thread - methodA - start");
            boolean flag = true;
            while(flag){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread - methodA - end");    
        }
    }

    Object o2 = new Object();
    public void methodB(){  //锁2
        synchronized (o2) {
            System.out.println("thread - methodB - start");
            System.out.println("thread - methodB - start");
        }
    }
}

输出结果:
thread - methodB - start
thread - methodB - start
thread - methodA - start

死锁–双锁互占

双锁互占的情况就是,线程1占用着锁1,线程2占用着锁2,但是线程1中的锁1中又用到了锁2,它需要拿到锁2执行完成,自己才能被释放,而线程2中的锁2又用到了锁1,它需要拿到锁1执行完成之后,自己才能被释放,这样的情况就是双锁互相等待释放情况。

public class Test{
    public static void main(String[] args) {
        final Service service = new Service();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    service.methodA();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                try {
                    service.methodB();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        t2.start();
    }
}

class Service{
    Object lock1 = new Object();
    Object lock2 = new Object();
    public  void methodA() throws InterruptedException{
        synchronized (lock1) {  //锁1
            System.out.println("thread - methodA - start");
            Thread.sleep(3000);
            synchronized (lock2) {
                System.out.println("methodA - lock2  start - end");
            }
            System.out.println("thread - methodA - end");    
        }
    }


    public void methodB() throws InterruptedException{  //锁2
        synchronized (lock2) {
            System.out.println("thread - methodB - start");
            Thread.sleep(3000);
            synchronized (lock1) {
                System.out.println("methodB - lock1 start-end");
            }
            System.out.println("thread - methodB - start");
        }
    }
} 

输出结果:
thread - methodB - start
thread - methodA - start


在jdk中提供了一种查看有没有死锁的办法,运行程序后,我们切换都jdk的bin目录,运行cmd,然后输入jps命令,得到Run的线程,然后再输入jstack -l run线程的id,然后就能在控制台看到死锁的详情了。

锁变向

当一个线程拿到锁之后,可以换锁,让该锁给其他线程使用。如下例。

public class Test{
    public static void main(String[] args) throws InterruptedException {
        final Service service = new Service();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    service.methodA();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                service.methodB();
            }
        };
        t1.start();
        Thread.sleep(50);
        t2.start();
    }
}

class Service{
    String lock1 = "aaa";
    public void methodA() throws InterruptedException{
        synchronized (lock1) {
            System.out.println("MethodA start");
            lock1 = "bbb";  //改变锁定向
            Thread.sleep(5000);
            System.out.println("MethodA end");
        }
    }

    public void methodB(){
        synchronized (lock1) {
            System.out.println("MethodB start");
            System.out.println("MethodB end");
        }
    }
}

输出结果:
MethodA start
MethodB start
MethodB end
MethodA end

Thread.sleep(50);是必要的,如果没有这句,那么t1.start();和t2.start();,那么t1和t2都在争抢锁,当抢不到该锁,那么它就等待t1执行完成才能执行t2或者相反。
这里是用常量来当锁,如果是其他对象,改变对象的属性,而未改变对象的指向,达不到上面的效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值