java多线程编程核心技术(第二章)-读书笔记

java多线程编程核心技术

第二章、对象及变量的并发访问

2.1 synchronized同步方法

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

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

2.1.2 实例变量非线程安全

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

在两个线程访问同一个对象中的同步方法时一定是线程安全的。

2.1.3 多个对象多个锁

两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果是以异步的方式运行的。

关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,所以哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。

如果多个线程访问多个对象(同一个类)的同一个同步方法,则jvm会创建多个锁。

2.1.4 synchronized方法与锁对象

如果A线程先持有了object对象的锁,线程B可以异步方式调用非synchronized类型的方法。

如果A线程先持有了object对象的锁,线程B如果在这时调用object对象中的synchronized类型的方法则需要等,也就是同步。

2.1.5 脏读

虽然赋值时进行了同步,但在取值时有可能出现一些意想不到的意外,这种情况就是脏读(dirtyRead)。发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。

/**
 * 脏读
 */
public class Test09DirtyRead {
    public static void main(String[] args) throws InterruptedException {
        DirtyReadObject dirtyReadObject = new DirtyReadObject();
        DirtyReadThread dirtyReadThread = new DirtyReadThread(dirtyReadObject);
        dirtyReadThread.start();
        Thread.sleep(1000);
        dirtyReadObject.getValue();

        /*
        由于setValue方法在修改了username后即休眠,主线程在调用getValue时username已经修改为B,而password还未修改,造成脏读
        运行结果:
        get method thread name=main username=B password=AA
        set method thread name=Thread-0 username=B password=BB

        解决办法是在getValue方法上加synchronized关键字,使其成为同步方法,这样就会等setValue释放锁后此方法才能执行
        getValue方法加锁后的运行结果:
        set method thread name=Thread-0 username=B password=BB
        get method thread name=main username=B password=BB
         */
    }
}

