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

synchronized 同步方法

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

方法内的变量为线程安全

非线程安全问题存在于实例变量中,如果是方法内部的私有变量,是不存在非线程安全

public 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);//此时当前线程休眠,会被另外的线程抢占CPU时间
            }else{
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadA extends Thread {
    private HasSelfPrivateNum numRef;
    public ThreadA(HasSelfPrivateNum numRef){
        super();
        this.numRef = numRef;
    }
    public void run(){
        super.run();
        numRef.addI("a");
    }
}

public class ThreadB extends Thread {
    private HasSelfPrivateNum numRef;
    public ThreadB(HasSelfPrivateNum numRef){
        super();
        this.numRef = numRef;
    }
    public void run(){
        super.run();
        numRef.addI("b");
    }
}

public class Run7 {
    public static void main(String[] args) {
        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        ThreadA athread = new ThreadA(numRef);
        athread.start();
        ThreadB bthread = new ThreadB(numRef);
        bthread.start();
    }
}

运行结果:
a set over!
b set over!
b num 200
a num 100

未出现线程安全问题。理解这个问题的关键是直到变量 num 是不是被多个线程所共享。我们知道,当执行athread.start(); 以后,athread 线程启动,当它获得 CPU 时间以后便执行 run 方法,执行 run 方法中的 addI 方法。利用 JVM 的相关知识,方法中的变量是存放在虚拟机栈的栈帧中的,JVM 会为每一个方法建立一个栈帧。所以,当 bthread 线程调用run 方法中 addI 方法的时候,又会建立一个 addI 栈帧。也就是说此时 athread 线程和 bthread 线程执行的每个 addI 方法中各有一个 num 变量,互不干涉,所以不会出现线程安全问题。
在这里插入图片描述

实例变量非线程安全

如果多个线程同时访问一个对象中的实例变量,则可能出现非线程安全问题。
如果对象仅有一个实例变量,则可能出现覆盖的情况。

public 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);//当前线程休眠2秒钟(此时会被别的线程的到CPU时间)
            }else{
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadA extends Thread {
    private HasSelfPrivateNum numRef;
    public ThreadA(HasSelfPrivateNum numRef){
        super();
        this.numRef = numRef;
    }
    public void run(){
        super.run();
        numRef.addI("a");
    }
}


public class Run7 {
    public static void main(String[] args) {
        HasSelfPrivateNum numRef = new HasSelfPrivateNum();
        ThreadA athread = new ThreadA(numRef);
        athread.start();
        ThreadB bthread = new ThreadB(numRef);
        bthread.start();
    }
}

运行结果:
a set over!
b set over!
b num 200
a num 200

出现了线程安全问题。从Run7这个类中,我们可以看出只有一个 HasSelfPrivateNum 实例,那就是 numRef。这是创建了一个 ThreadA 的实例 athread ,启动athread,这是 athread 准备就绪。它抢到了 CPU 时间,运行 run 方法,当运行到Thread.sleep(2000);的时候,athread 休眠,但此时实例变量 num 被赋值为 100,下面的代码还没有执行。bthread 线程抢占了 CPU 时间,运行 run 方法,实例变量被赋值为 200。这样,athread 本来向打印出 100,但是由于没有进行方法的同步,athread 执行run一半的时候 bthread 开始执行 run 方法,导致 实例变量 num 的值发生了改变,所以出现的非线程安全问题。
造成这个问题的原因之一就是线程 athread 和线程 bthread 同时使用一个实例变量 num。
在这里插入图片描述

解决的方法是给 public void addI(String username) 方法前加上关键字 synchronized。这样,如果线程 athread 取得锁,没有执行完代码之前是不会让 bthread 线程执行的,也就是说,athread 线程会打印出正确的内容之后 bthread 才会执行。

synchronized 方法与锁对象

调用关键字 synchronized 声明的方法一定是排队运行的。另外,要牢记共享这两个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本没有同步的必要。

public class MyObject {
    public void methodA(){ //methodA()方法没有synchronized关键字
        try {
            System.out.println("begin methodA threadName = " + Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadObjectA extends Thread {
    private MyObject object;
    public ThreadObjectA(MyObject object){
        super();
        this.object = object;
    }
    public void run(){
        super.run();
        object.methodA();
    }
}

public class ThreadObjectB extends Thread{
    private MyObject object;
    public ThreadObjectB(MyObject object){
        super();
        this.object = object;
    }
    public void run(){
        super.run();
        object.methodA();
    }
}

public class RunObject {
    public static void main(String[] args) {
        MyObject object = new MyObject();
        ThreadObjectA a = new ThreadObjectA(object);
        a.setName("A");
        ThreadObjectB b = new ThreadObjectB(object);
        b.setName("B");
        a.start();
        b.start();

    }
}

运行结果:
begin methodA threadName = A
begin methodA threadName = B
end
end

public class MyObject {
    synchronized public void methodA(){ //加上synchronized关键字
        try {
            System.out.println("begin methodA threadName = " + Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

运行结果:
begin methodA threadName = A
end
begin methodA threadName = B
end

线程 b 在线程 a 休眠的时候也不能抢走线程 a 的CPU时间,只有在线程 a 将方法methodA 方法执行完毕以后线程 b 才可以执行 methodA 方法。

脏读

在上面的例子中,已经实现多个线程调用同一方法时,为了避免数据出现交叉的情况,使用 synchronized 关键字来同步。发生脏读是在读取实例变量时,此值已经被其他线程修改过了。

public class PublicVar {
    public String userName = "A";
    public String passWord = "AA";
    synchronized public void setValue(String userName, String passWord){
        try {
            this.userName = userName;//给userName赋值“B”
            Thread.sleep(5000);//当前线程休眠5秒
            this.passWord = passWord;
            System.out.println("setValue method thread name = " + Thread.currentThread().getName() + " userName = " + userName + " passWord = " + passWord);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void getValue(){
        System.out.println("getValue method thread name = " + Thread.currentThread().getName() + " userName = " + userName + " passWord = " + passWord);
    }
}

public class ThreadObjectA extends Thread {
    private PublicVar publicVar;
    public ThreadObjectA(PublicVar publicVar){
        super();
        this.publicVar = publicVar;
    }
    public void run(){
        super.run();
        publicVar.setValue("B", "BB");
    }
}

public class RunObject {
    public static void main(String[] args) {
        try {
            PublicVar publicVar = new PublicVar();
            ThreadObjectA a = new ThreadObjectA(publicVar);//建立了a线程
            a.start();//a线程有资格抢夺CPU时间
            Thread.sleep(200);//main线程休眠200毫秒
            publicVar.getValue();//main线程执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:
getValue method thread name = main userName = B passWord = AA
setValue method thread name = Thread-0 userName = B passWord = BB

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

public class PublicVar {
    public String userName = "A";
    public String passWord = "AA";
    synchronized public void setValue(String userName, String passWord){
        try {
            this.userName = userName;//给userName赋值“B”
            Thread.sleep(5000);//当前线程休眠5秒
            this.passWord = passWord;
            System.out.println("setValue method thread name = " + Thread.currentThread().getName() + " userName = " + userName + " passWord = " + passWord);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    synchronized public void getValue(){//加上synchronized关键字
        System.out.println("getValue method thread name = " + Thread.currentThread().getName() + " userName = " + userName + " passWord = " + passWord);
    }
}

运行结果:
setValue method thread name = Thread-0 userName = B passWord = BB
getValue method thread name = main userName = B passWord = BB

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

synchronized 锁重入

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

package com.chao.com.chao.chapterTwentyThread.synchronizedReentrantLock;

public class Service {
    //当线程a调用service1方法,获得了对象锁,之后又调用service2方法
    //这时再次获得了这个对象锁,如果不可以,就会造成死锁
    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");
    }
}

package com.chao.com.chao.chapterTwentyThread.synchronizedReentrantLock;

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

package com.chao.com.chao.chapterTwentyThread.synchronizedReentrantLock;

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

运行结果:
service1
service2
service3

可重入锁的概念是:自己可以再次获得自己的内部锁。比如又一条线程获得了某个对象的锁,此时这个对象锁还没有释放,当再次想要获取这个对象锁的时候还是可以获取,如果不可锁重入的话,就会造成死锁。
当存在父子继承关系时,子类是完全可以通过可重入锁调用父类的同步方法。

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

public class Sub extends Main {
    synchronized public void operateISubMethod(){
        try {
            while(i > 0){
                i--;
                System.out.println("sub print i = " + i);
                Thread.sleep(1000);
                /*虽然operateIMainMethod方法也有锁,但是子类可以通过可重入锁来调用父类这
                *方法
                */
                this.operateIMainMethod();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

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

public class Run {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}
出现异常锁自动释放

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

同步不具有继承性

同步不可以继承。

public class Main {
    synchronized public void serviceMethod(){ //父类方法有synchronized关键字修饰
        try {
            System.out.println("int main 下一步 sleep begin threadName = "
                    + Thread.currentThread().getName() + " time = "
                    + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("int main 下一步 sleep end threadName = "
                    + Thread.currentThread().getName() + " time = "
                    + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Sub extends Main{
    public void serviceMethod(){ //子类方法没有synchronized关键字修饰
        try {
            System.out.println("int sub 下一步 sleep begin threadName = "
                    + Thread.currentThread().getName() + " time = "
                    + System.currentTimeMillis());
            /*
            程序到这里时当前线程休眠,被另一个线程抢走CPU时间,
            因为这个方法没有进行同步(使用synchronized修饰)
            说明不能从父类中继承synchronized锁
            */
            Thread.sleep(5000);
            System.out.println("int sub 下一步 sleep end threadName = "
                    + Thread.currentThread().getName() + " time = "
                    + System.currentTimeMillis());
            super.serviceMethod();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadA extends Thread {
    private Sub sub;
    public ThreadA(Sub sub){
        super();
        this.sub = sub;
    }
    public void run(){
        sub.serviceMethod();
    }
}

public class ThreadB extends Thread {
    private Sub sub;
    public ThreadB(Sub sub){
        super();
        this.sub = sub;
    }
    public void run(){
        sub.serviceMethod();
    }
}

public class Test {
    public static void main(String[] args) {
        Sub subRef = new Sub();
        ThreadA threadA = new ThreadA(subRef);
        threadA.setName("a");
        threadA.start();
        ThreadB threadB = new ThreadB(subRef);
        threadB.setName("b");
        threadB.start();
    }
}

synchronized 同步语句块

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

synchronized 同步代码块的使用

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

public class ObjectService {
    public void serviceMethod(){
        try {
            synchronized(this){
                System.out.println("begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("end time = " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

虽然上面代码使用了同步代码块,但是执行效率并没有提高,执行的效果还是同步的。

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

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

public class ObjectService {
    public void serviceMethodA(){
        try {
            synchronized (this){
                System.out.println("A begin time = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                /*
                这里当前线程虽然休眠4秒,但是由于线程a访问了 object 的一个
                synchronized(this) 代码块,所以线程b不能访问serviceMethodB()
                中object 的一个synchronized(this) 代码块。只有等到a使用这个代码块结束,
                线程b才能访问synchronized(this) 块中的代码                 
                */
                Thread.sleep(4000);
                System.out.println("A end time = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void serviceMethodB(){
        synchronized (this){
            System.out.println("A begin time = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
            System.out.println("A end time = " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
        }
    }
}
====================================================================================
public class ThreadA extends Thread {
    private ObjectService service;

    public ThreadA(ObjectService service) {
        super();
        this.service = service;
    }

    public void run(){
        super.run();
        service.serviceMethodA();
    }
}
====================================================================================
public class ThreadB extends Thread {
    private ObjectService service;

    public ThreadB(ObjectService service) {
        super();
        this.service = service;
    }

    public void run(){
        super.run();
        service.serviceMethodB();
    }
}
====================================================================================
public class Run {
    public static void main(String[] args) {
        ObjectService service = new ObjectService();
        ThreadA a = new ThreadA(service);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(service);
        b.setName("b");
        b.start();
    }
}

运行结果:
A begin time = a 1542852420883
A end time = a 1542852424885
A begin time = b 1542852424885
A end time = b 1542852424885

验证同步 synchronized(this) 代码块是锁定当前对象的
将任意对象作为对象监视器

多个线程调用同一个对象的不同名称的 synchronized 同步方法或 synchronized(this) 同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。这说明 synchronized 同步方法或 synchronized(this) 同步代码块分别有两种作用:

  • synchronized 同步方法
    1. 对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈现阻塞作用
    2. 同一时间只有一个线程可以执行 synchronized 同步方法中的代码
  • synchronized(this) 同步代码块
    1. 对其他 synchronized 同步方法或 synchronized(this) 同步代码块调用呈现阻塞作用
    2. 同一时间只有一个线程可以执行 synchronized(this) 同步代码块中的代码

前面一直使用 synchronized(this) 来同步代码块,其实 Java 还支持对“任意对象”作为“对象监视器”来实现同步功能。

  1. 多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行 synchronized(非 this 对象 x ) 同步代码块中的代码
  2. 当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非 this 对象 x ) 同步代码块中的代码
public class Service {
    private String usernameParam;
    private String passwordParam;
    private String anyString = new String();//新键一个对象anyString,注意这个对象只有一个
    public void setUsernamePassword(String username, String password){
        try {
        //在这里将任意对象作为对象监视器
            synchronized (anyString){
                System.out.println("线程的名称为:" + Thread.currentThread().getName() +
                        "在" + System.currentTimeMillis() + "进入同步块");
                usernameParam = username;
                Thread.sleep(3000);
                passwordParam = password;
                System.out.println("线程的名称为:" + Thread.currentThread().getName() +
                        "在" + System.currentTimeMillis() + "离开同步块");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadA extends Thread{
    private Service service;

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

    public void run(){
        service.setUsernamePassword("a", "aa");
    }
}

public class ThreadB extends Thread{
    private Service service;

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

    public void run(){
        service.setUsernamePassword("b", "bb");
    }
}

public class Run {
    public static void main(String[] args) {
        Service service = new Service();
        ThreadA threadA = new ThreadA(service);
        threadA.setName("A");
        threadA.start();
        ThreadB threadB = new ThreadB(service);
        threadB.setName("B");
        threadB.start();
    }
}

运行结果:
线程的名称为:A在1542856878715进入同步块
线程的名称为:A在1542856881716离开同步块
线程的名称为:B在1542856881716进入同步块
线程的名称为:B在1542856884717离开同步块

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

将上面的程序稍作修改:

public class Service {
    private String usernameParam;
    private String passwordParam;
    public void setUsernamePassword(String username, String password){
        try {
            String anyString = new String();//更改的地方,此对象在方法内,每个线程都会拥有一个此对象,所以不是一个对象
            synchronized (anyString){//更改的地方
                System.out.println("线程的名称为:" + Thread.currentThread().getName() +
                        "在" + System.currentTimeMillis() + "进入同步块");
                usernameParam = username;
                Thread.sleep(3000);
                passwordParam = password;
                System.out.println("线程的名称为:" + Thread.currentThread().getName() +
                        "在" + System.currentTimeMillis() + "离开同步块");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:
线程的名称为:A在1542870208211进入同步块
线程的名称为:B在1542870208212进入同步块
线程的名称为:A在1542870211211离开同步块
线程的名称为:B在1542870211212离开同步块

可见,使用 synchronized(非 this 对象 x) 同步代码块格式进行同步操作时,对象监视器必须是同一对象,如果不是同一对象,运行结果就是异步了。

这个实验的目的是验证多个线程调用同一方法是随机的。

public class MyList {

    private List list = new ArrayList();

    synchronized public void add(String username){
        System.out.println("ThreadName = " + Thread.currentThread().getName() + " 执行了add方法");
        list.add(username);
        System.out.println("ThreadName = " + Thread.currentThread().getName() + " 退出了add方法");
    }

    synchronized public int getSize(){
        System.out.println("ThreadName = " + Thread.currentThread().getName() + " 执行了getSize方法");
        int sizeValue = list.size();
        System.out.println("ThreadName = " + Thread.currentThread().getName() + " 退出了getSize方法");
        return sizeValue;
    }
}

====================================================================================
public class ThreadA extends Thread {
    private MyList myList;

    public ThreadA(MyList myList) {
        super();
        this.myList = myList;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            myList.add("threadA " + (i+1));//执行10000次add方法
        }
    }
}

public class ThreadB extends Thread {
    private MyList myList;

    public ThreadB(MyList myList) {
        super();
        this.myList = myList;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            myList.add("threadB " + (i+1));
        }
    }
}

public class Test {
    public static void main(String[] args) {
        MyList myList = new MyList();
        ThreadA threadA = new ThreadA(myList);
        threadA.setName("a");
        threadA.start();
        ThreadB threadB = new ThreadB(myList);
        threadB.setName("b");
        threadB.start();
    }
}

运行结果:
ThreadName = a 执行了add方法
ThreadName = a 退出了add方法
ThreadName = b 执行了add方法
ThreadName = b 退出了add方法
ThreadName = a 执行了add方法
ThreadName = a 退出了add方法
ThreadName = a 执行了add方法
ThreadName = a 退出了add方法
ThreadName = b 执行了add方法
ThreadName = b 退出了add方法

对监视器对象的再一次认识与容易出现的错误的地方(重点)

同步代码块中的代码是同步打印的,当前线程的“执行”与“退出”是成对出现的。但是线程A 和线程B的执行却是异步的,这就有可能出现脏读的环境。由于线程执行的顺序不确定,所以当A和B两个线程执行带有分支判断的方法时,就会出现逻辑上的错误。

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

public class MyOneList {
    private List list = new ArrayList();
    //这里的synchronized对象监视器是MyOneList的对象
    synchronized public void add(String data){
        list.add(data);
    }
    synchronized public int getSize(){
        return list.size();
    }
}
====================================================================================
public class Thread1 extends Thread {
    private MyOneList list;

    public Thread1(MyOneList list) {
        super();
        this.list = list;
    }

    public void run(){
        MyService myRef = new MyService();
        myRef.addServiceMethod(list, "A");
    }
}

public class Thread2 extends Thread {
    private MyOneList list;

    public Thread2(MyOneList list) {
        super();
        this.list = list;
    }

    public void run(){
        MyService myRef = new MyService();
        myRef.addServiceMethod(list, "B");
    }
}

public class MyService {
    //现在把synchronized放在方法这里监视器对象是MyService的对象
    //如果把监视器对象放到这里是起不到避免脏读的作用的,因为在上面
    //线程Thread1和Thread2中的run方法中各自有一个MyService对象
    //这就导致这两个线程其实使用的是不同的监视器对象,所以不能起到同步的作用
    //也就不能避免脏读
    synchronized public MyOneList addServiceMethod(MyOneList list, String data){
        try {
        //如果把在这里使用synchronized(非this对象x)就可以起到同步的作用,避免脏读
            //synchronized (list){
                if(list.getSize() < 1){
                    Thread.sleep(2000);
                    list.add(data);
                }
            //}
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return list;
    }
}
====================================================================================
public class Run {
    public static void main(String[] args) {
        try {
            MyOneList list = new MyOneList();
            Thread1 thread1 = new Thread1(list);
            thread1.setName("a");
            thread1.start();
            Thread2 thread2 = new Thread2(list);
            thread2.setName("b");
            thread2.start();
            Thread.sleep(6000);
            System.out.println("listSize = " + list.getSize());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
细化验证三个结论
  1. 当多个线程同时执行 synchronized(x){} 同步代码块时呈现同步效果
  2. 当其他线程执行 x 对象中 synchronized 同步方法时呈现同步效果
  3. 当其他线程执行 x 对象方法里的 synchronized 关键字方法时,还是异步调用
静态同步 synchronized 方法与 synchronized(class) 代码块

关键字 synchronized 还可以应用在 static 静态方法上,如果这样些,那是对 .java 文件对应的 Class 类进行持锁。

public class Service {
    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();
        }
    }
    synchronized public static void printB(){
        System.out.println("线程名称为:" + Thread.currentThread().getName()
                + " 在" + System.currentTimeMillis() + "进入printB");
        System.out.println("线程名称为:" + Thread.currentThread().getName()
                + " 在" + System.currentTimeMillis() + "离开printB");
    }
}
====================================================================================
public class ThreadA extends Thread {
    public void run(){
        Service.printA();
    }
}

public class ThreadB extends Thread {
    @Override
    public void run() {
        Service.printB();
    }
}

public class Run {
    public static void main(String[] args) {
        ThreadA a = new ThreadA();
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB();
        b.setName("b");
        b.start();
    }
}

运行结果:
线程名称为:a 在1542893561542进入printA
线程名称为:a 在1542893564543离开printA
线程名称为:b 在1542893564543进入printB
线程名称为:b 在1542893564543离开printB

从运行结果上看,并没有什么特别之处,和将 synchronized 关键字加到非静态方法上的效果是一样的。其实还是有本质的不同,synchronized 关键字加到 static 静态方法上是给 Class 上锁而 synchronized 加到非 static 方法上是给对象上锁Class 锁可以对类的所有对象实例起作用

数据类型 String 的常量池特性

在 JVM 中具有 String 常量池缓存的功能。

public class Service {
    public static void print(String stringParam){
        try {
            synchronized (stringParam){
                while(true){
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
====================================================================================
public class ThreadA extends Thread {
    private Service service;

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

    @Override
    public void run() {
        Service.print("AA");
    }
}

public class ThreadB extends Thread {
    private Service service;

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

    @Override
    public void run() {
        Service.print("BB");
    }
}
====================================================================================
public class Run {
    public static void main(String[] args) {
        Service service = new Service();
        ThreadA a = new ThreadA(service);
        a.setName("a");
        a.start();
        ThreadB b = new ThreadB(service);
        b.setName("b");
        b.start();
    }
}

运行结果:
a
a
a
a
a

出现这样的结果是因为两个线程使用的对象监视器是相同的,所以两个线程持有相同的锁,所以造成 B 线程不能执行。原因是存在 String 常量缓冲池。

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

同步方法容易造成死循环

多线程的死锁

Java 线程死锁是一个金典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有任务都无法继续完成。

public 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 Run {
    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();
        }
    }
}

线程 thread1 持有 lock1 锁,得到 lock2 锁之后才能执行后边的代码,之后才能释放 lock1 锁;线程 thread2 持有lock2 锁,得到 lock1 锁之后才能执行之后的代码释放 lock1锁,这样相互争夺锁都不释放的做法造成了死锁。

内置类与静态内置类
锁对象的改变

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

volatile 关键字

关键字 volatile 的主要作用是使变量在多个线程间可见。

解决同步死循环

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

解决异步死循环
public class RunThread extends Thread{
    //在启动RunThread.java线程时,变量private boolean isRunning = true;存在于公共堆栈及线程的私有堆栈中,
    //在JVM被设置为-server模式时为了线程运行效率,线程一直在私有堆栈中取得isRunning的值是true
    //代码runThread.setRunning(false);虽然被执行,但是跟新确实公共堆栈中的isRunning变量值为false
    //所以,一直是死循环的状态
    private boolean isRunning = true;
    public boolean isRunning(){
        return isRunning;
    }
    public void setRunning(boolean isRunning){
        this.isRunning = isRunning;
    }
    public void run(){
        System.out.println("进入run了");
        while(isRunning == true){
        }
        System.out.println("线程被停止了!");
    }
}

public class Run {
    public static void main(String[] args) {
        try {
            RunThread runThread = new RunThread();
            runThread.start();
            Thread.sleep(1000);
            runThread.setRunning(false);
            System.out.println("已经赋值为false");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这个问题其实就是私有堆栈中的值和公共堆栈中的值不同步造成的。解决这个问题的管家就是使用 volatile 关键字。它的主要作用就是当前线程访问 isRunning 这个变量时,强制从公共堆栈中进行取值。
改为:volatile private boolean isRunning = true;就解决了这个问题。

使用 volatile 关键字增加了实例变量在多个线程之间的可见性。但 volatile 关键字最致命的缺点是不支持原子性。
下面将 synchronized 和 volatile 进行以下比较:

  1. 关键字 volatile 是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 要好, 并且 volatile 只能修饰变量,而 synchronized 可以修饰方法,以及代码块。
  2. 多线程访问 volatile 不会发生阻塞,而 synchronized 会发生阻塞。
  3. volatile 能保证数据的可见性,但不能保证原子性;而 synchronized 可以保证原子性。
  4. 关键字 volatile 解决的是变量在多个线程之间的可见性;而 synchronized 解决的是多个线程之间访问资源的同步性。
    线程安全包含原子性和可见性两个方面,Java 的同步机制都是围绕这两个方面来确保线程安全的。
volatile 非原子的特性

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

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

出了在 i++ 操作时使用 synchronized 关键字实现同步外,还可以使用 AtomicInteger 原子类实现。原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子类型就是一个原子操作可用类型,它可以在没有锁的情况下做到线程安全。

synchronized 代码块有 volatile 同步的功能

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

public class Service {
    private boolean isContinueRun = true;
    public void runMethod(){
        while(isContinueRun == true){

        }
        System.out.println("停下来了!");
    }
    public void stopMethod(){
        isContinueRun = false;
    }
}

public class ThreadA extends Thread {
    private Service service;
    public ThreadA(Service service){
        super();
        this.service = service;
    }

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

public class ThreadB extends Thread {
    private Service service;
    public ThreadB(Service service){
        super();
        this.service = service;
    }

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

public class Run {
    public static void main(String[] args) {
        try {
            Service service = new Service();
            ThreadA threadA = new ThreadA(service);
            threadA.start();
            ThreadA.sleep(1000);
            ThreadB threadB = new ThreadB(service);
            threadB.start();
            System.out.println("已经发起停止的命令了!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:
已经发起停止的命令了!

利用前面的知识我们知道,当线程开启的时候,private boolean isContinueRun = true;变量是从公共空间被复制到线程私有空间中的,所以当线程 b 执行将变量isContinueRun改变为false的时候,公共空间中的isContinueRun变量并没有被改变,线程 a 中的 isContinue 变量也就一直都是 true 会一直运行下去。得到这个结果是各线程之间的数据没有可视性造成的。

将代码改为:

public class Service {
    boolean isContinueRun = true;
    public void runMethod(){
        String anyString = new String();
        //synchronized代码块可以保证在同一时刻,只有一个代码可以执行某一个方法或某一个代码块。
        while(isContinueRun == true){
            synchronized (anyString){}
        }
        System.out.println("停下来了!");
    }
    public void stopMethod(){
        isContinueRun = false;
    }
}

同步 synchronized 不仅可以解决一个线程看到对象处于不一致的状态,而且可以保证进入同步方法或者同步代码块的每个线程,看到由同一个锁保护之前所有的修改效果。这个程序更改之后,如果线程b将isContinue变量修改为false,线程 a 在执行后也可以看到,所以,就不会造成死锁,程序就可以顺利完成了。

方法 wait(long) 的使用

带一个参数的 wait(long) 方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。

通知过早

如果通知过早,则会打乱程序的正常运行逻辑。

public class MyRun {
    private String lock = new String("");
    private Runnable runnableA = new Runnable() {
        @Override
        public void run() {
            try {
                synchronized (lock){
                    System.out.println("begin wait");
                    lock.wait();
                    System.out.println("end wait");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    private Runnable runnableB = new Runnable() {
        @Override
        public void run() {
            synchronized (lock){
                System.out.println("begin notify");
                lock.notify();
                System.out.println("end notify");
            }
        }
    };

    public static void main(String[] args) {
        try {
            MyRun myRun = new MyRun();
            Thread b = new Thread(myRun.runnableB);
            b.start();
            Thread.sleep(100);
            Thread a = new Thread(myRun.runnableA);
            a.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:
begin notify
end notify
begin wait

先通知了,所以 wait() 也就没有执行必要了。

生产者/消费者模式的实现

等待/通知模式最经典的案例就是“生产者/消费者”模式。

多生产与多消费:操作值-假死

“假死”的现象其实就是线程进入 WAITING 等待状态。如果全部线程进入 WAITING 状态,则程序就不再执行任何业务功能了,整个项目呈现停止状态。

//生产者
import chapterThree.p_r_test.ValueObject;

public class P {
    private String lock;

    public P(String lock) {
        super();
        this.lock = lock;
    }

    public void setValue(){
        try {
            synchronized (lock){
                while(!ValueObject.value.equals("")){
                    System.out.println("生产者 " + Thread.currentThread().getName() + " WAITING");
                    lock.wait();
                }
                System.out.println("生产者 " + Thread.currentThread().getName() + " Runnable了");
                String value = System.currentTimeMillis() + "_" + System.nanoTime();
                ValueObject.value = value;
                //释放锁
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//消费者
public class C {
    private String lock;

    public C(String lock) {
        super();
        this.lock = lock;
    }

    public void getValue(){
        try {
            synchronized (lock){
                while(ValueObject.value.equals("")){
                    System.out.println("消费者 " + Thread.currentThread().getName() + " WAITING");
                    lock.wait();
                }
                System.out.println("消费者 " + Thread.currentThread().getName() + " Runnable了");
                ValueObject.value = "";
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//生产者线程
public class ThreadP extends Thread {
    private P p;

    public ThreadP(P p) {
        super();
        this.p = p;
    }

    @Override
    public void run() {
        while(true)
            p.setValue();
    }
}

//生产者线程
public class ThreadC extends Thread{
    private C c;

    //构造函数的主要目的是对消费者对象进行初始化
    public ThreadC(C c) {
        super();
        this.c = c;
    }

    @Override
    public void run() {
        while(true)
            c.getValue();
    }
}

public class Run {
    public static void main(String[] args) throws InterruptedException{
        String lock = new String("");
        C c = new C(lock);
        P p = new P(lock);
        ThreadP[] pThread = new ThreadP[2];
        ThreadC[] cThread = new ThreadC[2];
        for (int i = 0; i < 2; i++) {
            pThread[i] = new ThreadP(p);
            pThread[i].setName("生产者" + (i+1));
            cThread[i] = new ThreadC(c);
            cThread[i].setName("消费者" + (i+1));
            pThread[i].start();
            cThread[i].start();
        }
        Thread.sleep(5000);
        Thread[] threadArray = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
        Thread.currentThread().getThreadGroup().enumerate(threadArray);
        for (int i = 0; i < threadArray.length; i++) {
            System.out.println(threadArray[i].getName() + " " + threadArray[i].getState());
        }
    }
}

运行结果(部分):
【这里就是,生产者1生产完毕后,通过notify释放锁,之后唤醒的是同类,即下面的生产者,导致生产者处于wait模式,这里就是连续唤醒】
生产者 生产者1 Runnable了
生产者 生产者1 WAITING ?
main RUNNABLE
Monitor Ctrl-Break RUNNABLE
生产者1 WAITING
消费者1 WAITING
生产者2 WAITING
消费者2 WAITING

在代码中确实通过 wait/notify 进行通信了,但不保证 notify 唤醒的是异类,也许是同类。比如:“生产者”唤醒“生产者”,或”消费者“唤醒”消费者“。 假死出现的原因是有可能连续唤醒同类。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值