《Java多线程编程核心技术》(二)

非线程安全会在多个线程对同一个对象中的实例变量进行并发访问时发生。产生的后果是脏读,也就是取到的数据其实是被更改过的。
线程安全就是获得的实例变量的值是经过同步处理的,不会出现脏读的现象。

非线程安全问题存在于实例变量中,如果是方法内部的私有变量,则不存在非现场安全问题,所得的结果也就是线程安全的了。

synchronized同步方法

关键字synchronized取得的都是对象锁,而不是把一段代码和方法当做锁。 当多个线程访问同一个对象时,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock

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

调用关键字synchronized声明的方法一定是排队运行的。
既然使用了synchronized,那么肯定是对“共享”资源的读写访问。

如何查看Lock锁对象的效果呢?

public class MyObject {

    synchronized public void methodA(){
        try {
            System.out.println("begin 执行methodA, 线程为:"+Thread.currentThread().getName());
            Thread.sleep(2000);
            System.out.println("end  执行methodA, 线程为:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void methodB(){
        try {
            System.out.println("begin 执行methodB, 线程为:"+Thread.currentThread().getName());
            Thread.sleep(2000);
            System.out.println("end  执行methodB, 线程为:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Run {

    public static void main(String[] args){
        //同一个实例对象
        MyObject object = new MyObject();

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                object.methodA();
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                object.methodB();
            }
        });
        //启动两个线程,分别执行methodA和methodB方法
        threadA.start();
        threadB.start();
    }
}
/**
begin 执行methodA, 线程为:Thread-0
begin 执行methodB, 线程为:Thread-1
end  执行methodA, 线程为:Thread-0
end  执行methodB, 线程为:Thread-1
*/

结论:
threadA线程调用了synchronizedmethodA,持有了对象锁。但是threadB完全可以异步调用synchronizedmethodB。说明没有满足上述所说的排队执行。

改进:
methodB增加synchronized关键字

public class MyObject {

    synchronized public void methodA(){
        try {
            System.out.println("begin 执行methodA, 线程为:"+Thread.currentThread().getName());
            Thread.sleep(2000);
            System.out.println("end  执行methodA, 线程为:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public void methodB(){
        try {
            System.out.println("begin 执行methodB, 线程为:"+Thread.currentThread().getName());
            Thread.sleep(2000);
            System.out.println("end  执行methodB, 线程为:"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
begin 执行methodA, 线程为:Thread-0
end  执行methodA, 线程为:Thread-0
begin 执行methodB, 线程为:Thread-1
end  执行methodB, 线程为:Thread-1
*/

结论:
threadA线程调用了synchronizedmethodA,持有了对象锁。前者执行完后,threadB才开始执行methodB。满足了上述所说的排队执行。

A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法锁,更准确地讲,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,但B线程可以随意调用其他的非synchronized同步方法

A线程调用anyObject对象加入synchronized关键字的X方法时,A线程就获得了X方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用X方法,而B线程如果调用声明了synchronized关键字的非X方法时,必须等A线程X方法执行完,也就是释放对象锁后才可以调用。

synchronized锁重入

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

“可重入锁”的概念是:自己可以再次获取自己的内部锁。

比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。

public class Service {

    public synchronized void service1(){
        System.out.println("service1");
        service2();
    }

    public synchronized void service2(){
        System.out.println("service2");
        service3();
    }

    public synchronized void service3(){
        System.out.println("service3");

    }
}
public class Run {
    public static void main(String[] args){
        Service service = new Service();
        new Thread(new Runnable() {
            @Override
            public void run() {
                service.service1();
            }
        }).start();
    }
}
/**
service1
service2
service3
*/

可重入锁也支持在父子类继承的环境中,子类的同步方法可以调用父类的同步方法。

synchronized同步语句块

使用synchronized同步代码块,一方面缩小了需要同步的范围,提高了程序的执行效率。另一方面,可以指定锁对象。

使用格式:synchronized(this){ …. }

public class Task {
    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) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

如果使用同步方法,则每个线程都需要排队执行,导致每个线程最少执行3秒。而真正需要同步的地方只有getData1和getData2的赋值操作。耗时的操作没必要同步执行。

任意对象作为对象监视器

使用格式:synchronized(非this对象){ …. }

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

对象监视器不同,运行结果就是异步调用了。

public class MyObject {
    synchronized public void methodC(){
        try {
            System.out.println("methodC ____getLock time="
                    + System.currentTimeMillis() + " run ThreadName="
                    + Thread.currentThread().getName());
            Thread.sleep(2000);
            System.out.println("methodC releaseLock time="
                    + System.currentTimeMillis() + " run ThreadName="
                    + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Service {
    public void testMethod1(MyObject object) {
        synchronized (object) {
            try {
                System.out.println("testMethod1 ____getLock time="
                        + System.currentTimeMillis() + " run ThreadName="
                        + Thread.currentThread().getName());
                Thread.sleep(5000);
                System.out.println("testMethod1 releaseLock time="
                        + System.currentTimeMillis() + " run ThreadName="
                        + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Run {
    public static void main(String[] args){
        MyObject object = new MyObject();
        Service service = new Service();
        // 同步方法和同步代码块使用的是同一个对象锁object,结果同步打印
        new Thread(new Runnable() {
            @Override
            public void run() {
                object.methodC();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                service.testMethod1(object);
            }
        }).start();
    }
}
/**
methodC ____getLock time=1528446201224 run ThreadName=Thread-0
methodC releaseLock time=1528446203224 run ThreadName=Thread-0
testMethod1 ____getLock time=1528446203224 run ThreadName=Thread-1
testMethod1 releaseLock time=1528446208225 run ThreadName=Thread-1
*/

静态同步synchronized方法与synchronized(class)代码块

synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。

证明不是同一个锁:

public class Service {
    public synchronized void testMethod1() {
        try {
            System.out.println("testMethod1 ____getLock time="
                    + System.currentTimeMillis() + " run ThreadName="
                    + Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("testMethod1 releaseLock time="
                    + System.currentTimeMillis() + " run ThreadName="
                    + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

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

    public static void main(String[] args){
        //异步执行
        Service service = new Service();
        new Thread(new Runnable() {
            @Override
            public void run() {
                service.testMethod1();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                service.printA();
            }
        }).start();
    }
}
/**
testMethod1 ____getLock time=1528448721070 run ThreadName=Thread-0
线程名称为:Thread-1在1528448721070进入printA
线程名称为:Thread-1在1528448724070离开printA
testMethod1 releaseLock time=1528448726070 run ThreadName=Thread-0
*/

volatile关键字

作用:
  1. 使变量在多个线程间可见(只修饰于变量)
  2. 强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值

这里写图片描述

比较(与synchronized不同)


  1. 关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。
  2. 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
  3. volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
  4. 再次重申一下,关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性

线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。

应用场景

应用场景是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用,也就是用多线程读取共享变量时可以获得最新值使用。(仅仅是读取最新的值

关键字volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但在这里需要注意的是:如果修改实例变量中的数据,比如i++,也就是i=i+1,则这样的操作其实并不是一个原子操作,也就是非线程安全的。表达式i++的操作步骤分解如下:
1)从内存中取出i的值;
2)计算i的值;
3)将i的值写到内存中。
假如在第2步计算值的时候,另外一个线程也修改i的值,那么这个时候就会出现脏数据。解决的办法其实就是使用synchronized关键字,所以说volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青菜小王子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值