class DirtyReadThread extends Thread {
    private DirtyReadObject dirtyReadObject ;
    public DirtyReadThread(DirtyReadObject dirtyReadObject) {
        this.dirtyReadObject = dirtyReadObject;
    }

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

class DirtyReadObject {
    public String username = "A";
    public String password = "AA";
    synchronized public void setValue(String username, String password) {
        try {
            this.username = username;
            Thread.sleep(5000);
            this.password = password;
            System.out.println(String.format("set method thread name=%s username=%s password=%s", Thread.currentThread().getName(), username, password));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void getValue() {
//    synchronized public void getValue() { //加锁同步后不会出现脏读
        System.out.println(String.format("get method thread name=%s username=%s password=%s", Thread.currentThread().getName(), username, password));
    }
}

出现脏读时因为getValue()方法并不是同步的,所以可以在任意时候进行调用,解决办法是增加synchronized关键字。

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

2.1.6 synchronized锁重入

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

“可重入锁”的概念是:自己可以再次获取自己的内部锁。比如有一个线程获取了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。

可重入锁也支持在父子类继承的环境中。即当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。

2.1.7 出现异常,锁自动释放

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

2.1.8 同步不具有继承性

同步不可以继承。更具体的说法是子类重写父类的同步方法时,如果子类方法不加synchronized关键字,则子类的方法不是同步方法。

2.2 synchronized 同步语句块

用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待比较长时间。在这样的情况下可以使用synchronized同步语句块来解决。

2.2.2 synchronized 同步代码块的使用

当两个并发线程访问同一个对象中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

2.2.3 用同步代码块解决同步方法的弊端

代码示例:

/**
 * 同步代码块
 */
public class Test10Synchronized {
    public static void main(String[] args) {
        SynchronizedObjectTestThread01 test = new SynchronizedObjectTestThread01();
        Thread a = new Thread(test);
        a.setName("a");
        Thread b = new Thread(test);
        b.setName("b");
        a.start();
        b.start();
        /*
        可以看到,两个线程可以同时进入doLongTimeTask方法,只是在需要同步的地方才加锁
        运行结果:
        begin threadName=b
        begin threadName=a
        长时间处理任务后返回的值1 threadName=a
        长时间处理任务后返回的值2 threadName=a
        end threadName=a
        长时间处理任务后返回的值1 threadName=b
        长时间处理任务后返回的值2 threadName=b
        end threadName=b
         */
    }
}

class SynchronizedObjectTestThread01 extends Thread{
    private String getData1;
    private String getData2;

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

    public void doLongTimeTask() {
        try {
            System.out.println("begin threadName=" + Thread.currentThread().getName());
            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 threadName=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.2.5 synchronized 代码块间的同步性

当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。

2.2.6 验证同步synchronized(this)代码块是锁定当前对象的

和synchronized方法一样,synchronized(this)代码块也是锁定当前对象的。

2.2.7 将任意对象作为对象监视器

synchronized(非this对象)格式的作用只有1种:synchronized(非this对象x)同步代码块。

  • 在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码。
  • 当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码。

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

2.2.8 细化验证3个结论

“synchronized(非this对象x)”格式的写法是将x对象本身作为“对象监视器”,这样就可以得出以下3个结论:

  • 当多个线程同时执行synchronized(x){}同步代码块时呈同步效果。
  • 当其他线程执行x对象中synchronized同步方法时呈同步效果。
  • 当其他线程执行x对象方法里面的synchronized(this)代码块时也呈现同步效果。

但需要注意:如果其他线程调用不加synchronized关键字的方法时,还是异步调用。

代码示例:

/**
 * 锁其他对象
 */
public class Test11SynchronizedObject {
    public static void main(String[] args) {
        SynchronizedObjectTest test = new SynchronizedObjectTest();

        // t1线程执行test对象的同步方法a()
        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                test.a();
            }
        };

        // t2线程执行test对象的b()方法中的同步代码块
        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                test.b();
            }
        };

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

        // 主线程获取test对象的锁后执行自己的代码
        synchronized (test) {
            System.out.println("main同步代码块 begin ,thread=" + Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main同步代码块 end ,thread=" + Thread.currentThread().getName());
        }

        /*
        三个线程实际都是获取的同一个对象锁,所以方法调用都是同步(阻塞)的,当然b方法的同步代码块前后的代码是异步执行的
        执行结果:
        main同步代码块 begin ,thread=main
        b方法同步代码块之前的代码执行!
        main同步代码块 end ,thread=main
        b同步代码块 begin ,thread=Thread-1
        b同步代码块 end ,thread=Thread-1
        b方法同步代码块之后的代码执行!
        a同步方法 begin ,thread=Thread-0
        a同步方法 end ,thread=Thread-0
         */
    }
}

class SynchronizedObjectTest {

    synchronized public void a() {
        System.out.println("a同步方法 begin ,thread=" + Thread.currentThread().getName());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("a同步方法 end ,thread=" + Thread.currentThread().getName());
    }

    public void b() {
        // 其他代码
        System.out.println("b方法同步代码块之前的代码执行!");
        synchronized (this) {
            System.out.println("b同步代码块 begin ,thread=" + Thread.currentThread().getName());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("b同步代码块 end ,thread=" + Thread.currentThread().getName());
        }
        // 其他代码
        System.out.println("b方法同步代码块之后的代码执行!");
    }
}

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

关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对应的Class类进行持锁。同步synchronized(class)代码块的作用和synchronized static方法的作用一样,都是对Class类进行持锁。

代码示例:

/**
 * 静态方法同步是锁class
 */
public class Test12SynchronizedStatic {
    public static void main(String[] args) {
        SynchronizedStaticTest test = new SynchronizedStaticTest();

        // t1线程调用静态同步方法
        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                SynchronizedStaticTest.printA();
            }
        };

        // t2线程也是调用的静态同步方法
        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                test.printB();
            }
        };

        // t3线程调用的是非静态的同步方法
        Thread t3 = new Thread() {
            @Override
            public void run() {
                super.run();
                test.printC();
            }
        };

        t1.start();
        t2.start();
        t3.start();
        /*
        由于t1,t2线程调用的都是SynchronizedStaticTest类的静态同步方法,获取的都是对class的锁,所以他们互相之间是同步的效果
        而t3线程调用的是非静态的同步方法,获取的是对象的锁,和class锁不是同一个,所以他可以和其他两个线程异步执行
        运行结果:
        线程Thread-0在1570861548795进入printA
        线程Thread-2在1570861548795进入printC
        线程Thread-2在1570861551816离开printC
        线程Thread-0在1570861551816离开printA
        线程Thread-1在1570861551817进入printB
        线程Thread-1在1570861554821离开printB
         */
    }
}

