2.变量的并发访问

前记:师夷长技以自强

1.问题背景

在上一篇文章中我们已经讨论了线程具有异步运行的特性,因此当多线程同时访问同一个实例变量时就会引发脏读的问题。而这显然不是我们愿意看到的,解决办法也很简单,就是给访问该变量的程序部分加锁。多线程并发在一些追求效率的系统中常存在变量不可见的问题,由于变量的不可见也会导致程序运行的结果不是我们想要的。一句话,同步性和可见性问题是多线程中的两大重点内容,他们分别对应于synchronized和volitle关键字的使用。本文主要围绕了在各种情况下如何使用这两个关键字而展开的。

2.synchronized同步方法

2.1方法内变量是线程安全的

ex1:

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

class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        numRef.addI("a");
    }
}

class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        numRef.addI("b");
    }
}
public class Test{
    public static void main(String[] args) {
        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRef);
        threadA.start();
        ThreadB threadB = new ThreadB(numRef);
        threadB.start();
    }
}

output:
a set over!
b set over!
b num=200
a num=100
可以看到b的写覆盖对a是完全没有影响的。

2.2实例变量非线程安全

把上面的num变量改为实例变量
ex2:

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 (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        numRef.addI("a");
    }
}

class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        numRef.addI("b");
    }
}
public class Test{
    public static void main(String[] args) {
        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRef);
        threadA.start();
        ThreadB threadB = new ThreadB(numRef);
        threadB.start();
    }
}

output:
a set over!
b set over!
b num=200
a num=200
当然,解决办法也就是在addI函数前加synchronized,这里不再演示。

2.3给非静态方法加对象锁

如果同步方法属于多个对象,则每个方法属于不同的锁,因此其运行也是异步的。synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。
ex3:

class HasSelfPrivateNum{
    private int num = 0;
    synchronized 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 (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadA extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        numRef.addI("a");
    }
}

class ThreadB extends Thread{
    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        this.numRef = numRef;
    }

    @Override
    public void run() {
        numRef.addI("b");
    }
}
public class Test{
    public static void main(String[] args) {
        HasSelfPrivateNum numRefA = new HasSelfPrivateNum();
        HasSelfPrivateNum numRefB = new HasSelfPrivateNum();
        ThreadA threadA = new ThreadA(numRefA);
        threadA.start();
        ThreadB threadB = new ThreadB(numRefB);
        threadB.start();
    }
}

output:
a set over!
b set over!
b num=200
a num=100
可见,addI被两个线程调用运行时都是异步交叉运行的。

2.4synchronized锁可重入

也就是说,当一个线程得到一个对象锁后,再次请求此对象时是可以再次得到该对象的锁的。因此,一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
ex4:

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");
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        Service service = new Service();
        service.service1();
    }
}

public class Test{
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

output:
service1
service2
service3
假如不允许锁重入,那么程序将会进入死锁。
除此之外,子类的同步函数也可以重入从父类继承的函数。如下:
ex5:

class Main {
    public int i = 10;

    synchronized public void operateIMainMethod() {
        try {
            i--;
            System.out.println("main print i=" + i);
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


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

class MyThread extends Thread {
    @Override
    public void run() {
        Sub sub = new Sub();
        sub.operateISubMethod();
    }
}

public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

2.4异常自动释放锁

线程即使获得了锁,但在运行的过程中如果遇到异常就会自动释放锁。如下:
ex6:

class Service{
    synchronized public void testMethod(){
        if(Thread.currentThread().getName().equals("a")){
            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());
        }
    }
}

class ThreadA extends Thread{
    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

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

class ThreadB extends Thread{
    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }

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

public class Test {
    public static void main(String[] args) {
        try {
            Service service = new Service();
            ThreadA a = new ThreadA(service);
            a.setName("a");
            a.start();
            Thread.sleep(500);
            ThreadB b = new ThreadB(service);
            b.setName("b");
            b.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

output:
ThreadName = a run beginTime=1591535415742
ThreadName=a run exceptionTime=1591535416623
Exception in thread “a” Thread B run Time=1591535416624
java.lang.NumberFormatException: For input string: “a”
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Service.testMethod(Test.java:9)
at ThreadA.run(Test.java:27)
可以看到,在线程a最先获得了对象锁,但因为异常而释放锁,由线程b获得。

3.synchronized同步代码块

当同步方法执行耗时很长时,其他线程都要等待其运行完毕,因此总体的运行时间将会很长。这个时候就要同步代码块来解决了。同步代码块是作用于一个语句块而不是整个方法,同步代码块提供多线程间的互斥访问。
**synchronized(this)**以当前对象为对象监视器。
**synchronized(非this)**当一个类中有很多synchronized方法时,虽然能实现同步,但会受阻塞而影响运行效率。使用同步代码块非this对象不与其他锁this同步方法争抢this锁,可大大提高运行效率。
synchronized(非this)的三个结论
1).多个线程同时执行synchronized(x){}同步代码块呈同步效果。
2)当其他线程执行x对象中的synchronized同步代码块呈同步效果。
3)当其他线程执行x对象方法里面的synchronized(this)代码块时也呈同步效果。
多线程死锁
当线程相互等待对方释放锁时就会产生死锁。
ex7:

class DealThread implements Runnable{
    public String username;
    public Object lock1 = new Object();
    public Object lock2 = new Object();
    public void setFlag(String username){
        this.username = username;
    }
    @Override
    public void run() {
        if(username.equals("a")){
            synchronized (lock1){
                try{
                    System.out.println("username = "+username);
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

                synchronized (lock2){
                    System.out.println("按lock1->lock2代码顺序执行了");
                }
            }
        }
        if(username.equals("b")){
            synchronized (lock2){
                try {
                    System.out.println("username = "+username);
                    Thread.sleep(3000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (lock1){
                    System.out.println("按lock2->lock1代码顺序执行了");
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        try {
            DealThread t1 = new DealThread();
            t1.setFlag("a");
            Thread thread1 = new Thread(t1);
            thread1.start();
            Thread.sleep(100);
            t1.setFlag("b");
            Thread thread2 = new Thread(t1);
            thread2.start();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

output:
username = a
username = b
可以看到此时程序进入了死锁状态。

4.volatile

volatile关键字是为了强制编译器从主内存中访问数据,其与synchronized的区别主要如下:
1)从性能来看volatitle更好。
2)从修饰的对象来看,volatile只能修饰于变量,而synchronized可以修饰方法以及代码块。
3)多线程访问volatile不会发生阻塞,而synchronized会,也即volatile不支持原子性。

5.总结

本文对synchronized和volatile关键字都做了讲解,volatile演示部分较少,但因为其功能简单所以略去。学过操作系统我们都知道,线程之间不仅有竞争,还有同步,那么下一篇文章讲介绍线程间的通信。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值