《Java 多线程编程核心技术》笔记——第2章 对象及变量的并发访问(一)

声明:

本博客是本人在学习《Java 多线程编程核心技术》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

本章主要介绍 Java 多线程中的同步,也就是如何在 Java 语言中写出线程安全的程序,如何在 Java 语言中解决非线程安全的相关问题。

本章应该着重掌握如下技术点:

  • synchronized 对象监视器为 Object 时的使用
  • synchronized 对象监视器为 Class 时的使用
  • 非线程安全是如何出现的
  • 关键字 volatile 的主要作用
  • 关键字 volatile 和 synchronized 的区别及使用情况

2.1 synchronized 同步方法

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

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

2.1.1 方法内的变量为线程安全

“非线程安全” 问题存在于 “实例变量” 中,如果是方法内部的私有变量,则不存在 “非线程安全” 问题。这是方法内部的变量是私有的特性造成的。

下面通过一个示例来演示,方法内部的变量不存在 “非线程安全” 问题:

  1. 创建一个公共类

    public class HasSelfPrivateNum {
        public void addI(String username) {
            int num = 0;
            try {
                if(username.equals("a")) {
                    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();
            }
        }
    }
    
    
  2. 创建两个自定义的线程类

    public class MyThread1 extends Thread {
        private HasSelfPrivateNum hasSelfPrivateNum;
    
        public MyThread1(HasSelfPrivateNum hasSelfPrivateNum) {
            this.hasSelfPrivateNum = hasSelfPrivateNum;
        }
    
        @Override
        public void run() {
            super.run();
            hasSelfPrivateNum.addI("a");
        }
    }
    
    
    public class MyThread1_2 extends Thread {
        private HasSelfPrivateNum hasSelfPrivateNum;
    
        public MyThread1_2(HasSelfPrivateNum hasSelfPrivateNum) {
            this.hasSelfPrivateNum = hasSelfPrivateNum;
        }
    
        @Override
        public void run() {
            super.run();
            hasSelfPrivateNum.addI("b");
        }
    }
    
    
  3. 测试类

    public class MyThread1Test {
        public static void main(String[] args) {
            HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
            MyThread1 myThread1 = new MyThread1(hasSelfPrivateNum);
            myThread1.start();
            MyThread1_2 myThread1_2 = new MyThread1_2(hasSelfPrivateNum);
            myThread1_2.start();
        }
    }
    
    

    运行结果

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

2.1.2 实例变量非线程安全

如果多个线程共同访问一个对象中的实例变量,则有可能出现 “非线程安全” 问题。

如果访问的对象有多个实例变量,则运行结果有可能出现交叉的情况。

如果访问的对象仅有一个实例变量,则有可能出现覆盖的情况。

下面通过一个示例来演示:

  1. 创建一个公共类

    public class HasSelfPrivateNum2 {
        private int num = 0;
    
        public void addI(String username) {
            try {
                if(username.equals("a")) {
                    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();
            }
        }
    }
    
    
  2. 创建两个自定义的线程类

    public class MyThread2 extends Thread {
        private HasSelfPrivateNum2 HasSelfPrivateNum2;
    
        public MyThread2(HasSelfPrivateNum2 HasSelfPrivateNum2) {
            this.HasSelfPrivateNum2 = HasSelfPrivateNum2;
        }
    
        @Override
        public void run() {
            super.run();
            HasSelfPrivateNum2.addI("a");
        }
    }
    
    
    public class MyThread2_2 extends Thread {
        private HasSelfPrivateNum2 HasSelfPrivateNum2;
    
        public MyThread2_2(HasSelfPrivateNum2 HasSelfPrivateNum2) {
            this.HasSelfPrivateNum2 = HasSelfPrivateNum2;
        }
    
        @Override
        public void run() {
            super.run();
            HasSelfPrivateNum2.addI("b");
        }
    }
    
    
  3. 测试类

    public class MyThread2Test {
        public static void main(String[] args) {
            HasSelfPrivateNum2 hasSelfPrivateNum2 = new HasSelfPrivateNum2();
            MyThread2 myThread2 = new MyThread2(hasSelfPrivateNum2);
            myThread2.start();
            MyThread2_2 myThread2_2 = new MyThread2_2(hasSelfPrivateNum2);
            myThread2_2.start();
        }
    }
    
    

    运行结果

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

2.1.3 多个对象多个锁

下面看一个示例:

  1. 创建一个公共类

    public class HasSelfPrivateNum3 {
        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(1000);
                } else {
                    num = 200;
                    System.out.println("b set over !");
                }
                System.out.println(username + " num=" + num);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  2. 创建两个自定义的线程类

    public class MyThread3 extends Thread {
        private HasSelfPrivateNum3 HasSelfPrivateNum3;
    
        public MyThread3(HasSelfPrivateNum3 HasSelfPrivateNum3) {
            this.HasSelfPrivateNum3 = HasSelfPrivateNum3;
        }
    
        @Override
        public void run() {
            super.run();
            HasSelfPrivateNum3.addI("a");
        }
    }
    
    
    public class MyThread3_2 extends Thread {
        private HasSelfPrivateNum3 HasSelfPrivateNum3;
    
        public MyThread3_2(HasSelfPrivateNum3 HasSelfPrivateNum3) {
            this.HasSelfPrivateNum3 = HasSelfPrivateNum3;
        }
    
        @Override
        public void run() {
            super.run();
            HasSelfPrivateNum3.addI("b");
        }
    }
    
    
  3. 测试类

    public class MyThread3Test {
        public static void main(String[] args) {
            HasSelfPrivateNum3 hasSelfPrivateNum3 = new HasSelfPrivateNum3();
            HasSelfPrivateNum3 hasSelfPrivateNum3_2 = new HasSelfPrivateNum3();
            MyThread3 myThread3 = new MyThread3(hasSelfPrivateNum3);
            myThread3.start();
            MyThread3_2 myThread3_2 = new MyThread3_2(hasSelfPrivateNum3_2);
            myThread3_2.start();
        }
    }
    
    

    运行结果

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

分析:上面两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是以异步的方式运行的。这是因为关键字 synchronized 取得的锁都是对象锁,如果多个线程访问多个对象,则 JVM 会创建多个锁

2.1.4 synchronized 方法与锁对象

下面通过一个示例来证明前面说的线程锁的是对象:

  1. 创建一个公共类

    public class MyObject {
        public void methodA() {
            try {
                System.out.println("begin methodA threadName = " + Thread.currentThread().getName());
                Thread.sleep(1000);
                System.out.println("end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  2. 创建两个自定义的线程类

    public class MyThread4 extends Thread {
        private MyObject myObject;
    
        public MyThread4(MyObject myObject) {
            this.myObject = myObject;
        }
    
        @Override
        public void run() {
            super.run();
            myObject.methodA();
        }
    }
    
    
    public class MyThread4_2 extends Thread {
        private MyObject myObject;
    
        public MyThread4_2(MyObject myObject) {
            this.myObject = myObject;
        }
    
        @Override
        public void run() {
            super.run();
            myObject.methodA();
        }
    }
    
    
  3. 测试类

    public class MyThread4Test {
        public static void main(String[] args) {
            MyObject myObject = new MyObject();
            MyThread4 myThread4 = new MyThread4(myObject);
            myThread4.setName("A");
            myThread4.start();
            MyThread4_2 myThread4_2 = new MyThread4_2(myObject);
            myThread4.setName("B");
            myThread4_2.start();
        }
    }
    
    

    运行结果

    begin methodA threadName = A
    begin methodA threadName = B
    end
    end
    

分析:从打印结果可以看出,两个线程可同时进入没上锁的方法 methodA

接下来把 methodA 上锁,再次运行:

  1. 给 methodA 上锁

    public class MyObject {
        synchronized public void methodA() {
            try {
                System.out.println("begin methodA threadName = " + Thread.currentThread().getName());
                Thread.sleep(1000);
                System.out.println("end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  2. 运行结果

    begin methodA threadName = A
    end
    begin methodA threadName = B
    end
    

分析:线程调用上锁的方法 methodA 是排队运行的

那其他方法在被调用时,会是什么效果呢?如何查看 Lock 锁对象的效果呢?

下面通过一个示例来演示:

  1. 创建一个公共类

    public class MyObject2 {
        synchronized public void methodA() {
            try {
                System.out.println("begin methodA threadName = " + Thread.currentThread().getName());
                Thread.sleep(1000);
                System.out.println("end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        public void methodB() {
            try {
                System.out.println("begin methodB threadName = " + Thread.currentThread().getName());
                Thread.sleep(1000);
                System.out.println("end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  2. 创建两个自定义的线程类

    public class MyThread5 extends Thread {
        private MyObject2 myObject2;
    
        public MyThread5(MyObject2 myObject2) {
            this.myObject2 = myObject2;
        }
    
        @Override
        public void run() {
            super.run();
            myObject2.methodA();
        }
    }
    
    
    public class MyThread5_2 extends Thread {
        private MyObject2 myObject2;
    
        public MyThread5_2(MyObject2 myObject2) {
            this.myObject2 = myObject2;
        }
    
        @Override
        public void run() {
            super.run();
            myObject2.methodB();
        }
    }
    
    
  3. 测试类

    public class MyThread5Test {
        public static void main(String[] args) {
            MyObject2 myObject2 = new MyObject2();
            MyThread5 myThread5 = new MyThread5(myObject2);
            myThread5.setName("A");
            myThread5.start();
            MyThread5_2 myThread5_2 = new MyThread5_2(myObject2);
            myThread5_2.setName("B");
            myThread5_2.start();
        }
    }
    
    

    运行结果

    begin methodA threadName = A
    begin methodB threadName = B
    end
    end
    

分析:A 线程先持有了 myObject2 对象的 Lock 锁,B 线程可以以异步的方式调用 myObject2 对象中的非 synchronized 类型的方法

继续实验,给 methodB 上锁,再次运行:

  1. 给 methodB 上锁

    public class MyObject2 {
        synchronized public void methodA() {
            try {
                System.out.println("begin methodA threadName = " + Thread.currentThread().getName());
                Thread.sleep(1000);
                System.out.println("end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized public void methodB() {
            try {
                System.out.println("begin methodB threadName = " + Thread.currentThread().getName());
                Thread.sleep(1000);
                System.out.println("end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  2. 运行结果

    begin methodA threadName = A
    end
    begin methodB threadName = B
    end
    

分析:A 线程先持有了 myObject2 对象的 Lock 锁,B 线程如果在这时调用 myObject2 对象中的 synchronized 类型的方法则需等待,也就是同步

2.1.5 脏读

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

下面通过一个实例来演示脏读的情况:

  1. 创建一个公共类

    public class PublicVar {
        private String username = "A";
        private String password = "AA";
    
        synchronized public void setValue(String username, String password) {
            try {
                this.username = username;
                Thread.sleep(5000);
                this.password = password;
                System.out.println("setValue method name =" + Thread.currentThread().getName() + " username = " + username + " password = " + password);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public void getValue() {
            System.out.println("getValue method name =" + Thread.currentThread().getName() + " username = " + username + " password = " + password);
        }
    }
    
    
  2. 创建自定义的线程类

    public class MyThread6 extends Thread {
        private PublicVar publicVar;
    
        public MyThread6(PublicVar publicVar) {
            this.publicVar = publicVar;
        }
    
        @Override
        public void run() {
            super.run();
            publicVar.setValue("B","BB");
        }
    }
    
    
  3. 测试类

    public class MyThread6Test {
        public static void main(String[] args) {
            try {
                PublicVar publicVar = new PublicVar();
                MyThread6 myThread6 = new MyThread6(publicVar);
                myThread6.start();
                Thread.sleep(1000);
                publicVar.getValue();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    运行结果

    getValue method name =main username = B password = AA
    setValue method name =Thread-0 username = B password = BB
    

分析:出现脏读是因为 public void getValue() 方法不是同步的,所以可以在任意的时候进行调用,解决办法方然就是加上同步 synchronized 关键字。

2.1.6 synchronized 锁重入

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

下面通过一个示例演示:

  1. 创建一个公共类

    public class Service {
        synchronized public void service1() {
            System.out.println("service1");
            this.service2();
        }
        synchronized public void service2() {
            System.out.println("service2");
            this.service3();
        }
        synchronized public void service3() {
            System.out.println("service3");
        }
    }
    
    
  2. 创建自定义的线程类

    public class MyThread7 extends Thread {
        private Service service;
    
        public MyThread7(Service service) {
            this.service = service;
        }
    
    
        @Override
        public void run() {
            super.run();
            service.service1();
        }
    }
    
    
  3. 测试类

    public class MyThread7Test {
        public static void main(String[] args) {
            Service service = new Service();
            MyThread7 myThread7 = new MyThread7(service);
            myThread7.start();
        }
    }
    
    

    运行结果

    service1
    service2
    service3
    

当存在父子类继承关系时,子类完全可以通过 “可重入锁” 调用父类的同步方法,下面通过一个实例演示:

  1. 创建一个父类和一个子类

    public class Main {
        public int i = 10;
        synchronized public void operateIMainMethod() {
            i--;
            System.out.println("main print i: " + i);
        }
    }
    
    
    public class Sub extends Main {
        synchronized public void operateISubMethod() {
            while (i > 0) {
                i--;
                System.out.println("sub print i: " + i);
                this.operateIMainMethod();
            }
        }
    }
    
    
  2. 创建自定义的线程类

    public class MyThread8 extends Thread {
        @Override
        public void run() {
            super.run();
            Sub sub = new Sub();
            sub.operateISubMethod();
        }
    }
    
    
  3. 测试类

    public class MyThread8Test {
        public static void main(String[] args) {
            MyThread8 myThread8 = new MyThread8();
            myThread8.start();
        }
    }
    
    

    运行结果

    sub print i: 9
    main print i: 8
    sub print i: 7
    main print i: 6
    sub print i: 5
    main print i: 4
    sub print i: 3
    main print i: 2
    sub print i: 1
    main print i: 0
    

2.1.7 出现异常,锁自动释放

当一个线程执行的代码出现异常时,其所持有的锁会自动释放

下面通过一个示例来演示:

  1. 创建一个公共类

    public class Service2 {
        synchronized public void testMethod() {
            if (Thread.currentThread().getName() == "A") {
                System.out.println("thread A run time: " + System.currentTimeMillis());
                int i = 1/0;
            } else {
                System.out.println("thread B run time: " + System.currentTimeMillis());
            }
        }
    }
    
    
  2. 创建两个自定义的线程类

    public class MyThread9 extends Thread {
        private Service2 service2;
    
        public MyThread9(Service2 service2) {
            this.service2 = service2;
        }
    
        @Override
        public void run() {
            super.run();
            service2.testMethod();
        }
    }
    
    
    public class MyThread9_2 extends Thread{
        private Service2 service2;
    
        public MyThread9_2(Service2 service2) {
            this.service2 = service2;
        }
    
        @Override
        public void run() {
            super.run();
            service2.testMethod();
        }
    }
    
    
  3. 测试类

    public class MyThread9Test {
        public static void main(String[] args) {
            try {
                Service2 service2 = new Service2();
                MyThread9 myThread9 = new MyThread9(service2);
                myThread9.setName("A");
                myThread9.start();
                Thread.sleep(500);
                MyThread9_2 myThread9_2 = new MyThread9_2(service2);
                myThread9_2.setName("B");
                myThread9_2.start();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    运行结果

    thread A run time: 1574394712072
    Exception in thread "A" java.lang.ArithmeticException: / by zero
    	at Service2.testMethod(Service2.java:5)
    	at MyThread9.run(MyThread9.java:11)
    thread B run time: 1574394712572
    

2.1.8 同步不具有继承性

同步不具有继承性

下面通过一个示例来演示:

  1. 创建一个父类和一个子类

    public class Main2 {
        synchronized public void serviceMethod() {
            try {
                System.out.println("main2 begin thread name = " + Thread.currentThread().getName() + " begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("main2 end thread name = " + Thread.currentThread().getName() + " end time = " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
    public class Sub2 extends Main2 {
        public void serviceMethod() {
            try {
                System.out.println("sub2 begin thread name = " + Thread.currentThread().getName() + " begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("sub2 end thread name = " + Thread.currentThread().getName() + " end time = " + System.currentTimeMillis());
                super.serviceMethod();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  2. 创建自定义的线程类

    public class MyThread10 extends Thread {
        private Sub2 sub2;
    
        public MyThread10(Sub2 sub2) {
            this.sub2 = sub2;
        }
    
        @Override
        public void run() {
            super.run();
            sub2.serviceMethod();
        }
    }
    
    
  3. 测试类

    public class MyThread10Test {
        public static void main(String[] args) {
            Sub2 sub2 = new Sub2();
            MyThread10 myThread10 = new MyThread10(sub2);
            myThread10.setName("A");
            myThread10.start();
            MyThread10 myThread10_2 = new MyThread10(sub2);
            myThread10_2.setName("B");
            myThread10_2.start();
        }
    }
    
    

    运行结果

    sub2 begin thread name = A begin time = 1574396491420
    sub2 begin thread name = B begin time = 1574396491420
    sub2 end thread name = B end time = 1574396493420
    sub2 end thread name = A end time = 1574396493420
    main2 begin thread name = B begin time = 1574396493420
    main2 end thread name = B end time = 1574396495421
    main2 begin thread name = A begin time = 1574396495421
    main2 end thread name = A end time = 1574396497421
    

分析:线程 A 和 线程 B 在异步调用 Sub2 的 serviceMethod() 方法

接下来在 Sub2 的 serviceMethod() 方法上加上 synchronized 关键字后,再次运行:

sub2 begin thread name = A begin time = 1574397046198
sub2 end thread name = A end time = 1574397048199
main2 begin thread name = A begin time = 1574397048199
main2 end thread name = A end time = 1574397050199
sub2 begin thread name = B begin time = 1574397050199
sub2 end thread name = B end time = 1574397052199
main2 begin thread name = B begin time = 1574397052199
main2 end thread name = B end time = 1574397054199

分析:线程 A 和 线程 B 在同步调用 Sub2 的 serviceMethod() 方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

bm1998

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

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

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

打赏作者

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

抵扣说明:

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

余额充值