class SynchronizedStaticTest {
    synchronized public static void printA() {
        try {
            System.out.println(String.format("线程%s在%s进入printA", Thread.currentThread().getName(), System.currentTimeMillis()));
            Thread.sleep(3000);
            System.out.println(String.format("线程%s在%s离开printA", Thread.currentThread().getName(), System.currentTimeMillis()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public static void printB() {
        try {
            System.out.println(String.format("线程%s在%s进入printB", Thread.currentThread().getName(), System.currentTimeMillis()));
            Thread.sleep(3000);
            System.out.println(String.format("线程%s在%s离开printB", Thread.currentThread().getName(), System.currentTimeMillis()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public void printC() {
        try {
            System.out.println(String.format("线程%s在%s进入printC", Thread.currentThread().getName(), System.currentTimeMillis()));
            Thread.sleep(3000);
            System.out.println(String.format("线程%s在%s离开printC", Thread.currentThread().getName(), System.currentTimeMillis()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.2.10 数据类型String的常量池特性

在jvm中具有String常量池缓存的功能,将synchronized同步代码块与String联合使用时,要注意常量池带来的一些例外。

/**
 * 字符串作为锁时需要注意——字符串常量池
 */
public class Test13SynchronizedString {
    public static void main(String[] args) {
        SynchronizedStringTest test = new SynchronizedStringTest();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                super.run();
                String a = "test";
                test.print(a);
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                super.run();
                String b = "test";
                test.print(b);
            }
        };

        t1.start();
        t2.start();
        /*
        虽然线程t1传入的参数是a,线程t2传入的是b,但是他们都是同一个字符串test,用的是String常量池中的同一个对象,所以有同步的效果
        运行结果:
        Thread-0
        Thread-0
        Thread-0
        Thread-0
         */
    }
}
class SynchronizedStringTest {
    public static void print(String str) {
        synchronized (str) {
            for (;;) {
                try {
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

因此在大多数情况下,同步synchronized代码块都不使用String作为锁对象。

2.2.11 同步synchronized方法无限等待与解决

同步方法使用不当容易造成死循环或者效率低下。

下面是示例代码及解决方法(不同的方法锁定不同对象):

/**
 * 可以为不同的同步方法提供不同的锁
 */
public class Test14SynchronizedObject {
}

class SynchronizedObjectTest01 {
    public static void main(String[] args) {
        SynchronizedObjectTest01 test = new SynchronizedObjectTest01();
        // t1线程调用test对象的methodA同步方法,这个方法本身永远不会退出
        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                test.methodA();
            }
        };

        // t2线程调用test对象的methodB同步方法
        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                test.methodB();
            }
        };

        t1.start();
        t2.start();
        /*
        由于两个同步方法都获取了对象锁,所以a方法开始后,b方法永远无法执行
        运行结果:
        a begin
         */
    }
    synchronized public void methodA() {
        System.out.println("a begin");
        boolean flag = true;
        while (flag) {

        }
        System.out.println("a end");
    }

    synchronized public  void methodB() {
        System.out.println("b begin");
        System.out.println("b end");
    }
}

class SynchronizedObjectTest02 {
    public static void main(String[] args) {
        SynchronizedObjectTest02 test = new SynchronizedObjectTest02();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                test.methodA();
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                test.methodB();
            }
        };

        t1.start();
        t2.start();
        /*
        由于两个同步方法锁了不同的对象,所以a方法开始后,b方法也可以异步执行
        运行结果:
        a begin
        b begin
        b end
         */
    }

    Object lock1 = new Object();
    public void methodA() {
        synchronized (lock1) {
            System.out.println("a begin");
            boolean flag = true;
            while (flag) {

            }
            System.out.println("a end");
        }
    }

    Object lock2 = new Object();
    public  void methodB() {
        synchronized (lock2) {
            System.out.println("b begin");
            System.out.println("b end");
        }
    }
}

2.2.12 多线程的死锁

死锁的发生:不同的线程都在等待根本不可能被释放的锁,从而导致所有的人物都无法继续完成。

示例代码:

/**
 * 死锁
 */
public class Test15SynchronizedDeadLock {
    public static void main(String[] args) throws InterruptedException {
        SynchronizedDeadLockTestThread test = new SynchronizedDeadLockTestThread();
        // t1线程执行对lock1加锁后需要lock2才能继续运行
        Thread t1 = new Thread(test);
        test.setFlag("a");
        t1.start();
        Thread.sleep(1000);

        // t2线程则相反,对lock2加锁后需要lock1才能继续运行
        Thread t2 = new Thread(test);
        test.setFlag("b");
        t2.start();

        /*
        两个线程互相在等待对方持有对锁,造成死锁
        运行结果:
        username = a
        username = b
         */
    }
}

class SynchronizedDeadLockTestThread 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 ("a".equals(username)) {
            synchronized (lock1) {
                try {
                    System.out.println("username = " + username);
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("lock1 -> lock2 end");
                }
            }
        }
        if ("b".equals(username)) {
            synchronized (lock2) {
                try {
                    System.out.println("username = " + username);
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("lock1 -> lock2 end");
                }
            }
        }
    }
}

可以使用jdk自带的工具jstack来检测是否有死锁的现象。

2.2.13 内置类与静态内置类

内置类与静态内置类的关系,下面代码简单说明:

/**
 * 简单演示内部类及静态内部类
 */
public class Test16InnerClass {
    public static void main(String[] args) {
        //
        InnerClassTest innerClassTest = new InnerClassTest();
        // 创建内部类需要外部类的对象存在
        InnerClassTest.MyInnerClass myInnerClass = innerClassTest.new MyInnerClass();
        // 创建静态内部类不需要外部对象存在,可直接创建
        InnerClassTest.MyStaticInnerClass myStaticInnerClass = new InnerClassTest.MyStaticInnerClass();

        /*
        运行结果:
        inner...
        static inner...
         */
    }
}

class InnerClassTest {
    class MyInnerClass {
        MyInnerClass() {
            System.out.println("inner...");
        }
    }

    static class MyStaticInnerClass {
        MyStaticInnerClass() {
            System.out.println("static inner...");
        }
    }
}

2.2.15内置类与同步:实验

同步代码块synchronized(object)对静态内部类的对象object上锁后,其他线程只能以同步的方式调用object类的同步方法

首先看一个,示例代码:

/**
 * 内部类的锁和外部类的锁道理是一样的,主要看锁的是什么对象
 */
public class Test17InnerClass02 {
    public static void main(String[] args) {
        OutClass.InnerClass1 innerClass1 = new OutClass.InnerClass1();
        OutClass.InnerClass2 innerClass2 = new OutClass.InnerClass2();
        // method1方法对innerClass2对象加锁
        Thread t1 = new Thread() {
            @Override
            public void run() {
                super.run();
                innerClass1.method1(innerClass2);
            }
        };

        // method2方法对innerClass1对象加锁
        Thread t2 = new Thread() {
            @Override
            public void run() {
                super.run();
                innerClass1.method2();
            }
        };

        // method3方法对innerClass2对象加锁
        Thread t3 = new Thread() {
            @Override
            public void run() {
                super.run();
                innerClass2.method3();
            }
        };

        t1.start();
        t2.start();
        t3.start();

        /*
        由于t1、t3线程都是对innerClass2加锁,所以他们之间是同步的,t1和t2线程分别对不同对象加锁,所以他们可以异步执行
        运行结果:
        Thread-1进入InnerClass1类中的method2方法
        Thread-0进入InnerClass1类中的method1方法
        j=0
        i=0
        j=1
        i=1
        i=2
        j=2
        i=3
        j=3
        i=4
        j=4
        Thread-0离开InnerClass1类中的method1方法
        Thread-1离开InnerClass1类中的method2方法
        Thread-2进入InnerClass2类中的method3方法
        k=0
        k=1
        k=2
        k=3
        k=4
        Thread-2离开InnerClass2类中的method3方法
         */
    }
}

class OutClass {
    static class InnerClass1 {
        public void method1(InnerClass2 innerClass2) {
            String threadName = Thread.currentThread().getName();
            synchronized (innerClass2) {
                System.out.println(threadName + "进入InnerClass1类中的method1方法");
                for (int i = 0; i < 5; i++) {
                    System.out.println("i=" + i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(threadName + "离开InnerClass1类中的method1方法");
            }
        }

        synchronized public void method2() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "进入InnerClass1类中的method2方法");
            for (int j = 0; j < 5; j++) {
                System.out.println("j=" + j);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(threadName + "离开InnerClass1类中的method2方法");
        }
    }
    static class InnerClass2 {
        synchronized public void method3() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "进入InnerClass2类中的method3方法");
            for (int k = 0; k < 5; k++) {
                System.out.println("k=" + k);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(threadName + "离开InnerClass2类中的method3方法");
        }
    }
}

2.2.16 锁对象的改变

在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间就是同步的;如果分别获得锁对象,这些线程就是异步的。

/**
 * 将String类型作为锁时的另一个例子,即字符串发生变化时,获取的锁将会是不同的锁
 */
public class Test18LockChange {
    public static void main(String[] args) throws InterruptedException {
        StringLockTestThread test = new StringLockTestThread();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        // t1线程启动后,将lock值改变
        t1.start();
        Thread.sleep(200);
        // t2线程启动后,获取的lock和t1对象不同
        t2.start();

        /*
        因为两个线程锁住的对象不同,所以互相是异步执行
        执行结果:
        Thread-1 begin 1571025861442
        Thread-2 begin 1571025861646
        Thread-1 end 1571025863442
        Thread-2 end 1571025863648
         */
    }
}

class StringLockTestThread extends Thread {
    String lock = "123";
    public void testMethod() {
        try {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " begin " + System.currentTimeMillis());
                lock = "456";
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        super.run();
        testMethod();
    }
}

另外,只要对象不变,即使对象的属性被改变,运行的结果还是同步。

2.3 volatile关键字

关键字volatile的主要作用是使变量在多个线程间可见。另一个作用是禁止jvm为了优化而做的指令重排序。

2.3.3 异步死循环

由于主线程和子线程间的变量可见性而出现的异步死循环的示例代码:

/**
 * 主线程中改变变量值,子线程的工作内存并没有刷新,导致死循环
 */
public class Test19Volatile01 {
    public static void main(String[] args) throws InterruptedException {
        RunThread01 thread = new RunThread01();
        thread.start();
        Thread.sleep(1000);
        thread.setRunning(false);
        System.out.println("stop it!");

        /*
        由于thread.setRunning(false)更新的是公共堆栈中的isRunning变量值,
        而thread线程在私有堆栈中取得isRunning的值一直为true,
        所以thread线程无法停止,一直在死循环。
        注意,如果将注释掉的一行System.out.println放开,则不会一直死循环,
        因为println中有synchronized代码块,synchronized代码块会强制刷新
        运行结果:
        run...start
        stop it!
         */
    }
}

class  RunThread01 extends Thread {
    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean running) {
        isRunning = running;
    }

    @Override
    public void run() {
        System.out.println("run...start");
        long i = 0;
        while (isRunning) {
            i++;
//            System.out.println("thread name = " + Thread.currentThread().getName());
        }
        System.out.println("run...stop, i=" + i);
    }
}

解决办法就是增加volatile关键字,

示例代码:

/**
 * volatile可以强制子线程从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值
 */
public class Test20Volatile02 {
    public static void main(String[] args) throws InterruptedException {
        RunThread02 thread = new RunThread02();
        thread.start();
        Thread.sleep(1000);
        thread.setRunning(false);
        System.out.println("stop it!");
        /*
        在isRunning前加上volatile后,程序可以正常退出,不会再死循环
        运行结果:
        run...start
        stop it!
        run...stop, i=2882258431
         */
    }
}

class  RunThread02 extends Thread {
    volatile private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean running) {
        isRunning = running;
    }

    @Override
    public void run() {
        System.out.println("run...start");
        long i = 0;
        while (isRunning) {
            i++;
        }
        System.out.println("run...stop, i=" + i);
    }
}

在这里,volatile的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。

另,如果本节的代码无法复现,可以将jvm的运行参数设为-server。

造成死循环的原因就是线程的私有工作内存没有被更新,下面是线程私有堆栈的图示:

java多线程编程核心技术(第二章)-读书笔记_第1张图片

下面是使用volatile后线程直接从公共堆栈取数据的图示:

java多线程编程核心技术(第二章)-读书笔记_第2张图片

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

如上所说,synchronized代码块具有将线程工作内存中的私有变量与公共内存中变量同步的功能,下面是代码示例:

/**
 * synchronized代码块具有将线程工作内存中的私有变量与公共内存中变量同步的功能
 */
public class Test21Volatile03 {
    public static void main(String[] args) throws InterruptedException {
        RunThread03 thread = new RunThread03();
        thread.start();
        Thread.sleep(1000);
        thread.setRunning(false);
        System.out.println("stop it!");
        /*
        如果不在isRunning前加volatile
        则需要在适当位置增加synchronized代码块,可以锁任何东西
        注释中的System.out.println和synchronized是一个效果,因为其内部有synchronized代码块
        运行结果:
        run...start
        stop it!
        run...stop, i=47821973
         */
    }
}

class  RunThread03 extends Thread {
    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean running) {
        isRunning = running;
    }

    @Override
    public void run() {
        System.out.println("run...start");
        long i = 0;
        while (isRunning) {
            i++;
//            System.out.println("anything");
            synchronized ("anything") {

            }
        }
        System.out.println("run...stop, i=" + i);
    }
}

2.3.4 volatile非原子的特性

关键字volatile虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也就不具备原子性。

示例如下:

/**
 * volatile只保证可见性,不保证原子性
 */
public class Test22ValitileAtomic {
    public static void main(String[] args) {
        MyThread22[] myThread22s = new MyThread22[100];
        for (int i = 0; i < 100; i++) {
            myThread22s[i] = new MyThread22();
        }
        for (int i = 0; i < 100; i++) {
            myThread22s[i].start();
        }
        /*
        由于volatile只保证可见性,不保证原子性,而count++实际上并不是原子操作,会出现多线程问题,所以结果并非100000
        运行结果,最后几行为:
        count=95595
        count=96595
        count=97595
        count=98595
        count=99595
         */
    }

}

class MyThread22 extends Thread {
    volatile  public static int count;
    private static  void addCount() {
        for (int i = 0; i < 1000; i++) {
            count++;
        }
        System.out.println("count=" + count);
    }

    @Override
    public void run() {
        addCount();
    }
}

上述例子中,count++并不是一个原子操作,它包含取值,计算,赋值三个步骤。

关键字volatile主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用,也就是用多线程读取共享变量时可以获得最新值使用。

关键字volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。

volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存。

用图来演示一下使用关键字volatile时出现非线程安全的原因,变量在内存中的工作过程如下:

java多线程编程核心技术(第二章)-读书笔记_第3张图片

我们可以得出结论:

  • read和load阶段:从主存赋值变量到当前线程工作内存
  • use和assign阶段:执行代码,改变当前变量值
  • store和write阶段:用工作内存数据刷新主存对应变量的值

在多线程环境中,use和assign是多次出现的,但这一操作不是原子性的。

2.3.5 使用原子类进行i++操作

除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。

2.3.7 synchronized代码块有volatile同步的功能

关键字synchronized可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。

关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态的问题,还可以保证进入同步方法或同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。

相关代码示例在之前已经展示过,不再展示。

单例模式和volatile

前面提起过,volatile还有另外一个作用是禁止jvm做指令重排序。

示例代码:

import java.util.Random;

/**
 * Vlatile另一个功能:禁止指令重排序
 */
public class Test23VolatileSingleton {
    public static void main(String[] args) {
        Thread[] threads = new Thread[10000];
        for (int i = 0; i < 10000; i++) {
            threads[i] = new Thread() {
                @Override
                public void run() {
                    VolatileSingleton volatileSingleton = VolatileSingleton.getInstance();
                    System.out.println(volatileSingleton.hashCode());
                    volatileSingleton.method(); // error2
                }
            };
            threads[i].start();
        }

        /*
        分析:
        如果不加volatile,有个很大的隐患。实例化对象的那行代码(标记为error的那行),实际上可以分解成以下三个步骤:
        1、分配内存空间
        2、初始化对象
        3、将对象指向刚分配的内存空间
        但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
        1、1分配内存空间
        2、将对象指向刚分配的内存空间
        3、初始化对象
        就会导致某一个线程刚刚分配了内存空间但是还没初始化对象时,其他线程就已经认为这个对象已经初始化,从而调用了实际还没完成初始化的单例对象
        原本增加了对应的测试,即error2处的调用,想要测试未初始化成功的单例对象调用方法时报错,但是没有测试出来。
        执行结果:
        305859450
        305859450
        305859450
         */
    }

    /*
     */
}

class VolatileSingleton {
    private volatile static VolatileSingleton uniqueSingleton;

    private VolatileSingleton() {
    }

    /**
     * 用来测试没有初始化成功的单例对象调用方法时报错
     */
    public void method() {
    }

    public static VolatileSingleton getInstance() {
        if (null == uniqueSingleton) {
            try {
                Thread.sleep(new Random().nextInt(5));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (VolatileSingleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new VolatileSingleton(); // error
                }
            }
        }
        return uniqueSingleton;
    }
}

最后

1、所有代码示例都在github中

https://github.com/llbqhh/LlbStudy/tree/master/StudyJava/src/main/java/org/llbqhh/study/java/book/java_multi_thread_programming

2、感谢您的阅读,如果对内容感兴趣可以关注我的公众号,搜索【猫耳山自习室】或扫描下方二维码,会有定期更新

java多线程编程核心技术(第二章)-读书笔记_第4张图片

你可能感兴趣的:(读书笔记)